[SwiftUI 100天] 把 UIViewController 包装进一个 SwiftUI 视图

679 阅读4分钟
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

SwiftUI 框架对于构建应用是梦幻般的存在,但目前还远不到完善的程度 —— 还有很多它不能做的事情,假如你需要用到更多高级的功能,需要诉诸 UIKit,有的时候是把你之前的 UIKit 代码集成进来(比如,你的公司有一个现存的 UIKit 开发的应用),有的时候是 UIKit 或者其他的 Apple 框架里有我们需要的东西,我们希望在 SwiftUI 布局中显示它们。

在这个项目中,我们要让用户从相册中导入一张照片。UIKit 正好有专门的代码,但还没有被移植到 SwiftUI,所以我们需要自行编写桥接的代码。

在我们编码之前,你需要了解三个东西,这些东西在 UIKit 里都很基础,但假如你只学习了 SwiftUI,那它们可能对你来说是新知识。

首先,UIKit 有一个叫 UIView 的类,是布局中所有视图的基类。因此,标签,按钮,文本框,滑块 —— 它们都是视图。

其次,UIKit 有一个叫 UIViewController 的类,被设计用于持有实操视图的代码。正如 UIViewUIViewController 也有很多执行不同任务的子类。

再次,UIKit 采用委托设计模式来确定工作分工。因此,当我们要确定要如何响应某个文本框里的值变化时,我们会创建一个自定义类,充当文本框的委托。

SwiftUI 构筑代码的方式则很不一样,比如我们使用结构体而不是类来构建视图。但是,某种程度上,SwiftUI 把 UIViewUIViewController 混合到单一的 View 协议中,使得我们的代码更为简单。

不管怎么说,之所以要说明这些要点,是因为 UIKit 是采用一个叫 UIImagePickerController的视图控制来给用户从相册中选择照片,而它的委托协议叫 UINavigationControllerDelegateUIImagePickerControllerDelegate。SwiftUI 不能直接使用这两者,我们需要进行封装。

让我们先从简单的工作开始。封装 UIKit 视图控制器需要我们构建一个遵循 UIViewControllerRepresentable 协议的结构体。它基于 View 构建,因此可以直接用在 SwiftUI 的视图层级中,但我们不需要提供 body 属性,因为视图的 body 即视图控制器本身 —— 它会显示 UIKit 返回的任何东西。

遵循 UIViewControllerRepresentable 要求我们实现两个方法:一个叫 makeUIViewController(),表示创建初始的视图控制器,另一个叫 updateUIViewController(),用于在 SwiftUI 状态改变时更新视图控制器。

这些方法的签名实在是太准确了,我给你简要地说明一下。方法名之所以这么长,是因为 SwiftUI 需要知道结构体封装的视图控制器类型。因此,我们只要告诉 Swift 这个类型是什么,Xcode 会解决剩下的问题。

点击 Cmd+N 创建一个新文件,选择 Swift 文件,取名为 ImagePicker.swift。添加 import SwiftUI 到文件头部,然后编写下面的代码:

struct ImagePicker: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIImagePickerController
}

光有这些代码还不能编译,Xcode 会显示 “Type ImagePicker does not conform to protocol UIViewControllerRepresentable”,请点击左下角的红白小圆圈,选择 “Fix”。Xcode 会自动生成两个你需要实现的方法,由于这两行也足够 Xcode 推断视图控制器的类型,所以你也可以删除掉 typealias 那一行。

我们得到一个如下的结构体:

struct ImagePicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        code
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
        code
    }
}

updateUIViewController() 我们暂时不用。

但是,makeUIViewController() 方法很重要,不可放空。把 “code” 行替换成下面:

let picker = UIImagePickerController()
return picker

我们很快会再添加一些代码,不过基本上我们要做的是封装一个 UIKit 的视图控制器。

ImagePicker 结构体作为合法的 SwiftUI 视图,可以直接在 sheet 中显示。这个控件是设计来显示图像的,因此我们会用到一个可选的 Image 视图,加上决定 sheet 是否显示的状态属性。

ContentView 结构体替换成这样:

struct ContentView: View {
    @State private var image: Image?
    @State private var showingImagePicker = false

    var body: some View {
        VStack {
            image?
                .resizable()
                .scaledToFit()

            Button("Select Image") {
               self.showingImagePicker = true
            }
        }
        .sheet(isPresented: $showingImagePicker) {
            ImagePicker()
        }
    }
}

运行代码,点击按钮,应该会有一个默认的 UIKit 图像选择器滑出,让浏览器相册中的照片并供你选择,一旦选择了某张照片,选择器 sheet 就会消失。

不过,选择的图像不会显示在我们的视图上,因为我们还没有把从 UIImagePickerController选择的图片赋给 SwiftUI 的 Image

这就要接触到一个全新的概念了:协调器


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