6.3 UIKit 与 SwiftUI 共存与选型


6.3 UIKit 与 SwiftUI 共存与选型

本节摘要:病虫害手册合上,园子里最后一件事是规划枝条的去向:下一个页面,长在 UIKit 老枝上还是 SwiftUI 新枝上?本节给出选型五问(团队、工程年龄、系统版本、依赖库、页面性质),两种互嵌的完整代码——UIKit 园子里种 SwiftUI、SwiftUI 页面里请 UIKit 控件——加上共存的三条纪律与老工程渐进迁移的路线图。这是全册最后一节:从会写,到会选。

先问五个问题

先亮结论:不存在"哪个框架更好",只存在"哪个框架更合适"——把五个问题挨个问完,答案自己浮出来:

问题 倾向 UIKit 倾向 SwiftUI
团队现状 老团队 UIKit 功底深厚,招聘看即战力 新组建团队或个人项目,无历史包袱
工程年龄 存量页面上百个,改造成本高 全新工程,白纸一张
系统版本 还要照顾老系统的存量用户 最低版本敢上较新的系统
依赖库 核心依赖只有 UIKit 封装 依赖已有 SwiftUI 版本或不依赖界面库
页面性质 重文本交互、精细的触摸时序控制 表单、列表、设置这类声明式友好页面

五问的权重并不均等。前两问决定迁移成本,权重最大——一个百人团队、上百页面的成熟工程,就算后三问全部倒向 SwiftUI,也不该整体切换,共存才是常态;后三问决定单个新页面的边际成本,在"新页面用哪套"时才轮到它们发言。阳台花园的答案就是按表打勾打出来的:个人项目、全新工程、系统版本跟着最新开发工具走、无界面库依赖、页面以列表和表单为主——于是第 5 章整章 SwiftUI;教学上又故意保留 UIKit 版的花园页,正好凑出 6.1 那个双框架共存的现场。

互嵌之一:UIKit 园子里种 SwiftUI

官方给的嫁接接口叫 UIHostingController——它本质是个普通的视图控制器,根上顶着一棵 SwiftUI 视图树。6.1 已经用它装过收藏页,这里补全"装进去之后"要知道的细节:

// 在 UIKit 导航栈里推入一个 SwiftUI 页面 func openFavorites() { let fav = UIHostingController(rootView: FavoriteScreen(store: sharedStore)) fav.title = "收藏" // 导航栏标题归 UIKit 管,照常配置 navigationController?.pushViewController(fav, animated: true) print("根视图类型:\(type(of: fav.view))") // 输出:根视图类型:_UIHostingView // 两棵树的关系:UIKit 控制器在外层,SwiftUI 树整棵装进一个宿主视图里 }

嫁接后的生命周期完全按 UIKit 规则走:viewWillAppear 照常触发,SwiftUI 的 onAppear 跟着对齐;返回手势、导航栏、标签栏都归 UIKit 侧管。唯一要记在心里的差别还是那条老对照(5.1):SwiftUI 页面观察仓库自动刷新,UIKit 页面靠生命周期回调主动重读——共享同一个仓库时,6.1 的"仓库广播加主动重读"就是为这个组合准备的验收项。

互嵌之二:SwiftUI 页面里请 UIKit 控件

反向嫁接靠两个协议:UIViewRepresentable 包视图、UIViewControllerRepresentable 包控制器。第 5 章刻意没讲的自绘进度环,可以用 4.5 的 UIKit 旧实现包给 SwiftUI 用——这也是真实工程里最常见的反向场景(成熟控件舍不得扔):

