- 前端
- 移动开发
【免费下载链接】swift-composable-architecture
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
本指南基于 MigratingTo1.7.md 官方迁移文档,讲解如何将基于WithViewStore、IfLetStore、ForEachStore、SwitchStore等"旧观察工具"编写的 TCA 代码,升级到以@ObservableState宏 + Swift Observation / Perception 为核心的现代写法。读完本文,你将掌握@ObservableState与@Presents宏的用法、SwiftUI 原生修饰符接管 store 驱动的导航与弹窗的方法、@Bindable/@ViewAction的新绑定与视图动作方案,以及 iOS 17+ 与低版本平台两条迁移路径和增量迁移的注意事项。
重要:本指南假设你已经完成 1.4、1.5、1.6 的迁移,详见 MigratingTo1.4、MigratingTo1.5 与 MigratingTo1.6。
注意:本文大部分内容假设你面向 iOS 17、macOS 14、tvOS 17、watchOS 10 或更高版本;这些工具同样支持更旧的平台,低版本平台的用法可参考 ObservationBackport 文档。
为什么需要迁移:从对象观察走向值类型观察
Composable Architecture 一直在持续演进,常常为了引入更简练、更强大的 API 而废弃旧的 API。官方建议开发者尽快将代码升级到最新 API。1.7 版本的核心变化,是把以objectWillChange发布者驱动的"对象观察"(旧工具)替换为基于Observation框架与Perception背书的"值类型观察"(新工具)。
从源码结构可以清楚看到新旧两套体系的分界:旧 API 的软废弃声明集中放在 Internal/Deprecations.swift,其中大量条目带有@available(*, deprecated, ...)标记;新 API 则分布在 Observation 目录,包括ObservableState.swift、ObservationStateRegistrar.swift、Alert+Observation.swift、NavigationStack+Observation.swift、Binding+Observation.swift等。
第一步:给 State 加上 @ObservableState 宏
@ObservableState宏是整个迁移的基石。它把一个普通的struct State变成可被细粒度观察的值类型,从而让视图只在自己真正读到的字段变化时刷新。
1.6 时代的典型写法
迁移前的典型 Feature 是这样的:手写一个ViewState结构体,再用WithViewStore(store, observe:)把 store 投影到最小化的视图状态上,以降低视图观察开销:
@Reducer struct Feature { struct State { /* ... */ } enum Action { /* ... */ } var body: some ReducerOf<Self> { // ... } } struct FeatureView: View { let store: StoreOf<Feature> struct ViewState: Equatable { // ... init(state: Feature.State) { /* ... */ } } var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { Text(viewStore.count.description) Button("+") { viewStore.send(.incrementButtonTapped) } } } } }面向 iOS 16 及更低版本的迁移
如果你仍面向 iOS 16、macOS 13、tvOS 16、watchOS 9 或更低版本,请按下述方式修改(iOS 16 没有 SwiftUI 的@Observable,因此需要WithPerceptionTracking包裹视图,由 Perception 提供等价的观察能力):
@Reducer struct Feature { + @ObservableState struct State { /* ... */ } enum Action { /* ... */ } var body: some ReducerOf<Self> { // ... } } struct FeatureView: View { let store: StoreOf<Feature> - struct ViewState: Equatable { - // ... - init(state: Feature.State) { /* ... */ } - } var body: some View { - WithViewStore(store, observe: ViewState.init) { viewStore in + WithPerceptionTracking { Form { - Text(viewStore.count.description) - Button("+") { viewStore.send(.incrementButtonTapped) } + Text(store.count.description) + Button("+") { store.send(.incrementButtonTapped) } } } } }具体来说,你必须完成以下四点:
- 用
@ObservableState宏标记你的State; - 删除你定义的任何
ViewState类型; - 用
WithPerceptionTracking替换WithViewStore——它的尾随闭包不再接收参数,闭包内部访问到的 state 会被自动观察; - 直接通过
store读取状态、直接通过store.send发送动作,而不是经由viewStore。
面向 iOS 17 及更高版本的迁移
如果你的最低部署目标是 iOS 17、macOS 14、tvOS 17、watchOS 10 或更高版本,那么除了上述四点之外,视图的body还可以进一步简化——WithPerceptionTracking也可以去掉,因为此时 store 直接桥接了 SwiftUI 原生Observable:
var body: some View { - WithViewStore(store, observe: ViewState.init) { viewStore in Form { - Text(viewStore.count.description) - Button("+") { viewStore.send(.incrementButtonTapped) } + Text(store.count.description) + Button("+") { store.send(.incrementButtonTapped) } } - } }你不再需要WithViewStore或WithPerceptionTracking视图。
宏展开的底层细节
@ObservableState之所以能让值类型获得可观察能力,是因为它在编译期自动生成了观察注册器与访问器。以 Sources/ComposableArchitectureMacros/ObservableStateMacro.swift 中的实现与 Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift 的展开断言为例,对下面这段代码:
@ObservableState struct State { var count = 0 }宏会展开为(关键部分):
struct State { var count { @storageRestrictions(initializes: _count) init(initialValue) { _count = initialValue } get { _$observationRegistrar.access(self, keyPath: \.count) return _count } set { _$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual, shouldNotifyObservers) } _modify { let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count) defer { _$observationRegistrar.didModify(self, keyPath: \.count, &_count, oldValue, _$isIdentityEqual) } yield &_count } } var _$observationRegistrar = ComposableArchitecture.ObservationStateRegistrar() public var _$id: ComposableArchitecture.ObservableStateID { _$observationRegistrar.id } public mutating func _$willModify() { _$observationRegistrar._$willModify() } // 以及多个重载的 shouldNotifyObservers(...) 辅助函数 }也就是说:每个被观察的存储属性都会被重写成带_modify的访问器;读取走access(记录依赖),写入走mutate/willModify/didModify(依据身份变化决定是否通知观察者)。身份判断_$isIdentityEqual与ObservableStateID的定义可在 Sources/ComposableArchitecture/Observation/ObservableState.swift 中查到。观察注册器 ObservationStateRegistrar.swift 在 iOS 17+ 上桥接ObservationRegistrar,在旧平台则桥接 Perception 的PerceptionRegistrar,这正是新旧平台都能工作的原因。
值得注意的是,宏对class和actor类型会直接报错("'@ObservableState' cannot be applied to class type ..." / "... actor type ..."),因为值类型才是这套观察体系的预期形态;枚举类型则被支持,宏会为每个带单一关联值的 case 生成_$id(for: state)._$tag(n)形式的分支。
用 if let 替换 IfLetStore
IfLetStore视图曾是把"可选状态 store"变换为"非可选状态 store"再交给子视图的工具。使用新观察工具后它已不再需要,因此被软废弃(soft-deprecated)。
假设 reducer 长这样:
@Reducer struct Feature { @ObservableState struct State { var child: Child.State? } enum Action { case child(Child.Action) } var body: some ReducerOf<Self> { /* ... */ } }过去视图里要这样写:
IfLetStore(store: store.scope(state: \.child, action: \.child)) { childStore in ChildView(store: childStore) } else: { Text("Nothing to show") }现在只需用普通if let加store.scope(state:action:):
if let childStore = store.scope(state: \.child, action: \.child) { ChildView(store: childStore) } else { Text("Nothing to show") }用 ForEach 替换 ForEachStore
ForEachStore视图曾用于为集合中的每个元素派生一个 store,同样已被软废弃。
假设 reducer 中有一个IdentifiedArrayOf行集合:
@Reducer struct Feature { @ObservableState struct State { var rows: IdentifiedArrayOf<Child.State> = [] } enum Action { case rows(IdentifiedActionOf<Child>) } var body: some ReducerOf<Self> { /* ... */ } }过去的写法是:
ForEachStore( store.scope(state: \.rows, action: \.rows) ) { childStore in ChildView(store: childStore) }现在改为 SwiftUI 原生的ForEach,用\.state.id作为行的标识:
ForEach( store.scope(state: \.rows, action: \.rows), id: \.state.id ) { childStore in ChildView(store: childStore) }如果你的ForEachStore用法并不依赖每行状态的id(例如该id没有关联任何选中绑定),可以省略id参数——Store本身通过对象身份可辨识:
ForEach( - store.scope(state: \.rows, action: \.rows), - id: \.state.id, + store.scope(state: \.rows, action: \.rows) ) { childStore in ChildView(store: childStore) }提示:现在你可以把集合运算符与 store scoping 组合使用。例如用
Array.enumerated()枚举行,从而按奇偶行提供不同的背景样式:ForEach( Array(store.scope(state: \.rows, action: \.rows).enumerated()), id: \.element ) { position, childStore in ChildView(store: childStore) .background { position.isMultiple(of: 2) ? Color.white : Color.gray } }
用 switch/case 替换 SwitchStore 与 CaseLet
SwitchStore和CaseLet视图曾用于驱动 enum 状态中每个 case 对应的 store,如今也已被软废弃。
假设 reducer 的状态是枚举:
@Reducer struct Feature { @ObservableState enum State { case activity(ActivityFeature.State) case settings(SettingsFeature.State) } enum Action { case activity(ActivityFeature.Action) case settings(SettingsFeature.Action) } var body: some ReducerOf<Self> { /* ... */ } }过去视图里要用SwitchStore+CaseLet:
SwitchStore(store) { switch $0 { case .activity: CaseLet(/Feature.State.activity, action: Feature.Action.activity) { store in ActivityView(store: store) } case .settings: CaseLet(/Feature.State.settings, action: Feature.Action.settings) { store in SettingsView(store: store) } } }现在可以直接在视图中用原生switch和case,配合store.scope:
switch store.state { case .activity: if let store = store.scope(state: \.activity, action: \.activity) { ActivityView(store: store) } case .settings: if let store = store.scope(state: \.settings, action: \.settings) { SettingsView(store: store) } }用 @Presents 宏替换 @PresentationState
Swift 宏有一个广为人知的限制:不能与属性包装器(property wrapper)一起使用。这意味着如果你的 Feature 使用了@PresentationState,在应用@ObservableState宏时会遇到编译错误:
@ObservableState struct State { @PresentationState var child: Child.State? // 🛑 }正确的做法是改用新的@Presents宏:
@ObservableState struct State { @Presents var child: Child.State? // ✅ }从 Sources/ComposableArchitectureMacros/PresentsMacro.swift 的展开逻辑和 Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift 的断言可以看到,@Presents本质上把普通可选项重写为PresentationState包装存储,并生成一个$child投影属性:
struct State { var child: Child.State? { @storageRestrictions(initializes: _child) init(initialValue) { _child = PresentationState(wrappedValue: initialValue) } get { _$observationRegistrar.access(self, keyPath: \.child) return _child.wrappedValue } set { _$observationRegistrar.mutate(self, keyPath: \.child, &_child.wrappedValue, newValue, _$isIdentityEqual) } } var $child: ComposableArchitecture.PresentationState<Child.State> { get { _$observationRegistrar.access(self, keyPath: \.child) return _child.projectedValue } // ... } @ObservationStateIgnored private var _child = ComposableArchitecture.PresentationState<Child.State>(wrappedValue: nil) }同时,宏诊断机制会主动拦截旧写法:当你在@ObservableState类型里使用@PresentationState时,编译器会报错并给出 fix-it——"'@PresentationState' cannot be used in '@ObservableState',请改用@Presents"。这一诊断逻辑位于 ObservableStateMacro.swift 的MemberAttributeMacro展开中(对PresentationState属性统一提示改名)。
用 SwiftUI 原生修饰符替换库的导航修饰符
库曾提供许多模拟 SwiftUI、但专为"用 store 驱动导航"调校的视图修饰符(sheet、popover、fullScreenCover、navigationDestination 等)。现在这些库内修饰符都可以改用 SwiftUI 原生版本,因此它们被软废弃。
假设 reducer 如下:
@Reducer struct Feature { @ObservableState struct State { @Presents var child: Child.State? } enum Action { case child(PresentationAction<Child.Action>) } var body: some ReducerOf<Self> { /* ... */ } }过去驱动一个 sheet 的写法是:
.sheet(store: store.scope(state: \.$child, action: \.child)) { store in ChildView(store: store) }现在用原生sheet(item:)替换sheet(store:)。首先,视图需要用可绑定方式持有 store——iOS 17+ 用@Bindable:
@Bindable var store: StoreOf<Feature>面向更旧平台时用 Perception 的回退实现@Perception.Bindable:
@Perception.Bindable var store: StoreOf<Feature>然后使用sheet(item:):
.sheet(item: $store.scope(state: \.child, action: \.child)) { store in ChildView(store: store) }注意这里的状态 key path 是state: \.child而不是state: \.$child——不再需要 presentation 状态的投影值。这套改法同样适用于 popover、全屏 cover 和导航目标(navigation destinations)。
如果你是用"目标枚举"(destination enum)驱动导航,原来的代码可能是:
.sheet( store: store.scope( state: \.$destination.editForm, action: \.destination.editForm ) ) { store in ChildView(store: store) }现在改为:
.sheet( item: $store.scope( state: \.destination?.editForm, action: \.destination.editForm ) ) { store in ChildView(store: store) }注意状态 key path 从\.$destination.editForm变成了\.destination?.editForm。
另外需要提醒:navigationDestination(item:)在较旧平台上不可用,但可以通过包装器将其回退支持到 iOS 15。参见 TreeBasedNavigation 的向后兼容小节。
更新 alert 与 confirmationDialog
alert(store:)和confirmationDialog(store:)修饰符曾用于从 store 驱动弹窗;现在提供了新修饰符,可以用与sheet(item:)相同的"store 绑定 scope"机制驱动弹窗。
假设 reducer 要展示一个 alert:
@Reducer struct Feature { @ObservableState struct State { @Presents var alert: AlertState<Action.Alert>? } enum Action { case alert(PresentationAction<Alert>) enum Alert { /* ... */ } } var body: some ReducerOf<Self> { /* ... */ } }过去视图里的写法:
.alert(store: store.scope(state: \.$alert, action: \.alert))现在替换为新的alert(_:)修饰符(在 Sources/ComposableArchitecture/Observation/Alert+Observation.swift 中实现):
.alert($store.scope(state: \.alert, action: \.alert))confirmationDialog(_:)的迁移同理。查看 Alert+Observation.swift 的实现可以发现,新修饰符读取 store 中的AlertState,把每个按钮的.send/.animatedSend动作回发给 store,从而完成从"库专用修饰符"到"原生 SwiftUI 弹窗"的无缝衔接。
用 NavigationStack 替换 NavigationStackStore
NavigationStackStore视图曾用于从 store 驱动导航栈,现在被软废弃。
假设 reducer 如下:
@Reducer struct Feature { struct State { var path: StackState<Path.State> = [] } enum Action { case path(StackAction<Path.State, Path.Action>) } var body: some ReducerOf<Self> { /* ... */ } }过去的写法:
NavigationStackStore(store.scope(state: \.path, action: \.path)) { RootView() } destination: { switch $0 { case .activity: CaseLet(/Feature.State.activity, action: Feature.Action.activity) { store in ActivityView(store: store) } case .settings: CaseLet(/Feature.State.settings, action: Feature.Action.settings) { store in SettingsView(store: store) } } }迁移分三步。首先给 Feature 的 state 加上@ObservableState:
@Reducer struct Feature { @ObservableState struct State { // ... } // ... }Pathreducer 的 state 也要加上:
@Reducer struct Path { @ObservableState enum State { // ... } // ... }然后在视图中用@Bindable可绑定持有 store(旧平台用@Perception.Bindable):
@Bindable var store: StoreOf<Feature>最后使用库为NavigationStack提供的自定义初始化器NavigationStack(path:root:destination:)(实现在 Sources/ComposableArchitecture/Observation/NavigationStack+Observation.swift 中):
NavigationStack(path: $store.scope(state: \.path, action: \.path)) { RootView() } destination: { store in switch store.state { case .activity: if let store = store.scope(state: \.activity, action: \.activity) { ActivityView(store: store) } case .settings: if let store = store.scope(state: \.settings, action: \.settings) { SettingsView(store: store) } } }从 NavigationStack+Observation.swift 的源码可以看到,这个初始化器把path绑定底层映射为StackState.PathView,通过_NavigationDestinationViewModifier挂接navigationDestination(for:),并利用store.scope(component:)为栈内每个元素派生子 store;Binding.scope则在写入 path 时向 store 发送.push/.popFrom栈动作。
处理 @BindingState:一切归于普通属性 + @Bindable
绑定能力在 TCA 中历来由一堆类型共同支撑:BindingState、BindableAction、BindingAction、BindingViewState和BindingViewStore。举例来说,如果视图需要为 state 的多个字段派生绑定,reducer 可能长这样:
@Reducer struct Feature { struct State { @BindingState var text = "" @BindingState var isOn = false } enum Action: BindableAction { case binding(BindingAction<State>) } var body: some ReducerOf<Self> { /* ... */ } }视图里通过ViewStore的动态成员下标派生绑定:
WithViewStore(store, observe: { $0 }) { viewStore in Form { TextField("Text", text: viewStore.$text) Toggle(isOn: viewStore.$isOn) } }如果你的视图还用了ViewState,那步骤更多:
struct ViewState: Equatable { @BindingViewState var text: String @BindingViewState var isOn: Bool init(store: BindingViewStore<Feature.State>) { self._text = store.$text self._isOn = store.$isOn } } var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { TextField("Text", text: viewStore.$text) Toggle(isOn: viewStore.$isOn) } } }使用@ObservableState宏后,绝大多数样板代码都会消失。先用宏标注 state,并删除所有@BindingState:
+@ObservableState struct State { - @BindingState var text = "" - @BindingState var isOn = false + var text = "" + var isOn = false }重要:不要移除 Action 上的
BindableAction一致性,也不要移除 reducer 中的BindingReducer——绑定依然需要它们。
在视图中用@Bindable持有 store(旧平台用@Perception.Bindable):
@Bindable var store: StoreOf<Feature>然后直接在body中从 store 派生绑定,不再需要WithViewStore:
var body: some View { Form { TextField("Text", text: $store.text) Toggle(isOn: $store.isOn) } }替换 ViewStore.binding:$store.tab.sending(...)
ViewStore上还有一种派生绑定方式,样板代码更多但涉及的工具更少:为绑定显式定义一个 action。例如在 Tab 型应用中设置当前 tab:
@Reducer struct Feature { struct State { var tab = 0 } enum Action { case tabChanged(Int) } var body: some ReducerOf<Self> { Reduce { state, action in switch action { case let .tabChanged(tab): state.tab = tab return .none } } } }视图里用viewStore.binding(get:send:)派生绑定:
TabView( selection: viewStore.binding(get: \.tab, send: { .tabChanged($0) }) ) { // ... }既然ViewStore已被软废弃,这段代码可以简化得多。先用@ObservableState让 state 可观察:
@Reducer struct Feature { @ObservableState struct State { // ... } // ... }在视图中用@Bindable(或@Perception.Bindable)持有 store:
@Bindable var store: StoreOf<Feature>然后直接用$store.tab.sending(\.tabChanged)从 store 绑定派生绑定(sending(_:)定义于 Sources/ComposableArchitecture/Observation/Binding+Observation.swift):
TabView(selection: $store.tab.sending(\.tabChanged)) { // ... }如果绑定依赖更复杂的业务逻辑,可以在 store 上定义自定义get-set属性(如果该逻辑依赖外部状态,则定义下标)来吸收这些逻辑。例如:
// Before(视图中) ForEach(Flag.allCases) { flag in Toggle( flag.description, isOn: viewStore.binding( get: { $0.featureFlags.contains(flag) } send: { .flagToggled(flag, isOn: $0) } ) ) } // After(文件内定义下标) extension StoreOf<Feature> { subscript(hasFeatureFlag flag: Flag) -> Bool { get { featureFlags.contains(flag) } set { send(.flagToggled(flag, isOn: newValue)) } } } // After(视图中) ForEach(Flag.allCases) { flag in Toggle( flag.description, isOn: $store[hasFeatureFlag: flag] ) }提示:条件允许时,尽量把复杂绑定逻辑移入 reducer,这样更容易测试。
计算视图状态:移到 State 上
如果你正在使用ViewState模式,可能曾在初始化器里计算供视图使用的值:
struct ViewState: Equatable { let fullName: String init(state: Feature.State) { self.fullName = "\(state.firstName) \(state.lastName)" } }1.7 中ViewState结构体退场,你可以把这些计算直接放到 feature 的 state 上:
struct State { // State fields var fullName: String { "\(self.firstName) \(self.lastName)" } }视图动作:用 @ViewAction(for:) 收紧视图可发送的动作
TCA 社区有一个常见模式:把"视图发送的动作"与"feature 内部使用的动作(如 effect 的发射)"分开。典型写法如下:
@Reducer struct Feature { struct State { /* ... */ } enum Action { case loginResponse(Bool) case view(View) enum View { case loginButtonTapped } } // ... }过去视图中要配合WithViewStore的send:参数,声明视图只能访问哪一类动作:
struct FeatureView: View { let store: StoreOf<Feature> var body: some View { WithViewStore( store, observe: { $0 }, send: Feature.Action.view // 👈 ) { viewStore in Button("Login") { viewStore.send(.loginButtonTapped) } } } }这样既能省去.view(...)包装,又保证了视图只能发送view动作。例如,视图无法发送loginResponse:
viewStore.send(.loginResponse(false)) // 🛑 Type 'Feature.Action.View' has no member 'loginResponse'1.7 中这个模式依然可行,只需两处小改动。第一,让你的View动作枚举遵循ViewAction协议:
@Reducer struct Feature { // ... enum Action: ViewAction { // 👈 // ... } // ... }第二,在视图上用@ViewAction(for:)宏指定驱动该视图的 reducer。宏会为视图注入send方法,让你直接发送视图动作,而不再走store.send(_:):
+@ViewAction(for: Feature.self) struct FeatureView: View { let store: StoreOf<Feature> var body: some View { - WithViewStore( - store, - observe: { $0 }, - send: Feature.Action.view - ) { viewStore in Button("Login") { - viewStore.send(.loginButtonTapped) + send(.loginButtonTapped) } } - } }从 Sources/ComposableArchitectureMacros/ViewActionMacro.swift 与 Tests/ComposableArchitectureMacrosTests/ViewActionMacroTests.swift 可以看到@ViewAction(for:)的展开产物与诊断行为:宏要求视图具有store: StoreOf<Feature>属性(否则给出 fix-it 帮你添加),并为其添加ComposableArchitecture.ViewActionSending一致性;同时,如果检测到视图体内直接使用store.send(...),会给出警告"不要在使用@ViewAction时直接调用store.send",对store.send(.view(.tap))这类写法还能自动生成改为send(.tap)的 fix-it。
UIKit 观察:observe 与 scope 的取代
替换 Store.publisher
在观察工具出现之前,你通常会在UIViewController子类的入口(如viewDidLoad)用 Combine 发布者订阅 store 变化:
func viewDidLoad() { super.viewDidLoad() store.publisher.count .sink { [weak self] in self?.countLabel.text = "\($0)" } .store(in: &cancellables) }现在可以更简单地用定义在所有NSObject上的observe(_:)方法:
func viewDidLoad() { super.viewDidLoad() observe { [weak self] in guard let self else { return } self.countLabel.text = "\(self.store.count)" } }使用前请务必阅读NSObject.observe(_:)的文档,了解如何最好地运用这个工具。
替换 Store.ifLet
过去订阅可选子 store 通常使用库提供的 Combine 操作:
store .scope(state: \.child, action: \.child) .ifLet { childStore in // Use child store, _e.g._ create a child view controller } else: { // Perform clean up work, _e.g._ dismiss child view controller } .store(in: &cancellables)现在可以用observe加store.scope(state:action:)更简单地实现:
observe { if let childStore = store.scope(state: \.child, action: \.child) { // Use child store, _e.g._ create a child view controller } else { // Perform clean up work, _e.g._ dismiss child view controller } }增量迁移:从外向内,警惕混合观察模式
你很可能希望增量地把应用迁移到新观察工具,而不是一次全部做完。这是可行的,但混合"旧 feature"(使用ViewStore/WithViewStore)与"新 feature"(使用@ObservableState)时有一些坑需要注意。
最常见的问题是:新旧 feature 混用时,视图的body可能比必要的次数更频繁地被重新计算。这源于两种观察模式并存:旧 feature 使用objectWillChange发布者同步地使视图失效,而新 feature 使用withObservationTracking。这两者是根本不同的工具,会导致视图被"隔着一个线程跳转"地多次失效,使失效无法合并为一次——这正是 body 被多次重算的原因。
通常,多几次 body 重算不算大问题,但会给 SwiftUI 判断"视图里到底哪个状态变了"带来压力,可能引起卡顿、放大导航类 bug。如果你在转换某个 feature 为@ObservableState后注意到了问题,官方建议尝试把它交互的另外几个 feature 也一起转换,看看问题是否消失。
另外,实践中发现,"现代 feature 包含旧 feature 作为子 feature"比反过来表现得更好。因此官方建议从外向内更新:从根 feature 开始,先把它升级到新观察工具,再逐步向叶子 feature 推进。
参考链接
- 迁移指南:本文对应的原文档 MigratingTo1.7.md,以及后续版本迁移 MigratingTo1.8、MigratingTo1.9 等
- 新观察工具的旧平台用法:ObservationBackport.md
- 宏实现源码:ObservableStateMacro.swift、PresentsMacro.swift、ViewActionMacro.swift
- 观察运行时:ObservableState.swift、ObservationStateRegistrar.swift
- SwiftUI 集成:Alert+Observation.swift、NavigationStack+Observation.swift、Binding+Observation.swift
- 宏测试用例:ObservableStateMacroTests.swift、PresentsMacroTests.swift、ViewActionMacroTests.swift
- 旧 API 的废弃声明:Internal/Deprecations.swift
- 前端
- 移动开发
【免费下载链接】swift-composable-architecture
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
相关推荐
swift-composable-architecture SwiftUI 弃用 API 迁移指南:从 ViewStore 走向 @ObservableState
swift composable architecture SwiftUI 弃用 API 迁移指南:从 ViewStore 走向 @ObservableStat
前端移动开发HandBrake 翻译协作指南:基于 Transifex 的本地化流程、规则与实践
HandBrake 翻译协作指南:基于 Transifex 的本地化流程、规则与实践 HandBrake 是一款开源视频转码软件,其用户界面需要面向全球用户提供
前端移动开发迁移到 swift-composable-architecture 1.25:枚举 Scope、精简 onChange 与 2.0 弃用预演
迁移到 swift composable architecture 1.25:枚举 Scope、精简 onChange 与 2.0 弃用预演 本文是 swift
前端移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考