SwiftUI学习100天(Day63 - 项目 13,第二部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

今天,我们继续检查我们项目的技术,并且我们开始更多地涉足 SwiftUI 感觉不太好用的地方。今天你将看到 Core Image 如何与 SwiftUI 集成,答案是“不是很好”。我们还将开始研究 UIKit 如何与 SwiftUI 集成,同样,答案也不是很好——我们需要投入大量工作才能将 UIKit 的圆钉挤入 SwiftUI 形孔中。
我想在这里看到更好的东西吗?绝对 - 也许它会在 SwiftUI 的未来更新中出现。但有一句匿名的话我觉得用在这里很合适:“永远不要让你想要的东西,让你忘记你拥有的东西。”
是的,SwiftUI 与其他框架的集成现在有点不稳定,但这并不意味着它应该有损于 SwiftUI 为我们所做的其他伟大工作。
今天你只有两个主题要完成,其中你将学习如何使用 Core Image 操作图像,以及如何为 SwiftUI 包装 UIKit 代码。

将 Core Image 与 SwiftUI 集成
就像 Core Data 是 Apple 用于处理数据的内置框架一样,Core Image 是他们用于处理图像的框架。这不是绘图,或者至少在很大程度上它不是绘图,而是关于更改现有图像:应用锐化、模糊、晕影、像素化等。如果你曾经使用过 Apple 的 Photo Booth 应用程序中可用的所有各种照片效果,那应该会让你很好地了解 Core Image 的用途!
然而,Core Image 并没有很好地集成到 SwiftUI 中。事实上,我甚至不会说它很好地集成到 UIKit 中——Apple 做了一些工作来提供助手,但它仍然需要相当多的思考。不过请坚持我的看法:一旦你了解了它的工作原理,结果就会非常出色,并且你会发现它在未来为你的应用程序打开了一个完整的功能范围。
首先,我们将输入一些代码来为我们提供一个基本图像。我将以一种稍微奇怪的方式来构建它,但是一旦我们混合在 Core Image 中它就会有意义:我们将创建Image
视图作为一个可选@State
属性,强制它与屏幕宽度相同,然后添加onAppear()
修饰符以实际加载图像。
将示例图像添加到你的资产目录,然后将你的ContentView
结构修改为:
首先,请注意 SwiftUI 处理可选视图的流畅程度——它就是这么好用!但是,请注意我是如何将onAppear()
修改器附加到VStack
的图像周围的,因为如果可选图像是nil
那么它不会触发该onAppear()
功能。
无论如何,当该代码运行时,它应该会显示你添加的示例图像,并按比例整齐地适合屏幕。
现在是复杂的部分:实际上什么是Image
?如你所知,它是一个view,这意味着我们可以在 SwiftUI 视图层次结构中定位和调整它的大小。它还处理从我们的资产目录和 SF Symbols 加载图像,并且它也能够从少数其他来源加载图像。然而,最终它是要显示的东西——我们不能将它的内容写入磁盘或以其他方式转换它们,只能应用一些简单的 SwiftUI 过滤器。
如果我们想使用 Core Image,SwiftUI 的Image
视图是一个很好的终点,但在其他地方使用它没有用。也就是说,如果我们想要动态创建图像、应用 Core Image 滤镜、将它们保存到用户的照片库等,那么 SwiftUI 的图像就不能胜任这项工作。
Apple 为我们提供了其他三种图像类型供我们使用,如果我们想使用 Core Image,我们需要巧妙地使用所有这三种图像类型。它们听起来可能很相似,但它们之间有一些微妙的区别,如果你想从 Core Image 中获得任何有意义的东西,正确使用它们很重要。
除了 SwiftUI 的Image
视图之外,其他三种图像类型是:
UIImage
,它来自 UIKit。这是一种极其强大的图像类型,能够处理各种图像类型,包括位图(如 PNG)、矢量(如 SVG),甚至是构成动画的序列。UIImage
是 UIKit 的标准图像类型,在这三个图像类型中它最接近 SwiftUI 的Image
类型。CGImage
,来自 Core Graphics。这是一种更简单的图像类型,实际上只是一个二维像素阵列。CIImage
,来自 Core Image。这存储了生成图像所需的所有信息,但除非被要求,否则实际上不会将其转换为像素。Apple 称其CIImage
为“图像配方”而不是实际图像。
各种图像类型之间存在一些互操作性:
我们可以从
CGImage
创建一个
UIImage
,并从UIImage
创建一个
。CGImage
我们可以从一个
UIImage
和一个CGImage
创建一个CIImage
,也可以从CIImage
创建一个
CGImage
。我们可以从一个
UIImage
和一个CGImage
创建一个 SwiftUIImage
。
我知道,我知道:这很令人困惑,但希望你看到代码后会感觉好些。重要的是这些图像类型是纯数据——我们不能将它们放入 SwiftUI 视图层次结构中,但我们可以自由地操作它们,然后在 SwiftUI Image
中呈现结果。
我们将进行更改loadImage()
,以便UIImage
从我们的示例图像创建一个,然后使用 Core Image 对其进行操作。更具体地说,我们将从两个任务开始:
我们需要将我们的示例图像加载到一个
UIImage
中,它有一个初始化程序叫做UIImage(named:)
来从我们的资产目录加载图像。它返回一个可选的UIImage
,因为我们可能指定了一个不存在的图像。我们会将其转换为
CIImage
,这是 Core Image 想要使用的。
所以,首先用这个替换你当前的loadImage()
实现:
下一步将是创建一个 Core Image 上下文和一个 Core Image 过滤器。过滤器是完成以某种方式转换图像数据的实际工作的东西,例如模糊它、锐化它、调整颜色等等,上下文负责将处理过的数据转换成CGImage
我们可以使用的数据。
这两种数据类型都来自 Core Image,因此你需要添加两个导入以使它们可供我们使用。所以请首先在 ContentView.swift 的顶部附近添加这些:
接下来我们将创建上下文和过滤器。对于这个例子,我们将使用棕褐色调滤镜,它应用棕色调,使照片看起来像是很久以前拍的。
所以,// more code to come
用这个替换评论:
我们现在可以自定义我们的过滤器来改变它的工作方式。Sepia 是一个简单的滤镜,因此它只有两个有趣的属性:inputImage
是我们要更改的图像,以及intensity
应用棕褐色效果的强度,在 0(原始图像)和 1(全棕褐色)范围内指定。
所以,在前两行下面添加这两行代码:
这些都不是很难,但这里有一些变化:我们需要将过滤器的输出转换为Image
可以在视图中显示的 SwiftUI。这是我们需要同时依赖所有四种图像类型的地方,因为最简单的事情是:
从我们的过滤器中读取输出图像,这将是一个
CIImage
. 这可能会失败,所以它返回一个可选的。CGImage
要求我们的上下文从该输出图像创建一个。这也可能会失败,所以它再次返回一个可选的。将其
CGImage
转换为UIImage
.将其
UIImage
转换为 SwiftUIImage
。
你可以直接从一个CGImage
转到 SwiftUI Image
,但它需要额外的参数,而且只会增加更多的复杂性!
这是 loadImage()
的最终代码
:
如果你再次运行该应用程序,你应该会看到你的示例图像现在应用了棕褐色效果,这都要归功于 Core Image。
现在,你可能会认为仅仅为了获得一个相当简单的结果就需要做大量的工作,但是既然你已经掌握了 Core Image 的所有基础知识,那么切换到不同的过滤器就相对容易了。
话虽这么说,Core Image 有点……好吧……让我们说“创意”吧。它早在 iOS 5.0 中就被引入,到那时 Swift 已经在 Apple 内部开发,但你真的不知道它——在最长的时间里,它的 API 是你能想象到的最不 Swifty 的东西,尽管 Apple 已经慢慢有时你别无选择,只能挖掘它的弱点。
首先,让我们看看现代 API——我们可以用像这样的像素滤镜替换我们的棕褐色调:
当它运行时,你会看到我们的图像看起来像素化了。比例为 100 应该意味着像素跨度为 100 点,但因为我的图像太大,像素相对较小。
现在让我们试试这样的水晶效果:
或者我们可以像这样添加一个旋转失真过滤器:
因此,我们可以仅使用现代 API 做很多事情。但是对于这个项目,我们将使用旧的 API 来设置值,例如radius
和scale
因为它允许我们动态设置值——我们可以从字面上询问当前过滤器它支持什么值,然后将它们发送进去。
这是它的样子:
有了它,你现在可以将旋转失真更改为任何其他过滤器,并且代码将继续工作 - 每个调整值仅在支持时才发送。
请注意它是如何依赖于键的设置值的,这可能会让你想起UserDefaults
的工作方式
。事实上,所有这些kCIInput
键都在幕后实现为字符串,因此它比你可能意识到的还要相似!
如果你要实施精确的 Core Image 调整,你绝对应该使用使用精确属性名称和类型的较新 API,但在这个项目中,较旧的 API 很有用,因为它允许我们发送调整,而不管实际使用的是什么过滤器。



在 SwiftUI 视图中包装 UIViewController
SwiftUI 是一个非常棒的构建应用程序的框架,但现在它还远未完成——有很多事情它做不到,所以如果你想添加更多高级功能,你需要学会与 UIKit 对话。有时这是为了集成你为 UIKit 编写的现有代码(例如,如果你在一家拥有现有 UIKit 应用程序的公司工作),但有时这是因为 UIKit 和 Apple 的其他框架为我们提供了我们想要展示的有用代码在 SwiftUI 布局中。
在这个项目中,我们将要求用户从他们的照片库中导入图片。Apple 的 API 带有用于执行此操作的专用代码,但尚未移植到 SwiftUI,因此我们需要自己编写该桥接器。相反,它内置于一个名为 PhotosUI 的单独框架中,该框架旨在与 UIKit 一起使用,因此需要我们了解 UIKit 的工作方式。
在我们编写代码之前,你需要了解三件事,它们都有点像 UIKit 101,但如果你只使用过 SwiftUI,它们对你来说将是新的:
UIKit 有一个名为
UIView
的类
,它是布局中所有视图的父类。因此,标签、按钮、文本字段、滑块等等——这些都是视图。UIKit 有一个名为
UIViewController
的类
,它旨在保存所有代码,使视图栩栩如生。就像UIView
,UIViewController
有很多做不同种类工作的子类。UIKit 使用一种称为委托的设计模式来决定工作发生的位置。因此,在决定如何响应文本字段更改时,我们将创建一个具有我们功能的自定义类,并将其作为我们文本字段的委托。
所有这些都很重要,因为要求用户从他们的库中选择照片使用了一个名为 PHPickerViewController
的视图控制器
和委托协议PHPickerViewControllerDelegate
。SwiftUI 不能直接使用这两个,所以我们需要对它们进行包装。
我们将从简单的方式开始。包装 UIKit 视图控制器需要我们创建一个符合UIViewControllerRepresentable
协议的结构。
因此,按 Cmd+N 创建一个新文件,选择 Swift File,并将其命名为 ImagePicker.swift。添加import PhotosUI
和import SwiftUI
到新文件的顶部,然后给它这个代码:
该协议建立在View
之上
,这意味着我们定义的结构可以在 SwiftUI 视图层次结构中使用,但是我们不提供body
属性,因为视图的主体是视图控制器本身——它只显示 UIKit 发回的内容。
符合UIViewControllerRepresentable
要求我们用两种方法填充该结构:一种称为makeUIViewController()
,负责创建初始视图控制器,另一种称为updateUIViewController()
,旨在让我们在某些 SwiftUI 状态更改时更新视图控制器。
这些方法具有非常精确的签名,因此我将向你展示一个简洁的快捷方式。这些方法很长的原因是因为 SwiftUI 需要知道我们的结构正在包装什么类型的视图控制器,所以如果我们直接告诉 Swift 该类型 Xcode 将帮助我们完成剩下的工作。
现在将此代码添加到结构中:
这些代码不足以正确编译,但是当 Xcode 显示错误“Type ImagePicker does not conform to protocol UIViewControllerRepresentable”时,请单击错误左侧的红色和白色圆圈并选择“修复”。这将使 Xcode 编写我们实际需要的两个方法,事实上,这些方法实际上足以让 Swift 确定视图控制器类型,因此你可以删除该typealias
行。
你应该有这样的结构:
我们不会使用updateUIViewController()
,所以你可以从那里删除“代码”行,这样该方法就是空的。
但是,该makeUIViewController()
方法很重要,因此请将其现有的“代码”行替换为:
这会创建一个新的照片选择器配置,要求它仅向我们提供图像,然后使用它来创建并返回一个PHPickerViewController
执行选择图像的实际工作的。
我们很快会添加更多代码,但这实际上是我们包装基本视图控制器所需的全部。
我们的ImagePicker
结构是一个有效的 SwiftUI 视图,这意味着我们现在可以像任何其他 SwiftUI 视图一样在工作表中显示它。这个特殊的结构是为了显示图像而设计的,所以我们需要一个可选的Image
视图来保存选定的图像,以及一些确定工作表是否显示的状态。
用这个替换你当前的ContentView
结构:
继续并在模拟器或你的真实设备上运行它。当你点击按钮时,默认的 iOS 图像选择器应该向上滑动,让你浏览所有照片。
但是,选择图像时不会发生任何事情,取消按钮也不会执行任何操作。你看,即使我们已经创建并展示了一个有效的 PHPickerViewController
,我们实际上并没有告诉它如何响应用户交互。
要做到这一点需要一个全新的概念:协调员。


