← 所有文章

@State 宏:Xcode 27 不再编译的代码

Apple 的 iOS 27 发行说明在 @State 条目的开头,先描述了一个 SwiftUI 自 iOS 13 起就一直带着的缺陷:3“以表达式作为初始值声明的 @State,过去每次视图结构体重新实例化时都会重新求值该表达式。以 @State private var model = Model() 为例,这意味着 Model.init() 在视图的整个生命周期中会被调用很多次。”1

修复方式是重写。“Xcode 27 引入了新的 @State 实现,避免这种重复求值。新行为可向下部署到与 iOS 17 对齐的系统版本。新的 @State 由 Swift 宏实现,与属性包装器版本在源码层面基本兼容,但有少数例外。”1

请留意这句话里的触发条件:Apple 说的是 Xcode,不是部署目标。

要点速览

  • Xcode 27 把 @State 重新实现为 Swift 宏,而 Apple 的两个符号页面从正反两个方向都写明了这次切换:State 结构体页面说“当您使用 Xcode 27 或更高版本构建时,系统改用 State() 宏”,State() 宏页面说“当您使用 Xcode 26 或更早版本构建时,系统改用 State 属性包装器”。23
  • 部署目标无法帮您绕开这个宏。它与被它取代的属性包装器一样,可用性都从 iOS 13.0 起算,而切换与否只看 Xcode 版本,不看部署目标。运行时那一半只向下部署到“与 iOS 17 对齐的系统版本”,因此仍支持 iOS 15 或 16 的项目只拿到编译期的变化,拿不到运行时的改进。
  • 静默丢弃这一行为由来已久,并未改变。Apple 写道,它“并未因为宏而改变,只是其中一些情况不再能通过编译”。1宏把一个会吞掉初始化器赋值的缺陷,变成了一次构建失败。
  • 编译中断集中在两种写法:一是初始化器给一个在声明处已有初始值的 @State 属性赋值;二是扩展调用编译器为存储成员全部私有的结构体合成的逐成员初始化器。1Apple 的措辞是“其中一些情况”,不是全部,而且从未列举究竟是哪些。
  • 动笔之前,我先审查了自己已上架的四款 App:267 处 @State 声明,其中 201 处带声明处初始值,两种破坏性写法一处都没有,一处险些命中,外加一个在 macOS 上会伪造出“干净”结果的 grep 惯用写法。6

这条说明放在 iOS 与 iPadOS 27 发行说明的 SwiftUI 一节,而不是 Xcode 27 发行说明里——后者没有对应条目。7对于一项被 Apple 归因于 Xcode 的改动来说,这个位置颇为古怪,也足以解释为什么消息会传得很慢。

Apple 修掉的性能缺陷

就发行说明而言,Apple 对旧行为的描述直白得罕见:Model.init()“在视图的整个生命周期中会被调用很多次”。1SwiftUI 会不停地重新实例化视图结构体,而每一次重新实例化,都会把等号右边的表达式重新求值一遍。结果会被 SwiftUI 丢弃——已经存在的状态优先——但这份计算照样发生了。

宏的文档页用一句话给出了新的约定:“State() 属性只在 SwiftUI 首次实例化该视图时实例化其默认值。”2

Apple 的 SwiftUI 更新页面还补了一个发行说明没写的限定条件,而这个限定条件恰恰是排期时最该考虑的部分:“请使用 Xcode 27 或更高版本构建项目,这样 @State 特性才会使用 State() 宏在 AppSceneView 中创建状态值。仅当属性是类时,此项改动才只初始化并存储该属性一次。”4

“仅当属性是类时”把收益范围收窄了不少,而它指向的,恰恰是现代 SwiftUI 代码库里遍地都是的写法。把 @Observable 对象存进 @State 是 Apple 文档推荐的做法,宏文档页自己的示例就正是这样持有一个 @Observable class Library2结构体的初始化器通常开销很小;而一个要打开存储、发起查询或注册观察者的类初始化器,开销就不小了,Apple 说它会跑“很多次”。1

到底变了什么,又有什么没变

