【问题标题】:How can I listen to changes in a @AppStorage property when not in a view?不在视图中时如何收听@AppStorage 属性的更改?
【发布时间】:2022-07-05 10:41:35
【问题描述】:

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

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

注意:我最初是 sink 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())

【问题讨论】:

  • 我建议完全删除视图模型对象并按设计使用视图结构
  • 视图模型是我工作的许多客户端使用的 MVVM 架构的一部分。另外,无论是视图模型还是其他背景对象,问题仍然相同。监视由应用程序的另一部分触发的用户默认更改的最佳方式是什么? :-)

标签: swift swiftui combine


【解决方案1】:

ObservableObject 中的AppStorage 更改导致触发objectWillChange,因此我们可以使用它并且代码变得更加简单

class AppSettings: ObservableObject {
    @AppStorage("MyValue") var myValue = 0
}

class ValueViewModel: ObservableObject {
    private var settings = AppSettings()

    @Published var title: String = ""

    private var cancellable: AnyCancellable?

    init() {
        cancellable = settings.objectWillChange.sink { [weak self] _ in
            guard let self = self else { return }
            self.title = "Hello \(self.settings.myValue)"
        }
    }
}

是的,不知道确切更改了哪个属性,但分配相同(未修改)将不会生成后续更新(例如 .onChange 等)。

因此应逐案考虑,但可以适用。

顺便说一句,@ObservedObject 仅在 View 中有效,因此在 VM 中只是多余的。

【讨论】:

  • 没错,我最初的代码是这样工作的。但是,我也在考虑它可能会导致很多误报,我只想查看设置更改的子集。例如,背景类型只对一个特定设置感兴趣。因此,虽然这可行,但我对特定属性的方法感兴趣,而不是作为一个整体设置对象。
【解决方案2】:

目前我正在考虑是否可以创建一个新的属性包装器来包装@AppStorage。如果我可以让它工作,我会发布。

【讨论】:

    【解决方案3】:

    我写了这个属性包装器:

    /// Property wrapper that acts the same as @AppStorage, but also provides a ``Publisher`` so that non-View types
    /// can receive value updates.
    @propertyWrapper
    struct PublishingAppStorage<Value> {
    
        var wrappedValue: Value {
            get { storage.wrappedValue }
            set {
                storage.wrappedValue = newValue
                subject.send(storage.wrappedValue)
            }
        }
    
        var projectedValue: Self {
            self
        }
    
        /// Provides access to ``AppStorage.projectedValue`` for binding purposes. 
        var binding: Binding<Value> {
            storage.projectedValue
        }
    
        /// Provides a ``Publisher`` for non view code to respond to value updates.
        private let subject = PassthroughSubject<Value, Never>()
        var publisher: AnyPublisher<Value, Never> {
            subject.eraseToAnyPublisher()
        }
    
        private var storage: AppStorage<Value>
    
        init(wrappedValue: Value, _ key: String) where Value == String {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == Int {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value == Data {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value == Int {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == String {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value == URL {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value == Double {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        init(wrappedValue: Value, _ key: String) where Value == Bool {
            storage = AppStorage(wrappedValue: wrappedValue, key)
        }
    
        mutating func update() {
            storage.update()
        }
    }
    

    基本上它包装了@AppStorage 并添加了Publisher。从声明的角度来看,使用它是完全相同的:

    @PublishedAppStorage("myValue") var myValue = 0
    

    并且访问值完全相同,但是访问绑定略有不同,因为预计值投影Self,因此它通过$myValue.binding 完成,而不仅仅是$myValue

    当然,现在我的非视图可以访问这样的发布者:

    cancellable = settings.$myValue.publisher.sink {
        print("object changed")
        self.title = "Hello \($0)"
    }
    

    【讨论】:

      猜你喜欢
      • 2017-09-07
      • 1970-01-01
      • 1970-01-01
      • 2014-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多