Hacking with iOS: SwiftUI Editio
What you learned - 你学到了什么
这些项目开始向您介绍SwiftUI的更困难的部分,尽管这些都不是真正的SwiftUI的错——在SwiftUI与Apple的旧框架相遇的地方,事情变得有些粗糙。随着时间的流逝,这些粗糙的边缘会逐渐消失,但是可能要过几年,这些内容对于您想从Apple外部集成代码的时候仍然很重要。
在完成三个项目的同时,您还了解到:
- 属性包装器如何处理结构体
- 创建自定义绑定
- 展示带有很多按钮的操作表
- 使用Core Image操纵图像
- 将
UIImagePickerController
集成到 SwiftUI - 将
MKMapView
集成到SwiftUI - 编写可充当图像选择器和地图视图委托的协调器类
- 在地图上放置图钉
- 将图像保存到用户的照片库
- 为自定义类型添加
Comparable
一致性 - 查找并写入用户的文档目录
- 写入文件时启用文件加密
- 使用 Touch ID 和 Face ID 验证用户身份
这些主题中的每一个都是作为独立技术涵盖的,然后在实际项目中应用,因此希望它真的可以融入其中!
Key points - 关键点
我今天想深入探讨两个主题,这两个主题都相当先进,但是我都知道这两个事实将真正帮助您加深对前三个项目所涵盖内容的理解。
运算符重载
当我们向自定义类型添加Comparable
支持时,我们需要添加一种称为<
的方法。反过来,这使Swift可以比较诸如a < b
的表达式,这使我们可以访问不带参数的sorted()
版本。
这称为运算符重载,它使我们可以使用相同的+
运算符添加两个整数或连接两个字符串。您可以根据需要定义自己的运算符,但是扩展现有运算符以执行新操作也很容易。
例如,我们可以向Int
添加一些扩展名,以使我们可以将Int
和CGFloat
相乘——Swift默认情况下不允许这样做,这可能会很烦人:
extension Int {
static func * (lhs: Int, rhs: CGFloat) -> CGFloat {
return CGFloat(lhs) * rhs
}
}
请特别注意这些参数:它使用Int作
为左侧操作数,并使用CGFloat
作为右侧操作数,这意味着如果将这两个值互换,它将无法正常工作。因此,如果您想完成任务,您需要两次定义该方法。
但是,如果您想真正完成,那么扩展Int
是错误的选择:我们应该采用封装了Int
以及其他整数类型(例如与Core Data一起使用的Int16
)的协议。Swift 将所有整数类型放入称为BinaryInteger
的单个协议中,如果我们在该协议上编写扩展名,则Self
(大写S)表示正在使用的任何特定类型。因此,如果在Int
上使用,则Self
表示Int
;如果在Int16
上使用,则表示Int16
。
这是一个扩展,它在任何类型的整数与CGFloat
和Double
之间加*
,而不管整数是在左侧还是在右侧:
extension BinaryInteger {
static func * (lhs: Self, rhs: CGFloat) -> CGFloat {
return CGFloat(lhs) * rhs
}
static func * (lhs: CGFloat, rhs: Self) -> CGFloat {
return lhs * CGFloat(rhs)
}
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万亿和一。然后使用自定义扩展名将其乘以Double
1.0,从理论上讲,这意味着返回的数字应与Int
相同。但是,该String(format:_ :)
调用要求打印的数字不带小数位,并且您会发现它与整数不同:他丢弃了最后的1。现在,您可能问“乘以1.0的过程中发生了什么?”,这很好——我不是在这里告诉您什么是对还是错,只是说如果您要绝对准确,则应避免使用此类辅助方法。
更笼统地说,我想给您一个关于运算符重载的警告。当我在项目14中介绍它时,我说过操作员超载“既是福也是祸”,我想简要地谈一下为什么这样做。
考虑这样的代码:
let paul = User()
let swift = Language()
let result = paul + swift
reslut
是什么类型的数据?我可以想到几种可能性:
- 也许是另一个
User
对象,现在已对其进行了修改,以使其在一系列已知语言中包含Swift。 - 也许是结合用户和语言的
Programmer
对象。 - 也许这是对经典恐怖电影《苍蝇》的怪异翻拍。
关键就是我们不知道,如果不阅读相关+
运算符的源代码,我们就无法真正得到答案。
现在考虑以下代码:
let paul = User()
let swift = Language()
paul.learn(swift)
我想这要清楚得多:我们现在正在对一个对象运行一个简单的方法,您可能会猜想我们正在对paul
进行突变,以将 swift
包含在编程语言数组中。
任何优秀的开发人员都会告诉您,清晰度是编写良好的代码的最重要功能之一——我们需要在编写的内容中弄清楚我们的意思,因为将来它将被读取数十或数百次。
因此,总的来说,这会增加操作员的负担,使您可以部署解决问题的技能,确实,我在《Pro Swift》一书中对它们进行了详细介绍,但请务必谨慎使用。
自定义属性包装器
您已经看到,属性包装器实际上只是一些技巧:它们采用一个简单的值并将其包装在另一个值中,以便可以添加一些额外的功能。这可能是SwiftUI使用@State
在其他位置存储值的方式,或者是它如何使用@Environment
从共享数据源读取值的方式,但是原理是相同的:它采用一个简单的值并以某种方式赋予它超能力。
我们可以在我们自己的代码中使用属性包装器,而您可能要这样做的原因有很多。与运算符重载一样,如果尝试一下,您将了解更多事情的工作方式,但也值得深思熟虑地使用它们:如果它们是您的第一要务,则可能是您犯了一个错误。
为了演示属性包装器,我想从包装某种BinaryInteger
值的简单结构开始。设置包装值时,我们将为它提供一些自定义代码,以便在新值小于0时将其设置为0,以使该结构体永远不会为负。
我们的代码如下所示:
struct NonNegative<Value: BinaryInteger> {
var value: Value
init(wrappedValue: Value) {
if wrappedValue < 0 {
self.value = 0
} else {
self.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
}
现在,我们可以创建一个用户并自由添加或删除,这可以确保分数永远不会低于0:
var user = User()
user.score += 10
print(user.score)
user.score -= 20
print(user.score)
正如您所看到的,这里确实没有魔术:属性包装器仅仅是语法糖,它使一个数据被包装在另一个数据上,而且如果需要,我们也可以自己制作。
Challenge - 挑战
您是否去过会议或聚会,和一个新朋友聊天,然后在离开后几秒钟就意识到自己已经忘记了他们的名字?您并不孤单,今天正在构建的应用将有助于解决该问题以及其他类似问题。
您的目标是构建一个应用程序,要求用户从其照片库导入图片,然后将名称附加到他们导入的任何内容上。他们命名的图片的完整集合应显示在列表中,而轻按列表中的项目应显示带有更大图片版本的详细信息屏幕。
分解一下,您应该:
- 包装
UIImagePickerController
以便可以选择照片。 - 检测何时导入新照片,并立即要求用户命名照片。
- 将该名称和照片保存在安全的地方。
- 显示列表中的所有名称和照片,并按名称排序。
- 创建一个详细界面,显示完整尺寸的图片。
- 确定保存所有这些数据的方法。
我们已经介绍了如何使用UIImageWriteToSavedPhotosAlbum()
将数据保存到用户的照片库中,但是将图像保存到磁盘需要额外的一小步:您需要通过调用jpegData()
方法将UIImage
转换为Data
,如下所示:
if let jpegData = yourUIImage.jpegData(compressionQuality: 0.8) {
try? jpegData.write(to: yourURL, options: [.atomicWrite, .completeFileProtection])
}
compressionQuality
参数可以是0(非常低的质量)到1(最大的质量)之间的任何值;像0.8可以在尺寸和质量之间取得良好的平衡。
如果需要,您可以为此项目使用Core Data,但这不是必需的——写到documents
目录中的简单JSON文件很好,尽管您需要自定义Comparable
才能使数组排序起作用。
如果您确实选择使用Core Data,请确保不要将实际图像保存到数据库中,因为这样做效率不高。无论是否使用Core Data,最佳的做法是为图像文件名生成一个新的UUID
,然后将其写入documents
目录,并将该UUID
存储在数据模型中。
记住,您已经知道完成这项工作所需的一切——祝您好运!