在 Swift 中,UserDefaults 是一个常用的轻量级数据存储方式,用于保存用户设置或简单的数据。这些数据会保存在应用的沙盒中,并且在应用重启后仍然存在。
基础操作
保存数据
使用 set(_:forKey:) 方法将数据存储到 UserDefaults 中:
// 保存数据
UserDefaults.standard.set("John", forKey: "username") // 保存字符串
UserDefaults.standard.set(25, forKey: "age") // 保存整数
UserDefaults.standard.set(true, forKey: "isLoggedIn") // 保存布尔值
读取数据
使用 object(forKey:) 方法读取数据,或使用更具体的方法:
// 读取数据
let username = UserDefaults.standard.string(forKey: "username") // 返回 String?
let age = UserDefaults.standard.integer(forKey: "age") // 返回 Int
let isLoggedIn = UserDefaults.standard.bool(forKey: "isLoggedIn") // 返回 Bool
注意:UserDefaults.standard在读取时,需要根据存储的类型设置后缀。
比如存储的是String类型值,读取类型为:
UserDefaults.standard.string(forKey: "username")
如果存储的是Int类型值,读取类型为:
UserDefaults.standard.integer(forKey: "age")
如果读取的类型与存储的类型不一样,就会存在无法读取的情况。
删除数据
可以使用 removeObject(forKey:) 删除某个键对应的数据:
UserDefaults.standard.removeObject(forKey: "username")
存储复杂数据
如果需要保存更复杂的数据(例如数组或字典),UserDefaults 也可以支持,但数据必须是 PropertyList 类型(即 String、Data、Array、Dictionary 等)。
如果需要保存Set类型,需要先转换为Array类型。
保存和读取数组
let colors = ["Red", "Green", "Blue"]
UserDefaults.standard.set(colors, forKey: "favoriteColors")
if let savedColors = UserDefaults.standard.array(forKey: "favoriteColors") as? [String] {
print(savedColors) // 输出 ["Red", "Green", "Blue"]
}
保存和读取字典
let userDetails: [String: Any] = ["name": "John", "age": 25]
UserDefaults.standard.set(userDetails, forKey: "userDetails")
if let savedDetails = UserDefaults.standard.dictionary(forKey: "userDetails") {
print(savedDetails) // 输出 ["name": "John", "age": 25]
}
存储自定义对象
UserDefaults 不直接支持存储自定义对象。可以通过将对象编码为 Data 来实现。
自定义对象实现示例
定义一个自定义对象:
struct User: Codable {
let name: String
let age: Int
}
保存自定义对象:
let user = User(name: "Alice", age: 30)
if let encoded = try? JSONEncoder().encode(user) {
defaults.set(encoded, forKey: "currentUser")
}
读取自定义对象:
if let savedData = defaults.data(forKey: "currentUser"),
let decodedUser = try? JSONDecoder().decode(User.self, from: savedData) {
print(decodedUser.name) // 输出 "Alice"
}
自定义对象部分涉及编解码的知识,如果有精力,可以看一下《SwiftUI UserDefaults无法解码问题》。
注意事项
1、数据量限制
UserDefaults 适合存储小型数据。对于大量或频繁修改的数据,应考虑使用数据库(如 Core Data 或 SQLite)。
2、数据同步
如果在写入后需要立即同步数据到磁盘,可以调用 synchronize() 方法,但通常不推荐手动调用。UserDefaults 会自动处理同步。
defaults.synchronize() // 手动同步(不推荐)
3、线程安全
UserDefaults 是线程安全的,但建议在主线程中使用。
示例:保存用户设置
struct Settings {
static let usernameKey = "username"
static let notificationsKey = "notificationsEnabled"
}
let defaults = UserDefaults.standard
// 保存设置
defaults.set("Alice", forKey: Settings.usernameKey)
defaults.set(true, forKey: Settings.notificationsKey)
// 读取设置
let username = defaults.string(forKey: Settings.usernameKey) ?? "Guest"
let notificationsEnabled = defaults.bool(forKey: Settings.notificationsKey)
print("\(username) has notifications enabled: \(notificationsEnabled)")
总结
在实际的应用过程中,通过UserDefaults存储数据后,每次重新打开应用时,都需要读取数据,否则UserDefaults的数据不会自动加载出来,除非使用@AppStorage属性包装器,@AppStorage简化了存储和读取过程,有精力可以学习一下@AppStorage属性包装器。
@AppStorage("username") private var username: String = "Guest"
为什么重新打开应用时,需要重新读取数据?
这是因为SwiftUI并不会存储数据,当我们尝试将一个数据保存到UserDefaults时:
UserDefaults.standard.set("John", forKey: "username") // 保存字符串
数据会根据forKey把值存储起来。当我们关闭应用的时候,数据在UserDefaults中存储,而不会依据@State之类的属性包装器存储。
所以,我们需要通过下面的代码读取相关的值。
UserDefaults.standard.string
举一个例子,比如你到酒吧喝酒,就没有喝完,需要存起来(存储操作)。隔了一段时间,重新回到酒吧时,进入包间,发现酒迟迟没有上来。这是因为你没有到前台去领取你的酒(读取操作)。
这也就是为什么重新打开应用时,如果不去读取UserDefaults,数据不会重新展示出来。
相关文章
1、Userdefaults存储Set类型数据:https://fangjunyu.com/2024/12/24/userdefaults%e5%ad%98%e5%82%a8set%e7%b1%bb%e5%9e%8b%e6%95%b0%e6%8d%ae/
2、Swift使用UserDefaults保存数据:https://fangjunyu.com/2024/05/26/swift%e4%bd%bf%e7%94%a8userdefaults%e4%bf%9d%e5%ad%98%e6%95%b0%e6%8d%ae/
来时路
现在是2024年11月26日,重新更新本文。回过头来发现当初来UserDefaults的学习都这么困难,真心不易。
如果你刚开始学习SwiftUI肯定会遇到很多问题,原因在于未知的东西很多,经过一段时间的练习后,你会发现之前的问题非常简单,就像爬山一样。
起初爬到山脚可能都很艰难,山腰更是遥远。当你持之以恒的向上爬,爬得越来越高,快到山顶的时候,回过头来看之前望而却步的位置,你会发现已经哪怕是山腰,都已经离你十分遥远。
希望能够激励到你。
本文到这里已经结束。
===========完结============
下面是之前的文章内容(2024年11月26日之前的本文内容),通过排查的方式来分析为什么UserDefaults没有保存数据,留作纪念。
情景复现
这一次又遇到了一个棘手又无法解决的问题,经过长达2天的思考,才解决。
先说一下本次的问题,在Xcode项目中通过设置UserDefaults,但是关闭应用后重新打开应用,发现数据没有完成保存。
原因为,我设置的全局变量在初始化时,没有从UserDefaults中读取数据。
解决方案
ViewModel中,设置初始化变量时,除了给自身的变量赋值,还需要执行loadData()方法来加载保存的数据。
class AppData: ObservableObject {
@Published var appInfo : AppInfo
// 初始化变量
init() {
appInfo = AppInfo(currentStep: 0)
loadData() // 尝试加载保存的数据
}
...
// 保存数据到 UserDefaults
func saveData() {
UserDefaults.standard.set(appInfo.currentStep, forKey: "currentStep")
}
func loadData() {
appInfo.currentStep = UserDefaults.standard.integer(forKey: "currentStep")
}
}
在View视图中,给保存的按钮添加saveData()方法:
Button(action: {
// 按钮被点击时的操作
appData.currentStep = 1
appData.saveData()
}) {
Text(start)
}
这样,我们的数据就可以保存并调取了。
扩展知识
在解决该问题的过程中,我有将自身的应用改动了几处部分,在这里作为参考知识供各位查阅:
1、类文件拆分为Model和Model View格式
在解决过程中,发现每次关闭应用,重新打开,就是一个新的应用,因此怀疑是没有拆分为Model和Model View格式,而导致关闭应用后,对象生命周期解决,因此决定通过拆分来解决该问题。
拆分前:
class AppData: ObservableObject {
@Published var pigLetName: String = "" {
didSet { saveData() }
} // 存钱罐名称
...
}
拆分后:
1)Model文件夹下的PIgLet文件:
struct PigLet {
var pigLetName: String // 存钱罐名称
...
}
2)ViewModel文件夹下的AppData文件:
class AppData: ObservableObject {
@Published var pigLetInfo : PigLet
...
}
可以看到,拆分前,我把各参数都放到这一个类中,拆分后变成了两个文件并归纳到Model和ViewModel文件夹下,方便管理,也便于后面排查以及批量创建多个对象。
虽然拆分并没有直接解决本次问题,但也让我学习到可以更方便管理文件的途径。
2、将观察者数据绑定改为访问环境对象
也有怀疑过观察者数据绑定,导致UserDefaults无法保存数据,因此改为访问环境对象的形式,之前在学习的视频教程中有提到环境对象比观察者数据绑定更利于排查问题。
1)观察者数据绑定:
struct Welcome: View {
@ObservedObject var appData: AppData
...
#Preview {
Welcome(appData: AppData())
}
}
2)环境对象
struct Welcome: View {
@EnvironmentObject var appData: AppData
...
#Preview {
Welcome()
.environmentObject(AppData())
}
}
虽然将观察者数据绑定改为环境对象后,没有任何改变,但学会这两种全局对象的绑定也是有帮助的,不了解的可以去查阅一下相关的文章并进行学习。
这里可能还需要注意一点,那就是我们有这样的一个ViewModel文件:
class AppData: ObservableObject {
@Published var pigLetInfo : PigLet
...
}
我们在引入文件时,首先要在入口处设置@StateObject来监听这个文件,而不是用@ObservedObject监听,入口文件代码:
import SwiftUI
@main
struct pigletApp: App {
@StateObject var appData = AppData() // 声明全局变量
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData)
}
}
}
注意,我们在入口文件中设置的是@StateObject,而在后面的子视图中,如ContentView()或者Welcome()中,我们使用的是ObservedObject,示例代码:
struct initialize: View {
@ObservedObject var appData: AppData
...
}
这里我们需要了解的知识点是:
- @StateObject:用于在视图中声明和管理一个状态对象。SwiftUI 会负责对象的创建和生命周期管理,这意味着视图销毁时会自动销毁对象。@StateObject 主要用于视图的首次初始化和需要持久化状态的情况。
- @ObservedObject:用于在视图中观察一个已经存在的状态对象。它不负责对象的创建和销毁,只是观察对象的变化并根据变化更新视图。@ObservedObject 主要用于子视图中,这些子视图需要访问和观察父视图传递给它们的对象。
在 @main 标记的应用入口文件 中,我们需要声明和管理一个全局的 AppData 实例,这个实例的生命周期应该与应用程序的生命周期一致。因此,应该使用 @StateObject。
在 ContentView 中,我们只是观察 AppData 的变化,因此需要使用 @ObservedObject 来观察 AppData 的变化。
3、关闭应用后,Xcode将断开连接
在刚开始测试UserDefaults时,发现关闭应用并重新打开应用时,Xcode没有了输出,即便我在页面中设置点击按钮输出print内容,也是如此。
最开始考虑的是,是不是整个AppData对象没有了,或者是不是关闭应用时,应该把生成的AppData对象也通过UserDefaults进行保存?等一些问题,后来经过思考认为在应用关闭后,Xcode将断开连接,因此不会再有任何输出,也通过求证得出了这一结论:
当在 Xcode 中调试应用程序并将其部署到手机上时,Xcode 会通过设备上的调试工具来捕获和显示应用程序的输出。但是,一旦关闭了应用程序(通过滑动关闭或重启手机),应用程序和 Xcode 之间的调试连接就会断开。因此,当重新打开应用程序时,Xcode 将无法显示该应用程序的输出,除非重新开始调试会话。
4、调试Xcode应用
我们在调试应用排查问题时,在对应的按钮或函数等内容中要设置print输出,以便我们通过Xcode排查。
在排查的过程中,我们可以点击左侧顶部导航栏右边第一个功能
我们可以在这里点击Console看到全部到print输出,同时可以查看内容状态。
比如,我们在刚开始运行应用,在应用的视图中设置了初始化输出,这里如果没有输出初始化输出时,应用还是空白页,这说明应用进行初始化某些信息,而不是应用自身的问题。
当我们在这里看到初始化输出时,也可以在手机端/模拟器上看到我们的展示页面。
再比如刚开始安装应用时,这里显示的状态就是“Build”,因此调试过程中我们可以点击这个功能来检查相关的输出情况。
我们也可以点击左侧顶部导航栏左边第二个功能,来检查我们代码的修改记录,这里也是在本次调试中发现的有用知识点。
问题总结
以上就是本次的所有内容了,最后谈一下我的总结,在本次排查问题时,跟之前的配置Https等问题一样复杂,以至于折磨了我将近2天的时间,严重影响开发进度,但也变相的让我学习了一些知识。
后面再遇到同样问题时,首先就是通过在代码中标记输出,来检查问题。再者就是思考问题可能造成的原因和代码的逻辑,比如我们在关闭应用后,重新打开,数据丢失,这里我们应该重新通过load方法来读取UserDefaults,刚开始时也是有过这个思路的,但是我是在视图中添加的
.onAppear {
appData.load()
}
但经过当时测试发现并没有效果,然后关闭应用并重新打开时,Xcode也没有了任何输出,以至于误导了我认为UserDefaults配置错了,或者没有保存成功,又拐了回去,还把大量的时间浪费到学习UserDefaults的保存方式上。
如果这个问题迟迟无法解决,我就需要去通过其他的存储途径来保存我的数据,不能让一条路堵死。
最后问题解决并完成这一篇文章,希望能帮助其他人解决同样的问题,减少浪费更多的排查时间。