struct LegacyProgressRing: UIViewRepresentable { var progress: Double // 对 SwiftUI 侧只暴露一个参数 func makeUIView(context: Context) -> WaterRingView { // 创建:只跑一次 WaterRingView() // 4.5 写的那个 UIKit 自绘环 } func updateUIView(_ uiView: WaterRingView, context: Context) { uiView.progress = progress // 同步:每次视图体重算都会来 print("环同步进度:\(progress)") // 输出:环同步进度:0.6 } } // SwiftUI 侧用法:LegacyProgressRing(progress: plant.waterRatio) // makeUIView 只在视图首次进入树时跑一次;之后进度变化只触发 updateUIView

这套协议的核心是 make 与 update 的分工:创建归 make、同步归 update。update 会被 SwiftUI 频繁调用(任何触发重算的状态变化都会来一遍),所以别在 update 里做重活——布局、建对象、开定时器都属重活,它们的正确位置在 make 里。

共存的三条纪律

互嵌技术不难,难的是两套机制长期同园不乱。三条纪律立起来:

其一,模型层只写一份。Plant 结构体、PlantStore 仓库(第 2 章那套)是两套界面共用的唯一真相——界面可以有两套,真相只能有一个。任何"这个框架的页面自己再存一份数据"的念头,最终都会变成 6.2 案例里那种偶现病症。

其二,边界最小化。Representable 包装里只做"桥":参数进、属性出、代理转发,不塞业务逻辑。一个包装只干一件事——包进度环的就只包进度环,别顺手把点击统计也写进去。边界糊掉的混合页面,调试成本会吃掉声明式的全部收益。

其三,刷新责任分清。SwiftUI 侧自动观察,UIKit 侧主动重读,混用时不要试图把两边统一成一种机制——给 UIKit 页面强配观察、给 SwiftUI 页面手动刷,都是逆着框架天性用力。

渐进迁移路线

老工程不必推倒重来。按风险从小到大排,每个阶段都以"页面"为单位整体切换,不在单个页面里混到控件级:

阶段 迁什么 为什么先迁它 回滚成本
设置页、关于页 纯静态表单,SwiftUI 最擅长的地形 极低
新增的独立页面 不动存量代码,新枝只往空处长
表单与列表页 收益开始明显,但数据源要接仓库
导航与底座 牵一发动全身 高,留到最后

风险最高的是第四阶段:导航状态、深链跳转、手势冲突全部集中在那里,任何一个出问题都会波及全应用。务实的态度是——停在阶段三、两套长期共存,是完全正当的终点;为了"全 SwiftUI"的名声硬啃阶段四,往往得不偿失。每迁完一页,跑一遍 6.1 的回归清单(把跨框架那条挪到最前面),再动下一页。

案例:收藏页的选型过程

背景:阳台花园要做收藏页,需求三件事——列表展示收藏的植物、左滑取消收藏、无收藏时显示空态插画。操作:过五问。全新页面(工程年龄不拖后腿);列表加左滑是 SwiftUI 的 List 原生本事;要与花园页共享仓库(EnvironmentObject 直达);唯一犹豫点是想自定义左滑按钮的配色,估计要翻一阵文档。再对照 UIKit 实现的成本:表格数据源一套、滑动动作配置一套、空态视图切换又一套,粗估代码量是 SwiftUI 版的两倍。决策:SwiftUI。结果:半天完成;两周后追加"批量管理"需求时,发现 List 的编辑选择模式又是现成的,第二版几乎零成本:

// 第二版追加的批量管理:List 原生编辑模式,几行搞定 List { ForEach(store.favorites) { plant in PlantRow(plant: plant) } .onDelete { indexSet in store.removeFavorites(at: indexSet) // 改仓库,界面自动跟(5.3 纪律) } } .environment(\.editMode, .constant(.active)) // 编辑模式常开即"批量管理"

解读:选型的真实收益常常不在第一版而在第二版——UIKit 的第一版未必更慢,但每次改需求都要人肉同步界面状态;SwiftUI 的第一版要翻文档,从第二版起红利开始兑现。变式:假如这个页面要做"长按拖拽跨页排序加与触摸时序强绑定的动画",结论可能反转——精细的触摸时序控制是 UIKit 的主场,这正是第五问存在的意义。

本节要点回顾

  • 五问定框架:团队、工程年龄、系统版本、依赖库、页面性质;前两问管迁移成本,权重最大;
  • 双向嫁接都有官方接口:UIHostingController 装 SwiftUI 页面,Representable 包 UIKit 控件,make 与 update 分工不能混;
  • 三条纪律:模型只写一份、边界最小化、刷新责任分清;
  • 渐进迁移以页面为单位,从静态页一路到导航底座,风险递增,停在共存不丢人;
  • 选型收益看第二版:第一版比的是上手快慢,第二版比的是改动成本。

树长成了,园子开张了。这本日记从破土的一颗种子记到成林——浇水记录会提醒你回来看看,但接下来轮到你的园子自己生长了。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U