在这里插入图片描述

2026 年 5 月 19 日,一位常年维护 Apple 生态应用的 iOS 工程师,又一次在 Xcode 里对着 toolbar 按钮发呆。

WWDC 刚过去不久,iOS 26 把工具栏按钮默认改成了 SF Symbols 图标风;而他的 App 还要兼容 iOS 18——那边习惯的是带文字的按钮。两套 UI 规范,像两条平行时间线,中间只隔了一个 #available,却足以让代码库里悄悄长出一整片「临时补丁森林」。

更麻烦的是:这些补丁,往往活不过一年。😃

在这里插入图片描述


🕵️ 案发现场:版本鸿沟里的「 convenience API 」

每次 iOS 大版本更新,Apple 都会端上一批新 API——只能在最新系统上用,老系统只能干瞪眼。

这位工程师的老办法,大家都熟:自己写一层 convenience API(便利封装)。新系统走官方 API,旧系统走自定义实现或 stub(桩代码/占位实现)。App 通常只支持最近两个大版本,维护起来尚能应付。

在这里插入图片描述

但今年不一样:iOS 26iOS 18,细节差异多到令人发指。

toolbar 举例——

  • iOS 26:按钮展示 SF Symbols 或类似图标,简洁、图标化;
  • iOS 18:还是老派文字按钮,「Cancel」「Done」清清楚楚。

问题本身不难解,一行 #available 的事。可工程师心里清楚:这段代码,一年后就会变尸体。

在这里插入图片描述

等最低支持版本抬到 iOS 26,当年为 iOS 18 写的兼容层,全得删。删漏一处,就是技术债;删错一处,就是线上事故。

有没有办法,让编译器替你把这类「待拆除建筑」标出来?


🧩 第一幕:一个 innocuous 的 toolbar 示例

故事从一个极简 ContentView 开始:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Text("Hello, world!")
                .navigationTitle("Content")
                .toolbar {
                    // 取消按钮:iOS 26 想只显示 xmark 图标
                    ToolbarItem(placement: .cancellationAction) {
                        Button("cancel", systemImage: "xmark") {}
                    }

                    // 确认按钮:iOS 26 想只显示 checkmark 图标
                    ToolbarItem(placement: .confirmationAction) {
                        Button("done", systemImage: "checkmark") {}
                    }
                }
        }
    }
}

目标很明确:

  • iOS 26 → 只要图标,symbol-only 观感;
  • iOS 18 → 只要文字,text-only 观感。

工程师没有在每个 Button 里写 #available,而是抽了一个 ToolbarLabelStyle——专门伺候 toolbar 的 LabelStyle 定制类型。

在这里插入图片描述


🎭 第二幕:ToolbarLabelStyle 登场

struct ToolbarLabelStyle: LabelStyle {
    func makeBody(configuration: Configuration) -> some View {
        if #available(iOS 26, *) {
            // iOS 26+:只显示图标
            Label(configuration)
                .labelStyle(.iconOnly)
        } else {
            // iOS 18 等旧系统:只显示文字
            Label(configuration)
                .labelStyle(.titleOnly)
        }
    }
}

// 语法糖:用 .toolbar 就能套用这套样式
extension LabelStyle where Self == ToolbarLabelStyle {
    static var toolbar: Self { .init() }
}

用起来非常「SwiftUI 原生感」:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Text("Hello, world!")
                .navigationTitle("Content")
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("cancel", systemImage: "xmark") {}
                    }

                    ToolbarItem(placement: .confirmationAction) {
                        Button("done", systemImage: "checkmark") {}
                    }
                }
                .labelStyle(.toolbar)  // 一行搞定双版本差异
        }
    }
}

代码写得行云流水,同事看了都说好。

但工程师盯着 .toolbar 这个静态属性,心里却咯噔一下——

等最低版本 bump 到 iOS 26,这整套东西,就是 dead code(死代码)。

更可怕的是:这类封装往往不只有一处。toolbar、导航栏、Sheet 样式、权限弹窗……codebase 里可能散落着几十个「当时很合理、后来很尴尬」的 convenience API。

