GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

SwiftUI 2023 : Is @AppStorage the elephant in the room ? What's the solution now if you go the MVVM way !?

Forums > SwiftUI

So SwiftUI in 2023 makes it easier than ever to build your app using the MVVM architecture, thanks to @Observation and @Environment. But at the same time, it did not address how to save the app state, which has always been the elephant in the room with SwiftUI. Previously, I could use @AppStorage in @ObservableObject classes, now we can't, but it never really worked well though. It would fail intermittently.

We can use @AppStorage in our Views and it works, but IMO we need to change these variables in the ViewModels, otherwise it breaks the concept of MVVM. Also, I may want to access some user settings it in the ViewModel because it might change the logic of some functions.

How would you guys do to save the app state in an iOS App, in order to have the same settings back even after killing and relaunching the app ? So far I made this, and it works, but it's quite a lot of boiler plate code and I don't even know if making a macro is possible :

import SwiftUI
import Observation

enum AppStorageKeys: String {
    case forceDarkMode
}

@Observable class ContentViewModel {
    var forceDarkMode: Bool = UserDefaults.standard.bool(forKey: AppStorageKeys.forceDarkMode.rawValue)
}

struct ContentView: View {
    var viewModel = ContentViewModel()

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

            Button { viewModel.forceDarkMode.toggle() } label: {
                Text("Tap to toggle UI style")
            }
        }
        .onChange(of: viewModel.forceDarkMode) { _, newValue in
            UserDefaults.standard.set(newValue, forKey: AppStorageKeys.forceDarkMode.rawValue)
        }
    }
}

2      

I also noticed that this line works on @ObservableObject classes but not on @Observable : objectWillChange.send()

Is this normal behavior ?

2      

@Observable doesn't use Combine under the hood the way @ObservableObject does. That's why objectWillChange.send() doesn't work.

2      

How come doesn't this work ? I believe we could do a macro with it, but the code doesn't even compile.

Error : Cannot find 'newValue' in scope

@Observable class ContentViewModel {
    var forceDarkMode: Bool = UserDefaults.standard.bool(forKey: AppStorageKeys.forceDarkMode.rawValue) {
        didSet {
            UserDefaults.standard.set(newValue, forKey: AppStorageKeys.forceDarkMode.rawValue)
        }
    }
}

2      

Apparently, the only way to get this work is this, and since the same class encapsulates everything, it would be possible to make a macro :

@Observable
class ContentViewModel {
    var forceDarkMode: Bool {
        get {
            access(keyPath: \.forceDarkMode)
            return UserDefaults.standard.bool(forKey: AppStorageKeys.forceDarkMode.rawValue)
        }

        set {
            withMutation(keyPath: \.forceDarkMode) {
                UserDefaults.standard.setValue(newValue, forKey: AppStorageKeys.forceDarkMode.rawValue)
            }
        }
    }
}

2      

Thanks for the info, I appreciate it.

2      

struct ContentView: View {
    var viewModel = ContentViewModel()

    var body: some View {

Unfortunately the code above is a memory leak because View structs have no lifetime so the object is lost and re-init over and over again every time ContentView is init. Instead of attempting to shoe-horn your view data into classes it is better to embrace the SwiftUI architecture and keep your view data in the View struct hierarchy. In Data Essentials in SwiftUI at WWDC 2020 at 20:50 they said "Views are very cheap, we encourage you to make them your primary encapsulation mechanism". In other videos they explain that View structs arent actually the view layer at all, SwiftUI diffs the structs after every change and then uses the diff to init/update/deinit UIKit objects, hence the View structs are actually a view model already, thus SwiftUI's design is MVVM already so it doesn't really make sense to layer your own view data objects on top. Especially since the whole point of structs is to eliminate the kind of consistency bugs typical of objects and their shared mutable state. You can still have seperation and testability in SwiftUI without objects, you can group related state vars in a custom @State struct and use mutating func for logic. You can also extract your logic into a testable func that takes params and returns a result. That way your mutable state can be both in an @State in the view struct and in a var in your test and the problem is solved!

Regarding @AppStorage, it's best to use it as a source of truth in highest common parent in the hierarchy, pass it down as a let for read access or binding for read/write, use onChange for external actions. If you need deep access you can put its value or its binding in the Environment. Exactly the same as you would do for @State values.

   

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until February 9th.

Click to save your free spot now

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

Reply to this topic…

You need to create an account or log in to reply.

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.