Apple 把语义和编译行为写进了同一句话,而这句话的两半指向相反的方向:“如果您在 @State 声明处提供了初始值,同时又试图在初始化器中给它赋值,那么初始化器中的值会被丢弃。这一行为并未因为宏而改变,只是其中一些情况不再能通过编译。”1

代码的含义一点没变。在属性包装器时代,给一个声明处已有初始值的 @State 属性赋值的初始化器,什么也不做,而且悄无声息,编译一路绿灯。宏没有动规则,只是拿走了那份沉默。

被淘汰掉的,正是代价最高的那种失败模式:开发者写好初始化器,把标题一路传进去,却看到界面渲染出错误的标题,于是钻进视图 body 里到处翻找。编译器其实一直都知道,只是没有办法说出口。

Apple 自己的示例里,有两行注释把这件事说得很清楚:

struct StickerPageView: View {
    @State private var page = StickerPage()
    let title: String

    init(title: String) {
        // `title` won't have any effect
        // this also won't compile with @State macro
        self.page = StickerPage(title: title)
        self.title = title
    }
}

“不会有任何效果”和“无法通过编译”写在相邻两行。前者描述的是 Xcode 26,后者描述的是 Xcode 27。同一段代码,同一种含义,判决却不同。

修复办法是删掉声明处的初始值:

struct StickerPageView: View {
    @State private var page: StickerPage // no initial value expression
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title) // works!
        self.title = title
    }
}

Apple 把规则浓缩成一条指令:“当通过初始化器赋初始值时,不要在 @State 声明处再提供初始值。”1

所以准确的说法不是宏搞坏了初始化器赋值。初始化器赋值本来就是坏的,只是宏第一次把这件事挑明了。把这次改动说成倒退,方向就搞反了——尽管对一条发布分支而言,实际后果毫无二致:昨天还能通过的构建,今天挂了。

有一条保留意见值得一直记着。Apple 写的是“其中一些情况”,不是全部,而且从未列举是哪些。在 Xcode 27 下构建通过,只是关于您这份代码的证据,不足以证明规则本身的边界。

合成初始化器消失了

第二种例外与初始值无关,它波及的是调用处根本没出现过 @State 的代码。

“当一个结构体的所有存储成员都是 private 时,编译器会合成一个 private 的 init,可以在同类型的扩展中使用:”1

struct StickerPageView: View {
    @State private var page: StickerPage
    private let title: String
    ...
}

extension StickerPageView {
    init(title: String, _ page: StickerPage) {
        self.init(page: page, title: title) // using the synthesized init
    }
}

“state 宏会禁用这个合成的初始化器,因此上面的代码不再能通过编译。缓解办法是显式给各成员赋值:”1

extension StickerPageView {
    init(title: String, _ page: StickerPage) {
        self.title = title
        self.page = page
    }
}

这种写法比第一种更难揪出来,因为出问题的调用处与引发问题的 @State 根本不在同一个声明里。用 grep@State 是搜不到它的。

Apple 只说了后果,就此打住。而宏公开的声明与这个症状对得上:

@attached(accessor, names: named(init), named(get), named(set))
@attached(peer, names: prefixed(`_`), prefixed(`__`), prefixed(`$`))
macro State()

一个提供 getset 的访问器宏,会改变编译器眼中这个属性的性质,而逐成员初始化器的合成依赖的正是存储属性。2把这段声明读作原因,是我的推断,不是 Apple 的说法;不过缓解办法与机制无关:把赋值手写出来就是了。

泛型推断与属性包装器组合

剩下的两种例外,Apple 各给了一句话。虽然它们都不会波及太多项目,但迁移检查清单里仍值得各记一笔。

泛型推断是整条说明里写得最含糊的部分:“在少数情况下,@State 泛型参数的自动推断在宏实现下不如以前灵活。请把类型写得更明确。”1Apple 既没点名具体情形,也没给出可供搜索的诊断信息。缓解办法是在声明处显式标注类型,哪里报错就补哪里。

关于组合,Apple 的措辞是限制,而不是破坏:“不支持将 @State 与其他属性包装器或宏组合使用。”1如果您曾把 @State 包进自己写的属性包装器里,值得回头查一查;同时也要记住,Apple 把它划为“不受支持”的地带,而不是“以前能用”的东西。

