Hacking with iOS: SwiftUI Editio
创建基础UI
我们项目的第一步是构建基本的用户界面,此应用程序将是:
- 一个
NavigationView
,因此我们可以在顶部显示我们的应用名称。 - 一个大的灰色框,上面写着“轻按以选择图片”,我们将在上面放置导入的图片。
- “强度”滑块将影响我们应用核心图像滤镜的强度,该滤镜存储为0.0到1.0之间的值。
- 一个“保存”按钮,将修改后的图像写到用户的照片库中。
最初,用户不会选择图片,因此我们将使用@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
的子类,而是使用了委派系统:我们创建了一个自定义类,当发生有趣的事情时会告诉您。每个委托类通常需要符合一个或多个协议,在我们的情况下,这意味着UINavigationControllerDelegate
和UIImagePickerControllerDelegate
。代理的工作就像现实中的代理一样——如果您将工作委托给其他人,则意味着您要把工作交给他们完成。
SwiftUI通过让我们定义属于该结构体的协调器来处理这些委托类。此类可以完成我们需要做的所有事情,包括充当UIKit组件的委托,然后我们可以将任何相关信息传递回拥有它的ImagePicker
。
首先将其添加为ImagePicker
中的嵌套类:
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
}
您可以看到它符合我们使用UIKit的图像选择器所需的两个协议,并且还继承自NSObject
,NSObject
是 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
中实现该方法,使其设置其父ImagePicker
的image
属性,然后关闭视图。
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
参数,并提供一个绑定到inputImage
的ImagePicker
作为其内容。
因此,将其直接添加到现有的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