构建基本 UI
项目的第一步是构建基本的用户接口,对于这个应用来说包括:
- 一个 NavigationView,用以在顶部展示应用的名称
- 一个灰色的矩形,显示“点击以选择图片”,我们导入的图片会放在这里。
- 一个“强度”滑块,用来控制应用的 Core Image 滤镜的程度,存储从 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)
// 显示图片
}
.onTapGesture {// 选择图片}
HStack {Text("强度")
Slider(value: self.$filterIntensity)
}.padding(.vertical)
HStack {Button("切换滤镜") {// 切换滤镜}
Spacer()
Button("保存") {// 保存图片}
}
}
.padding([.horizontal, .bottom])
.navigationBarTitle("Instafilter")
} 复制代码
这里有很多的占位符,随着工程的推进,我们逐步填充。
首先,我们聚集到注释:// 显示图片。如果我们选择了图片,会在这里展示,否则显示一个提示,告诉用户点击可以触发图片选择。
你可能会想到替换这个注释的好方法是使用 if let,像下面这样:
if let image = image {
image
.resizable()
.scaledToFit()} else {Text("点击以选择图片")
.foregroundColor(.white)
.font(.headline)
} 复制代码
不过,如果你尝试编译会发现编译无法通过 —— 你会得到一个很明显的错误:“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 包含很多 case,所以都不能用。
修复这个问题,我们需要把 if let 替换成简单的条件,然后依赖 SwiftUI 对于可选视图的支持:
if image != nil {
image?
.resizable()
.scaledToFit()} else {Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
} 复制代码
这个代码完全可以通过编译,当 image 是 nil 时,你应该会看到“点击以选择图片”提示显示在我们的灰色矩形上。
译自 www.hackingwithswift.com/books/ios-s…
用 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,它用来给用户从相册中选取图片。
SwiftUI 会自动调用 ImagePicker 的 makeUIViewController() 方法,这个方法将创建并返回一个 UIImagePickerController。但是,我们的代码还没有对 image picker 内的事件做出响应。
相比创建一个 UIImagePickerController 的子类,UIKit 使用的是委托系统:我们创建一个自定义类,负责接收发生的事件。每个委托类通常都需要遵循一个或者多个协议,在我们的案例中,这些协议包括 UINavigationControllerDelegate 和 UIImagePickerControllerDelegate。这些委托的工作方式和实际生活中的代理工作方式很像 —— 假如你把工作委托给别人,就表示你把工作交给他们去完成。
SwiftUI 通过让我们定义从属与结构体的 coordinator 来处理委托事务。这个类会处理我们要做的事情,包括扮演 UIKit 组件的委托,然后我们再借助这个类向上传递相关的信息。
把下面这个嵌套类添加到 ImagePicker 中:
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {self.parent = parent}
} 复制代码
可以看到,这个类遵循两个协议,这两个协议是为了和 UIKit 的 image picker 协作。同时这个类还继承自 NSObject,它是 UIKit 里绝大多数类型的基类。
因为我们的协调器类遵循了 UIImagePickerControllerDelegate 协议,我们可以让它作为 UIKit image picker 的委托,修改 makeUIViewController() 方法:
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,意味着我们需要创建对 image picker 的引用,以便协调器可以把感兴趣的事件向上传递。所以,在 makeCoordinator() 方法里,我们要用 self 来构造 Coordinator 对象。
添加下面这个方法到 ImagePicker:
func makeCoordinator() -> Coordinator {Coordinator(self)
} 复制代码
ImagePicker 的最后步骤是给协调器提供某种功能。UIImagePickerController 提供了两个方法,但我们只用到其中一个:didFinishPickingMediaWithInfo。这个方法是在用户选择了图片之后,以返回选中图片的信息的字典的形式被回调。
我们需要实现 Coordinator 里的这个方法,在其中设置其 ImagePicker 引用的 image 属性,然后关闭视图。
UIKit 的方法名又长又复杂,所以最好是借助代码补全。你可以 Coordinator 类某个空白的地方输入“didFinishPicking”,然后点击 return 让 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 布尔属性,用来对应 image picker 是否显示,把下面这行添加到 ContentView:
@State private var showingImagePicker = false 复制代码
其次,需要在灰色矩形被点击时把这个布尔属性置为 true,把 // 选择图片 注释替换成下面这行:
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)
} 复制代码
最后,我们需要一个 sheet() modifier,它会用 showingImagePicker 作为条件来显示 ImagePicker,并且在 ImagePicker 关闭时执行 loadImage。
把下面这个 modifier 添加到已有的 navigationBarTitle() modifier 下方:
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {ImagePicker(image: self.$inputImage)
} 复制代码
这样就完成了用 SwiftUI 封装 UIKit 视图控制器的所有步骤。因为前面已经做过技术概览,这一次我们的节奏会比较快。
再次运行应用,你应该能够点击灰色矩形,导入图片,并且发现图片出现在我们的 UI 上。
提示:我们刚才构建的 ImagePicker 视图完全可以拿来重用 —— 思考一下,所有封装视图的复杂性都已经囊括在 ImagePicker.swift 文件内部了,意味着如果你想要在别的地方用它,唯一要做的事情就是显示 sheet 和绑定 image。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的 iOS 交流群:651612063 进群密码 111,不管你是小白还是大牛欢迎入驻,分享 BAT, 阿里面试题、面试经验,讨论技术,大家一起交流学习成长!
点击进群密码:111
进群领取大厂面试题
原文