Hacking with iOS: SwiftUI Edition 里程碑(一)之 Key points

Key points – 要点

有三个要点值得更详细地讨论。这在一定程度上是在回顾我们所学到的东西——用不同的例子重新审视一遍,以确保它们是清楚的——但我也希望借此机会回答一些迄今可能出现的问题。

Structs vs Classes – 结构体 VS 类

首先,有一些你应该记忆犹新的东西:结构体和类。这两种方法都可以让我们使用属性和方法构建复杂的数据类型,但它们的工作方式(更具体地说,它们的不同之处)很重要。

如果您还记得,结构体和类之间有五个关键区别:

1.结构体默认有一个自动实现的成员初始化器类没有需要自己实现。 2.类可以使用继承来构建功能;结构不能。 3.如果复制一个类,两个副本都指向同一个数据;结构体的副本总是唯一的。 4.类可以有反初始化器;结构体没有。 5.可以更改常量类中的变量属性;无论属性是常量还是变量,常量结构体中的属性都是固定的。

在苹果最初的编程语言Objective-C中,我们几乎什么都用类——我们别无选择,因为它确实融入了我们的工作方式。

在Swift,我们确实有另一个选择,这个选择应该基于上述因素。我之所以说“应该”,是因为经常会看到一些人不关心这些差异,所以总是使用类或结构体,而不考虑这种选择的后果。

选择结构体还是类取决于您和您正在解决的特定问题。不过,我想让你考虑一下它是如何传达你的意图的。Donald Knuth说“程序应该是给人读的,只是顺带让计算机执行(programs are meant to be read by humans and only incidentally for computers to execute)”,这真的触及了我所说的核心:当有人读到你的代码时,他们对你的意图清楚吗?

如果您大多数时候使用结构体,那么在一个特定情况使用类就会传达一些意图:这个东西是不同的,需要不同的使用方式。如果你总是使用类,这种区别就会消失——毕竟,你不太可能经常需要它们。

提示:SwiftUI的一个迷人的细节是它如何完全颠倒我们使用结构体和类的方式。在UIKit中,我们会使用struct来表示数据,使用class来表示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想知道如何识别数组中的每个项。

考虑一下:如果我们在一个包含四项的数组上循环,我们将创建四个视图,但是如果body被重新调用,并且我们的数组现在包含五项,那么SwiftUI需要知道哪个视图是新的,这样它就可以在UI中显示。SwiftIUI最不想做的就是扔掉它的整个布局,每次做一个小小的改变就从头开始。相反,它希望尽可能减少工作量:它希望保留现有的四个视图,只添加第五个视图。

所以,我们回到Swift如何识别数组中的值。当我们使用0..<50..<agents.count这样的范围时,Swift确信每个项目都是唯一的,因为它将使用范围中的数字——每个数字在循环中只使用一次,所以它肯定是唯一的。

在我们的字符串数组中,这是不可能的,但是我们可以清楚地看到每个值都是唯一的:在[“Cyril”,“Lana”,“Pam”,“Sterling”]中的值不会重复。所以,我们可以告诉SwiftUI字符串本身——“Cyril”、“Lana”等可以用来唯一地标识循环中的每个视图。

我们可以在代码里面这样写:

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

因此,我们现在直接读取数组中的项,而不是在整数上循环并使用它读入数组,就像for循环一样。

随着您使用SwiftUI的进展,我们将研究识别视图的第三种方法——使用Identifiable协议。

使用绑定

当我们使用PickerTextField等控件时,我们使用$propertyName为它们创建到某种@State属性的双向绑定。这对简单的属性非常有用,但是有时候——只是有时候,希望如此!——您可能需要更高级的东西:如果您想运行一些逻辑来计算当前值,该怎么办?或者,如果你不只是想在写的时候隐藏一个值呢?

如果我们想对绑定中的更改做出反应,我们可能会尝试利用Swift的didSet属性观察,但您会失望的。这就是自定义绑定的来源:它们可以像@State绑定一样使用,但我们无法完全控制它们的工作方式。

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和被操纵的底层状态值之间的一个填充。

但是,请注意,选择器现在是使用selection:binding进行的,不需要美元符号。我们不需要在这里明确要求双向绑定,因为它已经是了。

如果我们愿意的话,我们可以创建一个更高级的绑定,它不仅仅传递一个值。例如,假设我们有一个带有三个切换开关的表单:用户是否同意条款和条件,同意隐私策略,并同意接收有关发货的电子邮件。

我们可以将其表示为三个布尔型的@State属性:

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

尽管用户可以手动切换它们,但我们可以使用自定义绑定一次完成所有操作。如果这三个布尔值都为真,则此绑定将为真,但如果更改了绑定,则它将更新所有布尔值,如下所示:

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")              }          }      }  }

再说一遍,定制绑定并不是你经常想要的,但是花点时间看看幕后,了解发生了什么是非常重要的。尽管它非常聪明,但它只是一个工具,而不是魔法!

译自Milestone: Projects 1-3 Key points