阅读 276

[SwiftUI 100 天] Instafilter 基本 UI 和图片导入

译自 https://www.hackingwithswift.com/books/ios-swiftui/building-our-basic-ui
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

构建基本 UI

项目的第一步是构建基本的用户接口,对于这个应用来说包括:

  1. 一个 NavigationView ,用以在顶部展示应用的名称
  2. 一个灰色的矩形,显示 “点击以选择图片”,我们导入的图片会放在这里。
  3. 一个 “强度” 滑块,用来控制应用的 Core Image 滤镜的程度,存储从 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)

            // 显示图片
        }
        .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 时,你应该会看到 “点击以选择图片” 提示显示在我们的灰色矩形上。

用 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。



我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~

                                                                    



关注下面的标签,发现更多相似文章
评论