Hacking with iOS: SwiftUI Editio
使用 ActionSheet
自定义滤镜
到目前为止,我们已经将SwiftUI,UIImagePickerController
和 Core Image 集成在一起,但是该应用程序仍然没有什么用处——毕竟棕褐色调效果并不那么有趣。
为了使整个应用程序更好,我们将让用户自定义要应用的滤镜,然后使用操作表来完成此操作。在 iPhone上,这是一个从屏幕底部向上滑动的按钮列表,您可以添加任意数量的按钮——如果确实需要,它甚至可以滚动。
首先,我们需要一个属性来存储是否应该显示操作表,因此将其添加到 ContentView
中:
@State private var showingFilterSheet = false
现在,我们可以使用actionSheet()
修饰符添加一个动作表。这与sheet()
和alert
的工作原理相同:我们为它提供一个要监视的条件,一旦条件变为真,就会显示操作表。
首先在sheet()
下面添加此修饰符:
.actionSheet(isPresented: $showingFilterSheet) {
// action sheet here
}
现在替换修改滤镜的点击事件 // change filter
为如下代码:
self.showingFilterSheet = true
关于在操作表中显示的内容,我们可以提供标题,消息和要显示的按钮数组。这些按钮的工作方式类似于 Alert
:我们提供了文本标题,并提供了一个在选中后将要执行的操作。
对于此应用程序中的操作表,我们希望用户从一系列不同的 Core Image滤镜中进行选择,当他们选择一个时,应将其激活并立即应用。为了完成这项工作,我们将编写一个方法,将currentFilter
修改为他们选择的任何新滤镜,然后立即调用loadImage()
。
我们的计划有点小瑕疵,这是因为Apple包装了Core Image API以使其对 Swift更友好。您会看到,底层的Core Image API完全是字符串类型的,因此Apple创建一系列协议而不是返回一个新类供我们使用。
当我们将 CIFilter.sepiaTone()
分配给属性时,我们得到的是CIFilter
类的对象,该对象恰好符合名为CISepiaTone
的协议。然后,该协议会公开我们一直在使用的强度参数,但在内部它将仅将其映射到对setValue(_:forKey :)
的调用。
这种灵活性实际上对我们有利,因为这意味着我们可以编写适用于所有滤镜的代码,只要我们注意不要发送无效值即可。
因此,让我们开始解决问题。请将您的currentFilter
属性更改为如下形式:
@State private var currentFilter: CIFilter = CIFilter.sepiaTone()
因此,CIFilter.sepiaTone()
返回符合CISepiaTone
协议的CIFilter
对象。添加该显式类型注释意味着我们将丢弃一些数据:就是说该滤镜必须是CIFilter
,但不再必须符合CISepiaTone
。
由于此更改,我们无法访问intensity
属性,这意味着下方代码将不再起作用:
currentFilter.intensity = Float(filterIntensity)
相反,我们需要用对setValue(:_ forKey :)
的调用来替换它。无论如何,这就是协议所做的所有事情,但是它确实提供了宝贵的额外类型安全性。
用下面的代码替换如上的代码:
currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey)
kCIInputIntensityKey
是另一个 Core Image 常量值,其作用与设置棕褐色调滤镜的intensity
参数相同。
进行此更改后,我们可以返回操作表:我们希望能够将该滤镜更改为其他滤镜,然后调用loadImage()
。因此,将此方法添加到ContentView
中:
func setFilter(_ filter: CIFilter) {
currentFilter = filter
loadImage()
}
有了这个,我们现在可以用一系列尝试各种 Core Image 过滤器的按钮替换// action sheet here
处的注释。
ActionSheet(title: Text("Select a filter"), buttons: [
.default(Text("Crystallize")) { self.setFilter(CIFilter.crystallize()) },
.default(Text("Edges")) { self.setFilter(CIFilter.edges()) },
.default(Text("Gaussian Blur")) { self.setFilter(CIFilter.gaussianBlur()) },
.default(Text("Pixellate")) { self.setFilter(CIFilter.pixellate()) },
.default(Text("Sepia Tone")) { self.setFilter(CIFilter.sepiaTone()) },
.default(Text("Unsharp Mask")) { self.setFilter(CIFilter.unsharpMask()) },
.default(Text("Vignette")) { self.setFilter(CIFilter.vignette()) },
.cancel()
])
我们从大量的 Core Image 滤镜中挑选了这些过滤器,但是欢迎您尝试使用代码完成功能尝试其他操作——输入CIFilter
。看看会发生什么!
继续并运行该应用程序,选择一张图片,然后尝试将棕褐色色调更改为 Vignette —— 这会在照片的边缘周围应用暗化效果。(如果您使用的是模拟器,请给他一点时间,因为它很慢!)
现在尝试将其更改为高斯模糊(Gaussian Blur),它应该使图像模糊,但会导致我们的应用崩溃。现在,通过取消过滤器的CISepiaTone
限制,我们现在被迫使用setValue(_:forKey :)
发送值,这根本不提供安全性。在这种情况下,高斯模糊滤镜没有强度值,因此应用程序崩溃了。
为了解决这个问题——并使我们的单个滑块做更多的工作——我们将添加更多代码来读取可与setValue(_:forKey :)
一起使用的所有有效键,并且仅在以下情况下设置强度键当前滤镜支持。使用这种方法,我们实际上可以查询所需数量的键,并设置所有受支持的键。因此,对于棕褐色,它将设置强度,但对于高斯模糊,它将设置半径(模糊的大小),依此类推。
这种有条件的方法将与您选择应用的任何滤镜一起使用,这意味着您可以安全地与其他人进行实验。您唯一需要注意的就是确保将filterIntensity
按比例放大为一个有意义的数字,例如,一个1像素的模糊几乎是不可见的,因此,我将其乘以200使其更大。
替换此行:
currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey)
为:
let inputKeys = currentFilter.inputKeys
if inputKeys.contains(kCIInputIntensityKey) { currentFilter.setValue(filterIntensity, forKey: kCIInputIntensityKey) }
if inputKeys.contains(kCIInputRadiusKey) { currentFilter.setValue(filterIntensity * 200, forKey: kCIInputRadiusKey) }
if inputKeys.contains(kCIInputScaleKey) { currentFilter.setValue(filterIntensity * 10, forKey: kCIInputScaleKey) }
有了这些,您现在就可以安全地运行该应用程序,导入您选择的图片,然后尝试所有各种滤镜——不再有任何崩溃。尝试尝试不同的滤镜和设置,看看会发现什么!
使用 UIImageWriteToSavedPhotosAlbum()
保存图像
为了完成此项目,我们将使“保存”按钮做一些有用的事情:将经过滤镜处理后的照片保存到用户的照片库中,以便他们可以进一步编辑,共享等等。
正如我之前解释的那样,UIImageWriteToSavedPhotosAlbum()
函数可以完成我们需要的所有操作,但是有一个需要注意的地方,那就是它确实需要与SwiftUI中不太匹配的某些代码一起使用:它必须是一个继承自NSObject
的类,有一个标有@objc
的回调方法,然后使用#selector
编译器指令指向该方法。
就像我之前向您展示的一样,我们将把它隔离在一个单独的可重用的类中。创建一个名为 ImageSaver.swift 的新 Swift 文件,将其Foundation 导入更改为 UIKit,然后为其提供以下代码:
class ImageSaver: NSObject {
func writeToPhotoAlbum(image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveError), nil)
}
@objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
// save complete
}
}
我们将稍后再次介绍该功能,以使其更有用,但是首先我们需要确保我们正确地请求用户的照片保存权限:我们需要向 Info.plist 添加一个 key。如果您删除了先前添加的内容,请立即重新添加:
- 打开 Info.plist
- 右键单击空白
- 选择添加行
- key 选择“Privacy - Photo Library Additions Usage Description”。
- value 输入“我们要保存滤镜处理后的照片。”
有了这个,我们现在可以考虑如何使用 ImageSaver 类保存图像。现在,我们要是这样设置
image`属性的:
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
let uiImage = UIImage(cgImage: cgimg)
image = Image(uiImage: uiImage)
}
实际上,您可以直接从CGImage
转到SwiftUI Image
视图,我之前说过我们要通过UIImage
,因为CGImage
等效项需要一些额外的参数。没错,但是现在有一个重要的第二个原因变得很重要:我们需要一个UIImage
发送到我们的ImageSaver
类,这是创建它的理想场所。
因此,向ContentView
添加一个新属性,该属性将存储此中间UIImage
:
@State private var processedImage: UIImage?
现在,我们可以修改applyProcessing()
方法,以便将我们的UIImage
保存起来供以后使用:
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
let uiImage = UIImage(cgImage: cgimg)
image = Image(uiImage: uiImage)
processedImage = uiImage
}
现在,实现“保存”按钮是非常容易的:
Button("保存") {
guard let processedImage = self.processedImage else { return }
let imageSaver = ImageSaver()
imageSaver.writeToPhotoAlbum(image: processedImage)
}
现在,我们可以将其保留在那里,但是我们将ImageSaver
放入其自己的类的全部原因是方便我们可以了解保存是否成功。现在,这会通过ImageSaver
中的方法报告给我们:
@objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
// save complete
}
为了使该结果有用,我们需要使其向上传播,以便我们的ContentView
可以使用它。但是,我不希望@objc
出现,因此我们将隔离出现的混乱状况,使用闭包报告成功或失败——这是Swift开发人员更友好的解决方案。
首先将这两个属性添加到ImageSaver
类中,以表示处理成功和失败的闭包:
var successHandler: (() -> Void)?
var errorHandler: ((Error) -> Void)?
其次,填写didFinishSavingWithError
方法,以便它检查是否提供了错误,并调用这两个闭包之一:
if let error = error {
errorHandler?(error)
} else {
successHandler?()
}
现在,我们可以(如果需要)在使用ImageSaver
类时提供一个或两个闭包,如下所示:
let imageSaver = ImageSaver()
imageSaver.successHandler = {
print("Success!")
}
imageSaver.errorHandler = {
print("Oops: \($0.localizedDescription)")
}
imageSaver.writeToPhotoAlbum(image: processedImage)
尽管代码有很大不同,但是这里的概念与我们使用ImagePicker
所做的相同:我们包装了一些UIKit功能,从而以一种对SwiftUI友好的方式获得了所需的所有行为。更好的是,这为我们提供了另一段可重用的代码,我们可以在将来将其放入其他项目中——我们正在缓慢地建立一个库!
最后一步完成了我们的应用程序,因此继续并再次运行它,然后从头到尾进行尝试——导入图片,应用滤镜,然后将其保存到您的照片库中。做得好!
译自
Customizing our filter using ActionSheet
Saving the filtered image using UIImageWriteToSavedPhotosAlbum()