你学到了什么

你现在已经完成了前两个SwiftUI项目,并且还完成了一个技术项目–两个应用程序的相同节奏和一个技术项目一直持续到课程结束,并且将帮助你在花时间的同时快速提高知识水平返回并完善你所学。

尽管我们只是SwiftUI的三个项目,但你已经了解了一些最重要的概念:视图,修饰符,状态,堆栈布局等–这些是你将在SwiftUI中一次又一次使用的技能,为什么我想早点把它们弄下来,

当然,你还构建了一些实际项目并完成了许多编码挑战,以帮助巩固你的学习,因此希望你开始对自己的知识有所了解。

到目前为止,我们已经介绍了:

  • 构建将文本与控件(例如)混合的滚动表单,PickerSwiftUI会变成漂亮的基于表格的布局,在其中可以以新的选择滑动新屏幕。
  • 创建一个NavigationView并为其命名。这不仅使我们能够将新的视图推送到屏幕上,而且还使我们能够设置标题并避免日以继夜的内容出现问题。
  • 如何使用@State存储变化的数据,以及为什么需要它。请记住,我们所有的SwiftUI视图都是结构,这意味着如果没有类似的内容就无法更改它们@State。
  • 为TextField和等用户界面控件创建双向绑定Picker,了解如何使用$variable可以让我们读取和写入值。
  • 使用ForEach创造一个循环,这使我们能够使大量的意见一下子意见。
  • 使用VStack,,HStack和构建复杂的布局,ZStack并将它们组合在一起以构成网格。
  • 如何将颜色和渐变用作视图,包括如何为它们指定特定的帧,以便你可以控制它们的大小。
  • 如何通过提供一些文本或图像以及点击按钮时应执行的关闭操作来创建按钮。
  • 通过定义显示警报的条件来创建警报,然后从其他位置切换该状态。
  • SwiftUI如何(以及为什么!)广泛使用不透明的结果类型(some View),为什么将其与修饰符顺序如此紧密地联系起来很重要。
  • 如何使用三元运算符创建条件修饰符,这些条件修饰符根据你的程序状态应用不同的结果。
  • 如何使用视图组成和自定义视图修饰符将代码分解为小部分,从而使我们能够构建更复杂的程序而不会丢失代码。
  • 我想让你考虑的一件事是,在SwiftUI中成为“视图”意味着什么。在开始这门课程之前,你可能已经想过Color.red不可能是一个视图,但是确实如此。你还了解LinearGradient了视图的外观,这意味着在我们的布局中易于使用。

但是呢VStack?还是Group?还是ForEach?是那些意见吗?

是![]这些绝对是SwiftUI中的全部视图,这就是使该框架如此明显可组合的原因–我们可以将a ForEach内部in内部in ForEach内部a Group内部VStack,然后一切都可以正常工作。

请记住,所有的东西需要以符合做View协议已经称为单一的计算特性body是回报some View。

以前,我们非常仔细地研究了Swift中的协议,协议扩展和面向协议的编程,你可能想知道为什么这一切如此重要。好了,现在我希望你能看到:该View协议是SwiftUI的核心-只需遵循几行简单的代码,任何可以遵循该协议并开始参与布局的人。

在其他用户界面框架(包括Apple自己的UIKit)中,使用了类来完成这项工作。这意味着,如果你已有一些现有类型,并且想要使用它们进行布局,则需要使它们继承自UIView-进而意味着获得了200多种你可能不需要的属性和方法,以及大量的在幕后使用的其他功能。

在SwiftUI中,这一切都不会发生:我们只添加一个View一致性。这就是协议和协议扩展的强大功能,这就是使面向协议的编程如此重要的原因-如果我们body向类型添加单个属性,SwiftUI就会知道如何使用该属性进行布局和渲染。

现在,如果你注意的话,你可能已经注意到了一个好奇:当我们制作任何一种SwiftUI视图时,我们都需要使其返回some View–我们创建一个视图,并返回一个或多个其他视图。这些视图具有它们自己的body属性,这些属性又返回视图,而这些视图具有它们自己的body属性,……你明白了。

看来SwiftUI本身已经建立了一个无限循环:如果所有视图都由其他视图组成,它实际上在哪里结束?

显然,它确实结束了,否则我们的SwiftUI代码都将无法正常工作。诀窍在于Apple称之为原始视图的东西– SwiftUI的绝对裸露构建块,它们符合View但返回一些固定内容,而不是呈现其他某种视图。

有相当多的这些积木,他们会不会来一个惊喜的- Text是其中之一,例如,如Image,Color,Spacer,等。最终,我们构建的所有UI都是在这些构建块之上创建的,这打破了看似无限的循环。

关键点

有三个重要点值得详细介绍。这将部分地回顾我们所学到的知识-再次通过不同的示例进行遍历以帮助确保它们很清楚-但我也想借此机会回答到目前为止可能出现的一些问题。

结构与类

首先,希望在你的记忆中新鲜的东西:结构和类。这两种方法都可以使我们使用属性和方法构建复杂的数据类型,但是它们的工作方式(更具体地讲,它们之间的区别)很重要。

如果你还记得的话,结构和类之间有五个主要区别:

  • 类不带有成员初始化器。结构默认情况下获取这些。
  • 类可以使用继承来构建功能。结构不能。
  • 如果你复制一个类,则两个副本都指向相同的数据。结构的副本始终是唯一的。
  • 类可以具有反初始化器;结构不能。
  • 你可以在常量类中更改变量属性。常量结构内部的属性是固定的,无论属性是常量还是变量。
  • 在Apple最初的编程语言Objective-C中,我们几乎将所有类都使用了类–我们没有选择的余地,因为它确实融入了工作方式。