在这里插入图片描述

靠人脑记?靠 Code Review 碰运气?靠年底大扫除?

太不靠谱了。


💡 第三幕:让编译器当「拆迁办」——@available 注解

Swift 早就备好了武器:availability annotations(可用性注解),也就是 @available 属性。

工程师在 .toolbar 上贴了一行标注:

extension LabelStyle where Self == ToolbarLabelStyle {
    @available(iOS, deprecated: 26, obsoleted: 27, message: "You don't need .toolbar anymore")
    static var toolbar: Self { .init() }
}

就这一行,局面全变。

在这里插入图片描述


🔍 重点扩展:deprecatedobsoleted 到底差在哪?

这是全文最关键的「机关」。很多人混用,结果要么警告满天飞,要么升级目标版本后直接编译不过。

根据 Swift 官方语义(参见 The Swift Programming Language — Attributes 及 NSHipster 对 API Availability 的梳理):

参数 含义 编译器行为
deprecated: 26 iOS 26 起,该 API 已「过时」 当 App deployment target ≥ iOS 26 时,凡调用 .toolbar 的地方,编译器发出 warning(警告),并显示 message 里的提示
obsoleted: 27 iOS 27 起,该 API 已「废止」 当 App deployment target ≥ iOS 27 时,调用 .toolbar 会直接 error(报错),代码无法通过编译

可以把它理解成两阶段拆迁通知:

  1. deprecated —— 黄条警告:「这楼快拆了,请搬。」还能住,但别装新家具。
  2. obsoleted —— 红条封条:「已废止,禁止进入。」编译器直接拦你。

划重点:这些标记是相对于 App 的 deployment target(部署目标/最低支持版本) 生效的,不是看用户手机当前跑的是哪个系统。你把最低版本 bump 到 iOS 26,当年为兼容 iOS 18 写的 .toolbar,立刻在全项目里标黄——编译器替你完成了 dead code 的「地毯式搜索」。

在这里插入图片描述


⚡ 第四幕:激进派还是稳健派?

有人嫌 warning 不够狠,选择更激进的做法——跳过 deprecated,直接 obsoleted

extension LabelStyle where Self == ToolbarLabelStyle {
    @available(iOS, obsoleted: 26, message: "You don't need .toolbar anymore")
    static var toolbar: Self { .init() }
}

效果:App 目标一 bump 到 iOS 26,所有 .toolbar 调用当场编译失败,不是警告,是 error。

在这里插入图片描述

工程师的建议很实在:

  • deprecated,再 obsoleted —— 给团队一个缓冲期,边升级边清理;
  • 别堆 compiler warnings —— 警告一多,人就会习惯性忽略,technical debt(技术债) 就这样悄无声息地利滚利;
  • 真要用激进方案,至少得确保团队都清楚:这不是 bug,是你在逼自己删 dead code。

在这里插入图片描述


🎯 尾声:让代码库保持「诚实」

工程师越来越喜欢这套打法。

写 convenience API 时不必缩手缩脚——老平台需要 ergonomics(人体工学/易用性封装),该写就写;同时在 API 入口贴上 @available,等于给未来的自己留一张「拆除时间表」。

  • 不用靠记忆;
  • 不用靠文档里某行小字;
  • 不用靠「记得明年删」的 Post-it 便签;

编译器会一遍遍提醒你:这段代码,使命快结束了。

在这里插入图片描述


App 里的 Apple Watch 每 4 分钟测一次心率;CardioBot 帮用户读懂这些数据,改善生活方式。广告插播完毕——但那位工程师的故事,其实发生在每一个维护跨版本 SwiftUI 项目的人身上。

iOS 大版本更新从不是「换几个 API」那么简单。它更像一场无声的代码考古:你去年埋下的 convenience 层,今年可能是救命稻草,明年就是待拆违章建筑。

在这里插入图片描述

聪明的做法,不是不写这些代码——

而是写的时候,就告诉编译器:它迟早会死。

感谢观赏,我们下次不见不散!😎

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