美文网首页Hacking with iOS: SwiftUI Edition
Hacking with iOS: SwiftUI Editio

Hacking with iOS: SwiftUI Editio

作者: 韦弦Zhy | 来源:发表于2020-07-15 20:10 被阅读0次

    \color{red}{\Large \mathbf{Hacking \quad with \quad iOS: SwiftUI \quad Edition}}

    {\Large \mathbf{Instafilter,\ part \ 2}}

    使用 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 Editio

        本文链接:https://www.haomeiwen.com/subject/bnascktx.html