Hacking with iOS: SwiftUI Edition

Hacking with iOS: SwiftUI Editio

2020-07-09  本文已影响0人  韦弦Zhy

\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

上一篇下一篇

猜你喜欢

热点阅读