38
struct ContentView: View {
@State var settingsConfiguration: Settings
    struct Settings {
        var passwordLength: Double = 20
        var moreSpecialCharacters: Bool = false
        var specialCharacters: Bool = false
        var lowercaseLetters: Bool = true
        var uppercaseLetters: Bool = true
        var numbers: Bool = true
        var space: Bool = false
    }
  var body: some View {
    VStack {
                HStack {
                    Text("Password Length: \(Int(settingsConfiguration.passwordLength))")
                    Spacer()
                    Slider(value: $settingsConfiguration.passwordLength, from: 1, through: 512)
                }
                Toggle(isOn: $settingsConfiguration.moreSpecialCharacters) {
                    Text("More Special Characters")
                }
                Toggle(isOn: $settingsConfiguration.specialCharacters) {
                    Text("Special Characters")
                }
                Toggle(isOn: $settingsConfiguration.space) {
                    Text("Spaces")
                }
                Toggle(isOn: $settingsConfiguration.lowercaseLetters) {
                    Text("Lowercase Letters")
                }
                Toggle(isOn: $settingsConfiguration.uppercaseLetters) {
                    Text("Uppercase Letters")
                }
                Toggle(isOn: $settingsConfiguration.numbers) {
                    Text("Numbers")
                }
                Spacer()
                }
                .padding(.all)
                .frame(width: 500, height: 500)
  }
}

So I have all this code here and I want to use UserDefaults to save settings whenever a switch is changed or a slider is slid and to retrieve all this data when the app launches but I have no idea how I would go about using UserDefaults with SwiftUI (Or UserDefaults in general, I've just started looking into it so I could use it for my SwiftUI app but all the examples I see are for UIKit and when I try implementing them in SwiftUI I just run into a ton of errors).

rmaddy
  • 314,917
  • 42
  • 532
  • 579
SmushyTaco
  • 1,421
  • 2
  • 16
  • 32
  • Have you looked into `@EnvironmentObject`? Seems like it's part of the replacement for something so... `UIKit`. The other is scene-related versus app-related. (As in SceneDelegate versus AppDelegate.) As for *"...UserDefaults in general..."*? Maybe you need to ask one question at a time? Learn `UIKit` and `UserDefaults` first, then how to make it `SwiftUI`? –  Jun 30 '19 at 04:18
  • 2
    @dfd that's not really a helpful comment. It's perfectly valid for someone to start making apps for Apple platforms right now, starting with recommended technologies, and get lost since the documentation is incomplete. I don't think it's good to tell someone to go learn the old thing that's going away so they can do it their the new recommended way – Ky - Dec 21 '20 at 00:06
  • When phrased like that (create a test app to learn), yes that's wonderful advice. I read your 2019 comment as advising to use UIKit instead of SwiftUI for this project, since SmushyTaco hadn't yet learned UserDefaults. As an aside, though, there are aspects of UserDefaults (like how it works with Combine) which work very well in SwiftUI but have nearly zero examples in UIKit – Ky - Dec 21 '20 at 17:13

7 Answers7

56

The approach from caram is in general ok but there are so many problems with the code that SmushyTaco did not get it work. Below you will find an "Out of the Box" working solution.

1. UserDefaults propertyWrapper

import Foundation
import Combine

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }
    
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

2. UserSettings class

final class UserSettings: ObservableObject {

    let objectWillChange = PassthroughSubject<Void, Never>()

    @UserDefault("ShowOnStart", defaultValue: true)
    var showOnStart: Bool {
        willSet {
            objectWillChange.send()
        }
    }
}

3. SwiftUI view

