[SwiftUI 知识碎片] 结构体和类,ForEach,绑定

934 阅读5分钟
译自 a free Hacking with Swift tutorial
更多内容欢迎关注公众号 「Swift花园」

结构体 vs 类

Swift 里有两样东西对你来说一定不陌生:结构体和类。它们都是可以让我们构建拥有属性和方法的复杂数据类型的方法,但它们的工作方式,尤其是两者间的差异,是一个 (Swift 语言设计中) 很要紧的存在。

如果你还记得的话,结构体和类之间有五个关键的差异:

  1. 类没有逐一成员构造函数;结构体默认获得逐一构造成函数。
  2. 类可以使用继承来构建功能;结构不能。
  3. 如果你复制一个类,两份拷贝都会指向相同的数据;但结构体的拷贝,其数据是各自独立的。
  4. 类可以有析构函数;结构体没有。
  5. 你可以在常量类实例里改变变量属性的值;但常量结构体实例里的属性是固定的,不管它是常量还是变量。

在 Apple 原来的编程语言 Objective-C 中,我们几乎为所有的事物使用类 —— 因为没的选,几乎所有东西都基于类来运作。

Swift 中情况不一样,我们拥有选择权,选择应该基于上面提到的那些差异中的因素。之所以说”应该“,是因为即便你看到有人不关心两者的差异,总是只用 class 或者 struct, 也并不稀奇。

选择结构体或者类取决于你和你要解决的问题。不过我希望你思考它们是如何传递你的意图的。 Donald Knuth 说过,“程序是给人读的,偶尔给计算机运行”,这句话正合我意:当别人阅读你的代码时,你的意图是否能清晰地传达给他们?

如果你大部分时候用的是结构体,然后在一个特定的地方换成了类,这就传达了一个意图: 这个东西不一样,需要不同的用法。但如果你总是使用类,这种区别就消失了 —— 毕竟,不太可能你在大多数情况下都得使用类。

提示: SwiftUI 有一个迷人的细节是它扭转了我们使用结构体和类的方式。在 UIKit 中我们针对数据使用结构体,针对 UI 使用类,但在 SwiftUI 中完全相反 —— 这给我们提了醒,学习很重要,即便你认为新知识不会立刻派上用场。

ForEach

第二个要讨论的东西是 ForEach,我们可能会像这样使用:

ForEach(0 ..< 100) { number in
    Text("Row \(number)")
}

ForEach 是一个视图,跟 SwiftUI 中绝大多数视图一样,但它使得我们可以在一个循环内创建其他视图。通过这么做,我们可以打算 SwiftUI 10 个子视图的限制——ForEach 本身 是 10 个子视图限制的目标,而非它里面的东西。

现在,来看看下面这样的字符串数组:

let agents = ["Cyril", "Lana", "Pam", "Sterling"]

我们如何遍历这些字符串,以创建文本视图呢?

一个选项是用我们已经有的构建方式:

VStack {
    ForEach(0 ..< agents.count) {
        Text(self.agents[$0])
    }
}

不过 SwiftUI 提供了第二种选择:我们可以直接遍历数组。这种方式需要多费点思考,因为 SwiftUI 需要知道如何识别数组中的每一项。

思考一下:如果我们遍历一个 4 个元素的数组,我们会创建 4 个视图,但是如果 body 重新调用,我们的数组现在包含 5 个元素了,SwiftUI 需要知道哪个视图是新的以便展示它。 SwiftUI 最不愿意做的事情:每当一个小改变发生时,丢弃整个布局,从头开始。相反,它希望做尽可能少的工作 —— 它希望保持已经存在的 4 个视图,只添加第 5 个。

因此,让我们回到 Swift 识别数组中元素的地方。当我们用诸如 0 ..< 5 或者 0 ..< agents.count 这样的范围时,Swift 已经确信每个元素都是唯一的,因为每个元素在循环中都只使用一次,所以一定是唯一的。

而在我们的字符串数组中,这一点变得不可能。我们无法清晰地确信每个值是唯一的: 它要求 ["Cyril", "Lana", "Pam", "Sterling"] 不重复。因此,我们能做的是把字符串本身告诉 SwiftUI —— “Cyril” ,“Lana” ,等等 —— 它们是用来在循环中唯一标识每个视图的东西。

代码是这样的:

