当不在视图中时,如何侦听 @AppStorage 属性中的更改?

dre*_*kka 3 swift swiftui combine

下面是一个说明问题的playground的内容。基本上,我有一个值存储在属性包装器UserDefaults中包装的变量中并由该变量访问@AppStorage。这让我可以访问 a 中的更新值,View但我正在寻找一种方法来侦听ViewModels 和其他非View类型中的属性的更改。

我让它在下面的代码中工作,但我不确定这是最好的方法,我希望避免PassthroughSubject为我想观看的每个属性声明 a 。

注意:我最初做了'ssink属性,但这将反映对象的任何更改,我想做一些更细粒度的事情。ObservableObjectobjectWillChange

那么有人对如何改进这项技术有任何想法吗?

import Combine
import PlaygroundSupport
import SwiftUI

class AppSettings: ObservableObject {
    var myValueChanged = PassthroughSubject<Int, Never>()
    @AppStorage("MyValue") var myValue = 0 {
        didSet { myValueChanged.send(myValue) }
    }
}

struct ContentView: View {

    @ObservedObject var settings: AppSettings
    @ObservedObject var viewModel: ValueViewModel

    init() {
        let settings = AppSettings()
        self.settings = settings
        viewModel = ValueViewModel(settings: settings)
    }

    var body: some View {
        ValueView(viewModel)
            .environmentObject(settings)
    }
}

class ValueViewModel: ObservableObject {

    @ObservedObject private var settings: AppSettings
    @Published var title: String = ""
    private var cancellable: AnyCancellable?

    init(settings: AppSettings) {
        self.settings = settings
        title = "Hello \(settings.myValue)"

        // Is there a nicer way to do this?????
        cancellable = settings.myValueChanged.sink {
            print("object changed")
            self.title = "Hello \($0)"
        }
    }
}

struct ValueView: View {

    @EnvironmentObject private var settings: AppSettings
    @ObservedObject private var viewModel: ValueViewModel

    init(_ viewModel: ValueViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        Text("This is my \(viewModel.title) value: \(settings.myValue)")
            .frame(width: 300.0)
        Button("+1") {
            settings.myValue += 1
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())
Run Code Online (Sandbox Code Playgroud)

Pab*_*nez 5

绑定视图时,接受的解决方案PublishingAppStorage类不能完全工作

@propertyWrapper
public struct PublishedAppStorage<Value> {
    
    // Based on: https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombine/Published.swift
    
    @AppStorage
    private var storedValue: Value
    
    private var publisher: Publisher?
    internal var objectWillChange: ObservableObjectPublisher?
    
    /// A publisher for properties marked with the `@Published` attribute.
    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: ObservableObject>(
        _enclosingInstance object: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedAppStorage<Value>>
    ) -> Value {
        get {
            return object[keyPath: storageKeyPath].storedValue
        }
        set {
            // /sf/answers/4134732381/
            (object.objectWillChange as? ObservableObjectPublisher)?.send()
            object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
            object[keyPath: storageKeyPath].storedValue = newValue
        }
    }
    
    // MARK: - Initializers

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

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

    // Bool
    init(wrappedValue: Bool, _ key: String, store: UserDefaults? = nil) where Value == Bool {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
    }
}
Run Code Online (Sandbox Code Playgroud)

这个的工作原理就像你正在使用一样@AppStorage

viewModel.$fade.sink { [weak self] newFadeSetting in

}
Run Code Online (Sandbox Code Playgroud)

哪里fade

@PublishedAppStorage("fade") var fade: ScreenFadeSettingItem = .on

对于绑定,你可以简单地使用

SettingSegmentPicker<ScreenFadeSettingItem>(
        titles: ScreenFadeSettingItem.allCases,
        selection: $viewModel.fade
)
Run Code Online (Sandbox Code Playgroud)