【译】[SwiftUI 100 天]集成 Core Image 到 SwiftUI
2020/5/19 23:25:48
本文主要是介绍【译】[SwiftUI 100 天]集成 Core Image 到 SwiftUI,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
正如 Core Data 是 Apple 内建的操作数据的框架,Core Image 是操作图片的框架。它不是关于绘制,至少其中的大部分不是关于绘制,而是关于修改现有的图片:应用锐化,模糊,暗角,像素化,等等。如果你曾经使用过 Apple 的 Photo Booth 应用里的各种效果,那你对于 Core Image 擅长的东西应该就比较有概念了。
不过,Core Image 并没有很好地集成到 SwiftUI,实际上,即便是 UIKit,也不算集成地很完善 —— Apple 确实提供了一些辅助,但还是挺费心思的。但请你跟随我的步伐:一旦你理解它的工作机制,你会发现从此就打开了一扇新世界的大门。
首先,我们要放一些基础的代码,显示一张图片。我会以一种稍微有点奇怪的方式构筑它,但对于 Core Image 将是合理的:我们要以可选的 @State
属性来创建图像,强制它铺满屏幕宽度,并且在 onAppear()
modifier 里实际加载图像。
添加一个示例图片到你的 asset catalog,然后把 ContentView
修改成这样:
struct ContentView: View { @State private var image: Image? var body: some View { VStack { image? .resizable() .scaledToFit() } .onAppear(perform: loadImage) } func loadImage() { image = Image("Example") } }复制代码
首先,留意一下 SwiftUI 是如何处理可选视图的 —— 它竟然能工作!但是你注意到没,我是在 VStack
的 onAppear()
modifier 里加载图片,而不是 image
,这是因为如果可选图像本身是 nil
,就无法触发 onAppear()
函数了。
不管怎么说,当代码运行起来后,应该要显示你放的示例图片,并且整齐地缩放成铺满屏幕宽度的状态。
然后是复杂的部分:一个 Image
? 本质上是什么。正如你知道的,它是一个 视图,意味着我们可以在 SwiftUI 视图层级里放置它。它也处理从我们的 asset catalog 和 SF Symbols 加载图片的事情,同时也能够从许多其他资源里加载。但是,最终它是一种用于显示的东西 —— 我们不能把它的内容写进磁盘,或者给它应用滤镜。
假如要使用 Core Image,SwiftUI 的 Image
视图是一个很好的终点,但并不能在各个环节都发挥作用,包括动态地创建图像,应用 Core Image 滤镜,保存到用户相册,等等。SwiftUI 的图像对这些是无能为力的。
Apple 给了我们三种图像类型,在我们使用 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
。 - SwiftUI 的
Image
既可以通过UIImage
创建,也可以通过CGImage
创建。
是不是有点绕晕了?希望你看到代码的时候感觉好一点。其中的关键在于,这些图像类型都是纯数据 —— 我们无法将它们直接放置到 SwiftUI 的视图层级中,但我们可以自由地维护这些图像,然后以 SwiftUI 的 Image
来呈现。
我们将要修改 loadImage()
以便从示例图片从创建出 UIImage
,然后用 Core Image 维护它。具体地,包含两个任务:
- 我们需要将示例图片载入
UIImage
,它有一个叫UIImage(named:)
的构造器,可以从我们的 asset catalog 中加载图像。由于图像可能不存在,所以它返回的是UIImage
可选型。 - 我们将转换成
CIImage
,以便使用 Core Image 处理。
现在,把 loadImage()
实现替换成下面这样:
func loadImage() { guard let inputImage = UIImage(named: "Example") else { return } let beginImage = CIImage(image: inputImage) // more code to come }复制代码
下一步是创建 Core Image 上下文和 Core Image 滤镜。滤镜负责实际变换图像数据工作的东西,比如模糊图像,锐化图像,调整颜色,等等,而上下文则负责将处理好的数据转成 CGImage
,以便我们能使用。
下面两个数据类型都来自 Core Image,因此你需要添加两条 import:
import CoreImage import CoreImage.CIFilterBuiltins复制代码
对于这个例子,我们要使用墨色调滤镜,它会应用一个墨色调到照片上,让照片看起来就像老照片。
把 // more code to come
注释替换成:
let context = CIContext() let currentFilter = CIFilter.sepiaTone()复制代码
我们可以自定义滤镜的参数,墨色调滤镜相对简单,只有两个属性:inputImage
是我们要改变的图像, 而 intensity
是墨色效果要应用的强度,范围从 0 (原始图像)到 1(完全的墨色调)。
把下面两行代码添加到之前的两行代码之后:
currentFilter.inputImage = beginImage currentFilter.intensity = 1复制代码
所有这些都不难,不过这里就要转折了:我们需要把滤镜的输出转成一个 SwiftUI Image
,以用于视图的显示。
- 从滤镜中读取输出文件,它是一个
CIImage
,因为可能失败,所以返回可选型。 - 让上下文基于输出图像创建一个
CGImage
,这同样可能失败,所以也是返回可选型。 - 从
CGImage
生成UIImage
- 再把
UIImage
变成 SwiftUIImage
。
你可以直接从 CGImage
得到 SwiftUI Image
,但需要传入额外的参数,这样只会更复杂。
loadImage()
的最终代码如下:
// get a CIImage from our filter or exit if that fails guard let outputImage = currentFilter.outputImage else { return } // attempt to get a CGImage from our CIImage if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) { // convert that to a UIImage let uiImage = UIImage(cgImage: cgimg) // and convert that to a SwiftUI image image = Image(uiImage: uiImage) }复制代码
再次运行应用,你会看到一张看起来像老照片的示例图片,这都要归功于 Core Image。
现在,你不免会开始想,我只要这么简单的一个结果,怎么需要这么多的工作?但想想看,上面的代码基本上了覆盖了 Core Image 的常规操作,相对来说,你要切换不同的滤镜效果是很简单的。
前面提到过,Core Image 有一点… 怎么说呢?… 不如说是 “创造性” 吧。它在 iOS 5.0 就引入了,彼时 Swift 已经在苹果内部开发了,但你不会想知道 —— 长期以来, Core Image 的 API 对 Swift 难以想象地不友好。尽管苹果目前已经逐步擦除其中的遗留 API,但 Core Image 仍有一些地方的行为比较怪异。
让我们举例说明吧,把墨色调滤镜替换成像素风滤镜:
let currentFilter = CIFilter.pixellate() currentFilter.inputImage = beginImage currentFilter.scale = 100复制代码
运行代码,你会发现图像看起来就像棋盘格一样。100 的 scale
表示跨度是 100 个点,但由于我的图像很大,像素相对来很小。
现在,让我们尝试一下水晶效果:
let currentFilter = CIFilter.crystallize() currentFilter.inputImage = beginImage currentFilter.radius = 200复制代码
代码运行起来我们本应该看到一个水晶效果,但实际上代码会崩溃。我们的代码是合法的 Swift 和 Core Image 代码,但就是无法工作。
这其实是一个 bug,在你阅读本文时可能已经修复了,假如我们切换到一个更老的 API,像下面这样:
let currentFilter = CIFilter.crystallize() currentFilter.setValue(beginImage, forKey: kCIInputImageKey) currentFilter.radius = 200复制代码
kCIInputImageKey
是一个指定滤镜要处理的图片的常量,深挖进去你会发现它实际上是一个字符串 —— Core Image 在幕后其实一套基于字符串的 API。
当你发现只有部分 Apple 的 Core Image 滤镜是很好地适配了 Swift API 这个事实时,就会发现我提出的问题愈发明显了。举个例子,假如你想要一个旋转扭曲的滤镜效果,你只能使用老版的 API,这就有点痛苦了:
- 用滤镜名创建
CIFilter
实例 - 重复多次调用
setValue()
,每次用不同的 key。 - 因为
CIFilter
不是一个特定的滤镜,Swift 允许我们传入这个滤镜可能并不支持的参数。
举个例子,使用旋转扭曲滤镜的代码如下:
guard let currentFilter = CIFilter(name: "CITwirlDistortion") else { return } currentFilter.setValue(beginImage, forKey: kCIInputImageKey) currentFilter.setValue(2000, forKey: kCIInputRadiusKey) currentFilter.setValue(CIVector(x: inputImage.size.width / 2, y: inputImage.size.height / 2), forKey: kCIInputCenterKey)复制代码
提示: CIVector
是 Core Image 中存储点和方向的类型。
运行代码,你会看到结果还是不赖的,希望 Apple 在未来的时间里继续清理 API。
尽管新的 API 更易用,为了能够使用任意种类的滤镜,我们在这个项目中主要会使用老 API。
我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~
这篇关于【译】[SwiftUI 100 天]集成 Core Image 到 SwiftUI的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值