导读:在 SwiftUI 的舞台上,其以现代化的声明式编程语法和卓越的性能赢得了业界的广泛赞誉。然而,作为一个仅发展了四年的新兴框架,SwiftUI 仍面临一些亟待解决的挑战。尤为突出的是,导航管理的不足成为限制其广泛应用的绊脚石。尽管 UIKit 的传统导航方式已相当成熟,但如何在保持 SwiftUI 现代特性的同时,有效融合这两种框架,成为众多开发者面临的难题。
如何借助 Coordinator 模式破解 SwiftUI 中的导航难题。我们将通过实战演示,利用 UIHostingController 和导航协议,构建一套既高效又灵活的应用导航系统。此方案旨在融合 UIKit 的经典优势与 SwiftUI 的创新特性,为开发者提供一套完美的解决方案。无论你是 SwiftUI 的新手还是资深专家,本文都将为你奉上实用的技巧和深刻的洞见,助你跨越复杂应用开发中的导航难关。加入我们,一同探索如何在 SwiftUI 与 UIKit 之间搭建桥梁,解锁更多强大的功能潜力,让你的应用导航更加流畅、高效!
今天,我将与大家分享我在三家初创公司中运用 SwiftUI 的实践经验,并深入探讨我是如何克服那些阻碍你全面采用 SwiftUI 的关键挑战的。
本文改编自我于 2023 年 6 月在 Monzo 伦敦办公室举办的 NSLondon 演讲。
SwiftUI 的演进历程
自 SwiftUI 问世以来,我有幸一直是其主要用户。
作为 SwiftUI 的忠实拥趸,我亲眼见证了它的成长与发展。回溯至 2019 年,当 SwiftUI 的初版测试版横空出世时,我正投身于一个名为 Patcher 的副业初创项目之中,该项目旨在成为汽车维修领域的 Uber。如同许多初创团队的工程师一样,我们怀揣着满腔热情,决定在赢得首批用户青睐之前,先全力以赴地完善所有功能。
Patcher 应用功能繁多,涵盖了地图导航、新手引导、用户资料管理、工作请求处理、支付系统、维修流程跟踪等各个环节,甚至还包括了一个专为维修技师设计的配套应用。然而,这一项目的复杂性不言而喻,而那时的 SwiftUI,尤其是在 2019 年底至 2020 年初的阶段,显然还难以驾驭如此庞大的系统。
初期的 UIKit 与 SwiftUI 之间的互操作性并不理想,状态管理也成为了一项挑战,尤其是在整合像 Google Maps SDK(当时 SwiftUI 中的 MapView 尚未问世)和摄像头功能时,这些问题尤为凸显。更令人沮丧的是,SwiftUI 1.0 版本的导航功能几乎形同虚设,迫使我们不得不大量依赖模态视图来实现应用内的导航。
时间推移至 2020 年末,我参与了 Carbn 的创立,这是一家旨在引导人们培养更环保生活方式并减少碳足迹的初创企业。幸运的是,此时 iOS 14 已经发布,SwiftUI 也迎来了它的黄金时期。随着 @StateObject、惰性堆栈(lazy stacks)、ScrollViewReader 等强大工具的加入,以及我个人极为偏爱的 matchedGeometryEffect 效果的引入,SwiftUI 的功能与性能得到了显著提升。但更为重要的是,开发者们开始逐渐掌握如何利用 SwiftUI 构建出既合理又高效的应用程序。
现在,作为 Gener8 公司的移动工程负责人,我带领团队致力于帮助用户掌握并利用自己的数据创造价值。我们非常幸运地能够专注于 iOS 15 及以上版本的开发,这使得我们能够充分利用材质效果、可刷新视图、任务修饰符以及 Markdown 渲染等先进功能。
可以说,SwiftUI 已经全面成熟,从四年前的一个实验性工具,成长为能够支撑起严肃商业项目的强大 UI 框架。
它真的适合在生产环境中大展拳脚吗?
简而言之,如果你在生产环境中选择使用 SwiftUI,那么它绝对能够胜任!
回顾过去,Swift 在实现 ABI(应用程序二进制接口)稳定性之前,许多 Objective-C 开发者曾对 Swift 能否应对大型项目持怀疑态度。同样地,到了 2030 年,我们或许还会听到有人坚持推崇 UIKit(尽管我尚未见到有人仍在使用 Storyboard)。
接下来,我将简要分析 SwiftUI 的优势、存在的不足,以及为何一些工程师尚未完全全面转向 SwiftUI。
SwiftUI
优势
关于 SwiftUI 的优点,我不再一一赘述,仅简要提及几点核心优势。它速度快、采用声明式编程模式、具备高度响应性,并内置了单向数据流,让开发者能够轻松驾驭数据流向。无疑,SwiftUI 代表着未来的发展方向。
不足
当然,SwiftUI 也非尽善尽美。其导航功能稳定性一直是个问题,对于传统 UIKit 框架中的 AV(音频视频)、Camera(相机)和 Maps(地图,尤其是 iOS 17 之前的版本)等功能的支持较为有限。由于 SwiftUI 尚处于不断完善之中,有时开发者可能仍需借助 UIKit 来实现特定功能。此外,SwiftUI 的渲染引擎背后是 CoreAnimation、UIKit 和 Metal 的复杂交织,这种 “幕后魔法” 使得精确的性能调优变得更具挑战性。
难题
坦率而言,苹果似乎更倾向于将焦点放在单视图或主从结构上,对其他类型的应用架构则略显忽视。与所有新兴框架一样,SwiftUI 所依赖的操作系统版本不断变化,这意味着开发者需要在代码中频繁使用 if #available
语句来确保兼容最新功能。此外,SwiftUI 的状态管理机制与现有的 UIKit 代码体系存在较大差异,两者之间的无缝融合仍是一个待解的难题。
SwiftUI 虽然备受赞誉,但仍有其不足之处。作为一个仅诞生四年的新兴框架,某些缺陷在预料之中,然而,有一个核心问题显著地限制了它的广泛应用:
导航难题亟待解决
导航问题无疑是开发者在全面拥抱 SwiftUI 时遇到的首要障碍。特别是对于构建大型、复杂的应用程序而言,目前还缺乏一个理想的导航解决方案。虽然利用应用数据状态来控制导航逻辑在小规模应用中颇为有效,但尝试将这种方法扩展到大型项目时,其复杂性和工作量会急剧增加。开发者不应被迫去记忆整个应用的状态变化,以此推断出应该展示哪个界面。
问题的根源在于……
“大型视图控制器” 问题重现江湖
为了更深入地理解这一挑战,我们来看看苹果在 iOS 16 中对 SwiftUI 导航模式的改进尝试:
NavigationStack
、NavigationLink
、.navigationDestination
、.sheet
和 NavigationSplitView
。
这些功能都紧密集成在 SwiftUI 的视图体系中,允许视图根据用户的交互行为或应用的状态变化来动态决定哪些子视图应该被呈现。
这迫使开发者不得不将视图与导航逻辑紧密绑定在一起,极大地挑战了代码的可测试性,也违背了工程开发的最佳实践原则。实际上,iOS 开发界的工程师们才刚刚挣脱出这种不良习惯的束缚。回想起过去,使用字符串类型的 segue、在 viewDidLoad()
方法中直接调用 URLSession、以及将整个应用逻辑堆砌在单个 Main.storyboard
文件中的日子,简直如同梦魇般让人心有余悸。
对于初涉 iOS 开发领域的你,若想深入了解 “大型视图控制器” 综合症的根源与影响,Paul Hudson 的这篇教程无疑是极佳的入门指南。它不仅深刻剖析了因关注点混乱导致的种种问题,还系统介绍了 UIKit 的最佳实践方法,为开发者指明了优化代码结构的路径。
导航逻辑的抽象化
我们已经明确了问题的症结所在:SwiftUI 尚不支持将导航逻辑进行有效的抽象化处理,这给大型工程团队在协作开发复杂应用时带来了不小的困扰。
但好消息是,解决方案并非遥不可及,它其实就隐藏在我们日常的编码实践中。
Coordinator 模式
这一模式,有时也被称为 Router 模式或 Navigator 模式,其核心思想高度一致:即将导航逻辑进行封装,实现与视图层的解耦。
我个人倾向于在 AppDelegate
或 SceneDelegate
中引入一个 AppCoordinator 来实施此模式。该 Coordinator 作为整个应用的导航中枢,负责全局的导航管理,并可以根据需要创建多个子 Coordinator 来分别处理不同的业务逻辑流程。这些子 Coordinator 又可以进一步细化,管理更为具体的导航逻辑,从而形成一个层次分明、结构清晰的导航管理体系。
使用 Coordinator 模式的基础应用架构
Coordinator 的协议设计相对简洁明了:
Coordinator.swift
在 Coordinator 的设计中,Route
通常被定义为一个枚举类型,它列举了在该 Coordinator 所负责的导航流程中可能出现的各个界面。整个应用程序的导航逻辑都被巧妙地封装在 Coordinator 的 navigate(to:)
方法中。
尽管这种方法在 UIKit 框架中颇为常见,但接下来我将详细说明如何对其进行调整,以实现与 SwiftUI 框架的完全兼容。为此,我们只需引入两个额外的关键组件:
UIHostingController
UIHostingController 扮演了一个桥梁的角色,它将 SwiftUI 的上下文包裹在 UIViewController 之中,从而实现了这两种 UI 框架之间的无缝对接。
导航协议
为了进一步提升应用的灵活性和可维护性,我们将引入两个新的协议。这两个协议旨在抽象化 UINavigationController 和 UITabBarController 的功能,这两者都是构建复杂 UIKit 应用的基石。这样一来,无论你是否有 UIKit 的开发背景,都能轻松理解和应用你应用的导航结构。对于熟悉 UIKit 的开发者而言,这样的设计更是能够让他们迅速上手,并充分利用他们在 UIKit 中的经验来优化 SwiftUI 应用的导航体验。
通过 Coordinator 模式构建的导航架构
NavigationContext
此协议明确了我们对导航功能的基本需求:即能够展示和隐藏 SwiftUI 视图,同时将呈现逻辑与视图代码清晰分离。
NavigationContext.swift
在这个文档中,我们定义了一系列呈现函数,这些函数接收一个泛型视图作为参数,并实现了所有核心的导航操作,如推送、弹出视图,呈现和关闭视图,以及在导航结构的根位置创建初始视图。
协议的具体实现则是通过继承自 UINavigationController
的子类完成的:
MyNavController.swift
在这个文档中,你会发现实现过程与标准的 UIKit 导航方法非常相似,但多了一步操作 —— 即将 SwiftUI 视图包裹在 UIHostingController 中,以实现两者之间的无缝衔接。
接下来,我们可以轻松地为第一个 Coordinator 的 navigate(to:)
方法填充实现逻辑:
MyCoordinator.swift
NavigationRoot
此协议则是对 UITabBarController
功能的抽象,通常你的应用中仅会有一个这样的控制器,由 AppCoordinator 负责管理。
NavigationRoot.swift
尽管此部分不直接涉及 SwiftUI,但它负责管理多个导航上下文,并允许用户通过标签栏在不同上下文之间切换。此外,还提供了在标签栏控制器上模态展示上下文的功能,这在处理登录认证等场景时尤为实用。
拓展功能
在成功整合了所有主要构建块之后,我们不仅能够利用拥有 16 年历史的 Cocoa Touch API 来开发 SwiftUI 应用,还可以通过进一步子类化 UIHostingController
来增强功能。这样,我们可以:
在每个视图上实现个性化的样式,无需依赖
UIAppearance
API利用
UIViewController
的生命周期事件,在不同NavigationContexts
之间传递状态
以下是一个基本实现的示例:
MyHostingController.swift
这一实现将引导我们创建一个更加复杂的 navigate(to:)
方法:
MyCoordinator.swift
通过利用视图的生命周期事件,在父视图上动态地显示和隐藏覆盖层。最终,我们展示了如何通过标准的 Coordinator 模式实现 UIKit 与 SwiftUI 之间的无缝互操作性,并以展示 SFSafariViewController
为例进行了演示。
结论
虽然 Coordinator 模式并非构建复杂 SwiftUI 应用的唯一途径,但它无疑是一个高效且实用的选择。当然,你也可以根据需求选择基于状态的范式,或者如果你的目标平台是 iOS 16 及以上版本,还可以考虑使用更新的导航 API,甚至尝试采用 The Composable Architecture 等先进架构。
这些不同方案的共同之处在于,它们都在不断推动技术发展。通过采用 Coordinator 模式,你可以在享受 SwiftUI 带来的响应式 UI 和快速开发优势的同时,充分利用 UIKit 在导航和定制方面的强大功能。此外,你还可以轻松实现即插即用的互操作性,几乎无需担心状态管理的问题。如果你尚未全面转向 SwiftUI,那么现在就是利用这些经过时间考验的范式的好时机,让我们从今天开始,迈向更加高效的开发之路。
作者简介:
Jacob,现任伦敦一家初创公司的首席 iOS 工程师。
原文链接:
https://jacobbartlett.substack.com/p/swiftui-apps-at-scale
声明:本文为 InfoQ 翻译整理,未经许可禁止转载。
评论