深入理解@State 与Struct结构的传递关系
深入理解@State 与Struct结构的传递关系

深入理解@State 与Struct结构的传递关系

本篇文章的教程主要是通过实际案例分析,学习@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仍然可以修改实例,所以可能得等下次学到相关知识点再写了。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注