Hacking with iOS: SwiftUI Editio
What you learned - 你学到了什么
最近我们进行了一些非常漫长的项目,但这主要是由于您的SwiftUI技能真正得到了增长——您现在已经超出了基础知识,因此您能够解决更大的项目来解决更大的问题。我意识到在这些较大的项目上工作会感到很累,但我希望您能够回顾自己的成果并感觉良好——您走了很长一段路!
在完成这些项目时,您还了解了:
- 使用
@EnvironmentObject
读取环境值。 - 使用
TabView
创建 Tabs。 - 使用 Swift 的
Result
类型返回成功或失败信息。 - 使用
objectWillChange.send()
手动发布ObservableObject
的变更。 - 控制图像插值。
- 将按钮放在
ContextMenu
中。 - 使用
UserNotifications
框架创建本地通知。 - 通过Swift package dependencies 使用第三方代码。
- 使用
map()
和filter()
基于现有数组创建新数组。 - 如何创建动二维码。
- 将自定义手势附加到SwiftUI视图。
- 使用
UINotificationFeedbackGenerator
使iPhone振动。 - 使用·allowHitTesting()`控制用户交互。
- 使用计时器重复触发事件,或通过从
NotificationCenter
接收事件来触发事件。 - 支持色盲,减少动画等辅助性功能。
- 横向使用带有
StackNavigationViewStyle
的NavigationView
。 - SwiftUI的三步布局系统。
- Alignment,alignment guides和自定义alignment guides。
- 使用
position()
修饰符绝对定位视图。 - 使用
GeometryReader
和GeometryProxy
实现特殊效果。
…并且您还构建了一些真正的应用程序来将这些技能付诸实践——真的很忙,希望您为自己的成就感到自豪!
Key points - 关键点
在我们继续进行该项目的挑战之前,我想深入探讨两点,以确保您已充分理解它们:map()
和filter()
如何适应更大的函数式编程世界,以及Swift的Result
类型。
Functional programming - 函数式编程
尽管我在《Pro Swift》一书中对函数式编程进行了很多介绍,但我也想在这里进行介绍,因为我们在项目16中使用了两次,一次是与map()
一起使用,一次是与filter()
一起使用。这两种方法都是为了让我们指定想要的东西,而不是如何达到目的而设计的,这两种方法都是广泛编程方法的一部分它被称为函数式编程。
为了演示此方法与称为命令式编程的通用替代方法有何不同,请看以下代码:
let numbers = [1, 2, 3, 4, 5]
var evens = [Int]()
for number in numbers {
if number.isMultiple(of: 2) {
evens.append(number)
}
}
这将创建一个整数数组,一个一个地循环遍历,然后将2的倍数添加到名为偶数的新数组中——我们需要确切说明我们希望过程如何发生。该代码易于阅读,易于编写并且运行良好,但是如果我们要使用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
可以抛出异常函数方法。
如果您有一个Result
并想使用do
/catch
,只需调用Result
的get()
方法——如果成功 -> 值存在 -> 它将返回成功值,否则将抛出错误。
例如,如下代码:
enum NetworkError: Error {
case badURL
}
func createResult() -> Result<String, NetworkError> {
return .failure(.badURL)
}
let result = createResult()
它定义了某种错误,创建了一个返回字符串或错误的函数(但实际上总是返回一个错误),然后调用该函数并将其返回值放入结果中。如果要使用具有该值的do / catch
,可以按如下的方式使用get()
:
do {
let successString = try result.get()
print(successString)
} catch {
print("Oops! There was an error.")
}
反之——从抛出代码创建Result
值——您会发现Result
具有一个接受抛出闭包的初始化程序。如果闭包返回一个正常可用的值,则会赋值为成功的case,否则将引发的错误放入失败的case。
例如:
let result = Result { try String(contentsOf: someURL) }
在该代码中,结果将为Result<String, Error>
——它没有特定类型的Error
,因为String(contentsOf :)
没有返回。
关于Result
,您应该了解的最后一件事是它具有您已经习惯的功能方法,包括map()
和mapError()
。例如,map()
方法在Result
内部查找,并使用您指定的闭包将成功值转换为另一种值——例如,它可能会将字符串转换为整数。但是,如果发现失败,它将直接使用它,而忽略您的转换。另外,mapError()
可以将错误从一种类型转换为另一种类型,如果您想在一个位置上统一错误类型,这可能会很有用。
这是关于函数式编程的众多爱好之一:一旦了解了map()
的“采用闭包并将其用于转换东西”的性质,您就会发现它存在于数组,Result
甚至Optional
中!
Challenge - 挑战
这次挑战可能很容易,也可能很难,具体取决于您想挑战多远,但该项目的核心很简单:您需要构建一个可帮助用户掷骰子然后存储其结果的应用程序。
至少应该有一个选项卡视图,其中第一个选项卡允许用户掷骰子,第二个选项卡显示先前掷骰的结果。但是,如果您想进一步提高自己的实力,可以尝试以下一种或多种方法:
- 让用户自定义滚动的骰子:骰子的数量和类型:4面,6面,8面,10面,12面,20面,甚至100面。
- 显示掷骰子的总数。
- 使用 Core Data 存储结果,使结果持久化。
- 掷骰子时添加触控反馈。
- 对于真正的挑战,请在决定最终数字之前,使骰子滚动的值经过各种可能的值。
当我说“掷骰子”时,您无需创建精美的3D效果-只需显示“掷骰子”的数字即可。
唯一可能需要您做些事情的是步骤5:在确定最终数字之前,使结果在各种值之间滑动。解决此问题的最简单方法是通过在一定数量的调用后取消Timer
的计时器,但是如果您想使用更高级的解决方案,则可以尝试使用增加的延迟来调用DispatchQueue.main.asyncAfter()
,这样它的启动速度快于其减速速度, “骰子滚”放慢。
在工作时,请花点时间记住代码的可访问性——尝试将其与VoiceOver一起使用,并确保它能正常工作。