Milo Wielondek
Milo Wielondek

Reputation: 4362

Combining custom property wrapper with @Published

I wish to apply a custom property wrapper to a variable already wrapped in @Published, nesting them like
(A) @Custom @Published var myVar or
(B) @Published @Custom var myVar
(notice the application order of the wrappers).

In the case of (A) I get the error

'wrappedValue' is unavailable: @Published is only available on properties of classes

and for (B)

error: key path value type 'Int' cannot be converted to contextual type 'Updating<Int>'

neither of which are particularly helpful. Any ideas how to make it work?

Minimal code example

import Combine

class A {
    @Updating @Published var b: Int
    
    init(b: Int) {
        self.b = b
    }
}

@propertyWrapper struct Updating<T> {
    var wrappedValue: T {
        didSet {
            print("Update: \(wrappedValue)")
        }
    }
}

let a = A(b: 1)
let cancellable = a.$b.sink {
    print("Published: \($0)")
}
a.b = 2
// Expected output:
// ==> Published: 1
// ==> Published: 2
// ==> Update: 2

Upvotes: 10

Views: 3654

Answers (3)

D.Rain
D.Rain

Reputation: 13

I tried MechEthan's solution and it didn't work for me... and I figured out by Simone's answer that I still need to send the objectWillChange signal. So I ended up with this:

@propertyWrapper
class _CustomPublished<Instance: ObservableObject, Value> where Instance.ObjectWillChangePublisher == ObservableObjectPublisher {
    @available(*, unavailable)
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }

    var projectedValue: Published<Value>.Publisher {
        get { $value }
        set { $value = newValue }
    }

    static subscript(
        _enclosingInstance instance: Instance,
        wrapped _: KeyPath<Instance, Value>,
        storage storageKeyPath: KeyPath<Instance, _CustomPublished>
    ) -> Value {
        get { instance[keyPath: storageKeyPath].value }
        set {
            instance.objectWillChange.send()
            instance[keyPath: storageKeyPath].value = newValue
        }
    }

    init(wrappedValue: Value) {
        value = wrappedValue
    }

    @Published private var value: Value
}

extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
    typealias CustomPublished<T> = _CustomPublished<Self, T>
}

I replaced some @Published in my project and it seems working, but not sure whether there's some other hidden differences

Really, swift should have done better in composing property wrappers

Upvotes: 0

MechEthan
MechEthan

Reputation: 5703

The only solution I've found is a workaround: Make a custom @propertyWrapper that has a @Published property inside of it.

Example:

/// Workaround @Published not playing nice with other property wrappers. 
/// Use this to replace @Published to temporarily help debug a property being accessed off the main thread.

@propertyWrapper
public class MainThreadPublished<Value> {
    @Published
    private var value: Value
    
    
    public var projectedValue: Published<Value>.Publisher {
        get {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            return $value
        }
        @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
        set {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            $value = newValue
        }
    }
    
    public var wrappedValue: Value {
        get {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            return value
        }
        set {
            assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
            value = newValue
        }
    }
    
    public init(wrappedValue value: Value) {
        self.value = value
    }

    public init(initialValue value: Value) {
        self.value = value
    }
}

Further reading:


EDIT:

I also just found this article that may provide an alternate approach, but I don't have time to investigate:

Upvotes: 2

Simone
Simone

Reputation: 111

None of the options you provided can be applicated to make a custom property wrapper behave like it was marked with @Published (A and B)

The real question is, how do i observe property/status update changes?

  1. Using @Published wrapper, which handles status update automatically

  2. Tracking the status update manually, by implementing

    willSet {objectWillChange.send()}

Considering the option 1 cant work as you cant apply 2 property wrappers, you can go for manual status update tracking. In order to accomplish this, you will need to make your class conforming the ObservableObject protocol.

class A: ObservableObject {
    @Updating var b: Int{
       willSet {objectWillChange.send()}
    }
    
    init(b: Int) {
        self.b = b
    }
}

Your views will now be able to refresh whenever var b changes, having at the same time your @Updating wrapper fully working.

Upvotes: 2

Related Questions