Share settings from SwiftUI in-app settings view to other views

In SwiftUI we can use @AppStorage to save app settings. In my app, I have a settings view which allows the user to change various settings. Since there are many of them, it's not practical to declare a binding for each setting between the settings view and whatever other views effectively use that setting.

Is there a more convenient way to store a setting in one view and access it in another view?

Answered by DTS Engineer in 790811022

I'll add to @Claude31 suggestions,

Since @AppStorage reflects values from UserDefaults and User Defaults supports types such as Data, Arrays, Dictionaries which might be more appropriate.

Using binary data as your type allows you store and manage multiple settings in a single Settings struct. For example:

struct Settings: Codable {
    var isLoggedIn: Bool
    var isActive: Bool
    ...
    
}

You could easily convert to and from binary data:

struct Settings: Codable {
    var isLoggedIn: Bool
    var isActive: Bool

    func toBinaryData() -> Data? {
        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(self)
            return data
        } catch {
            print("Failed to encode Settings to binary data: \(error)")
            return nil
        }
    }

    static func fromBinaryData(_ data: Data) -> Settings? {
        let decoder = JSONDecoder()
        do {
            let settings = try decoder.decode(Settings.self, from: data)
            return settings
        } catch {
            print("Failed to decode binary data to Settings: \(error)")
            return nil
        }
    }
}

You could then convertSettings struct to binary data when the scenePhase changes or if the value changes:

struct ContentView: View {
    @AppStorage("Keyvalue") var settingsBinaryData: Data?
    @State private var appSettings: Settings = Settings(isLoggedIn: false, isActive: true)
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        VStack {
            Toggle(isOn: $appSettings.isLoggedIn) {
                Text("Is Logged In")
            }
            .padding()
            
            Toggle(isOn: $appSettings.isActive) {
                Text("User is active")
            }
            .padding()
        }
        .onChange(of: scenePhase) { _, phase in
            if phase == .inactive {
                if let settingsData = appSettings.toBinaryData() {
                    settingsBinaryData = settingsData
                }
            } else if phase == .active {
                if let binaryData = settingsBinaryData, let settings = Settings.fromBinaryData(binaryData) {
                    appSettings = settings
                }
            }
        }
    }
}

Keep in mind that UserDefaults isn't suitable for storing large data, so you might want to consider alternatives such as: File Storage or Core Data.

There are several options:

  • use environment var, in a class that groups all the settings
  • AppStorage may be a good option. However, if you have a lot of var, that becomes tedious.

How many var have you, of which type ?

An idea here is to multiplex the var.

  • imagine you have 8 Int var, which value is in fact between 0 and 255.
  • Then you can create a function to multiplex them:
var1 + 256*var2 * 256*256*var3 etc
  • and a demux function, using a succession of % and DIV to get each var back

The best would be you show some code so that we can be more specific.

I have more than 10 variables, of different types. Your suggestion of multiplexing doesn't work in this case.

I have something like this, where I want to bind ContentView.var1 to SettingsView.var1, for each of the var listed in SettingsView, and possibly to other views as well.

struct ContentView: View {
    var body: some View {
        VStack {
            if var1 {
                Text("Text")
            }
        }
        SettingsView()
    }
}

struct SettingsView: View {
    @AppStorage("var1") private var var1 = false
    @AppStorage("var2") private var var2 = true
    @AppStorage("var3") private var var3 = false
    @AppStorage("var4") private var var4 = MyType.Nested.value
    @AppStorage("var5") private var var5: Double?
    @AppStorage("var6") private var var6 = 0.0
    @AppStorage("var7") private var var7 = 0.0
    @AppStorage("var8") private var var8: String?
    @AppStorage("var9") private var var9: Data?
    @AppStorage("var10") private var var10 = true
    @AppStorage("var11") private var var11 = 0
    
    var body: some View {
        Toggle("Var 1", isOn: $var1)
        ...
    }
}
Accepted Answer

I'll add to @Claude31 suggestions,

Since @AppStorage reflects values from UserDefaults and User Defaults supports types such as Data, Arrays, Dictionaries which might be more appropriate.

Using binary data as your type allows you store and manage multiple settings in a single Settings struct. For example:

struct Settings: Codable {
    var isLoggedIn: Bool
    var isActive: Bool
    ...
    
}

You could easily convert to and from binary data:

struct Settings: Codable {
    var isLoggedIn: Bool
    var isActive: Bool

    func toBinaryData() -> Data? {
        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(self)
            return data
        } catch {
            print("Failed to encode Settings to binary data: \(error)")
            return nil
        }
    }

    static func fromBinaryData(_ data: Data) -> Settings? {
        let decoder = JSONDecoder()
        do {
            let settings = try decoder.decode(Settings.self, from: data)
            return settings
        } catch {
            print("Failed to decode binary data to Settings: \(error)")
            return nil
        }
    }
}

You could then convertSettings struct to binary data when the scenePhase changes or if the value changes:

struct ContentView: View {
    @AppStorage("Keyvalue") var settingsBinaryData: Data?
    @State private var appSettings: Settings = Settings(isLoggedIn: false, isActive: true)
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        VStack {
            Toggle(isOn: $appSettings.isLoggedIn) {
                Text("Is Logged In")
            }
            .padding()
            
            Toggle(isOn: $appSettings.isActive) {
                Text("User is active")
            }
            .padding()
        }
        .onChange(of: scenePhase) { _, phase in
            if phase == .inactive {
                if let settingsData = appSettings.toBinaryData() {
                    settingsBinaryData = settingsData
                }
            } else if phase == .active {
                if let binaryData = settingsBinaryData, let settings = Settings.fromBinaryData(binaryData) {
                    appSettings = settings
                }
            }
        }
    }
}

Keep in mind that UserDefaults isn't suitable for storing large data, so you might want to consider alternatives such as: File Storage or Core Data.

Share settings from SwiftUI in-app settings view to other views
 
 
Q