How can I get @AppStorage to work in an MVVM / SwiftUI framework?

Viewed 541

I have a SettingsManager singleton for my entire app that holds a bunch of user settings. And I've got several ViewModels that reference and can edit the SettingsManager.

The app basically looks like this...

import PlaygroundSupport
import Combine
import SwiftUI

class SettingsManager: ObservableObject {
    static let shared = SettingsManager()

    @AppStorage("COUNT") var count = 10
}

class ViewModel: ObservableObject {
    @Published var settings = SettingsManager.shared
    
    func plus1() {
        settings.count += 1
        objectWillChange.send()
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    var body: some View {
        VStack {
            Button(action: viewModel.plus1) {
                Text("\(viewModel.settings.count)")
            }
        }
    }
}

let viewController = UIHostingController(rootView: ContentView())

PlaygroundPage.current.liveView = viewController

Frustratingly, it works about 85% of the time. But 15% of the time, the values don't update until navigating away from the view and then back.

How can I get @AppStorage to play nice with my View Model / MVVM framework?!

2 Answers

Came across this question researching this exact issue. I came down on the side of letting SwiftUI do the heavy lifting for me. For example:

// Use this in any view model you need to update the value
extension UserDefaults {
   static func setAwesomeValue(with value: Int) {
     UserDefaults.standard.set(value, forKey: "awesomeValue")
   }

  static func getAwesomeValue() -> Int {
    return UserDefaults.standard.bool(forKey: "awesomeValue")
  }
}

// In any view you need this value
struct CouldBeAnyView: some View {
   @AppStorage("awesomeValue") var awesomeValue = 0
}

AppStorage is just a wrapper for UserDefaults. Whenever the view model updates the value of "awesomeValue", AppStorage will automatically pick it up. The important thing is to pass the same key when declaring @AppStorage. Probably shouldn't use a string literal but a constant would be easier to keep track of?

This SettingsManager in a cancellables set solution adapted from the Open Source ACHN App:

import PlaygroundSupport
import Combine
import SwiftUI

class SettingsManager: ObservableObject {
    static let shared = SettingsManager()

    @AppStorage("COUNT") var count = 10 {
        willSet { objectWillChange.send() }
    }
}

class ViewModel: ObservableObject {
    @Published var settings = SettingsManager.shared
    var cancellables = Set<AnyCancellable>()
    
    init() {
        settings.objectWillChange
            .sink { [weak self] _ in
                self?.objectWillChange.send()
            }
            .store(in: &cancellables)
    }
    
    func plus1() {
        settings.count += 1
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    var body: some View {
        VStack {
            Button(action: viewModel.plus1) {
                Text(" \(viewModel.settings.count) ")
            }
        }
    }
}

let viewController = UIHostingController(rootView: ContentView())

PlaygroundPage.current.liveView = viewController

Seems to be slightly less glitchy, but still isn't 100% rock-solid consistent :(

Leaving this here to hopefully inspire someone with my attempt

Related