第77天 里程碑 项目 13-15
你已经完成了两个有难度的项目和一个技术项目,现在你对SwiftUI的掌握应该已经相当全面了——我真心希望你已经开始体会到,它比苹果之前的框架要简单得多!
今天是时候停下来回顾一下一下所学内容了。我们还会深入探讨两个主题——运算符重载和属性包装器,因为通过深入学习这两个主题,你能更深刻地理解我们一直在使用的功能。此外,今天还有一个挑战任务,你需要运用所学技能构建一个全新的应用程序。
挑战能让你以实践的方式检验自己的学习成果,为你提供一个可以充分展现自我的“沙盒”,如果你愿意,还能借此机会突破自我。我也希望这些挑战能让你看到,只要用心去做,你就能做到多么出色。正如西塞莉·泰森所说:“挑战能让你发现自己从未未曾了解的一面”,希望今天你能发现自己已经取得了多么大的进步!
今天你需要学习三个主题,其中一个是挑战任务。
- 所学内容
- 重点要点
- 挑战任务
注意: 如果当天没有完成挑战也不用担心——在之后的学习中,你会发现时不时会有一些空闲时间,所以挑战任务可以留到以后再完成。
所学内容
这些项目开始带你接触SwiftUI中较难的部分,但这些难点其实并非SwiftUI本身的问题——而是当SwiftUI与苹果旧框架交互时,事情才会变得有些复杂。随着时间的推移,这些“粗糙的边缘”会逐渐得到改善,但这可能需要几年时间。无论如何,当你需要集成来自苹果生态之外的代码时,这些知识仍然非常重要。
在完成这三个项目的过程中,你还学到了以下内容:
- 属性包装器如何转化为结构体
- 显示带有多个按钮的确认对话框
- 使用Core Image处理图像
- 在SwiftUI中导入照片
- 在SwiftUI中集成地图
- 在地图上添加图钉
- 从应用程序中分享内容
- 为自定义类型添加Comparable协议一致性
- 找到并写入用户的文档目录
- 写入文件时启用文件加密
- 使用Touch ID和Face ID对用户进行身份验证
这些主题都先以独立技术点的形式讲解,然后在实际项目中应用,希望你能真正理解并掌握这些内容!
重点要点
今天我想深入探讨两个主题,这两个主题都颇具一定难度,但我相信它们能帮助你更深入地理解前三个项目中涉及的内容。
运算符重载
当我们为自定义类型添加Comparable协议一致性时,需要实现一个名为<的方法。这个方法能让Swift比较a < b这类表达式,从而让我们可以使用无需参数的sorted()方法。
这就是所谓的“运算符重载”,正是因为有了它,我们才能用同一个+运算符实现两个整数的相加或两个字符串的拼接。你可以自定义运算符,也可以轻松扩展现现有运算符的功能,让它们实现新的操作。
举个例子,我们可以为Int添加一些扩展,实现Int与Double的乘法运算——Swift默认不支持这种操作,这有时会带来不便:
extension Int {
static func *(lhs: Int, rhs: Double) -> Double {
return Double(lhs) * rhs
}
}请特别注意参数:该方法的左操作数是Int类型,右操作数是Double类型,这意味着如果交换两个操作数的位置,该方法将无法生效。因此,如果你想让两种顺序的运算都能生效,就需要定义两个这样的方法。
不过,如果你想做到“完全兼容”,那么扩展Int并不是最佳选择:我们应该向上扩展到一个既能包含Int,又能包含其他整数类型(比如我们在Core Data中使用的Int16)的协议。Swift将所有整数类型都归到了BinaryInteger这个协议中。如果我们为这个协议编写扩展,那么Self(首字母大写)就代表具体使用的类型。也就是说,当用于Int类型时,Self表示Int;当用于Int16类型时,Self就表示Int16。
下面这个扩展实现了任意任意整数类型与Double的乘法运算,无论整数在左侧还是右侧:
extension BinaryInteger {
static func *(lhs: Self, rhs: Double) -> Double {
return Double(lhs) * rhs
}
static func *(lhs: Double, rhs: Self) -> Double {
return lhs * Double(rhs)
}
}你可能会问,为什么Swift不默认启用这些运算符?原因是这种运算的精度无法保证达到预期。举个简单的例子,试试下面的代码:
let exampleInt: Int64 = 50_000_000_000_000_001
print(exampleInt)
let result = exampleInt * 1.0
print(String(format: "%.0f", result))这段代码创建了一个64位整数,值为50万亿零1。然后通过我们自定义的扩展将其与Double类型的1.0相乘,理论上结果应该与原整数相同。但String(format:_:)方法要求不保留小数位输出,此时你会发现结果与原整数并不一致:结果是50万亿,少了末尾的1。你可能会问“对于50万亿来说,1又算得了什么”,这没问题——我并不是要评判对错,只是想告诉你,如果你需要绝对的精度,就应该避免使用这类辅助方法。
更广泛地说,我想给你一个关于运算符重载的提醒。在第14个项目中介绍运算符重载时,我说过它“既是福音福也是祸”,现在我想简要说明一下原因。
看看下面这段代码:
let paul = User()
let swift = Language()
let result = paul + swiftresult是什么类型的数据呢?我能想到几种可能性:
- 可能是另一个
User对象,只不过该对象已被修改,其已知语言数组中包含了Swift。 - 可能是一个结合了用户和语言信息的
Programmer对象。 - 可能是经典经典恐怖电影《变蝇人》的奇怪翻拍(此处为调侃,无实际意义)。
关键在于,我们无法直接判断result的类型,必须阅读相关+运算符的源代码才能知晓。
再看看下面这段代码:
let paul = User()
let swift = Language()
paul.learn(swift)我认为这段段代码要清晰得多:我们在一个对象上调用了一个简单的方法,你很可能会猜到,这段代码的作用是修改paul的属性,将Swift添加到其编程语言数组中。
任何优秀的开发者都会告诉你,清晰度是编写优质代码的重要特质之一——我们需要让代码清晰地表达出我们的意图,因为这些代码在未来会被阅读数十次甚至数百次。
因此,你当然可以将运算符重载纳入自己的技能库,用它来解决问题(事实上,我在我的著作《Pro Swift》中对运算符重载有更详细的讲解,链接:https://www.hackingwithswift.com/store/pro-swift),但使用时一定要谨慎。
自定义属性包装器
你已经了解到,属性包装器本质上是一种“障眼法”:它将一个简单的值包装在另一个值中,以便添加额外的功能。比如SwiftUI用@State在其他位置存储值,或者用@Environment从共享数据源中读取值,原理都是如此——给一个简单的值赋予某种“超能力”。
我们可以在自己的代码中使用属性包装器,而且有很多使用它的理由。和运算符重载一样,尝试使用属性包装器能让你更深入地理解其工作原理,但使用时也需要深思熟虑:如果一遇到问题就想到用属性包装器,那你很可能犯了错。
为了演示属性包装器,我们先创建一个简单的结构体,用于包装某种种BinaryInteger类型的值。我们会为这个结构体设置自定义代码来处理其包装值:如果新值小于0,就将其设为0,确保这个结构体的值永远不会是负数。
代码如下:
struct NonNegative<Value: BinaryInteger> {
var value: Value
init(wrappedValue: Value) {
if wrappedValue < 0 {
value = 0
} else {
value = wrappedValue
}
}
var wrappedValue: Value {
get { value }
set {
if newValue < 0 {
value = 0
} else {
value = newValue
}
}
}
}现在我们可以用一个整数来创建这个结构体,但如果这个整数小于0,它会被限制为0。因此,下面的代码会输出0:
var example = NonNegative(wrappedValue: 5)
example.wrappedValue -= 10
print(example.wrappedValue)属性包装器的作用是,让我们可以在结构体或类的任意属性上使用这种包装逻辑。更方便的是,只需一步操作:在NonNegative结构体前加上@propertyWrapper,代码如下:
@propertyWrapper
struct NonNegative<Value: BinaryInteger> {这样就可以了——我们已经创建了自己的属性包装器!
想必你已经从名称中猜到,属性包装器只能只能只能用于“属性”,而不能用于普通的变量或常量。因此,为了测试我们创建的属性包装器,我们将它放在User结构体中,代码如下:
struct User {
@NonNegative var score = 0
}现在我们可以创建一个用户对象,随意增减分数,并且完全不用担心分数会变成负数:
var user = User()
user.score += 10
print(user.score)
user.score -= 20
print(user.score)由此可见,这其中并没有什么“魔法”:属性包装器只是过是一种语法糖,让一个数据被另一个数据包装起来,而且我们也可以根据需要自行创建属性包装器。
挑战任务
你是否有过这样的经历:在会议或聚会中,和陌生人聊了一会儿,走开后几秒钟就忘了对方的名字?不止你一个人有这种情况,而你今天要构建的应用程序就能能解决这类问题。
你的目标是构建一个应用程序,让用户从照片库中导入一张图片,然后为导入的内容添加名称。应用程序需要在列表中显示所有已命名的图片,点击列表中的项目时,会跳转到详情细页面,展示该图片的放大版本。
具体分解解任务如下:
- 使用
PhotosPicker让用户从照片库中导入照片。 - 检测到新照片导入后,立即提示用户为照片命名。
- 将名称和照片妥善保存。
- 在列表中显示所有名称和照片,并按名称排序。
- 创建详情页面,以全屏形式展示图片。
- 确定保存所有数据的方式。
记住,要将用户导入的照片以Data(数据)形式存储,这样便于写入文件。
你可以选择使用SwiftData来完成这个项目,但这并非必需——将数据写入文档目录中的JSON文件也可以。不过,要实现数组排序,你需要为相关类型添加自定义的Comparable协议一致性。
如果你确实选择使用SwiftData,这里有一个重要提示:当在模型中存储图片或视频等大型数据时,需要使用特殊的@Attribute宏来定义相关属性,代码如下:
@Attribute(.externalStorage) var photo: Data这会告诉SwiftData不要将图片数据直接保存在数据库中,而是将其存储在数据库旁边——这种方式效率高得多。
记住,你已经掌握了完成这个项目所需的全部知识——祝你好运!