VStack {
    ForEach(agents, id: \.self) {
        Text($0)
    }
}

相比遍历数字,然后再作为下标访问数组,我们现在是直接读取数组,就像 for 循环那样做。

随着你对 SwiftUI 的精进,我们会看到第三种标识视图的方式,它是用 Identifiable协议,不过我们之后再来讨论。

绑定

当我们使用像 PickerTextField 这样的控件时,我们通过属性前注解 @State 为它们添加双向绑定。对于简单属性来说,这种用法很奏效。不过有的时候 —— 希望只是偶尔 —— 你可能需要更高级的东西:假如你的当前数值需要通过运行代码逻辑来计算呢?或者当数值被写入时,你想的事情不局限于存储呢?

对于绑定中的改变,如果我们需要做出反应,我们可能需要借助 Swift 的didSet 属性观察者。不过你可能要失望了。自定义绑定的工作方式是这样的:自定义绑定跟 @State绑定一模一样,除了我们拥有完全掌控而已。

绑定并非魔法, @State 只是帮我们拿掉了一些无聊的样本代码,如果你愿意,自行创建和管理绑定代码完全没问题。重申一下,演示下面的写法并非它常见,实际上它并不常见,我希望你也别这么写。相反,我演示下面这样的写法是为了消除你可能认为 SwiftUI 在你的代码上施了什么魔法的念头。

SwiftUI 为我们做的事情,我们自己手动也能做到。尽管依赖自动方案总是更好。下面的写法只是帮我们理解行为下面发生了什么。

先来看下自定义绑定的最简单形式,它存了另外一个 @State 属性的值:

struct ContentView: View {
    @State var selection = 0

    var body: some View {
        let binding = Binding(
            get: { self.selection },
            set: { self.selection = $0 }
        )

        return VStack {
            Picker("Select a number", selection: binding) {
                ForEach(0 ..< 3) {
                    Text("Item \($0)")
                }
            }.pickerStyle(SegmentedPickerStyle())
        }
    }
}

所以,这里的绑定扮演的角色是透传 —— 它自己实际上并不存储或者计算任何数据,只是充当我们的 UI 和下面的状态值之间的一个 ”夹片“ 。

不过,注意一下,现在 picker 是通过 selection: binding 创建,不再需要 $ 符号了。 我们并不需要显式要求双向绑定,因为它本身已经是了。

如果我们愿意,还可以创建更高级的绑定,不仅仅是透传一个数值。举个例子,想象我们有一个表单,里面有三个开关:用户同意条款,用户同意隐式政策,用户同意接收邮件。

我们可能用三个布尔型的 @State 属性表示它们:

@State var agreedToTerms = false
@State var agreedToPrivacyPolicy = false
@State var agreedToEmails = false

虽然用户是逐个触发它们的,我们可以用一个自定义绑定来实现它们。这个绑定只有在三个布尔值都为 true 时才为 true ,像这样:

let agreedToAll = Binding(
    get: {
        self.agreedToTerms && self.agreedToPrivacyPolicy && self.agreedToEmails
    },
    set: {
        self.agreedToTerms = $0
        self.agreedToPrivacyPolicy = $0
        self.agreedToEmails = $0
    }
)

现在我们还可以做四个开关的实现:每个独立布尔值一个,一个同意或者不同意的总开关:

struct ContentView: View {
    @State var agreedToTerms = false
    @State var agreedToPrivacyPolicy = false
    @State var agreedToEmails = false

    var body: some View {
        let agreedToAll = Binding<Bool>(
            get: {
                self.agreedToTerms && self.agreedToPrivacyPolicy && self.agreedToEmails
            },
            set: {
                self.agreedToTerms = $0
                self.agreedToPrivacyPolicy = $0
                self.agreedToEmails = $0
            }
        )

        return VStack {
            Toggle(isOn: $agreedToTerms) {
                Text("Agree to terms")
            }

            Toggle(isOn: $agreedToPrivacyPolicy) {
                Text("Agree to privacy policy")
            }

            Toggle(isOn: $agreedToEmails) {
                Text("Agree to receive shipping emails")
            }

            Toggle(isOn: agreedToAll) {
                Text("Agree to all")
            }
        }
    }
}

再次重申,自定义绑定不是你会希望经常用到的东西。尽管 Swift 惊人的聪明,但它不是魔法。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~