Home > Back-end >  How can I listen to changes in a @AppStorage property when not in a view?
How can I listen to changes in a @AppStorage property when not in a view?

Time:05-24

The following is the content of a playground that illustrates the problem. Basically I have a value stored in UserDefaults and accessed by a variable wrapped in the @AppStorage property wrapper. This lets me access the updated value in a View but I'm looking for a way to listen to changes in the property in ViewModels and other non-View types.

I have it working in the follow code but I'm not sure it's the best way to do it and I'd love to avoid having to declare a PassthroughSubject for each property I want to watch.

Note: I did originally sink the ObservableObject's objectWillChange property however that will reflect any change to the object and I'd like to do something more fine grained.

So does anyone have any ideas on how to improve this technique?

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())

CodePudding user response:

The AppStorage changes in ObservableObject result in firing objectWillChange, so we can use it and code becomes much simpler

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)"
        }
    }
}

Yes, it is not known which property did change exactly, but assigning same (not modified) will not generate following updates (eg. .onChange, etc.).

So should be considered case-by-case, but can be applicable.

BTW, @ObservedObject works only in View, so in VM is just redundant.

CodePudding user response:

Currently I'm considering whether I can create a new property wrapper that wraps @AppStorage. Will post if I can get it to work.

  • Related