Reputation: 1541
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).
Upvotes: 39
Views: 23026
Reputation: 4180
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.
Upvotes: 3
Reputation: 6406
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.
Upvotes: 1
Reputation: 5320
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.
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)
}
}
}
final class UserSettings: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
@UserDefault("ShowOnStart", defaultValue: true)
var showOnStart: Bool {
willSet {
objectWillChange.send()
}
}
}
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")
}
}
}
Upvotes: 57
Reputation: 861
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.
Upvotes: 38
Reputation: 41648
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") }
}
}
Upvotes: 2
Reputation: 1297
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") }
}
}
Upvotes: 28
Reputation: 1719
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)")
}
}
}
Upvotes: 7