DynamicProperty PropertyWrapper is not working as I expect it to work

This will be my last update, I've finally got it working!

#if canImport(SwiftUI)
import Foundation
import SwiftUI
import Combine

/// A property wrapper that reads and writes to iCloud.
///
/// Example:
/// ```
/// @iCloudStorage("key") var value: String = "default"
/// ```
@propertyWrapper
public struct iCloudStorage<T>: DynamicProperty {
    // swiftlint:disable:previous type_name
    /// The key to read and write to.
    let key: String

    /// The default value to use if the value is not set yet.
    let defaultValue: T

    /// The cancellables to store.
    var cancellables = Set<AnyCancellable>()

    /// The observed storage
    @ObservedObject private var store: Storage

    /// Creates an `iCloudStorage` property.
    ///
    /// - Parameter wrappedValue: The default value.
    /// - Parameter key: The key to read and write to.
    public init(wrappedValue: T, _ key: String) {
        self.key = key
        self.defaultValue = wrappedValue
        self.store = Storage(
            NSUbiquitousKeyValueStore.default.object(forKey: key) as? T ?? defaultValue
        )

        // Set-up notification for changed key.
        NotificationCenter.default.publisher(
            for: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
            object: NSUbiquitousKeyValueStore.default
        )
        .receive(on: DispatchQueue.main)
        .sink { [self] notification in
            if let keys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String],
               keys.contains(key) {
                self.store.value = NSUbiquitousKeyValueStore.default.object(forKey: key) as? T ?? defaultValue
            }
        }
        .store(in: &cancellables)
    }

    /// The value of the key in iCloud.
    public var wrappedValue: T {
        get {
            return store.value
        }

        nonmutating set {
            NSUbiquitousKeyValueStore.default.set(newValue, forKey: key)
            store.value = newValue
        }
    }

    /// A binding to the value of the key in iCloud.
    public var projectedValue: Binding<T> {
        $store.value
    }

    // MARK: - Storage
    private final class Storage: ObservableObject {
        var parentWillChange: ObservableObjectPublisher?

        var value: T {
            willSet {
                objectWillChange.send()
                parentWillChange?.send()
            }
        }

        init(_ value: T) {
            self.value = value
        }
    }

    // MARK: - Get parent
    /// Get the parent, to send a willChange event to there.
    public static subscript<OuterSelf: ObservableObject>(
        _enclosingInstance instance: OuterSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, T>,
        storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
    ) -> T {
        get {
            instance[keyPath: storageKeyPath].store.parentWillChange = (
                instance.objectWillChange as? ObservableObjectPublisher
            )

            return instance[keyPath: storageKeyPath].wrappedValue
        }
        set {
            instance[keyPath: storageKeyPath].wrappedValue = newValue
        }
    }
}
#endif

@vanvoorden Thanks for your mention to How is the `Published` property wrapper implemented? this led me to Accessing a Swift property wrapper’s enclosing instance | Swift by Sundell

1 Like