第95天 里程碑 项目 16-18
完成了两个大型项目和另一个技术项目后,是时候停下来反思所学内容,更深入地探讨几个关键主题,然后迎接新的挑战了。这是你在这100天里的最后一个挑战,所以我特意挑选了一个灵活度高的任务——如果你愿意,30分钟就能完成,但你也可以根据自己的兴趣完成一些可选的额外任务。
这个挑战的好处在于,它为你提供了各种空间,可以按照自己的意愿开发应用,实现你认为最有趣或最有用的功能。核心在于,你有一块一块空白的画布,能否把它变成实实在在的作品全看你自己——你需要选择想做的内容,设计用户界面,修复自己遇到的漏洞,然后让应用准备就绪。
和往常一样,不要害怕犯错,因为犯错是很正常的事。正如罗杰·克劳福德曾经说过的:“生活中遇到挑战是不可避免的,但被打败却是可以选择的。”
今天你需要学习三个主题,其中一个就是你的挑战任务。
- 所学内容
- 关键点
- 挑战任务
注意:如果没有在指定日期完成挑战也不用担心——在之后的日子里,你会发现时不时会有一些空闲时间,所以挑战任务是可以在之后回头再完成的。
所学内容
最近我们完成了几个耗时较长的项目,这主要是因为你的SwiftUI技能确实有了很大提升——你已经远远超出基础水平了,所以现在能够处理更大的项目,解决更复杂的问题。我知道开发这些大型项目可能会让人觉得疲惫,但希望你回头看看自己构建的成果时能感到满意——你已经取得了巨大的进步!
在完成这些项目的过程中,你还学到了以下内容:
- 使用
TabView创建标签页。 - 利用Swift的
Result类型返回成功或失败的结果。 - 控制图像插值。
- 为列表行添加滑动操作。
- 在
ContextMenu中放置按钮。 - 使用UserNotifications框架创建本地通知。
- 通过Swift包依赖使用第三方代码。
- 如何创建动态二维码。
- 为SwiftUI视图附加自定义手势。
- 利用感官反馈让iPhone产生震动效果。
- 使用
allowsHitTesting()禁用用户交互。 - 使用
Timer重复触发事件。 - 跟踪应用在后台和前台之间切换时的场景状态变化。
- 支持色盲模式、减少动态效果等辅助功能。
- SwiftUI的三步布局系统。
- 对齐方式、对齐指南以及自定义对齐指南。
- 使用
position()修饰符绝对定位视图。 - 利用
GeometryReader、GeometryProxy和containerRelativeFrame()制作特殊效果。
而且你还构建了一些实际的应用来实践这些技能——这段时间确实很忙碌,希望你能为自己取得的成就感到自豪!
关键点
在开始这个项目的挑战任务之前,我想更深入地探讨两个要点,确保你能完全理解它们:map()和filter()在更广泛的函数式编程领域中处于什么位置,以及Swift的Result类型。
函数式编程
虽然我在我的《Pro Swift》一书中详细介绍了函数式编程,但在这里我还是想简单提一下,因为我们在第16个项目中简要使用过map()方法。这个方法的设计初衷是让我们明确“想要什么”,而不是“如何实现”,这两者都属于一种更广泛的编程方法,即“函数式编程”。
为了展示这种方法与另一种常见编程方法(命令式编程)的区别,我们来看下面这段代码:
let numbers = [1, 2, 3, 4, 5]
var evens = [Int]()
for number in numbers {
if number.isMultiple(of: 2) {
evens.append(number)
}
}这段代码创建了一个整数数组,逐个遍历数组中的元素,并将能被2整除的元素添加到一个名为evens的新数组中——我们需要详细说明整个过程应该如何执行。
这段代码易于阅读、易于编写,而且运行效果很好,但我们可以使用一种名为filter()的函数式编程方法,将其重写为:
let numbers = [1, 2, 3, 4, 5]
let evens = numbers.filter { $0.isMultiple(of: 2) }现在,我们不需要详细说明过程应该如何执行,而是专注于“想要实现什么”:我们向filter()提供一个判断条件,剩下的工作就由它来完成。这意味着代码更简洁,这当然很棒,但除此之外,代码还在三个方面得到了改进:
- 不再可能在循环中意外添加
break语句——filter()总会处理数组中的每个元素,这种额外的简洁性让我们可以专注于判断条件本身。 - 除了提供闭包,我们还可以调用一个共享函数,这对代码复用非常有利。
- 最终得到的
evens数组现在是常量,因此之后不会不小心修改它。
代码更短总是好的,但写出更简洁、更易于复用且变量更少的代码会更好!
接受函数作为参数,或者返回函数作为结果的函数,被称为“高阶函数”,map()和filter()都是高阶函数的例子。Swift中还有很多类似的函数,其中compactMap()非常实用,它的作用如下:
- 和
map()一样,对数组中的每个元素执行一个转换函数。 - 解包转换函数返回的可选值,并将结果放入一个新数组中返回。
- 所有为
nil的可选值都会被丢弃。
因此,map()会创建一个与原数组元素数量相同的新数组,而compactMap()返回的数组元素数量可能与原数组相同,也可能更少,甚至可能为空!
要直观看到map()和compactMap()的区别,可以尝试下面这个例子:
let numbers = ["1", "2", "fish", "3"]
let evensMap = numbers.map(Int.init)
let evensCompactMap = numbers.compactMap(Int.init)这段代码创建了一个字符串数组,然后分别使用map()和compactMap()将其转换为整数数组。代码运行后,evensMap会包含两个可选整数、一个nil和另一个可选整数,而evensCompactMap会包含三个实际的整数——没有可选类型,也没有nil。这样的结果要好得多!
Result类型
我们使用Swift的Result类型作为一种简单的方式,返回一个要么成功(带有对应值)要么失败(带有对应错误)的单一结果,但它还有一些重要特性,我认为在你自己的代码中会很有用。
首先,仔细想想就会发现,Result有点像一种更高级的可选类型。可选类型要么包含某种值(比如整数、字符串等),要么什么都不包含;而Result同样包含某种值,但在“另一种情况”下,它不是什么都没有,而是必须包含某种错误。
从底层实现来看,可选类型和Result都是用Swift枚举实现的,且都有两个枚举情况。对于可选类型,枚举名为Optional,枚举情况分别是表示nil的.none和带有关联值(如整数、字符串等)的.some;对于Result,枚举情况则是带有关联值的.success和带有另一种关联值的.failure。
两者之间唯一真正的区别在于,Swift为可选类型提供了语法糖——专门设计的简化语法,因为可选类型的使用非常普遍。比如,if let和可选链都是为可选类型设计的,而Result则没有这类特殊的语法支持。
其次,正如你所见,Result要么包含成功值,要么包含错误值,但如果需要,我们可以在Result和抛出函数(throwing function)之间灵活转换。
如果你有一个Result值,并且想回到do/catch的处理方式,只需调用Result的get()方法——如果Result是成功状态,这个方法会返回成功值;如果是失败状态,则会抛出对应的错误。
例如,看看下面这段代码:
enum NetworkError: Error {
case badURL
}
func createResult() -> Result<String, NetworkError> {
return .failure(.badURL)
}
let result = createResult()这段代码定义了一种错误类型,创建了一个函数(该函数本应返回字符串或错误,但实际上总是返回错误),然后调用这个函数,并将其返回值赋给result。如果你想使用do/catch来处理这个值,可以像这样使用get()方法:
do {
let successString = try result.get()
print(successString)
} catch {
print("哎呀!出现了错误。")
}反过来,如果要从抛出代码创建Result值,你会发现Result有一个接受抛出闭包的初始化器。如果闭包成功返回一个值,这个值就会作为Result的成功情况;如果闭包抛出错误,这个错误就会作为Result的失败情况。
例如:
let result = Result { try String(contentsOf: someURL) }在这段代码中,result的类型是Result<String, Error>——它没有特定的错误类型,因为String(contentsOf:)方法不会返回特定类型的错误。
关于Result,你需要知道的最后一点是,它拥有一些你已经熟悉的函数式方法,包括map()和mapError()。例如,map()方法会检查Result的状态,如果是成功状态,就会使用你提供的闭包将成功值转换为另一种类型(比如将字符串转换为整数);但如果是失败状态,它就会直接保留失败结果,忽略你的转换操作。另一方面,mapError()会将错误从一种类型转换为另一种类型,如果你想在某个地方统一错误类型,这个方法会很有帮助。
这正是函数式编程的魅力之一:一旦你理解了map()这种“接受闭包并使用它进行转换”的本质,就会发现它不仅存在于数组中,还存在于Result甚至可选类型中!
挑战任务
这次的挑战难度可高可低,取决于你想深入到什么程度,但项目的核心很简单:你需要构建一个应用,帮助用户掷骰子,并存储他们掷出的结果。
至少,你需要实现让用户掷骰子的功能,以及让用户查看之前掷骰子结果的功能。不过,如果你想进一步挑战自己,可以尝试完成以下一项或多项任务:
- 让用户自定义要掷的骰子:骰子的数量,以及骰子的类型(4面、6面、8面、10面、12面、20面,甚至100面)。
- 显示掷出的骰子的总和。
- 使用JSON或SwiftData存储结果——只要能持久化存储就行。
- 掷骰子时添加触觉反馈。
- 更高难度的任务:让骰子掷出的数值在最终确定前,快速切换显示多个可能的值。
这里说的“掷骰子”不需要制作复杂的3D效果——只需显示“掷出”的数字即可。
可能需要多花些功夫的是第5项任务:让结果在最终确定前快速切换显示多个值。最简单的实现方法是使用Timer,并在调用一定次数后取消定时器。
在开发过程中,请花点时间关注代码的可访问性——尝试用VoiceOver(屏幕阅读器)测试应用,确保它能尽可能正常地工作。