Hacking with iOS: SwiftUI Edition - 滤镜项目(二)

使用 ActionSheet 自定义滤镜

到目前为止,我们已经将SwiftUIUIImagePickerControllerCore 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()

你可能感兴趣的:(Hacking with iOS: SwiftUI Edition - 滤镜项目(二))