UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Proper implementation for @AppStorage with a MVVM structure ?

Forums > SwiftUI

I'm learning the MVVM structure, but I just can't figure out a proper implementation for the User Settings (i.e. @AppStorage). My ViewModel sometimes has to interact with persistent data, such as a toggle to Force Dark Mode.

I wrote this very basic example. It works, but ContentView does not get refreshed to reflect the new value. I have to restart the app manually.

What am I doing wrong ? Thanks !

class Settings: ObservableObject {
    static let shared = Settings()
    private init() { }
    @AppStorage("forceDarkMode") var forceDarkMode: Bool = false
}

class ContentViewModel: ObservableObject {
    func toggleUI() -> Void {
        Settings.shared.forceDarkMode.toggle()
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text("Force Dark Mode : " + Settings.shared.forceDarkMode.description)

            Button(action: viewModel.toggleUI, label: {
                Text("Tap to toggle UI style")
            })
        }
    }
}

3      

@AppStorage can (currently? we'll see what new goodies Apple gives us next week!) only be used from within a View. You need to access UserDefaults from within your Settings class some other way.

3      

This is what I thought. :-/

Is there any way to force ContentView to refresh when a Settings is updated?

I am not too good with the Combine framework, but I suspect the solution is there.

3      

hi,

if you are changing a user default within your view model, just execute objectWillChange.send() to get the view to redraw.

if you are changing a user default outside your view model and outside its associated view (maybe you are changing a user default in another view, perhaps in another tab, and your view model needs to know about that change), i have used this idea.

use NotificationCenter so that whenever you change UserDefaults directly, you agree to also post a notification that you have done this.

NotificationCenter.default.post(name: Notification.Name(rawValue: "someUserPreferenceChanged"), object: nil)

your view model can then listen for such a notification and respond by executing objectWillChange in response, which should get its associated view to redraw.

so, in your VM, you'd need three things:

// 1.
import Combine

// 2.  a variable to hold a subscription
private var cancellables = Set<AnyCancellable>()

// 3.  sign up to process notifications when you initialize the view model
        NotificationCenter.default.publisher(for: Notification.Name(rawValue: "someUserPreferenceChanged"))
            .sink { _ in self.objectWillChange.send() }
      .store(in: &cancellables)

hope that helps,

DMG

3      

Is there any way to force ContentView to refresh when a Settings is updated?

Just use a @Published property in your view model. You can add a didSet observer to save it out to UserDefaults when it changes.

So something like this:

class TestSettings: ObservableObject {
    @Published var setting1: Bool = true {
        didSet {
            UserDefaults.standard.set(setting1, forKey: "setting1")
        }
    }

    init() {
        self.setting1 = UserDefaults.standard.bool(forKey: "setting1")
    }
}

struct TestSettingsView: View {
    @StateObject var settings = TestSettings()

    var body: some View {
        Toggle("setting1", isOn: $settings.setting1)
    }
}

3      

just execute objectWillChange.send() to get the view to redraw

Wow I did not know about this, thank you very much for the tip !

-

class TestSettings: ObservableObject {
    @Published var setting1: Bool = true {
        didSet {
            UserDefaults.standard.set(setting1, forKey: "setting1")
        }
    }

    init() {
        self.setting1 = UserDefaults.standard.bool(forKey: "setting1")
    }
}

I just came across this solution earlier, and while I think this is the best approach to this whole problem, it results in a lot of boilerplate code. It's not a problem per se, but I just wished a (custom?) property wrapper would do all of this.

3      

I just came across this solution earlier, and while I think this is the best approach to this whole problem, it results in a lot of boilerplate code. It's not a problem per se, but I just wished a (custom?) property wrapper would do all of this.

It's certainly possible to make a custom property wrapper to do just this. Wrapping UserDefaults seems to be probably the most popular example when people write up tutorials for property wrappers. It should be easy enough to roll your own, or there are numerous Swift packages on GitHub that can do it.

3      

I've seen plenty of custom property wrappers that simplified UserDefaults interactions, but none of them would work in a class in such a way that it would publish the new value to the view struct. It's basically the same problem as @AppStorage. Basically you cannot use objectWillChange.send() in property wrappers.

I've worked my mind to combine both of your solutions @delawaremathguy and @rooserboy, and found a way to remove the init() and standardize the code in didSet to reduce error risks. It still has some boilerplate though.

It works even when other views are impacted.

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

    @AppStorage("forceDarkMode") var forceDarkMode = false {
        didSet {
            objectWillChange.send()
        }
    }
}

class ContentViewModel: ObservableObject {
    func toggleUI() -> Void {
        Settings.shared.forceDarkMode.toggle()
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()
    @StateObject private var settings = Settings.shared

    var body: some View {
        VStack {
            Text("Force Dark Mode : " + Settings.shared.forceDarkMode.description)

            Button(action: viewModel.toggleUI, label: {
                Text("Tap to toggle UI style")
            })
        }
    }
}

3      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.