struct ContentView: View {

@ObservedObject var settings = UserSettings()

var body: some View {
    VStack {
        Toggle(isOn: $settings.showOnStart) {
            Text("Show welcome text")
        }
        if settings.showOnStart{
            Text("Welcome")
        }
    }
}
Grimxn
  • 22,115
  • 10
  • 72
  • 85
Marc T.
  • 5,090
  • 1
  • 23
  • 40
  • 2
    Don't forget to `import Combine` or you will get errors. – stardust4891 Nov 13 '19 at 00:07
  • 1
    Nice, thanks! One question though: How can I access the user default outside of SwiftUI code? I'm getting `Unknown attribute 'ObservedObject'` and I don't want to `import SwiftUI` in my non-ui classes. – appleitung Jan 01 '20 at 21:23
  • 2
    i use same code but when i set UserDefault Value on beginning till end it same. i update value but it not getting change in the UserDefault. – Ravindra_Bhati Feb 26 '20 at 09:54
  • Can anyone explain why do we need Property wrapper in 1 and why can't we use @Published in 2? – AnaghSharma Jun 09 '20 at 20:03
  • 1
    With propertyWrapper you can define your own custom Property Wrapper. Since we already use the UserDefault we can not use an additional Published. Thats why we use a willSet { objectWillChange.send() } to get the Published behavior back. – Marc T. Jun 12 '20 at 16:39
  • @MarcT. Thanks for the response. I am using `@EnvironmentObject` for user settings page. And anytime I change a setting, the whole view refreshes. – AnaghSharma Jun 14 '20 at 15:42
  • It seems neither iOS 14 `@AppStorage` nor your solution works when the user defaults key is not actually bound to anything on screen, but instead used to recompute some other value that's displayed on screen. – Abhijit Sarkar Jul 19 '20 at 05:41
  • 3
    do you actually need to define "objectWillChange"? Its gets synthesized automatically. – Xaxxus Dec 08 '20 at 23:10
33

Starting from Xcode 12.0 (iOS 14.0) you can use @AppStorage property wrapper for such types: Bool, Int, Double, String, URL and Data. Here is example of usage for storing String value:

struct ContentView: View {
    
    static let userNameKey = "user_name"
    
    @AppStorage(Self.userNameKey) var userName: String = "Unnamed"
    
