4、UIViewControllerRepresentable协议:将UIKit控制器包装为SwiftUI组件、协调器模式详解
4.1 为什么需要UIViewControllerRepresentable?
说实话,SwiftUI虽然好用,但生态还不够完善。很多成熟的UIKit组件,比如地图、相机、WebView,SwiftUI还没提供原生替代品。
我在项目中遇到过好几次这种情况:产品经理说「这个页面用地图展示」,我一看SwiftUI的MapKit支持,嗯…功能太基础了。最后还是得把MKMapView搬过来。
这时候就需要UIViewControllerRepresentable协议了。它就像一座桥,把UIKit的控制器无缝接入SwiftUI的世界。
核心思想:SwiftUI管理生命周期,UIKit负责具体渲染。两者各司其职,互不干扰。
4.2 协议的基本结构
先看一个最简单的例子。把UIActivityViewController包装成SwiftUI组件:
struct ActivityViewController: UIViewControllerRepresentable {
let activityItems: [Any]
let applicationActivities: [UIActivity]?
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities
)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
// 不需要更新逻辑
}
}
这里有两个必须实现的方法:
- makeUIViewController:创建并返回UIKit控制器。只调用一次。
- updateUIViewController:当SwiftUI状态变化时,更新控制器。每次刷新都会调用。
你想想看,这跟UIViewRepresentable是不是很像?对,设计思路完全一致。
4.3 协调器模式详解
光能展示还不够。UIKit控制器有各种代理方法、事件回调,怎么把这些事件传回SwiftUI?
答案就是协调器(Coordinator)。
我个人习惯把协调器理解为「翻译官」。它懂UIKit的语言,也懂SwiftUI的语言,负责在两者之间传话。
4.3.1 创建协调器
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
@Environment(\.dismiss) private var dismiss
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.dismiss()
}
}
}
注意几个关键点:
- 协调器必须继承
NSObject,因为UIKit代理都是OC时代的产物 - 通过
context.coordinator访问协调器实例 - 协调器持有parent引用,用来更新SwiftUI状态
小技巧:协调器里不要直接持有parent的强引用,否则会造成循环引用。但这里parent是struct,所以没问题。如果协调器里持有的是class,记得用weak。
4.3.2 协调器的生命周期
我记得刚学的时候有个疑问:协调器什么时候创建?什么时候销毁?
其实很简单:
- 创建时机:在makeUIViewController之前,SwiftUI会先调用makeCoordinator()
- 销毁时机:当SwiftUI视图被移除时,协调器也会被释放
- 更新时机:每次updateUIViewController调用时,context.coordinator都是同一个实例
4.4 实战:包装一个完整的WebView
光说不练假把式。咱们来包装一个WKWebView,支持加载URL、进度回调、JavaScript交互。
struct WebView: UIViewControllerRepresentable {
let url: URL
@Binding var isLoading: Bool
@Binding var progress: Double
var onPageTitleChanged: ((String) -> Void)?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIViewController {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
webView.addObserver(
context.coordinator,
forKeyPath: #keyPath(WKWebView.estimatedProgress),
options: .new,
context: nil
)
let controller = UIViewController()
controller.view = webView
webView.load(URLRequest(url: url))
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
guard let webView = uiViewController.view as? WKWebView else { return }
if webView.url != url {
webView.load(URLRequest(url: url))
}
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "estimatedProgress" {
if let webView = object as? WKWebView {
parent.progress = webView.estimatedProgress
parent.isLoading = webView.estimatedProgress < 1.0
}
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.title") { result, error in
if let title = result as? String {
self.parent.onPageTitleChanged?(title)
}
}
}
deinit {
print("协调器被释放了")
}
}
}
注意:KVO观察者一定要在协调器deinit时移除,否则会崩溃。但这里我们没写移除代码,因为协调器销毁时WKWebView也会被销毁,所以没问题。如果你把webView作为属性持有,记得在deinit里调用removeObserver。
4.5 常见坑点与避坑指南
做混合开发,坑是少不了的。我踩过的坑,你就不用再踩了。
4.5.1 状态不同步问题
我曾经遇到一个bug:SwiftUI里改了@State,但UIKit控制器没反应。查了半天,发现是updateUIViewController没写对。
记住:updateUIViewController只在SwiftUI状态变化时调用。如果你在UIKit内部修改了状态,需要手动触发刷新。
4.5.2 内存泄漏
协调器里如果持有parent的闭包,很容易造成循环引用。我建议:
- 能用值类型就用值类型
- 闭包中使用[weak self]
- 代理方法里不要强引用parent
4.5.3 视图层级问题
有时候UIKit控制器需要嵌入到SwiftUI的NavigationView里。这时候要注意:
- UIKit控制器自己不要管理导航栏
- 把导航逻辑交给SwiftUI处理
- 可以用@Environment(\.dismiss)来控制返回
4.6 性能优化建议
| 场景 | 建议 | 原因 |
|---|---|---|
| 频繁更新UI | 使用协调器批量处理 | 减少SwiftUI刷新次数 |
| 大型列表 | 用UICollectionView代替List | UIKit的复用机制更成熟 |
| 动画 | 在UIKit侧处理 | SwiftUI动画与UIKit动画可能冲突 |
| 内存敏感 | 及时释放协调器 | 避免UIKit对象常驻内存 |
4.7 总结
UIViewControllerRepresentable说白了就是SwiftUI给UIKit开的一扇后门。用好它,你就能在SwiftUI项目里随意调用任何UIKit组件。
核心要点就三个:
- makeUIViewController负责创建,updateUIViewController负责更新
- 协调器是UIKit和SwiftUI之间的桥梁,处理所有事件回调
- 注意生命周期管理,避免内存泄漏和状态不同步
嗯,掌握了这些,你就能在SwiftUI和UIKit之间自由穿梭了。下一章咱们聊聊UIViewRepresentable,原理类似但细节不同,到时候再细说。