如何在 SwiftUI 中使用 UIKit 组件?
在 SwiftUI 中使用 UIKit 组件(或 AppKit 组件用于 macOS)是非常常见的需求,特别是当你需要使用 SwiftUI 尚未提供的功能(如复杂的 UITextView、WKWebView、UIImagePickerController 等)时。
核心机制是包装(Wrapping)。SwiftUI 提供了两个主要的协议来实现这一点:
UIViewRepresentable: 用于包装UIView。UIViewControllerRepresentable: 用于包装UIViewController。
以下是详细的步骤和示例:
1. 基础概念:生命周期方法
无论你使用哪个协议,都需要实现两个核心方法:
makeUIView(或makeUIViewController):- 作用: 创建并配置 UIKit 视图/控制器的初始实例。
- 执行时机: 只在 SwiftUI 视图初始化时执行一次。
updateUIView(或updateUIViewController):- 作用: 当 SwiftUI 的状态(State/Binding)发生变化时,更新 UIKit 视图的属性。
- 执行时机: 每次 SwiftUI 视图重绘且数据有变化时执行。
2. 示例一:包装简单的 UIView
假设我们需要使用 UIActivityIndicatorView(虽然 SwiftUI 现在有 ProgressView,但这作为一个简单的例子很合适)。
plaintext
import SwiftUI
import UIKit
struct LoadingIndicator: UIViewRepresentable {
// 1. 定义属性,通常是用来控制视图样式的
var style: UIActivityIndicatorView.Style
@Binding var isAnimating: Bool
// 2. 创建视图
func makeUIView(context: Context) -> UIActivityIndicatorView {
let indicator = UIActivityIndicatorView(style: style)
indicator.hidesWhenStopped = true
return indicator
}
// 3. 更新视图 (SwiftUI -> UIKit)
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
if isAnimating {
uiView.startAnimating()
} else {
uiView.stopAnimating()
}
}
}
// 使用方法
struct ContentView: View {
@State private var isLoading = true
var body: some View {
VStack {
LoadingIndicator(style: .large, isAnimating: $isLoading)
Button("Toggle Loading") {
isLoading.toggle()
}
}
}
}
3. 进阶:使用 Coordinator 处理代理 (Delegate) 和数据回传
如果 UIKit 组件需要向 SwiftUI 发送数据(例如 UITextView 用户输入了文字,或者 UIScrollView 发生了滚动),你需要使用 Coordinator。
Coordinator 充当 UIKit 组件的 Delegate(代理)或 Target。
示例:包装 UITextView 并实现双向绑定
plaintext
import SwiftUI
import UIKit
struct CustomTextView: UIViewRepresentable {
@Binding var text: String
// 1. 创建 Coordinator
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// 2. 创建视图
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.systemFont(ofSize: 18)
// 设置代理为 coordinator
textView.delegate = context.coordinator
return textView
}
// 3. 更新视图 (SwiftUI -> UIKit)
func updateUIView(_ uiView: UITextView, context: Context) {
// 防止循环更新:只有当文字确实不同时才赋值
if uiView.text != text {
uiView.text = text
}
}
// 4. 定义 Coordinator 类 (作为 Delegate)
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomTextView
init(_ parent: CustomTextView) {
self.parent = parent
}
// 实现 UITextViewDelegate 方法 (UIKit -> SwiftUI)
func textViewDidChange(_ textView: UITextView) {
// 将 UIKit 的变化传回 SwiftUI
self.parent.text = textView.text
}
}
}
// 使用方法
struct TextViewExample: View {
@State private var text = "Hello UIKit"
var body: some View {
VStack {
Text("SwiftUI Text: \(text)")
CustomTextView(text: $text)
.frame(height: 200)
.border(Color.gray, width: 1)
.padding()
}
}
}
4. 示例二:包装 UIViewController
最经典的例子是调用系统的相册 (UIImagePickerController)。这需要使用 UIViewControllerRepresentable。
plaintext
import SwiftUI
import UIKit
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Environment(\.presentationMode) var presentationMode
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) {
// 这里通常不需要做什么,因为 Picker 主要是用户交互
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
// 选中图片后的回调
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
// 关闭页面
parent.presentationMode.wrappedValue.dismiss()
}
// 点击取消
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.presentationMode.wrappedValue.dismiss()
}
}
}
// 使用方法
struct ImagePickerExample: View {
@State private var inputImage: UIImage?
@State private var showImagePicker = false
var body: some View {
VStack {
if let inputImage = inputImage {
Image(uiImage: inputImage)
.resizable()
.scaledToFit()
} else {
Text("点击按钮选择图片")
}
Button("选择图片") {
showImagePicker = true
}
}
.sheet(isPresented: $showImagePicker) {
ImagePicker(image: $inputImage)
}
}
}
总结与最佳实践
- 数据流向:
- SwiftUI -> UIKit: 通过
updateUIView方法。 - UIKit -> SwiftUI: 通过
Coordinator(代理/Target-Action) 修改@Binding变量。
- SwiftUI -> UIKit: 通过
- Context:
context参数非常重要,它包含了coordinator、environment(环境值)和transaction(动画事务)。 - 性能:
updateUIView可能会被频繁调用。在设置属性前,最好检查新值是否与旧值不同(如上面的CustomTextView示例),以避免不必要的重绘或循环更新。 - 清理: 如果需要手动清理资源(如移除观察者),可以实现
dismantleUIView(或dismantleUIViewController) 静态方法。