UIKit 混编之道

在成熟项目中优雅迁移。学习如何在 SwiftUI 中封装组件,或在现有导航体系中嵌入托管视图。

在成熟项目中平滑迁移,实现 UIKit 和 SwiftUI 的无缝协作。渐进式迁移是大型项目的最佳选择。


1. 混编策略

迁移路径选择

策略 适用场景 风险
全新模块用 SwiftUI 新功能开发
逐页面迁移 中型项目
组件级混用 大型项目
完全重写 小型项目

推荐方案

现有 UIKit 项目
       │
       ├── 新功能 → SwiftUI
       │
       ├── 简单页面 → 逐步迁移到 SwiftUI
       │
       └── 复杂页面 → 保留 UIKit,嵌入 SwiftUI 组件

2. UIViewRepresentable

在 SwiftUI 中使用 UIKit 视图。

基础结构

struct MapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    
    // 1. 创建 UIView
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }
    
    // 2. 更新 UIView(SwiftUI 状态变化时调用)
    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)
    }
    
    // 3. 创建 Coordinator(处理代理)
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // 4. Coordinator 类
    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView
        
        init(_ parent: MapView) {
            self.parent = parent
        }
        
        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            parent.region = mapView.region
        }
    }
}

// 使用
struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 31.23, longitude: 121.47),
        span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    )
    
    var body: some View {
        MapView(region: $region)
            .ignoresSafeArea()
    }
}

WKWebView 封装

struct WebView: UIViewRepresentable {
    let url: URL
    @Binding var isLoading: Bool
    
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        return webView
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: WebView
        
        init(_ parent: WebView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            parent.isLoading = true
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.isLoading = false
        }
    }
}

处理 UITextField

struct CustomTextField: UIViewRepresentable {
    @Binding var text: String
    var placeholder: String
    var onCommit: () -> Void
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.placeholder = placeholder
        textField.borderStyle = .roundedRect
        textField.delegate = context.coordinator
        textField.addTarget(
            context.coordinator,
            action: #selector(Coordinator.textChanged),
            for: .editingChanged
        )
        return textField
    }
    
    func updateUIView(_ textField: UITextField, context: Context) {
        textField.text = text
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: CustomTextField
        
        init(_ parent: CustomTextField) {
            self.parent = parent
        }
        
        @objc func textChanged(_ textField: UITextField) {
            parent.text = textField.text ?? ""
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            parent.onCommit()
            textField.resignFirstResponder()
            return true
        }
    }
}

3. UIViewControllerRepresentable

在 SwiftUI 中使用 UIViewController。

系统选择器

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(\.dismiss) var dismiss
    
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        picker.sourceType = .photoLibrary
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    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()
        }
        
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.dismiss()
        }
    }
}

// 使用
struct ContentView: View {
    @State private var showPicker = false
    @State private var image: UIImage?
    
    var body: some View {
        VStack {
            if let image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            }
            Button("选择图片") {
                showPicker = true
            }
        }
        .sheet(isPresented: $showPicker) {
            ImagePicker(selectedImage: $image)
        }
    }
}

文档选择器

struct DocumentPicker: UIViewControllerRepresentable {
    @Binding var fileURL: URL?
    
    func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
        let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf, .text])
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UIDocumentPickerDelegate {
        let parent: DocumentPicker
        
        init(_ parent: DocumentPicker) {
            self.parent = parent
        }
        
        func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
            parent.fileURL = urls.first
        }
    }
}

4. UIHostingController

在 UIKit 中使用 SwiftUI 视图。

基础用法

// SwiftUI 视图
struct ProfileView: View {
    let user: User
    
    var body: some View {
        VStack {
            Image(systemName: "person.circle.fill")
                .font(.system(size: 80))
            Text(user.name)
                .font(.title)
        }
    }
}

// 在 UIKit 中使用
class ProfileViewController: UIViewController {
    var user: User!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let swiftUIView = ProfileView(user: user)
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        // 添加为子控制器
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)
        
        // 设置约束
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

在 UITableViewCell 中使用

class SwiftUITableViewCell: UITableViewCell {
    private var hostingController: UIHostingController<AnyView>?
    
    func configure<Content: View>(with view: Content, parent: UIViewController) {
        // 移除旧的
        hostingController?.view.removeFromSuperview()
        hostingController?.removeFromParent()
        
        // 添加新的
        let hosting = UIHostingController(rootView: AnyView(view))
        hostingController = hosting
        
        parent.addChild(hosting)
        contentView.addSubview(hosting.view)
        hosting.didMove(toParent: parent)
        
        hosting.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hosting.view.topAnchor.constraint(equalTo: contentView.topAnchor),
            hosting.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            hosting.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }
}

导航集成

class MainViewController: UINavigationController {
    func pushSwiftUIView() {
        let swiftUIView = DetailView(item: selectedItem)
        let hostingController = UIHostingController(rootView: swiftUIView)
        hostingController.title = "详情"
        pushViewController(hostingController, animated: true)
    }
}

5. 数据传递

UIKit → SwiftUI

// 使用 @ObservableObject 共享数据
class SharedData: ObservableObject {
    @Published var message = ""
}

class UIKitViewController: UIViewController {
    let sharedData = SharedData()
    
    func showSwiftUIView() {
        let view = SwiftUIView()
            .environmentObject(sharedData)
        let hosting = UIHostingController(rootView: view)
        present(hosting, animated: true)
    }
    
    func updateData() {
        sharedData.message = "来自 UIKit 的消息"
    }
}

struct SwiftUIView: View {
    @EnvironmentObject var data: SharedData
    
    var body: some View {
        Text(data.message)
    }
}

SwiftUI → UIKit(通过 Coordinator)

struct CustomView: UIViewRepresentable {
    var onTap: () -> Void
    
    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle("点击", for: .normal)
        button.addTarget(context.coordinator, action: #selector(Coordinator.handleTap), for: .touchUpInside)
        return button
    }
    
    func updateUIView(_ uiView: UIButton, context: Context) { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(onTap: onTap)
    }
    
    class Coordinator: NSObject {
        var onTap: () -> Void
        
        init(onTap: @escaping () -> Void) {
            self.onTap = onTap
        }
        
        @objc func handleTap() {
            onTap()
        }
    }
}

6. 最佳实践

✅ 推荐

// 1. 使用 Coordinator 处理代理
func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

// 2. 正确管理 HostingController 生命周期
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)

// 3. 使用 @ObservableObject 共享状态
class SharedState: ObservableObject {
    @Published var data: [Item] = []
}

// 4. 保持视图层薄,逻辑放 ViewModel

❌ 避免

// 1. 不要频繁创建 HostingController
// ❌ 在 cellForRowAt 中每次创建
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let hosting = UIHostingController(rootView: ItemView())  // ❌ 性能问题
}

// 2. 不要忘记移除子控制器
hostingController.willMove(toParent: nil)
hostingController.view.removeFromSuperview()
hostingController.removeFromParent()

// 3. 不要混用多种状态管理方案
// 选择一种主要方案:@ObservableObject 或 Combine