在苹果推出 SwiftUI 时,那句隐形的口号 “Learn once, apply anywhere” 成了许多开发者的终极梦想。对于我自己的项目而言,刚开始我也是这么想的。毕竟能用同一套代码同时跑在 iOS、iPadOS 和 macOS 上,听起来简直是效率神器。
然而,随着项目的野蛮生长,这套看似完美的“跨平台理论”在实践中狠狠地给我上了一课。今天就来复盘一下,这个项目是如何从起初的“大锅饭”,一步步走向混乱,最后狠下心走向“精细化拆分”的重构之路。
一、 蜜月期:图一时的爽快,万物皆在一个 Target
项目刚刚起步时,为了追求上线速度和开发便利,我做了一个最符合直觉的决定:直接把 iOS、iPadOS 和 macOS 放在了同一个 Target 之下。
当时的想法很简单粗暴:既然 SwiftUI 跨平台,那么针对 macOS,直接勾选 Mac Catalyst 或者 Designed for iPad,让 Mac 复用 iPad 的页面布局和交互逻辑不就好了?
当时的开发体验确实“爽”。写完一个色彩列表、一个详情页,全平台直接 Build 就能跑起来,仿佛自己掌握了开发效率的魔法。但正如所有速成架构一样,这只是一场暴风雨前的宁静。随着我对 macOS 版本的深度体验,各种违和感扑面而来:
- 大面积不合理的留白
- 强行模拟触摸的滑动交互
- 全屏 Push 而不是多栏导航
- 缺乏真正的桌面端 Toolbar 和快捷键支持……
它在 Mac 上看起来,根本不像是一个正经的桌面软件,更像是一个生硬的“放大版手机模拟器”。
二、 补救与妥协:抽离 Shared 层与噩梦的开端
意识到不能对待 Mac 桌面用户如此敷衍后,我决定对工程动手。
在架构上,我做了一次理性的初步调整:新建了单独的 macOS Target,并将数据库模型(Data Model)、核心业务逻辑(Service)等与界面无关的代码抽离出来,作为一个 Shared 层(共享组件库)。这也是苹果官方比较推崇的底层复用方式。
但是——也就是这最让我后悔的一个决定——为了“最大化代码复用”,我在视图层偷了懒,让这两个端依然共享着同一套 UI View 视图文件。
起初遇到排版差异怎么解决?很简单,哪里不合适加哪里。于是我开始频繁地使用条件编译:
#if os(macOS)
// Mac 下增加按钮,改变字体,使用桌面端特有的 PickerStyle
.pickerStyle(.radioGroup)
#else
// iOS 下的样式
.pickerStyle(.segmented)
#endif
刚开始只有几处时还能忍受,我还暗自得意,觉得通过几个宏定义就巧妙搞定了跨平台差异。
三、 技术债爆发:被 if os(macOS) 支配的恐惧
随着页面越来越多,图片色彩提取、专业调色板编辑、相机动态取色等功能越来越复杂,这些“共享的 UI 文件”彻底变成了一片沼泽。代码里密密麻麻地长满了 #if os(macOS) 的藤蔓。
这种高度耦合的“混合态代码”带来了几个灾难性的后果:
- 可读性跌破底线:一个原本 100 行就能写得很清晰的 View,硬生生被条件编译撑到了 300 多行。阅读代码时,大脑需要像编译器一样不断进行上下文切换和分支剔除,心智负担极重。
- UI 极度不稳定,牵一发而动全身:改一个共享文件的布局,简直就是排雷。由于 iOS (UIKit 底层) 和 macOS (AppKit 底层) 在 SwiftUI 渲染上的细微差异,经常是“按下了葫芦浮起了瓢”。今天在 iOS 上修好了一个 Padding,明天 Mac 上的视图就诡异塌陷了。
- 开发效率逆向衰退:初衷本来是为了少写代码,结果到了后期,大部分时间反而都浪费在了两端排版的兼容、反复编译排错和修 Bug 上。连动手加个新小组件都要战战兢兢。
四、 刮骨疗毒:彻底的物理拆分与原生拥抱
忍无可忍,无需再忍。为了项目未来的可维护性,我终于下定决心做一次架构上的大手术:彻底打破 UI 层的假性复用,进行全面的物理隔离。
重构的核心思路非常清晰且坚决:
- 坚守底层共享(Shared):数据层、状态管理和各种 Service 等非 UI 逻辑依然沿用共享层。因为经过时间检验,这部分业务逻辑在双端确实是高度一致的。
- 视图层彻底分家:把所有的混合 View 文件全部“分家”。iOS 和 iPadOS 保留原有的 View 体系;而对于 macOS,单独创建一套以
Mac前缀开头的专属 View(比如把曾经混在一起的PhotoPalettePickerView彻底解耦成纯 iOS 版的和纯桌面版的MacPhotoPalettePickerView)。 - 放开手脚拥抱原生:拆分后,Mac 端的页面终于解开了束缚。可以直接深入使用针对鼠标交互优化的悬浮态(Hover)、原生的侧边栏(Sidebar)、多窗口管理,再也不用受制于 iOS 的手势滑动交互逻辑。
总结:该写两遍的代码,千万别省
“Write once, run anywhere” 这句话,对于简单的展示类 App 和纯逻辑代码是极其美好的。但一旦涉及复杂的交互设计、高度定制的控件,以及要求精细化体验的商业级应用时,硬凑在一起复用只会反噬你的项目。
对于 SwiftUI 跨平台开发,“Learn once, write anywhere” 才应该是真正的指导思想。
适度的 UI 代码冗余,不仅不是坏事,反而是保证项目稳定性、提升后续可迭代性,以及为用户提供极致“本平台原生体验”的基础保障。不要害怕拆分代码,该隔离的时候果断物理隔离,这不仅是对产品的负责,更是对你未来发际线的拯救。