情景复现
这一次又遇到了一个棘手又无法解决的问题,经过长达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的保存方式上。
如果这个问题迟迟无法解决,我就需要去通过其他的存储途径来保存我的数据,不能让一条路堵死。
最后问题解决并完成这一篇文章,希望能帮助其他人解决同样的问题,减少浪费更多的排查时间。