基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何在 SwiftUI 中使用 UIKit 组件?

知识点图片

在 SwiftUI 中使用 UIKit 组件(或 AppKit 组件用于 macOS)是非常常见的需求,特别是当你需要使用 SwiftUI 尚未提供的功能(如复杂的 UITextViewWKWebViewUIImagePickerController 等)时。

核心机制是包装(Wrapping)。SwiftUI 提供了两个主要的协议来实现这一点:

  1. UIViewRepresentable: 用于包装 UIView
  2. 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)
        }
    }
}

总结与最佳实践

  1. 数据流向:
    • SwiftUI -> UIKit: 通过 updateUIView 方法。
    • UIKit -> SwiftUI: 通过 Coordinator (代理/Target-Action) 修改 @Binding 变量。
  2. Context: context 参数非常重要,它包含了 coordinatorenvironment(环境值)和 transaction(动画事务)。
  3. 性能: updateUIView 可能会被频繁调用。在设置属性前,最好检查新值是否与旧值不同(如上面的 CustomTextView 示例),以避免不必要的重绘或循环更新。
  4. 清理: 如果需要手动清理资源(如移除观察者),可以实现 dismantleUIView (或 dismantleUIViewController) 静态方法。
00:00
00:00