Tao
Tao

Swift UserDefaults 完全指南:从基础到进阶实践

UserDefaults 是 Swift 中一个主要用于存储少量持久性数据的 API,例如用户偏好设置和应用程序设置。它提供了一个接口,用于访问用户的默认设置数据,将键值对持久化存储,确保数据在应用重新启动后仍然存在。


UserDefaults 通常用于存储基本数据类型,包括 StringIntBoolArrayDictionary。从底层来看,它支持属性列表(property list)格式的数据,例如 NSStringNSNumberNSDataNSArrayNSDictionary

UserDefaults 不直接支持存储自定义对象。但是,通过利用 Swift 的 Codable 协议(它是 EncodableDecodable 协议的结合),可以轻松地实现自定义对象的存储和检索。

  • 存储过程:必须使用 JSONEncoder 将自定义对象实例编码Data 对象。然后将这个 Data 对象存储到 UserDefaults 中,并指定一个键(key)。
  • 检索过程:首先使用相同的键从 UserDefaults 中检索出 Data,然后使用 JSONDecoder 将其解码回原始的自定义对象。
  • 自定义对象的使用场景包括用户配置文件(如包含 nameageemailUser 结构体)或应用配置。

UserDefaults 中的数据存储在设备文件系统中的属性列表(.plist)文件里。对于沙盒化进程,这些文件通常位于应用程序容器文件夹的 Library/Preferences 目录中。

在使用 UserDefaults 时,有两项重要的限制和安全准则需要遵守:

  • 避免存储敏感数据:绝对不应该使用 UserDefaults 来存储敏感数据,如密码、API 密钥、敏感令牌、加密密钥或内购状态。这是因为这些数据是以未加密的形式存储在设备文件系统上的,攻击者可以轻易访问该文件,导致严重的安全漏洞(CWE-311)。对于此类敏感信息,应该使用系统提供的密钥链(Keychain,这是一个加密的安全数据库。
  • 限制数据大小UserDefaults 仅适用于少量轻量级数据。应避免存储大型对象、复杂结构或图像数据。这是因为整个 plist 文件会在应用程序启动时被加载到内存中,存储过多数据会显著影响应用性能。

以下是一个完整的 SwiftUI 示例,展示了如何使用 @AppStorage 属性包装器与 UserDefaults 交互:

swift

import SwiftUI

struct ContentView: View {
    // MARK: - @AppStorage 定义
    // 语法: @AppStorage("UserDefaults中的Key") var 变量名 = 默认值
    
    @AppStorage("username") private var username: String = "Guest"
    @AppStorage("isDarkMode") private var isDarkMode: Bool = false
    @AppStorage("volumeLevel") private var volumeLevel: Double = 0.5
    @AppStorage("loginCount") private var loginCount: Int = 0

    var body: some View {
        NavigationView {
            Form {
                // Section 1: 用户信息 (String)
                Section(header: Text("个人信息")) {
                    HStack {
                        Image(systemName: "person.circle.fill")
                            .foregroundColor(.blue)
                            .font(.largeTitle)
                        
                        VStack(alignment: .leading) {
                            Text("欢迎, \(username)!")
                                .font(.headline)
                            Text("登录次数: \(loginCount)")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                    }
                    
                    TextField("修改昵称", text: $username)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                }

                // Section 2: 设置 (Bool & Double)
                Section(header: Text("应用设置")) {
                    // Toggle 直接绑定到 isDarkMode,切换时自动保存
                    Toggle(isOn: $isDarkMode) {
                        HStack {
                            Image(systemName: "moon.fill")
                                .foregroundColor(isDarkMode ? .purple : .gray)
                            Text("暗黑模式")
                        }
                    }
                    
                    VStack(alignment: .leading) {
                        Text("音量: \(Int(volumeLevel * 100))%")
                        Slider(value: $volumeLevel, in: 0...1)
                    }
                }
                
                // Section 3: 操作 (Int)
                Section {
                    Button("增加登录计数 (测试数据持久化)") {
                        loginCount += 1
                    }
                    
                    Button("重置所有设置") {
                        resetSettings()
                    }
                    .foregroundColor(.red)
                }
            }
            .navigationTitle("UserDefaults Demo")
        }
        // 根据存储的值动态改变整个 App 的外观
        .preferredColorScheme(isDarkMode ? .dark : .light)
    }
    
    // 重置逻辑
    func resetSettings() {
        // 这里可以直接修改变量,AppStorage 会自动同步到 UserDefaults
        username = "Guest"
        isDarkMode = false
        volumeLevel = 0.5
        loginCount = 0
        
        // 或者使用传统的删除方法(彻底移除 Key)
        // if let bundleID = Bundle.main.bundleIdentifier {
        //     UserDefaults.standard.removePersistentDomain(forName: bundleID)
        // }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

在 SwiftUI 中,@AppStorage 是一个属性包装器,它简化了与 UserDefaults 的交互。使用 @AppStorage 可以轻松地存储和检索全局应用程序数据(如用户偏好),并且当底层 UserDefaults 中的值发生变化时,它会自动更新关联的视图。

  • 删除对象:要删除 UserDefaults 中存储的键值对,可以使用 removeObject(forKey:) 方法。
  • 同步:历史上曾使用 synchronize() 方法来强制数据立即持久化到磁盘,但此方法现在已不推荐使用,并且被认为是不必要的。系统会自动处理异步更新。

UserDefaults 就像一个轻量级的存储,非常适合记录应用程序的设置和简单状态。但因为它未加密且容量有限,绝对不能把它当成保险箱(存储敏感数据,应使用 Keychain)或图书馆(存储大量复杂数据,应使用 Core Data 或 FileManager)。