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

Hacking with iOS: SwiftUI Editio

作者: 韦弦Zhy | 来源:发表于2020-07-09 18:44 被阅读0次

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

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

    创建基础UI

    我们项目的第一步是构建基本的用户界面,此应用程序将是:

    1. 一个NavigationView,因此我们可以在顶部显示我们的应用名称。
    2. 一个大的灰色框,上面写着“轻按以选择图片”,我们将在上面放置导入的图片。
    3. “强度”滑块将影响我们应用核心图像滤镜的强度,该滤镜存储为0.0到1.0之间的值。
    4. 一个“保存”按钮,将修改后的图像写到用户的照片库中。

    最初,用户不会选择图片,因此我们将使用@State可选图片属性表示该图片。

    首先将这两个属性添加到ContentView中:

    @State private var image: Image?
    @State private var filterIntensity = 0.5
    

    现在,将其body属性的内容修改为如下内容:

    NavigationView {
        VStack {
            ZStack {
                Rectangle()
                    .fill(Color.secondary)
    
                // display the image
            }
            .onTapGesture {
                // select an image
            }
    
            HStack {
                Text("强度")
                Slider(value: self.$filterIntensity)
            }.padding(.vertical)
    
            HStack {
                Button("修改滤镜") {
                    // change filter
                }
    
                Spacer()
    
                Button("保存") {
                    // save the picture
                }
            }
        }
        .padding([.horizontal, .bottom])
        .navigationBarTitle("Instafilter")
    }
    

    那里有很多占位符,我们将在完成此项目的过程中逐一填充它们。

    现在,我要重点关注以下注释:// display the image。如果我们有一个图像,则需要在此处显示所选图像,否则,我们应该显示一个提示,告诉用户点击该区域以触发图像选择。

    现在,您可能会认为,这是一个使用if let的绝佳的使用场所,即使用如下内容替换该注释:

    if let image = image {
        image
            .resizable()
            .scaledToFit()
    } else {
        Text("点击以选择图片")
            .foregroundColor(.white)
            .font(.headline)
    }
    

    但是,如果您尝试进行构建,将会发现它不起作用——会出现“包含闭包的控制流语句不能与函数构建器ViewBuilder一起使用(Closure containing control flow statement cannot be used with function builder ViewBuilder)”的错误消息。

    Swift试图说的是,它仅支持SwiftUI布局内的少量逻辑——我们可以使用if someCondition,但是if let, for, while, switch等,则不能使用。

    实际上,这里发生的是,Swift能够将if someCondition转换为称为ConditionalContent的特殊内部视图类型:它存储条件以及真假视图,并可以在运行时进行检查。但是,if let创建一个常量,并且switch可以有任意多种情况,则都不能使用。

    因此,这里的解决方法是替换成简单的条件,然后依靠SwiftUI对可选视图的支持:

    if image != nil {
        image?
            .resizable()
            .scaledToFit()
    } else {
        Text("点击以选择图片")
            .foregroundColor(.white)
            .font(.headline)
    }
    

    该代码现在将编译,并且由于图像为nil,因此您应该在灰色矩形上方看到“点击以选择图片”提示。

    注意:Xcode 12 之前,以上内容成立,译者我,已经更新了 Xcode 12 beta ,if let SwiftUI 已经优化并可以使用

    使用UIImagePickerController将图像导入SwiftUI

    为了使该项目栩栩如生,我们需要让用户从图库中选择一张照片,然后将其显示在ContentView中。我已经向您展示了这一切的工作原理,因此现在只需要将其放入我们的应用即可——希望这次可以使它更有意义!

    首先制作一个名为 ImagePicker.swift 的新Swift文件,将其“Foundation”导入替换为“SwiftUI”,然后为其提供以下基本结构体:

    struct ImagePicker: UIViewControllerRepresentable {
        @Environment(\.presentationMode) var presentationMode
        @Binding var image: UIImage?
    
        func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
            let picker = UIImagePickerController()
            return picker
        }
    
        func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
    
        }
    }
    

    如果您还记得的话,使用UIViewControllerRepresentable意味着ImagePicker已经是一个SwiftUI视图,可以将其放置在视图层次结构体中。在这种情况下,我们包装了UIKit的UIImagePickerController,它使用户可以从他们的照片库中选择一些东西。

    创建该ImagePicker结构体时,SwiftUI将自动调用其makeUIViewController()方法,该方法将继续创建并发送回UIImagePickerController。但是,我们的代码实际上并没有响应图像选择器中的任何事件——用户可以搜索图像并选择该图像以关闭视图,但是我们对此不做任何事情。

    UIKit并没有使我们创建UIImagePickerController的子类,而是使用了委派系统:我们创建了一个自定义类,当发生有趣的事情时会告诉您。每个委托类通常需要符合一个或多个协议,在我们的情况下,这意味着UINavigationControllerDelegateUIImagePickerControllerDelegate。代理的工作就像现实中的代理一样——如果您将工作委托给其他人,则意味着您要把工作交给他们完成。

    SwiftUI通过让我们定义属于该结构体的协调器来处理这些委托类。此类可以完成我们需要做的所有事情,包括充当UIKit组件的委托,然后我们可以将任何相关信息传递回拥有它的ImagePicker

    首先将其添加为ImagePicker中的嵌套类:

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: ImagePicker
    
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
    }
    

    您可以看到它符合我们使用UIKit的图像选择器所需的两个协议,并且还继承自NSObjectNSObject是 UIKit 的大多数类型的基类。

    因为我们的协调器类符合UIImagePickerControllerDelegate协议,所以我们可以通过修改makeUIViewController()使其成为UIKit 图像选择器的委托:

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }
    

    我们需要对ImagePicker进行两项更改,以使其有用。第一个是添加一个makeCoordinator()方法,该方法告诉 SwiftUI 将Coordinator 类用于ImagePicker协调器。从我们的角度来看,这很明显,因为我们在ImagePicker结构内部创建了一个名为Coordinator的类,但是该makeCoordinator()方法使我们可以控制协调器的创建方式。

    如果您还记得的话,我们给Coordinator类一个单一的属性:let parent:ImagePicker。这意味着我们需要参考拥有它的图像选择器来创建它,以便协调器可以转发有趣的事件。因此,在我们的makeCoordinator()方法中,我们将创建一个Coordinator对象并传入self

    现在将此方法添加到ImagePicker结构体中:

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    

    ImagePicker的最后一步是为协调器提供某种功能。UIImagePickerController类寻找两种方法,但是在这里我们只使用一种方法:didFinishPickingMediaWithInfo。当用户选择了图像时,将调用此方法,并且将提供有关所选图像的信息字典。

    为了使ImagePicker有用,我们需要在Coordinator中实现该方法,使其设置其父ImagePickerimage属性,然后关闭视图。

    UIKit 的方法名称又长又复杂,因此最好使用代码补全来编写。在Coordinator类中留一些空间,然后键入“didFinishPicking”,然后按回车键以让Xcode为您填充整个方法。现在修改它以具有以下代码:

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
        if let uiImage = info[.originalImage] as? UIImage {
            parent.image = uiImage
        }
    
        parent.presentationMode.wrappedValue.dismiss()
    }
    

    这样就完成了 ImagePicker.swift ,所以请回到 ContentView.swift ,以便我们可以使用它。

    首先,我们需要一个@State布尔值来跟踪是否显示图像选择器,因此首先将其添加到ContentView中:

    @State private var showingImagePicker = false
    

    其次,当点击大灰色矩形时,我们需要将该Boolean设置为true,因此用下面代码替换// select an image注释:

    self.showingImagePicker = true
    

    第三,我们需要一个属性来存储用户选择的图像。我们为ImagePicker结构体提供了一个@Binding属性,该属性附加到UIImage上,这意味着在创建图像选择器时,我们需要传递一个UIImage才能链接到它。当@Binding属性更改时,外部值也会更改,这使我们可以读取该值。

    因此,将此属性添加到ContentView

    @State private var inputImage: UIImage?
    

    第四,我们需要一个在关闭ImagePicker视图后将会调用的方法。现在,这只是将所选图像直接放置到UI中,因此请立即将此方法添加到ContentView中:

    func loadImage() {
        guard let inputImage = inputImage else { return }
        image = Image(uiImage: inputImage)
    }
    

    最后,我们需要在ContentView中的某个地方添加一个sheet()修饰符。这将使用showingImagePicker作为其条件,将引用loadImage作为其onDismiss参数,并提供一个绑定到inputImageImagePicker作为其内容。

    因此,将其直接添加到现有的navigationBarTitle()修饰符下方:

    .sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
        ImagePicker(image: self.$inputImage)
    }
    

    这就完成了包装UIKit视图控制器以在SwiftUI中使用所需的所有步骤。这次我们更快地通过了它,但是希望它仍然有意义!

    继续并再次运行该应用程序,您应该可以点击灰色矩形以导入图片,找到图片后,它将出现在我们的用户界面中。

    提示:我们刚才创建的ImagePicker视图是完全可重用的——您可以将Swift文件放到一边,然后轻松地将其用于其他项目。如果考虑一下,包装视图的所有复杂性都包含在 ImagePicker.swift 中,这意味着如果您选择在其他地方使用它,则只需要显示一个sheet()然后绑定一个Image即可。

    使用 Core Image 使用基本滤镜对图片处理

    既然我们的项目有一个用户选择的图像,下一步就是让用户对其应用不同的Core Image 滤镜。首先,我们将只使用一个滤镜,但不久之后,我们将使用操作表对其进行扩展。

    如果要在应用程序中使用Core Image,我们首先需要在 ContentView.swift 的顶部添加两个导入:

    import CoreImage
    import CoreImage.CIFilterBuiltins
    

    接下来,我们需要上下文和滤镜。Core Image 上下文是负责将CIImage渲染为CGImage的对象,或者更实际地说是用于将图片转换为我们可以使用的真实像素序列。创建上下文非常昂贵,因此,如果您打算渲染许多图像,则最好一次创建一个上下文并将其保持活动状态。至于滤镜,我们将使用CISepiaTone作为默认设置,但是由于稍后将使它变得灵活,我们对滤镜使用@State,以便可以对其进行更改。

    因此,将这两个属性添加到 ContentView 中:

    @State private var currentFilter = CIFilter.sepiaTone()
    let context = CIContext()
    

    有了这两个之后,我们现在可以编写一种方法来处理导入的任何图像——这意味着它将根据filterIntensity中的值设置棕褐色滤镜的强度,从滤镜中读取输出图像,要求CIContext渲染它,然后将结果放入我们的图片属性中,以便在屏幕上可见。

    func applyProcessing() {
        currentFilter.intensity = Float(filterIntensity)
    
        guard let outputImage = currentFilter.outputImage else { return }
    
        if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
            let uiImage = UIImage(cgImage: cgimg)
            image = Image(uiImage: uiImage)
        }
    }
    

    下一个工作是更改loadImage()的工作方式。现在赋值给image属性,但我们不再想要了。相反,它应该将选择的任何图像传递到棕褐色调滤镜中,然后调用applyProcessing()使魔术发生。

    Core Image 滤镜具有专用的inputImage属性,可让我们发送CIImage供滤镜使用,但通常会彻底损坏并导致您的应用程序崩溃——使用滤镜的setValue()方法和键kCIInputImageKey会更安全。

    因此,将您现有的loadImage()方法替换为:

    func loadImage() {
        guard let inputImage = inputImage else { return }
    
        let beginImage = CIImage(image: inputImage)
        currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
        applyProcessing()
    }
    

    如果您现在运行代码,您将看到我们的应用程序基本流程很好:我们可以选择一张图片,然后应用棕褐色效果查看它。但是,即使我们将其添加到了它的强度滑块上,它也没有任何作用,即使从filterIntensity中读取到了我们绑定的值。

    这里发生的事情应该不会太令人惊讶:即使滑块正在更改filterIntensity的值,更改该属性也不会自动再次触发我们的applyProcessing()方法。取而代之的是,我们需要手动执行此操作,这并不像在filterIntensity上创建属性观察器那样容易,因为由于使用了@State属性包装器,它们无法正常工作。

    相反,我们需要的是一个自定义绑定,该绑定将在读取时返回filterIntensity,但是在写入时,它会更新filterIntensity并调用applyProcessing(),以便最新的强度设置立即在我们的滤镜中使用。

    需要在视图的body属性内创建依赖于我们视图属性的自定义绑定,因为Swift不允许一个属性引用另一个属性。因此,将其添加到body属性的开头:

    let intensity = Binding<Double>(
        get: {
            self.filterIntensity
        },
        set: {
            self.filterIntensity = $0
            self.applyProcessing()
        }
    )
    

    重要提示:body属性中已经包含了一些逻辑,您必须将return放置在NavigationView之前,如下所示:

    return NavigationView {
    

    现在我们有了一个自定义绑定,我们应该将滑块附加到该绑定,而不是直接附加到@State属性,这样对滑块的更改将触发applyProcessing()

    因此,将滑块代码更改为这样:

    Slider(value: intensity)
    

    请记住,因为intensity已经具有约束力,所以我们不需要在之前使用美元符号——您需要输入value: intensity而不是value:$ intensity

    您可以立即开始运行该应用程序,但请注意:尽管 Core Image 在所有iPhone上都非常快,但在模拟器中却非常慢。这意味着您可以尝试确保一切正常,但是如果您的代码运行速度与携带沉重购物袋的哮喘病蚂蚁一样快,也不要感到惊讶。

    译自
    Building our basic UI
    Importing an image into SwiftUI using UIImagePickerController
    Basic image filtering using Core Image

    相关文章

      网友评论

        本文标题:Hacking with iOS: SwiftUI Editio

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