Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I use UserDefaults with SwiftUI?

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).

like image 273
SmushyTaco Avatar asked Jun 30 '19 03:06

SmushyTaco


People also ask

How do I get UserDefaults in SwiftUI?

First, create a single view iOS app using SwiftUI. Then, create a new Swift File in your Xcode project and call it UserSettings. swift. Inside the new file, implement a class called UserSettings, conforming to the ObservableObject, with one @Published String variable holding username from the UI form.

What do you use UserDefaults for?

At runtime, you use UserDefaults objects to read the defaults that your app uses from a user's defaults database. UserDefaults caches the information to avoid having to open the user's defaults database each time you need a default value.

What is UserDefaults in Swift?

The UserDefaults class looks up the value for the key that is passed to the object(forKey:) method and returns a value if a value exists for the given key. It returns nil if no value exists for the given key. The UserDefaults class also defines a number of convenience methods for retrieving values of a specific type.


4 Answers

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")
        }
    }
}
like image 171
Marc T. Avatar answered Oct 19 '22 12:10

Marc T.


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.

like image 21
SerhiiK Avatar answered Oct 19 '22 12:10

SerhiiK


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") }
    }
}
like image 25
protasm Avatar answered Oct 19 '22 12:10

protasm


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)")
        }
    }
}
like image 7
caram Avatar answered Oct 19 '22 12:10

caram