没有哪个部署目标能豁免

来自 Apple 三个页面的三句话,堵死了所有退路。

State() 宏这个符号的可用性从 iOS 13.0、iPadOS 13.0、macOS 10.15、tvOS 13.0、watchOS 6.0 和 visionOS 1.0 起算,与它取代的属性包装器完全一致,所以调低最低支持版本并不能躲开这个宏。23而且切换取决于编译器,不是部署目标:“当您使用 Xcode 27 或更高版本构建时,系统改用 State() 宏。”3

这次改动的运行时那一半,有一条编译期那一半没有的下限。Apple 写道,新行为“向下部署到与 iOS 17 对齐的系统版本”,这在下方留出了一道缝隙:部署目标为 iOS 15 或 16 的项目,在编译期照样拿到宏,连同那些源码兼容性例外一起,却拿不到向下部署的运行时行为。1至于这些部署目标究竟拿到了什么,Apple 没有说。如果您还支持早于 iOS 17 的系统,就把构建中断当作必然,把重复初始化的修复当作在最老的设备上尚未确认。

用 Xcode 27 打开项目、构建,您拿到的就是新的 @State。没有 Info.plist 键、没有构建设置,也没有可用性判断参与这个决定。

27 这一轮带来了三项破坏性改动,开发者习惯把它们归在同一个标题下,可它们触发的时机各不相同。启动屏要求约束的是“使用 27.0 SDK 或更高版本构建的 App”,代价是被 App Store 拒审。场景生命周期强制要求约束的是“使用最新 SDK 构建”的 App,代价是 App 根本启动不起来。而 @State 宏约束的是 Xcode 版本,代价是一次构建失败。前两项 Apple 以 SDK 为准绳,第三项则以工具链为准绳,这让 @State 排在了最前面:您在第一次构建时就会撞上它,那时还没碰过任何 plist 键或部署目标。

逼您打开新工具链的压力,是按公开时间表来的,只不过 iOS 27 的时间表尚未公布。Apple 的要求页面目前写着:“自 2026 年 4 月 28 日起,上传至 App Store Connect 的 App 必须使用 Xcode 26 或更高版本,并针对 iOS 26、iPadOS 26、tvOS 26、visionOS 26 或 watchOS 26 的 SDK 构建。”5针对 iOS 27 SDK 的同类日期,Apple 还没有公布。近几年 Apple 每年春天都会抬高 SDK 的最低版本,因此 27 的截止日期是一种合理预期,而非事实;您在别处读到的任何具体日期,都是推断。

四款已上架 App 里到底有什么

在评说别人的代码之前,我先把自己的代码审了一遍:四款 App Store 上架应用,全部使用 SwiftUI,目前全部以 Xcode 26.6 构建。6

App Swift 文件数 @State 声明数 带声明处初始值 声明 @State 的类型数
Reps 77 120 83 31
Return 57 57 42 14
Ace Citizenship 26 63 54 11
Banana List 55 27 22 8
总计 215 267 201 64

两条命令就能把范围收窄。第一条找出带初始值的 @State 声明,其中 @State 后面那个字符类不是摆设:[^A-Za-z0-9_] 能把 @StateObject 挡在结果之外,而直接搜 @State 做不到这一点。

grep -rn --include="*.swift" \
  --exclude-dir=.build --exclude-dir=DerivedData --exclude-dir=build \
  -E '@State[^A-Za-z0-9_][^=]*[^!<>=]=[^=]' .

这条命令假定特性与声明写在同一行。如果 @State 单独占一行、下面才是 private var page = StickerPage(),就匹配不到——这与下文那种“假干净”的失败其实是同一回事,只是换了身衣服。漏掉的部分应当读作“尚未检查”,而不是“没问题”。

第二条把范围缩到同时声明了初始化器的文件,那是第一种例外唯一能咬到人的地方:

