Mimic Swift 结合@Published 创建@PublishedAppStorage

Mimic Swift Combine @Published to create @PublishedAppStorage

我正在尝试模仿 Combine @Published 属性 包装器。我的最终目标是使用嵌套的 @AppStorage 创建一个新的自定义 属性 包装器(例如 @PublishedAppStorage)@Published。 所以我只是开始尝试模仿@Published。

我的问题是在从接收器块中访问原始值时崩溃并出现错误: Thread 1: Simultaneous accesses to 0x600000103328, but modification requires exclusive access

我花了几天时间寻找方法。

这是我的自定义@DMPublished:

@propertyWrapper
struct DMPublished<Value> {
    private let subject:CurrentValueSubject<Value, Never>
    
    init(wrappedValue: Value) {
        self.wrappedValue = wrappedValue
        self.subject = CurrentValueSubject(wrappedValue)
    }
    
    var wrappedValue: Value {
        willSet {
            subject.send(newValue)
        }
    }

    var projectedValue: AnyPublisher<Value, Never> {
        subject.eraseToAnyPublisher()
    }
}

定义我的属性的 ObservableObject:

import Combine

public class DMDefaults: ObservableObject {
    
    static public let shared = DMDefaults()
    private init(){}
    
    @Published public var corePublishedString = "dd"
    @DMPublished public var customPublishedString = "DD"

}

这是我的测试函数:

public func testSink()
{
    let gdmDefaults = DMDefaults.shared
    gdmDefaults.corePublishedString = "ee"; gdmDefaults.customPublishedString = "EE"
    
    gdmDefaults.corePublishedString = "ff"; gdmDefaults.customPublishedString = "FF"

    let coreSub = gdmDefaults.$corePublishedString.sink { (newVal) in
        print("coreSub: oldVal=\(gdmDefaults.corePublishedString) ; newVal=\(newVal)")
    }
    let custSub = gdmDefaults.$customPublishedString.sink { (newVal) in
        print("custSub: oldVal=\(gdmDefaults.customPublishedString) ; newVal=\(newVal)") // **Crashing here**
    }
    
    gdmDefaults.corePublishedString = "gg"; gdmDefaults.customPublishedString = "GG"

}

在此感谢任何帮助...谢谢...

感谢 David 的指导,我已经成功完成了。我在这里发布我的最终 属性 包装,以防有人觉得它有帮助。

@propertyWrapper
public struct PublishedAppStorage<Value> {
    
    @AppStorage
    private var storedValue: Value
    
    private var publisher: Publisher?
    internal var objectWillChange: ObservableObjectPublisher?
        
    public struct Publisher: Combine.Publisher {
        
        public typealias Output = Value
        
        public typealias Failure = Never
        
        public func receive<Downstream: Subscriber>(subscriber: Downstream)
        where Downstream.Input == Value, Downstream.Failure == Never
        {
            subject.subscribe(subscriber)
        }
        
        fileprivate let subject: Combine.CurrentValueSubject<Value, Never>
        
        fileprivate init(_ output: Output) {
            subject = .init(output)
        }
    }

    public var projectedValue: Publisher {
        mutating get {
            if let publisher = publisher {
                return publisher
            }
            let publisher = Publisher(storedValue)
            self.publisher = publisher
            return publisher
        }
    }
    
    @available(*, unavailable, message: """
               @Published is only available on properties of classes
               """)
    public var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    
    public static subscript<EnclosingSelf: AnyObject>(
        _enclosingInstance object: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedAppStorage<Value>>
    ) -> Value {
        get {
            return object[keyPath: storageKeyPath].storedValue
        }
        set {
            object[keyPath: storageKeyPath].objectWillChange?.send()
            object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
            object[keyPath: storageKeyPath].storedValue = newValue
        }
    }
    
    // MARK:- Initializers

    // String
    init(wrappedValue: String, _ key: String, store: UserDefaults? = DMDefaults.userDefaults) where Value == String {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }

    // Data
    init(wrappedValue: Data, _ key: String, store: UserDefaults? = DMDefaults.userDefaults) where Value == Data {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }
    
    // Int
    init(wrappedValue: Int, _ key: String, store: UserDefaults? = DMDefaults.userDefaults) where Value == Int {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }
    
    // URL
    init(wrappedValue: URL, _ key: String, store: UserDefaults? = DMDefaults.userDefaults) where Value == URL {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }
    
    // Double
    init(wrappedValue: Double, _ key: String, store: UserDefaults? = DMDefaults.userDefaults) where Value == Double {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }

    // Bool
    init(wrappedValue: Bool, _ key: String, store: UserDefaults? = DMDefaults.userDefaults) where Value == Bool {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }

}