SwiftUI View 和 UIKit UIView 互相调用

111 阅读4分钟

不能直接相互调用,需要通过特定的包装器进行桥接。

一、相互调用机制

1. SwiftUI → UIKit:使用 UIViewRepresentable

import SwiftUI
import UIKit

// 🌟 UIKit 组件
class MyCustomLabel: UILabel {
    var onTap: (() -> Void)?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        isUserInteractionEnabled = true
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc private func handleTap() {
        onTap?()
    }
}

// 🌟 包装成 SwiftUI 可用组件
struct MyCustomLabelRepresentable: UIViewRepresentable {
    var text: String
    var onTap: (() -> Void)?
    
    // 创建 UIKit 视图
    func makeUIView(context: Context) -> MyCustomLabel {
        let label = MyCustomLabel()
        label.text = text
        label.onTap = onTap
        return label
    }
    
    // 更新 UIKit 视图(数据变化时调用)
    func updateUIView(_ uiView: MyCustomLabel, context: Context) {
        uiView.text = text
        uiView.onTap = onTap
    }
    
    // 处理 UIKit 的代理、目标-动作等
    func makeCoordinator() -> Coordinator {
        Coordinator(onTap: onTap)
    }
    
    class Coordinator {
        var onTap: (() -> Void)?
        
        init(onTap: (() -> Void)?) {
            self.onTap = onTap
        }
    }
}

// 🌟 在 SwiftUI 中使用
struct ContentView: View {
    @State private var labelText = "Hello from UIKit!"
    
    var body: some View {
        VStack {
            Text("这是 SwiftUI 文本")
            
            MyCustomLabelRepresentable(text: labelText) {
                print("UIKit 标签被点击了!")
                labelText = "点击后的文本"
            }
            .frame(height: 50)
            
            Button("更新文本") {
                labelText = "更新后的文本 \(Date())"
            }
        }
    }
}

2. UIKit → SwiftUI:使用 UIHostingController

import SwiftUI
import UIKit

// 🌟 SwiftUI 视图
struct MySwiftUIView: View {
    var title: String
    var onButtonTap: () -> Void
    
    var body: some View {
        VStack(spacing: 20) {
            Text(title)
                .font(.title)
                .foregroundColor(.blue)
            
            Button("SwiftUI 按钮") {
                onButtonTap()
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
        }
        .padding()
    }
}

// 🌟 在 UIKit ViewController 中使用
class MyUIKitViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setupSwiftUIViews()
    }
    
    private func setupSwiftUIViews() {
        // 方式1:作为子视图控制器(嵌入到现有界面)
        let swiftUIView1 = MySwiftUIView(title: "嵌入的 SwiftUI 视图") {
            print("第一个 SwiftUI 按钮点击")
            self.showAlert(message: "来自第一个 SwiftUI 视图")
        }
        
        let hostingController1 = UIHostingController(rootView: swiftUIView1)
        addChild(hostingController1)
        view.addSubview(hostingController1.view)
        hostingController1.view.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            hostingController1.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            hostingController1.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            hostingController1.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            hostingController1.view.heightAnchor.constraint(equalToConstant: 150)
        ])
        
        hostingController1.didMove(toParent: self)
        
        // 方式2:全屏呈现
        let presentButton = UIButton(type: .system)
        presentButton.setTitle("呈现 SwiftUI 视图", for: .normal)
        presentButton.addTarget(self, action: #selector(presentSwiftUI), for: .touchUpInside)
        presentButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(presentButton)
        
        NSLayoutConstraint.activate([
            presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            presentButton.topAnchor.constraint(equalTo: hostingController1.view.bottomAnchor, constant: 40)
        ])
    }
    
    @objc private func presentSwiftUI() {
        let swiftUIView = MySwiftUIView(title: "模态呈现的 SwiftUI") {
            print("模态视图按钮点击")
            self.dismiss(animated: true)
        }
        
        let hostingController = UIHostingController(rootView: swiftUIView)
        present(hostingController, animated: true)
    }
    
    private func showAlert(message: String) {
        let alert = UIAlertController(title: "来自 UIKit", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "确定", style: .default))
        present(alert, animated: true)
    }
}

二、实际应用场景

1. 在 SwiftUI 中使用 UIKit 组件

import SwiftUI
import MapKit

// 🌟 使用 UIKit 的 MKMapView
struct MapView: UIViewRepresentable {
    let coordinate: CLLocationCoordinate2D
    
    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}

// 🌟 使用 UIKit 的 WKWebView
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

// 🌟 在 SwiftUI 中组合使用
struct MixedView: View {
    var body: some View {
        VStack {
            Text("SwiftUI 文本组件")
                .font(.largeTitle)
            
            // UIKit 地图组件
            MapView(coordinate: CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074))
                .frame(height: 200)
                .cornerRadius(10)
            
            // UIKit WebView 组件
            WebView(url: URL(string: "https://www.apple.com")!)
                .frame(height: 300)
        }
        .padding()
    }
}

