我该如何在SwiftUI中使用UserDefaults?

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)
  }
}
所以我有所有这些代码,我想在开关改变或滑块滑动时使用UserDefaults保存设置,并在应用程序启动时检索所有这些数据,但我不知道如何在SwiftUI中使用UserDefaults(或者一般情况下使用UserDefaults,我刚开始研究它,以便在我的SwiftUI应用程序中使用它,但我看到的所有示例都是针对UIKit的,当我尝试在SwiftUI中实现它们时,我遇到了很多错误)。

你有研究过 @EnvironmentObject 吗?它似乎是替代某些 UIKit 组件的一部分。另一个区别是与场景相关还是与应用程序相关(例如,SceneDelegate 与 AppDelegate)。至于“...UserDefaults in general...”?也许你需要一次只问一个问题?先学习 UIKitUserDefaults,然后再学习如何将其应用到 SwiftUI 中。 - user7014451
2
@dfd 这并不是一个有帮助的评论。现在对于某人来说,从推荐的技术开始制作苹果平台的应用程序是完全有效的,但由于文档不完整而迷失方向也是很正常的。我认为告诉别人去学习即将被淘汰的旧方法,以便能够按照新的推荐方式进行操作并不是一个好建议。 - Ky -
当以这种方式表达时(创建一个测试应用程序来学习),是的,那是很好的建议。我将您2019年的评论解释为建议在此项目中使用UIKit而不是SwiftUI,因为SmushyTaco尚未学习UserDefaults。顺便说一下,虽然UserDefaults的某些方面(例如它如何与Combine一起工作)在SwiftUI中非常有效,但在UIKit中几乎没有任何示例。 - Ky -
7个回答

56

caram的方法总体来说还不错,但是SmushyTaco在使用代码时遇到了很多问题。下面是一个“开箱即用”的解决方案。

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

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

2
不要忘记 import Combine,否则会出现错误。 - stardust4891
1
好的,谢谢!不过有一个问题: 我怎样才能在 SwiftUI 代码之外访问用户默认设置? 我得到了“未知属性'ObservedObject'”的错误提示,而我不想在非 UI 类中导入 SwiftUI。 - appleitung
2
我使用相同的代码,但是当我在开头设置UserDefault值时,直到结束它都是相同的。我更新了值,但是在UserDefault中没有改变。 - Ravindra_Bhati
1
使用propertyWrapper,您可以定义自己的自定义属性包装器。由于我们已经使用了UserDefault,因此无法使用额外的Published。这就是为什么我们使用willSet { objectWillChange.send() }来恢复Published行为的原因。 - Marc T.
3
你实际上需要定义“objectWillChange”吗?它会自动生成。 - Xaxxus
显示剩余3条评论

33

从 Xcode 12.0 (iOS 14.0) 开始,您可以使用 @AppStorage 属性包装器来存储以下类型的数据: Bool、Int、Double、String、URL Data。以下是用于存储字符串值的示例:

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

在这里,您正在声明具有默认值的userName属性,该值并未进入UserDefaults本身。当您首次对其进行更改时,应用程序将该值写入UserDefaults并自动使用新值更新视图。

此外,如果需要,还可以通过store参数设置自定义UserDefaults提供程序,例如:

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

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

注意:如果该值在应用程序之外更改(比如手动打开plist文件并更改值),则View将不会接收到更新。

P.S. 还有一个新的View扩展,它添加了func defaultAppStorage(_ store: UserDefaults) -> some View,可以用于更改View使用的存储方式。如果有很多@AppStorage属性并且为每个属性设置自定义存储方式很麻烦,这将非常有帮助。


1
友情提示...您在公共论坛中提到了beta软件。最好在Apple Dev论坛中讨论beta软件。 - andrewbuilder
16
我不同意。这是一个好地方,你的评论已经过时了。 - 3h4x
2
还有一种可能是使用@AppStorage与其他类型(如Array)-请参见此答案 - pawello2222
1
我要那个。谢谢!很棒的语法糖。 - Nick Rossik
我不明白在SwiftUI中如何从用户默认设置中获取给定键的内容。应该使用什么? - user5306470

26

以下代码是根据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") }
    }
}

7

首先,创建一个属性包装器,让我们可以轻松地将您的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)
        }
    }
}

然后,创建一个数据存储来保存你的设置:

import Combine
import SwiftUI

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

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

现在,请进入您的设置:

import SwiftUI

struct SettingsView : View {
    @EnvironmentObject var dataStore: DataStore

    var body: some View {
        Toggle(isOn: $settings.space) {
            Text("\(settings.space)")
        }
    }
}

1
在最终的类中,它报错说无法访问Settings,因此当我将结构体移动到该位置时,它会报错,说“通用结构体'UserDefault'要求'DataStore.Settings'符合'Decodable',通用结构体'UserDefault'要求'DataStore.Settings'符合'Encodable',初始化程序'init(key:defaultValue:)'要求'DataStore.Settings'符合'Decodable',以及初始化程序'init(key:defaultValue:)'要求'DataStore.Settings'符合'Encodable'”,并且在使用dataStore变量时,当尝试访问settings时,它表示找不到。 - SmushyTaco
@SmushyTaco,你解决了这个问题吗?我也遇到了同样的问题。当BindableObject消失时,我的代码全部崩溃了,现在我被困在尝试使用UserDefaults中的多个设置上。 - Michael Rowe

2

我很惊讶没有人写这个新方法,不管怎样,现在苹果已经迁移到了这种方法,你不需要所有旧的代码,可以像这样读写它:

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

这相当于在旧的UserDefaults中进行读/写操作。您可以像使用常规变量一样使用它。


我很惊讶没有人写出新的方法。实际上,@AppStorage已经在其他一些答案中提到了。 - Eric Aya
1
@EricAya 你是对的,它被提到了,我可能读得太快了。然而,他们加入了额外的不必要代码,这个更简洁。 - Arturo

2

如果您正在持久化一个一次性的结构体,使用属性包装器会过度,您可以将其编码为JSON。在解码时,对于无数据的情况,请使用空的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") }
    }
}

1
另一个很好的解决方案是使用非官方静态下标API的@propertyWrapper,而不是wrappedValue,这样可以简化很多代码。这是定义:
@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)
        }
    }
}

然后,您可以像这样定义设置对象:
final class Settings: ObservableObject {
  let userDefaults: UserDefaults

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

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

然而,要小心这种实现方式。用户倾向于将所有应用程序设置放入此类对象中,并在依赖于一个设置的每个视图中使用它。这可能会导致由许多不必要的 objectWillChange 通知引起的减速。 您应该通过将设置分解为许多小类来明确关注点。 @AppStorage是一个很好的本地解决方案,但缺点是它有点打破了唯一真理源范例,因为您必须为每个属性提供默认值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接