在Swift中,我们确实有一个选择,并且该选择应该基于上述因素。我之所以说“应该做到”,是因为看到人们不在乎这些差异的情况并不少见,因此总是不停地使用class或struct不考虑这些选择的后果。

选择结构还是类取决于你和要解决的特定问题。但是,我要你做的是考虑它如何传达你的意图。唐纳德·克努斯(Donald Knuth)说:“程序只能被人类读取,并且只能由计算机来执行”,这确实引起了我所谈论的重点:当有人读取你的代码时,你的意图是否清晰明了?

如果大多数时候使用结构,那么在一个特定的地方切换到类可以传达一些意图:这件事与众不同,需要以不同的方式使用。如果你始终使用类,那么这种区别就会消失–毕竟,你极不可能经常需要它们。

提示: SwiftUI的一个迷人细节是它如何完全颠倒我们使用结构和类的方式。在UIKit中,我们将为数据使用结构,为UI使用类,但在SwiftUI中则完全相反–很好地提醒了学习事物的重要性,即使你认为它们并没有立即有用。

与ForEach合作

我想讨论的第二件事是ForEach,我们已将其与以下代码一起使用:

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

ForEach与SwiftUI中的大多数其他视图一样,它是一个视图,但是它允许我们在循环内创建其他视图。这样,它还允许我们绕过SwiftUI施加的十个孩子的限制– ForEach本身成为十个限制之一,而不是其中的内容。

现在考虑这样的字符串数组:

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

我们怎样才能遍历这些并制作文本视图?

一种选择是使用我们已经拥有的相同构造:

1
2
3
4
5
VStack {
ForEach(0 ..< agents.count) {
Text(self.agents[$0])
}
}

但是SwiftUI为我们提供了第二种选择:我们可以直接在数组上循环。这需要更多的思考,因为SwiftUI想要知道如何识别数组中的每个项目。

想想看:如果我们遍历四个项目的数组,我们将创建四个视图,但是如果body重新调用并且我们的数组现在包含五个项目,SwiftUI需要知道哪个视图是新视图,以便可以在UI中显示。SwiftUI要做的最后一件事是丢弃整个布局,并在每次进行小的更改时从头开始。相反,它希望完成的工作量最少:它想保留现有的四个视图,而仅添加第五个。

因此,我们回到Swift如何识别数组中的值的方法。当我们使用诸如0 ..< 5或的范围时0 ..< agents.count,Swift会确定每个项目都是唯一的,因为它将使用该范围内的数字-每个数字在循环中仅使用一次,因此绝对是唯一的。

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

在代码中,我们可以这样写:

1
2
3
4
5
VStack {
ForEach(agents, id: \.self) {
Text($0)
}
}

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

随着SwiftUI的发展,我们将介绍使用该Identifiable协议识别视图的第三种方法,但这将适时出现。

使用自定义绑定

当使用诸如Picker和的控件时,我们使用TextField为其创建双向绑定到某种@State属性$propertyName。这对于简单的属性非常有用,但有时–只是有时,希望如此!–你可能需要更高级的功能:如果要运行一些逻辑来计算当前值怎么办?或者,如果你不仅想在写入值时隐瞒值,该怎么办?

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

绑定不是魔术:@State为我们删除了一些无聊的样板代码,但是如果你愿意的话,完全可以手动创建和管理绑定。再说一次,我没有给你看这个是因为它很常见,因为实际上不是。我希望你很少需要这样做。相反,我向你展示它是因为我想消除SwiftUI代表你进行某种魔术的想法。

一切SwiftUI确实为我们可以通过手工来完成,虽然它几乎总是更好的依靠自动化解决方案也可以是真正有用的,所以你明白采取幕后偷看什么它做你的名义。

首先,让我们看一下自定义绑定的最简单形式,它只是将值存储在另一个@State属性中并读回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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())
}
}
}

get 为所对应的值,set 为当该值发生改变时设置其他的值。

因此,该绑定实际上只是充当传递对象-它本身并不存储或计算任何数据,而只是充当我们的UI和所操纵的基础状态值之间的垫片。

但是,请注意,现在使用的是采摘机selection: binding–不需要美元符号。我们不需要在这里明确要求双向绑定,因为它已经是一个。

如果我们愿意,我们可以创建一个更高级的绑定,该绑定不仅可以传递单个值,还可以执行更多操作。例如,假设我们有一个带有三个切换开关的表单:用户是否同意条款和条件,是否同意隐私政策以及是否同意收到有关运输的电子邮件。

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

1
2
3
@State var agreedToTerms = false
@State var agreedToPrivacyPolicy = false
@State var agreedToEmails = false

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

1
2
3
4
5
6
7
8
9
10
let agreedToAll = Binding(
get: {
self.agreedToTerms && self.agreedToPrivacyPolicy && self.agreedToEmails
},
set: {
self.agreedToTerms = $0
self.agreedToPrivacyPolicy = $0
self.agreedToEmails = $0
}
)

因此,现在我们可以创建四个拨动开关:一个用于单个布尔值,另一个用于一次同意或不同意所有三个的控制开关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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")
}
}
}
}

再一次,自定义绑定不是你经常需要的东西,但是花时间回顾幕后并了解发生的事情非常重要。尽管它非常聪明,但是SwiftUI只是一个工具,而不是魔术!

参考资料

查看下一天的SwiftUI学习笔记

关于100days英文课程