本篇文章的教程主要是通过实际案例分析,学习@State在Struct结构中使用的注意事项,以及如何修改现有的实例参数。
问题切入
开发iOS应用《汇率仓库》的时候,发现汇率仓库的同步时间是10月16日的内容,而我通过调取json文件,发现返回的是今天(10月17日)的数据。
代码结构为:
struct UpdateER {
@State var updateERInfo: ForexDataStruct = loadForexData()
static func loadForexData() -> ForexDataStruct {
...
}
func update() async {
print("进入update函数")
...
do {
...
let decodedResponse = try JSONDecoder().decode(ForexDataStruct.self, from: data)
updateERInfo = decodedResponse
print("获取数据成功")
} catch {
print("获取数据失败")
}
}
}
UpdateER是我用来从网络上获取、加载汇率的一个结构。
其中loadForexData()方法是加载本地的一个之前下载的汇率JSON文件,将这个文件存储起来,以便无法通过互联网下载汇率JSON文件时,使用默认的JSON文件。
update()方法是从互联网上下面最新的汇率JOSN文件,同时我在这个update()方法中设置了返回的JSON文件,按照格式解码后赋值给updatERInfo变量。
所以正常情况下,使用update()方法,updateERInfo变量存储的应该是最新的JSON数据,如果update()方法同步失败,则默认使用本地的JSON文件。(当然,这里还应该增加一个最新下载的文件替换本地的文件,让汇率一直保持比较新的日期,这是后话了。)
下面是文件视图部分代码:
struct ContentView: View {
@State private var updateER = UpdateER()
var body: some View {
TabView(selection: $selectedTab) {
HomeView(updateER: $updateER)
...
}
.task {
print("进入task")
await updateER.update()
}
}
}
在ContenView视图结构当中,我定义了一个updateER实例。然后将updatER实例传递给HomeView视图,HomeView视图就是前面看到的页面,同时设置了一个task异步执行updateER.update()的代码。
struct HomeView: View {
@Binding var updateER: UpdateER
Button(action: {
Task {
await updateER.update()
}
})
}
HomeViwe视图的按钮,就是异步调取updateER.update()方法,以便更新汇率数据,这里的汇率数据是下图中右下角的汇率同步时间。
前文都已经介绍清楚了,接着我们启动程序查看一下问题。
通过该截图可以看到,日期还是2024年10月16日,而Xcode输出最新的汇率JSON字符串对应的时间格式为2024年10月17日:
data":{"lastDateEn":"17/10/2024 9:15","lastDate":"2024-10-17...}
因此,问题为我们update()方法调取成功,但是并没有覆盖到当前的实例。也就是说我们的update()方法在调取成功后,默认是会将解码的数据赋值给我们的updateERInfo属性。
如果赋值成功的话,因为我们的View视图读取的也是updateR的时间字段,按理来说应该是同步更新的。
Text("\(updateER.updateERInfo.data?.lastDate ?? "N/A")")
排查问题
经过排查发现,实际问题在于@State仅限于在SwiftUI视图内部直接使用,不能从结构体内部修改。
@State是SwiftUI特有的属性包装器,用于在视图层管理状态,意味着Swift UI会管理@Stat的值,例如自动检测变化和触发视图更新等。
struct UpdateER {
@State var updateERInfo: ForexDataStruct = loadForexData()
...
func update() async {
do {
...
let decodedResponse = try JSONDecoder().decode(ForexDataStruct.self, from: data)
updateERInfo = decodedResponse
print("updateERInfo:\(updateERInfo)")
}
}
}
在UpdateER结构中,我们将从互联网获取到的汇率JSON文件复制给updateERInfo参数。实际上Struct结构不会直接修改这个属性的底层存储值(即@State本身的状态),而是会修改一个Struct实例的副本。
所以当我们尝试通过给updateERInfo赋值,实际是给一个副本赋值。
updateERInfo = decodedResponse
这里我们可以通过添加注释来查看是否赋值完成。
let decodedResponse = try JSONDecoder().decode(ForexDataStruct.self, from: data)
// decodedResponse 为解码的数据
print("decodedResponse:\(decodedResponse.data?.lastDate ?? "N/A")")
updateERInfo = decodedResponse
// updateERInfo 为Struct结构的数据
print("updateERInfo:\(updateERInfo.data?.lastDate ?? "N/A")")
最终Xcode输出内容为:
decodedResponse:2024-10-17 9:15
updateERInfo:2024-10-16 9:15
因此,可以判定我们虽然有赋值语句,但实际上并未修改updateERInfo参数信息。
同时,我们使用updateER.update()修改副本时,这个副本只在方法内部使用,方法执行完成后就会被丢弃。
即使我们将update()声明为mutating,仍然无法修改实例本身:
mutating func update() async { }
最终结果就是,当我们把updateERInfo声明为@State时,它的更新由SwiftUI视图的声明周期来管理。@State本质上时一种引用,在SwiftUI内部使用特殊机制,确保状态变更能够触发视图更新。
当我们直接在Struct内部修改@State时,Swift并不会真正修改它管理的状态,而是修改一个独立的副本,并且SwiftUI无法检测到。
因此,我们应该移除updateERInfo的@State包装器:
struct UpdateER {
var updateERInfo: ForexDataStruct = loadForexData()
...
}
当移除@State属性包装器后,Xcode会提示赋值代码就会报错,并提示我们使用“mutating”关键词来修改update()方法。
赋值代码:
updateERInfo = decodedResponse
报错内容为:
Cannot assign to property: 'self' is immutable
Mark method 'mutating' to make 'self' mutable
当我们移除@State属性以及设置同步函数为“mutating”关键词后,我们的代码恢复正常,并可以修改实例的参数信息。
总结
本篇文章实际也是Struct结构的值传递有关,因为 Struct是值类型,所以传递时会创建副本。如果使用Class进行传递数据,可能会更好一些?对于Struct和Class传递数据问题,可能还需要进一步研究并分析相关的知识文章。
最后补充一点,我将UpdateER结构改为Class类型,updateERInfo参数仍然使用@State属性包装器,运行模拟器发现,仍然存在无法赋值的情况。因此,即使修改为Class,也会因为@State属性包装器的影响导致无法赋值。
class UpdateER {
@State var updateERInfo: ForexDataStruct = loadForexData()
...
}
当我将Class添加@Observable属性包装器后,Xcode显示对应的报错信息:
@Observable
class UpdateER {
@State var updateERInfo: ForexDataStruct = loadForexData()
...
}
报错信息为:
Property wrapper cannot be applied to a computed property
这个报错表示属性包装器不能用于计算属性,所以Class还是会检测这一问题的。
最后,原本还计划进一步学习@Published和ObservableObject,现在看来使用Struct仍然可以修改实例,所以可能得等下次学到相关知识点再写了。