不能直接相互调用,需要通过特定的包装器进行桥接。
一、相互调用机制
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()
}
}
四、总结
关键点:
- 不能直接混合使用 - SwiftUI View 和 UIKit UIView 不能直接互相添加
- 必须通过包装器:
- SwiftUI → UIKit:
UIViewRepresentable - UIKit → SwiftUI:
UIHostingController
- SwiftUI → UIKit:
- 数据流需要特殊处理:
- 使用
Coordinator处理 UIKit 的回调 - 使用
@Binding实现双向数据流
- 使用
- 适用场景:
- 逐步迁移:在 UIKit 项目中逐步引入 SwiftUI
- 复用组件:继续使用现有的复杂 UIKit 组件
- 利用优势:SwiftUI 适合新界面,UIKit 适合复杂自定义组件
对于你的小组件开发:小组件必须用 SwiftUI,但你可以:
- 复用现有的数据模型和业务逻辑
- 通过 App Groups 共享数据
- 在小组件完成后再考虑主App的SwiftUI迁移
这样你就能在保持现有UIKit代码的同时,顺利开发SwiftUI小组件了!