    var body: some View {
        VStack {
            Text(userName)
            
            Button("Change automatically ") {
                userName = "Ivor"
            }
            
            Button("Change manually") {
                UserDefaults.standard.setValue("John", forKey: Self.userNameKey)
            }
        }
    }
}

Here you are declaring userName property with default value which isn't going to the UserDefaults itself. When you first mutate it, application will write that value into the UserDefaults and automatically update the view with the new value.

Also there is possibility to set custom UserDefaults provider if needed via store parameter like this:

@AppStorage(Self.userNameKey, store: UserDefaults.shared) var userName: String = "Mike"

and

extension UserDefaults {
    static var shared: UserDefaults {
        let combined = UserDefaults.standard
        combined.addSuite(named: "group.myapp.app")
        return combined
    }
}

Notice: ff that value will change outside of the Application (let's say manually opening the plist file and changing value), View will not receive that update.

P.S. Also there is new Extension on View which adds func defaultAppStorage(_ store: UserDefaults) -> some View which allows to change the storage used for the View. This can be helpful if there are a lot of @AppStorage properties and setting custom storage to each of them is cumbersome to do.

SerhiiK
  • 781
  • 8
  • 12
  • 1
    Friendly hint... you're mentioning beta software in a public forum. Better to discuss beta software in Apple Dev Forums. – andrewbuilder Aug 05 '20 at 12:11
  • 15
    I disagree. This is good place and your comment is already outdated – 3h4x Oct 25 '20 at 03:57
  • 2
    There's also a possibility to use `@AppStorage` with other types (like `Array`) - see [this answer](https://stackoverflow.com/a/62563773/8697793) – pawello2222 Jan 09 '21 at 19:54
  • 1
    I want dat. Thanks! Great syntactic sugar. – Nick Rossik Jan 22 '21 at 18:49
  • I don't understand how to get something at a given key from user defaults in swiftui. What exactly should one use? –  Nov 17 '22 at 13:32
26

The code below adapts Mohammad Azam's excellent solution in this video:

import SwiftUI

struct ContentView: View {
    @ObservedObject var userDefaultsManager = UserDefaultsManager()

    var body: some View {
        VStack {
            Toggle(isOn: self.$userDefaultsManager.firstToggle) {
                Text("First Toggle")
            }

            Toggle(isOn: self.$userDefaultsManager.secondToggle) {
                Text("Second Toggle")
            }
        }
    }
}

class UserDefaultsManager: ObservableObject {
    @Published var firstToggle: Bool = UserDefaults.standard.bool(forKey: "firstToggle") {
        didSet { UserDefaults.standard.set(self.firstToggle, forKey: "firstToggle") }
    }

    @Published var secondToggle: Bool = UserDefaults.standard.bool(forKey: "secondToggle") {
        didSet { UserDefaults.standard.set(self.secondToggle, forKey: "secondToggle") }
    }
}
protasm
  • 1,209
  • 12
  • 20
7

First, create a property wrapper that will allow us to easily make the link between your Settings class and UserDefaults:

import Foundation

@propertyWrapper
struct UserDefault<Value: Codable> {    
    let key: String
    let defaultValue: Value

    var value: Value {
        get {
            let data = UserDefaults.standard.data(forKey: key)
            let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
            return value ?? defaultValue
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            UserDefaults.standard.set(data, forKey: key)
        }
    }
}

Then, create a data store that holds your settings:

import Combine
import SwiftUI

final class DataStore: BindableObject {
    let didChange = PassthroughSubject<DataStore, Never>()

    @UserDefault(key: "Settings", defaultValue: [])
    var settings: [Settings] {
        didSet {
            didChange.send(self)
        }
    }
}

Now, in your view, access your settings:

import SwiftUI

struct SettingsView : View {
    @EnvironmentObject var dataStore: DataStore

    var body: some View {
        Toggle(isOn: $settings.space) {
            Text("\(settings.space)")
        }
    }
}
caram
  • 1,494
  • 13
  • 21
  • 1
    In the final class it errors saying that it can't access Settings so when I move the struct in so it can it then errors saying "Generic struct 'UserDefault' requires that 'DataStore.Settings' conform to 'Decodable', Generic struct 'UserDefault' requires that 'DataStore.Settings' conform to 'Encodable', Initializer 'init(key:defaultValue:)' requires that 'DataStore.Settings' conform to 'Decodable', and Initializer 'init(key:defaultValue:)' requires that 'DataStore.Settings' conform to 'Encodable'" and when using the dataStore var when it tries accessing settings it says they can't find it. – SmushyTaco Jun 30 '19 at 17:40
  • @SmushyTaco did you get this worked out? I am having the same issue, when BindableObject went away, my code all broke, and I am now stuck trying to work with multiple settings in UserDefaults. – Michael Rowe Sep 29 '19 at 15:40
2

If you are persisting a one-off struct such that a property wrapper is overkill, you can encode it as JSON. When decoding, use an empty Data instance for the no-data case.

final class UserData: ObservableObject {
    @Published var profile: Profile? = try? JSONDecoder().decode(Profile.self, from: UserDefaults.standard.data(forKey: "profile") ?? Data()) {
        didSet { UserDefaults.standard.set(try? JSONEncoder().encode(profile), forKey: "profile") }
    }
}
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
2

I'm supriced no one wrote the new way, anyway, Apple migrated to this method now and you don't need all the old code, you can read and write to it like this:

@AppStorage("example") var example: Bool = true

that's the equivalent to read/write in the old UserDefaults. You can use it as a regular variable.

Arturo
  • 3,254
  • 2
  • 22
  • 61
  • `I'm supriced no one wrote the new way` Actually @AppStorage has been mentioned already in some of the other answers. – Eric Aya Apr 20 '22 at 17:34
  • 1
    @EricAya you are right, it was mentioned, I prob. read too fast. However they put extra unnecessary code, this is more concise – Arturo Apr 20 '22 at 17:36
1

Another great solution is to use the unofficial static subscript API of @propertyWrapper instead of the wrappedValue which simplifies a lot the code. Here is the definition:

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value

    init(wrappedValue: Value, _ key: String) {
        self.key = key
        self.defaultValue = wrappedValue
    }

    var wrappedValue: Value {
        get { fatalError("Called wrappedValue getter") }
        set { fatalError("Called wrappedValue setter") }
    }

    static subscript(
        _enclosingInstance instance: Preferences,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
    ) -> Value {
        get {
            let wrapper = instance[keyPath: storageKeyPath]
            return instance.userDefaults.value(forKey: wrapper.key) as? Value ?? wrapper.defaultValue
        }

        set {
            instance.objectWillChange.send()
            let key = instance[keyPath: storageKeyPath].key
            instance.userDefaults.set(newValue, forKey: key)
        }
    }
}

Then you can define your settings object like this:

final class Settings: ObservableObject {
  let userDefaults: UserDefaults

  init(defaults: UserDefaults = .standard) {
    userDefaults = defaults
  }

  @UserDefaults("yourKey") var yourSetting: SettingType
  ...
}

However, be careful with this kind of implementation. Users tend to put all their app settings in one of such object and use it in every view that depends on one setting. This can result in slow down caused by too many unnecessary objectWillChange notifications in many view. You should definitely separate concerns by breaking down your settings in many small classes.

The @AppStorage is a great native solution but the drawback is that is kind of break the unique source of truth paradigm as you must provide a default value for every property.

Louis Lac
  • 5,298
  • 1
  • 21
  • 36