2. 在 UIKit 项目中使用 SwiftUI 屏幕

import SwiftUI
import UIKit

// 🌟 复杂的 SwiftUI 视图
struct ProductDetailView: View {
    let product: Product
    var onBuy: () -> Void
    var onShare: () -> Void
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                // 图片区域
                AsyncImage(url: product.imageURL) { image in
                    image.resizable()
                        .aspectRatio(contentMode: .fill)
                } placeholder: {
                    ProgressView()
                }
                .frame(height: 200)
                .clipped()
                
                // 信息区域
                VStack(alignment: .leading, spacing: 8) {
                    Text(product.name)
                        .font(.title2)
                        .fontWeight(.bold)
                    
                    Text(\(product.price, specifier: "%.2f")")
                        .font(.title3)
                        .foregroundColor(.red)
                    
                    Text(product.description)
                        .font(.body)
                        .foregroundColor(.secondary)
                }
                .padding(.horizontal)
                
                // 按钮区域
                HStack(spacing: 12) {
                    Button("立即购买") {
                        onBuy()
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
                    
                    Button("分享") {
                        onShare()
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.gray)
                    .foregroundColor(.white)
                    .cornerRadius(8)
                }
                .padding(.horizontal)
            }
        }
    }
}

struct Product {
    let id = UUID()
    let name: String
    let price: Double
    let description: String
    let imageURL: URL?
}

// 🌟 在 UIKit 中呈现
class ProductViewController: UIViewController {
    
    func presentProductDetail() {
        let product = Product(
            name: "iPhone 15",
            price: 5999.0,
            description: "最新款 iPhone",
            imageURL: URL(string: "https://example.com/iphone.jpg")
        )
        
        let swiftUIView = ProductDetailView(
            product: product,
            onBuy: { [weak self] in
                self?.handlePurchase()
            },
            onShare: { [weak self] in
                self?.handleShare()
            }
        )
        
        let hostingController = UIHostingController(rootView: swiftUIView)
        hostingController.title = "商品详情"
        
        // 可以推入导航栈
        navigationController?.pushViewController(hostingController, animated: true)
        
        // 或者模态呈现
        // present(hostingController, animated: true)
    }
    
    private func handlePurchase() {
        print("处理购买逻辑")
        // UIKit 的业务逻辑
    }
    
    private func handleShare() {
        print("处理分享逻辑")
        // UIKit 的分享功能
    }
}

三、数据传递和通信

1. 双向数据绑定

import SwiftUI
import UIKit

// 🌟 支持 Binding 的 UIKit 包装器
struct SwitchControl: UIViewRepresentable {
    @Binding var isOn: Bool
    
    func makeUIView(context: Context) -> UISwitch {
        let switchControl = UISwitch()
        switchControl.isOn = isOn
        switchControl.addTarget(
            context.coordinator,
            action: #selector(Coordinator.valueChanged(_:)),
            for: .valueChanged
        )
        return switchControl
    }
    
    func updateUIView(_ uiView: UISwitch, context: Context) {
        uiView.isOn = isOn
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject {
        var parent: SwitchControl
        
        init(_ parent: SwitchControl) {
            self.parent = parent
        }
        
        @objc func valueChanged(_ sender: UISwitch) {
            parent.isOn = sender.isOn
        }
    }
}

// 🌟 使用示例
struct ControlPanel: View {
    @State private var isSwitchOn = false
    @State private var sliderValue = 0.5
    
    var body: some View {
        VStack(spacing: 20) {
            Text("开关状态: \(isSwitchOn ? "开" : "关")")
            
            // UIKit 的 UISwitch,但支持 SwiftUI 的 Binding
            SwitchControl(isOn: $isSwitchOn)
            
            Text("Slider 值: \(sliderValue, specifier: "%.2f")")
            
            // 原生 SwiftUI Slider
            Slider(value: $sliderValue)
        }
        .padding()
    }
}

四、总结

关键点:

  1. 不能直接混合使用 - SwiftUI View 和 UIKit UIView 不能直接互相添加
  2. 必须通过包装器
    • SwiftUI → UIKit:UIViewRepresentable
    • UIKit → SwiftUI:UIHostingController
  3. 数据流需要特殊处理
    • 使用 Coordinator 处理 UIKit 的回调
    • 使用 @Binding 实现双向数据流
  4. 适用场景
    • 逐步迁移:在 UIKit 项目中逐步引入 SwiftUI
    • 复用组件:继续使用现有的复杂 UIKit 组件
    • 利用优势:SwiftUI 适合新界面,UIKit 适合复杂自定义组件

对于你的小组件开发:小组件必须用 SwiftUI,但你可以:

  • 复用现有的数据模型和业务逻辑
  • 通过 App Groups 共享数据
  • 在小组件完成后再考虑主App的SwiftUI迁移

这样你就能在保持现有UIKit代码的同时,顺利开发SwiftUI小组件了!