• SwiftUI ☞ @State 相关问题


    这个问题是我在开发中碰巧遇到的。

    @State

    SwiftUI 中,我们使用 @State 进行页面私有属性设置,驱动 View 的动态显示。比如使用按钮将显示的数字 +1

    struct StateView: View {
        @State private var number: Int = 10
        
        var body: some View {
            VStack {
                Text("Hello, World! number = \(number)")
                Button("+") {
                    number += 1
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    当我们想要将这个值传递给下层子View的时候,如果想回传该值(也就是可以在子View中修改值)时,使用 @Binding 承接,如果不想回传值时,直接用 let 承接即可

    struct DetailView: View {
    	let number: Int
    	// @Binding var number: Int
    }
    
    • 1
    • 2
    • 3
    • 4

    当然,当我们希望 StateView 页面和 DetailView 页面中各有一个 number,并且均可修改并互不干扰的时候,可以直接使用 @State var number: Int 这种方式接收值。

    struct DetailView: View {
    	@State var number: Int
    }
    
    • 1
    • 2
    • 3

    这种方式是能够传值的,但是却违背了 @State 文档中关于这个属性标签的说明。那么如果该标签被设为 private,那么只能使用 init 方法来对它进行设置了,这也是我最开始说的遇到的那个问题。

    struct DetailView: View {
        @State private var number: Int?
        init(_ number: Int) {
            self.number = number
        }
        
        var body: some View {
            VStack {
                Text("Hello, World! number = \(number ?? 0)")
                Button("+") {
                    (number ?? 0) += 1
                }
            }
        }
    }
    // 调用
    NavigationLink {
    	DetailView(10)
    } label: {
    	Text("detail view")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    请问:运行结果是什么?

    你以为 number10,其实它是 nil

    那为什么呢?

    原因是虽然我们在 init 中设置了 self.number = number,但在 body 被第一次求值时, number 的值是 nil

    @State 内部

    问题出在 @State 上,SwiftUI 通过 propertyWrapper 简化并模拟了普通的变量读写,但是我们必须始终牢记,@State Int 并不等同于 Int,它根本就不是一个传统意义的存储属性,这个 propertyWrapper 做的事情大体上说有三件:

    1. 为底层的存储变量 State 这个struct提供一组 gettersetter,这个 State struct 中保存了 Int 的具体数字
    2. body 被首次求值前,将 State 关联到当前 View 上,为它在堆中对应当前 View 分配一个存储位置。
    3. @State 修饰的变量设置观察,当值改变时,触发新一次的 body 求值,并刷新屏幕。

    我们可以看到的 Statepublic 的部分只有几个初始化方法和 propertyWrapper 的标准的value

    @frozen @propertyWrapper public struct State<Value> : DynamicProperty {
        public init(wrappedValue value: Value)
    
        public init(initialValue value: Value)
    
        public var wrappedValue: Value { get nonmutating set }
    
        public var projectedValue: Binding<Value> { get }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    关于nonmutating关键字,可以查看另一篇文章标红的修饰词

    对于 @State 的声明,会在当前 View 中带来一个自动生成的私有存储属性,来存储真实的 State struct 值。比如上面的 DetailView ,由于 @State 的存在,实际上相当于:

    struct DetailView: View {
    	@State private var number: Int?
    	// 自动生成
    	private var _number: State<Int?>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    DetailView2 中应该是:

    struct DetailView2: View {
    	@State private var number: Int
    	// 自动生成
    	private var _number: State<Int>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Int? 的声明在初始化时会默认赋值为 nil,让 _number 完成初始化(它的值为State>(_value: nil)),而非Optionalnumber 则需要明确的初始化值,否则需要在查找 init 方法,如果 init 方法中没有对 number 进行设置,则说明 _number 是没有完成初始化的。

    于是“为什么Int?init 中的设置无效”的问题也迎刃而解了。对于 @State 的设置,只有在body 被首次求值前或者告知在 init 方法中设置才有效。

    解决方案

    最简单的一种是:

    struct DetailView: View {
        @State private var number: Int
        init(_ number: Int) {
            self.number = number
        }
        
        var body: some View {
            VStack {
                Text("Hello, World! number = \(number)")
                Button("+") {
                    number += 1
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    当然,对于这种方法,有一个更好的设置初始值的地方,onAppear 中:

    struct StateView: View {
        @State private var number: Int = 0
        private var tempNumber: Int
        
        init(_ number: Int) {
            self.tempNumber = number
        }
        
        var body: some View {
            VStack {
                Text("Hello, World! number = \(number)")
                Button("+") {
                    number += 1
                }
            }
            .onAppear {
                number = tempNumber
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    虽然上一次页面的中 body 被求值时,DetailViewinit 方法都会将 tempNumber 设置为最新的传入值,但是在 DetailView body 中的 onAppear 只在最初出现在屏幕上时被调用一次,在拥有一定初始化逻辑的同时,避免了多次设置。

    如果一定要从外部给 @State 一个初始值,这种方式是比较推荐的方式:从外部在 initializer 中直接对 @State 进行初始化时一种反模式的做法。一方面它事实上违背了 @State 应该是纯私有状态这一假设,另一方面由于 SwiftUI 中的 View 只是一个“虚拟”的结构,而非真是的渲染对象,即使表现为同一视图,它在别的 View的body 中是可能被重复多次创建的。在初始化方法中做 @State 赋值,很可能导致已经改变的现有状态被意外覆盖,这往往不是我们想要的结果。

    总结

    对于 @State 来说,严格遵循文档所预想的使用方式,避免在 body 以外的地方获取和设置它的值,会避免不少麻烦。正确理解 @State 的工作方式和各个变化发送的时机,能让我们在迷茫是找到正确的分析方向,并最终对这些行为给出合理的解释和预测。

  • 相关阅读:
    Tomcat 源码分析 (连接器) (六)
    万行代码计划A,Code Shit Mountain, Enlarging
    传来喜讯,优维又获奖了!!!
    【力扣精选算法100道】——二进制求和
    mannose-Biotin|甘露糖-生物素|甘露糖-聚乙二醇-生物素|生物素-PEG-甘露糖
    【操作系统基础】实践部分
    算法:贪心---跳一跳
    8-8归并排序
    Challenges and Opportunities for Students
    MySQL 高级知识之使用 mysqldump 备份和恢复
  • 原文地址:https://blog.csdn.net/LiqunZhang/article/details/126055358