简体   繁体   English

如何在 SwiftUI 中使用 UserDefaults?

[英]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).所以我在这里有所有这些代码,我想在切换开关或滑动滑块时使用 UserDefaults 保存设置,并在应用程序启动时检索所有这些数据,但我不知道如何将 UserDefaults 与 SwiftUI 一起使用(或者一般的 UserDefaults,我刚刚开始研究它,所以我可以将它用于我的 SwiftUI 应用程序,但我看到的所有示例都是针对 UIKit 的,当我尝试在 SwiftUI 中实现它们时,我遇到了大量错误)。

The approach from caram is in general ok but there are so many problems with the code that SmushyTaco did not get it work. caram 的方法总体上还可以,但是代码存在很多问题,SmushyTaco 没有让它工作。 Below you will find an "Out of the Box" working solution.您将在下面找到一个“开箱即用”的工作解决方案。

1. UserDefaults propertyWrapper 1. UserDefaults 属性包装器

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 2. 用户设置类

final class UserSettings: ObservableObject {

    let objectWillChange = PassthroughSubject<Void, Never>()

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

3. SwiftUI view 3. SwiftUI 视图

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")
        }
    }
}

Starting from Xcode 12.0 (iOS 14.0) you can use @AppStorage property wrapper for such types: Bool, Int, Double, String, URL and Data .从 Xcode 12.0 (iOS 14.0) 开始,您可以对以下类型使用@AppStorage属性包装器: Bool, Int, Double, String, URLData 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.在这里,您使用默认值声明userName属性,该默认值不会进入UserDefaults本身。 When you first mutate it, application will write that value into the UserDefaults and automatically update the view with the new value.当你第一次改变它时,应用程序会将该值写入UserDefaults并使用新值自动更新视图。

Also there is possibility to set custom UserDefaults provider if needed via store parameter like this:如果需要,还可以通过如下store参数设置自定义UserDefaults提供程序:

@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.注意:如果该值将在应用程序之外更改(假设手动打开 plist 文件并更改值),View 将不会收到该更新。

PS 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. PS 还有一个新的View扩展,它添加了func defaultAppStorage(_ store: UserDefaults) -> some 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.如果有很多@AppStorage属性并且为每个属性设置自定义存储很麻烦,这会很有帮助。

The code below adapts Mohammad Azam's excellent solution in this video :下面的代码改编自 Mohammad Azam 在此视频中的出色解决方案:

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") }
    }
}

First, create a property wrapper that will allow us to easily make the link between your Settings class and UserDefaults:首先,创建一个属性包装器,让我们可以轻松地在您的 Settings 类和 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)")
        }
    }
}

If you are persisting a one-off struct such that a property wrapper is overkill, you can encode it as JSON.如果您要持久化一次性结构,以致属性包装器过于繁琐,您可以将其编码为 JSON。 When decoding, use an empty Data instance for the no-data case.解码时,对无数据情况使用空的Data实例。

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") }
    }
}

Another great solution is to use the unofficial static subscript API of @propertyWrapper instead of the wrappedValue which simplifies a lot the code.另一个很好的解决方案是使用wrappedValue @propertyWrapper这大大简化了代码。 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.这可能会导致在许多视图中由于过多不必要的objectWillChange通知而导致速度变慢。 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. @AppStorage是一个很棒的原生解决方案,但缺点是它打破了唯一的真实来源范式,因为您必须为每个属性提供默认值。

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 .这相当于旧UserDefaults中的读/写。 You can use it as a regular variable.您可以将其用作常规变量。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM