Can @Published be implemented in Swift?

I want to implement something like the @Published property wrapper, but instead of storing it's value to an instance variable, it would store data in UserDefaults. (I know SwiftUI has AppStorage, but that only works on Views. I want to refer to the property from model code.)

I found this, that shows how to get access to the enclosing object in a property wrapper.

Here, I try to use code in that thread to implement a Bool in UserDefaults:

@propertyWrapper
public final class BoolWrapper {
    
    public static subscript<EnclosingSelf: ObservableObject>(
      _enclosingInstance observed: EnclosingSelf,
      wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Bool>,
      storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, BoolWrapper>
    ) -> Bool {
      get {
          let key = observed[keyPath: storageKeyPath].key
          UserDefaults.standard.bool(forKey: key)
      }
      set {
          observed.objectWillChange.send() // <-- Error here
          let key = observed[keyPath: storageKeyPath].key
          UserDefaults.standard.set(newValue, forKey: key)          
      }   
    }
    
    public var wrappedValue: Bool {
      get { fatalError("called wrappedValue getter") }
      set { fatalError("called wrappedValue setter") }
    }
    
    public init(key: String) {
        self.key = key
    }
    private let key: String
}
...
class MyModel: ObservableObject {
    @BoolWrapper(key: "testKey") var testProp: Bool

I get the error:

Value of type 'EnclosingSelf.ObjectWillChangePublisher' has no member 'send'

How does @Published do it?

3 Likes

@Published is written completely in swift; the key portion that is missing here is that default value that ObservableObject grants for its objectWillChange property. The failure you are seeing is that type is user definable - that means you can implement your own property objectWillChange as long as it is a publisher of Void and a failure of Never. Perhaps you could cast out to a ObservableObjectPublisher and send() on that.

P.S. You will want to make the public var wrappedValue: Bool unavailable; that is one component of how that _enclosingInstance method works.

Also to make sure this actually works the way you would want; make sure to have at least 1 @Published property even if you don't use it. That will make sure that the ObservableObject has storage to store the objectWillChange guts. (else wise you go down a pretty non-performant path to accomidate ObservbleObject types that don't have any @Published properties)

2 Likes

Thank you, that got it working. I can use the cast, or add a where constraint to the subscript function.

I'm looking at ObservableObject. I don't think I've seen a protocol associatedtype that is set equal to a concrete type like this. Maybe I'm not understanding that Swift syntax correctly. If the associated type must always be ObservableObjectPublisher, then what's the point of an associatedtype? Just use ObservableObjectPublisher without the indirection. What am I missing?

public protocol ObservableObject : AnyObject {
    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}

The equal syntax there says: "this protocol requires an associated type that is a Publisher of Void with a failure of Never but if you don't specify one; here is one by default"

FWIW, I hate that associated type with a passion.

It is forever getting in the way when you want to do protocols of ObservableObjects for mocking and testing; when you want to provide different implementations to @StateObject or .environmentObject; when you want do container-based dependency injection, and on, and on.

It makes working with SwiftUI about 10 times harder than it should.

just ran into this today.... between that associated object and protocols not alloing @Published in the declararion (i know the workaround) its a bit annoying...

i wonder if the new any keyword in 5.7 + type-aliasing can help with that issue?