译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
Instafilter:介绍
在这个项目中我们将构建一个可以让用户从相册中导入照片,然后对照片使用各种滤镜进行修改的应用。我们会涉及许多新的即时,其中核心的两项技能包括使用苹果的 Core Image 框架来开发,以及一项重要的 SwiftUI 技能,集成 UIKit。当然也有别的东西,但这两项是最重要的。
Core Image 是 Apple 为图片维护提供的高性能框架,非常地强大。Apple 设计了大量范例的滤镜供我们使用,包括模糊,色移,像素化等等,而这些滤镜都做了优化,以充分利用 iOS 设备的图形处理单元(GPU)。
提示: 尽管我们可以在模拟器上运行 Core Image 应用,但在那上面大部分东西运行很慢 —— 只有在物理设备上才能得到很好的性能。
至于集成 UIKit,你可能会好奇为什么它是必需的 —— 毕竟,SwiftUI 被设计用来取代 UIKit,不是吗?某种程度上,的确是这样的,但在 SwiftUI 发布之前,几乎所有的 iOS 应用都基于 UIKit 构建,意味着市面上可能有数以亿计的 UIKit 代码。因此,假如你想把 SwiftUI 集成到现有的项目中去,你就需要学会如何让两者协同工作。
很有一个原因,并且我希望它不会一直是原因:现在 Apple 框架的许多地方还没有 SwiftUI 的封装,也就是说,如果你想要集成 MapKit,Safari,或者其他重要的 APIs,你需要学会封装它们,以便给 SwiftUI 使用。我得坦诚地说,让它们工作起来的代码并不算优美,但在目前这个节点,你还需要掌握它们。
跟之前一样,我们用 Single View App 模板创建一个新的 iOS 应用,然后取名 “Instafilter”。
译自 www.hackingwithswift.com/books/ios-s…
属性包装器是如何构成结构体的?
你已经了解了 SwiftUI 是如何让我们在结构体中使用 @State 属性包装器来存储会改变的数据,如何用 $ 绑定状态到 UI 控件,以及状态的变化时如何自动地触发 SwiftUI 重新调用结构体的 body 属性。
这一切使得我们可以编写像下面这样的代码:
struct ContentView: View {
@State private var blurAmount: CGFloat = 0
var body: some View {
VStack {
Text("Hello, World!")
.blur(radius: blurAmount)
Slider(value: $blurAmount, in: 0...20)
}
}
}运行代码,你会发现,通过拖拉滑竿改变模糊值,我们能够调整文本标签的模糊程度,正如你预期的那样。
现在,假如我们要让绑定能够基于模糊效果半径的变化做更多的事,比如说我们要把这个值存入 UserDefault,或者运行一个方法,又或者只是为了调试而输出这个值,你可能会这样修改属性:
@State private var blurAmount: CGFloat = 0 {
didSet {
print("New value is \(blurAmount)")
}
}运行代码,你要失望了:当你拖动滑竿时,模糊数值是变化了,但你不会看到 print() 语句被触发 —— 实际上,什么也不会输出。
为了理解这里头发生的事情,我需要你回忆我们学习 Core Data 时的情形:我们用了 @FetchRequest 属性包装器来查询数据,同时我还向你展示了如何对创建 FetchRequest 做出更多精细的控制。
属性包装器之所以名叫属性包装器是因为它们通过另一个结构体来封装我们的属性。对于许多属性包装器来说,用于封装的结构体来包装器本身同名,但对于 @FetchRequest 我向你展示了,我们实际上是读取的是fetched results —— 而不是请求。
也就是说,当我们用 @State 封装字符串时,我们得到的实际类型是一个 State。类似的,当我们用 @Environmentand 封装其他类型时,我们得到的是一个包含了其他类型的 Environment 结构体。
之前我解释过我们无法直接修改视图的属性,因为它们是结构体,属性是固定的。现在,你知道了 @State 本身也产出结构体,那我们就奇怪了:这个结构体是如何被修改的呢?
Xcode 有一个很有用的命名叫 “Open Quickly” (快捷键是 Cmd+Shift+O ),可以让你查找项目中任意的文件或者类型,亦或是你导入的某个框架。激活它,然后输入 “State” —— 希望第一个结果就是 SwiftUI 里的 State,假如不是的话,你需要找到正确的结果并选中。
你会被带到一个 SwiftUI 的生成接口文件,这里基本上汇聚了 SwiftUI 暴露给我们的所有接口。这里没有实现的代码,只是许多协议,结构体,modifier 等的定义。
我们找到 State 看一下,第一行代码是这样的:
@propertyWrapper public struct State<Value> : DynamicProperty {@propertyWrapper 属性的作用正是把这个结构体变成 @State 给我们用。
然后再往下看,你会看到这样一行:
public var wrappedValue: Value { get nonmutating set }封装的值时我们实际要存储的值,比如一个字符串。这个生成接口告诉我们的信息是,这个属性可以被读取(get),也可以被写入(set),但当我们设置它的值时,结构体本身不会实际改变。在幕后,它会把值发送给 SwiftUI,存储到一个专门的地方。在这里值可以被自由地修改。所以说,结构体本身没有发生改变仍然是成立的。
你已经了解到这些知识,让我们返回无效的代码:
@State private var blurAmount: CGFloat = 0 {
didSet {
print("New value is \(blurAmount)")
}
}表面上看,上述代码的诉求是“当 blurAmount 改变时,打印它的新值”。然后,由于 @State实际上封装了它的内容,我们实际的表述变成当封装 blurAmount 的 State 改变时,打印新的 blurAmount。
还跟得上我的节奏吧?那我们就继续:你已经知道 State 用了一个 nonmutating setter 来封装它的值,意味着不管是 blurAmount ,或者是封装它的 State 结构体,其实都没有在变化 —— 我们的绑定是在直接修改内部存储的值,换句话说,属性观察者永远都不会被触发。
那我们要怎么达到我们的目的呢?—— 如何把我们的函数附加给被封装的属性?对此,我们需要用到自定义绑定 —— 让我们下文分解...
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~