find . -name "*.swift" -not -path "*/build/*" -not -path "*/DerivedData/*" -print0 |
while IFS= read -r -d '' f; do
  grep -qE '@State[^A-Za-z0-9_][^=]*[^!<>=]=[^=]' "$f" || continue
  grep -qE '^[[:space:]]*(private |public |internal |fileprivate )?init[[:space:]]*[(<]' "$f" || continue
  echo "$f"
done

这个循环看上去比直接管进 xargs 笨重,但在 macOS 上,这份笨重是值得的。BSD 版 grep-Z 并不像 GNU grep 那样输出 NUL 分隔符,于是 grep -rlZ ... | xargs -0 grep -l 交给第二个 grep 的,是一整串用换行拼接起来的内容。如果项目路径里含有空格(比如 Banana List),标准错误上会刷出一屏 “No such file or directory”,标准输出上则什么都没有——看起来与一次干净的审查一模一样。6把空输出读成“没有匹配”,恰恰会在最需要检查的项目上得出相反的结论。

在 215 个 Swift 文件中,最终名单是 20 个:Reps 9 个、Return 6 个、Ace Citizenship 3 个、Banana List 2 个。把这 20 个逐一手工读完,四个项目得到的结论完全一致。

  • Apple 描述的第一种形态,即声明处有初始值、同时又在同一类型的初始化器里给同一属性赋值:零处
  • 第二种例外,即扩展调用某个声明了 @State 的结构体的合成逐成员初始化器:零处
  • 组合例外,即同一处声明上 @State 与另一个属性包装器或宏并存:零处

有一处险些命中,值得说一说,因为它离 Apple 要您删掉的那种形态只差一行。Reps 里的一个资料设置视图声明了 @State private var healthWriteStatus: HealthAuthorizationState = .notRequested,随后在初始化器中写下 self._healthWriteStatus = State(initialValue: ...)。两处都带值,因此 Apple 那句缓解指令逐字适用:不要在 @State 声明处提供初始值。1只是这里的赋值用的是下划线形式,而不是 Apple 示例中的包装值形式;Apple 的发行说明从头到尾都没提过下划线形式。

于是就剩下我的审查回答不了的那个问题。_x = State(initialValue:) 这个惯用写法在四款 App 中的三款里出现了 15 次,至今仍是用初始化器参数为状态设定初始值的标准做法。Apple 的说明没有涉及它。宏的声明确实会生成一个以下划线为前缀的同级声明,2这具有提示意义,但不是保证。我这边用的是 Xcode 26.6(build 17F113),所以既没法验证这个写法,也没法验证由此产生的编译错误文本。6手上有 Xcode 27 beta 的人,一个下午就能把两件事都定下来。在有人这么做之前,请按声明的形态去搜代码,而不是按错误字符串去搜。

267 处声明得出的老实结论是:绝大多数 SwiftUI 代码毫发无损地通过,这与 Apple “源码层面基本兼容”的说法吻合。1真正有风险的是那些手写了初始化器的视图,而且它们扎堆得厉害:四个代码库里,同时含有手写初始化器和带自身初始值的 @State 的 Swift 文件,不到十分之一。

常见问题

我需要改动 _page = State(initialValue:) 这类调用吗?

Apple 的说明没有给出答案。下划线形式直接给投影存储赋值,而不是给包装值赋值,它在发行说明里完全没出现过:没有 initialValue,没有下划线示例,正反两方面都只字未提。1宏公开的声明确实会生成一个以 _ 为前缀的同级声明,这具有提示意义,但不是保证。2我审查的四款 App 中有三款用到这个写法,合计 15 次,因此这个问题涉及的代码库比那两条明文例外还要多。6在 Apple 给出说法之前,请用 Xcode 27 构建项目,让编译器来回答,不要凭猜测去重构。

宏改变了初始化器中所赋之值的处理方式吗?

没有。Apple 明确表示,丢弃行为“并未因为宏而改变,只是其中一些情况不再能通过编译”。1在 Xcode 26 下,给一个声明处已有初始值的 @State 属性赋值的初始化器,什么也不做,照样编译通过;到了 Xcode 27,其中一部分会构建失败。语义原地未动,诊断信息终于到位——所以构建在这里挂掉,是编译器在报告一个您本来就有的缺陷。

我该怎么在项目里找出有风险的代码?

先搜出带初始值的 @State 声明,再把结果限定在同时声明了初始化器的文件里,然后逐一手工阅读这份名单。用字符类排除 @StateObject@State[^A-Za-z0-9_]);在 macOS 上,优先用 find -print0 循环,而不是 grep -rlZ | xargs -0——BSD 版 grep 不输出 NUL 分隔符,任何含空格的路径都会产出看起来像“通过”的空结果。6四款 App、215 个 Swift 文件筛下来,名单是 20 个文件。此外还要单独搜索在视图结构体上调用 self.init(...) 的扩展,因为第二种例外在引发它的 @State 附近不留任何痕迹。

我的 App 真的会变快吗?

只在特定情形下会,而且 Apple 划定的范围比发行说明给人的印象更窄。发行说明谈的是笼统地避免重复求值,1而 SwiftUI 更新条目说这项改动“仅当属性是类时,才只初始化并存储该属性一次”。4收益落在 @State private var model = SomeObservableClass() 这类写法上——旧实现会在视图每次重新实例化时运行类的初始化器,然后把结果扔掉。而 @State private var isPresented = false 从来就不是问题所在。

关键要点

给 iOS 开发者: - 要审查两种形态,而不是一种。第一种就住在 @State 声明旁边、紧挨着初始化器;第二种住在扩展里,形式是对一个存储成员全部私有的视图结构体调用 self.init(...)1 - 修第一种时,删掉声明处的初始值,而不是删掉初始化器里的赋值。Apple 的指令是:让初始化器作为唯一来源,声明处留空。1

给维护老旧 SwiftUI 代码的团队: - 评审时间按名单来排,而不是按整个代码库来排。在我的四款 App 里,同时含有带初始值的 @State 和手写初始化器的文件是 215 个中的 20 个,而第一种写法的每一处都必然落在其中。第二种要单独搜,因为它藏在扩展里。6 - 把构建失败当作一次缺陷的发现。SwiftUI 本来就在丢弃那行如今被编译器拒绝的初始化器赋值,所以凡是这次改动咬到的地方,原本就已经存在错误行为。1

给发布负责人: - 把 @State 审查排在同一轮里由 SDK 触发的工作之前。启动屏键场景生命周期约束的都是您构建时所针对的 SDK;@State 约束的是您用来构建的 Xcode,因此它最先落地。13 - 不要按 iOS 27 SDK 的截止日期来做计划。Apple 已公布的最低要求仍是 iOS 26 SDK,自 2026 年 4 月 28 日起生效;针对 27 的要求,Apple 什么都还没宣布。5


三个强制点在同一轮里发布,却在三个不同的地方失败:启动屏键挡住提交,场景强制要求挡住启动,@State 宏挡住构建。同一个 SDK 里还有哪些变化,请参阅 iOS 27 SwiftUI 新特性。整个系列的入口是 Apple 生态系统系列

参考资料


  1. Apple,iOS & iPadOS 27 Release Notes,SwiftUI 一节,New Features(radar 105893279)。以下内容的来源:旧行为的描述(“A @State declared with an expression as its initial value used to evaluate the expression each time the view struct re-instantiates. In the case of @State private var model = Model(), this means Model.init() gets called many times throughout the view’s lifetime”)、新实现(“Xcode 27 introduces a new @State implementation that avoids this repeated evaluation. This new behavior back-deploys to iOS 17 aligned OSes. The new @State is implemented with a Swift macro. It is largely source compatible with the property wrapper version, with a few exceptions”)、第一种例外及其指令(“If you provide an initial value at @State declaration, and also try to assign a value to it in an initializer, the initializer value is discarded. This behavior has not changed because of the macro, but some such cases no longer compile” 与 “When assigning initial value via an initializer, do not provide an initial value at the @State declaration”)、第一种例外的两段 StickerPageView 代码、第二种例外(“When all stored members of a struct are private, the compiler synthesizes a private init that can be used in an extension of the same type” 与 “The state macro disables this synthesized initializer. So the code above no longer compiles. To mitigate, assign value to members explicitly”)及其两段代码、泛型推断说明(“In rare situations, the automatic inference of generic arguments of @State is less flexible with the macro implementation. Write the type with more specificity”),以及组合说明(“Composing @State with other property wrappers or macros is not supported”)。所有代码均逐字复现。由于 HTML 页面通过 JavaScript 渲染内容,已于 2026 年 7 月 25 日对照 Apple 文档 JSON 核实。 

  2. Apple,State() macro,SwiftUI 宏参考。以下内容的来源:公开声明(@attached(accessor, names: named(init), named(get), named(set))@attached(peer, names: prefixed(_), prefixed(__), prefixed($))macro State())、可用性列表(iOS 13.0、iPadOS 13.0、Mac Catalyst 13.0、macOS 10.15、tvOS 13.0、visionOS 1.0、watchOS 6.0)、关于工具链的旁注(“When you build with Xcode 26 or earlier, the system uses the State property wrapper instead”)、新的初始化约定(“A State() property instantiates its default value the first time SwiftUI instantiates the view”),以及在 @State 中持有 @Observable class Library 的 “Store observable objects” 示例。 

  3. Apple,State,SwiftUI 结构体参考。仍声明为 @frozen @propertyWrapper struct State<Value>,可用性从 iOS 13.0、iPadOS 13.0、Mac Catalyst 13.0、macOS 10.15、tvOS 13.0、visionOS 1.0 和 watchOS 6.0 起算。反向工具链旁注的来源:“When you build with Xcode 27 or later, the system uses the State() macro instead.” 

  4. Apple,SwiftUI Updates,2026 年 6 月,General。类限定条件的来源:“Build your project in Xcode 27 or later so that the @State attribute uses the State() macro to create a state value in an App, Scene, or View. This change only initializes and stores your property once when it’s a class.” 

  5. Apple,Upcoming requirements,Apple Developer News。当前 SDK 最低要求的来源:“Since April 28, 2026 Apps uploaded to App Store Connect must be built with Xcode 26 or later using an SDK for iOS 26, iPadOS 26, tvOS 26, visionOS 26, or watchOS 26.” 于 2026 年 7 月 25 日核查;该页面没有列出任何涉及 iOS 27 SDK 的要求。 

  6. 作者对四款已上架 SwiftUI App(Reps、Return、Ace Citizenship 和 Banana List)的审查,环境为 macOS 26.5.2 与 Xcode 26.6(build 17F113),时间为 2026 年 7 月 25 日。各项计数来自一个按花括号深度解析每个类型声明、并将初始化器体限定在其范围内的脚本,并与上文公布的 grep 命令交叉核对——后者在四个项目中都精确复现了各自的声明处初始值计数(83、42、54 和 22)。BSD grep 的行为已直接确认:在 macOS 上,grep -rlZ 输出的是换行分隔而非 NUL 分隔的结果,因此 xargs -0 收到的是一个拼接起来的单一参数,任何含空格的路径都会让整条管线以 “No such file or directory” 失败。_x = State(initialValue:) 惯用写法的计数(在 Reps、Return 和 Banana List 中共出现 15 次)来自同一次扫描。Xcode 27 下的行为未经测试,此处也不报告任何编译错误文本,因为所用机器上并未安装 Xcode 27。 

  7. Apple,Xcode 27 Release Notes。于 2026 年 7 月 25 日检索 radar 105893279 以及任何描述 @State 宏的条目,两者均未出现。唯一提到 @State 的地方是一处与本文无关的 MusicKit 修复(radar 176947544)。 

相关文章

Xcode 27 移除 ld64,并要求模块名称唯一

Xcode 27 移除了 ld64 链接器,并要求 Clang 模块名称唯一。两项变更都在工具链升级时触发。七个项目的审计结果与实测命令。

7 分钟阅读

iOS 27 启动屏幕新规:四个键,否则被拒

使用 iOS 27 SDK 构建的应用必须声明启动屏幕,否则 App Store 会拒绝。本文讲清这四个键,以及如何审查使用自动生成 plist 的 target。

4 分钟阅读