
前端移动开发【免费下载链接】swift-composable-architectureA library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.项目地址https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture点击查看免费下载本指南基于 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、AlertObservation.swift、NavigationStackObservation.swift、BindingObservation.swift等。第一步给 State 加上 ObservableState 宏ObservableState宏是整个迁移的基石。它把一个普通的struct State变成可被细粒度观察的值类型从而让视图只在自己真正读到的字段变化时刷新。1.6 时代的典型写法迁移前的典型 Feature 是这样的手写一个ViewState结构体再用WithViewStore(store, observe:)把 store 投影到最小化的视图状态上以降低视图观察开销Reducer struct Feature { struct State { /* ... */ } enum Action { /* ... */ } var body: some ReducerOfSelf { // ... } } struct FeatureView: View { let store: StoreOfFeature 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 ReducerOfSelf { // ... } } struct FeatureView: View { let store: StoreOfFeature - 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 原生Observablevar 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 替换 IfLetStoreIfLetStore视图曾是把可选状态 store变换为非可选状态 store再交给子视图的工具。使用新观察工具后它已不再需要因此被软废弃soft-deprecated。假设 reducer 长这样Reducer struct Feature { ObservableState struct State { var child: Child.State? } enum Action { case child(Child.Action) } var body: some ReducerOfSelf { /* ... */ } }过去视图里要这样写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 替换 ForEachStoreForEachStore视图曾用于为集合中的每个元素派生一个 store同样已被软废弃。假设 reducer 中有一个IdentifiedArrayOf行集合Reducer struct Feature { ObservableState struct State { var rows: IdentifiedArrayOfChild.State [] } enum Action { case rows(IdentifiedActionOfChild) } var body: some ReducerOfSelf { /* ... */ } }过去的写法是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 与 CaseLetSwitchStore和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 ReducerOfSelf { /* ... */ } }过去视图里要用SwitchStoreCaseLetSwitchStore(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.scopeswitch 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 宏替换 PresentationStateSwift 宏有一个广为人知的限制不能与属性包装器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.PresentationStateChild.State { get { _$observationRegistrar.access(self, keyPath: \.child) return _child.projectedValue } // ... } ObservationStateIgnored private var _child ComposableArchitecture.PresentationStateChild.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(PresentationActionChild.Action) } var body: some ReducerOfSelf { /* ... */ } }过去驱动一个 sheet 的写法是.sheet(store: store.scope(state: \.$child, action: \.child)) { store in ChildView(store: store) }现在用原生sheet(item:)替换sheet(store:)。首先视图需要用可绑定方式持有 store——iOS 17 用BindableBindable var store: StoreOfFeature面向更旧平台时用 Perception 的回退实现Perception.BindablePerception.Bindable var store: StoreOfFeature然后使用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 与 confirmationDialogalert(store:)和confirmationDialog(store:)修饰符曾用于从 store 驱动弹窗现在提供了新修饰符可以用与sheet(item:)相同的store 绑定 scope机制驱动弹窗。假设 reducer 要展示一个 alertReducer struct Feature { ObservableState struct State { Presents var alert: AlertStateAction.Alert? } enum Action { case alert(PresentationActionAlert) enum Alert { /* ... */ } } var body: some ReducerOfSelf { /* ... */ } }过去视图里的写法.alert(store: store.scope(state: \.$alert, action: \.alert))现在替换为新的alert(_:)修饰符在 Sources/ComposableArchitecture/Observation/AlertObservation.swift 中实现.alert($store.scope(state: \.alert, action: \.alert))confirmationDialog(_:)的迁移同理。查看 AlertObservation.swift 的实现可以发现新修饰符读取 store 中的AlertState把每个按钮的.send/.animatedSend动作回发给 store从而完成从库专用修饰符到原生 SwiftUI 弹窗的无缝衔接。用 NavigationStack 替换 NavigationStackStoreNavigationStackStore视图曾用于从 store 驱动导航栈现在被软废弃。假设 reducer 如下Reducer struct Feature { struct State { var path: StackStatePath.State [] } enum Action { case path(StackActionPath.State, Path.Action) } var body: some ReducerOfSelf { /* ... */ } }过去的写法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 加上ObservableStateReducer struct Feature { ObservableState struct State { // ... } // ... }Pathreducer 的 state 也要加上Reducer struct Path { ObservableState enum State { // ... } // ... }然后在视图中用Bindable可绑定持有 store旧平台用Perception.BindableBindable var store: StoreOfFeature最后使用库为NavigationStack提供的自定义初始化器NavigationStack(path:root:destination:)实现在 Sources/ComposableArchitecture/Observation/NavigationStackObservation.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) } } }从 NavigationStackObservation.swift 的源码可以看到这个初始化器把path绑定底层映射为StackState.PathView通过_NavigationDestinationViewModifier挂接navigationDestination(for:)并利用store.scope(component:)为栈内每个元素派生子 storeBinding.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(BindingActionState) } var body: some ReducerOfSelf { /* ... */ } }视图里通过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: BindingViewStoreFeature.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并删除所有BindingStateObservableState struct State { - BindingState var text - BindingState var isOn false var text var isOn false }重要不要移除 Action 上的BindableAction一致性也不要移除 reducer 中的BindingReducer——绑定依然需要它们。在视图中用Bindable持有 store旧平台用Perception.BindableBindable var store: StoreOfFeature然后直接在body中从 store 派生绑定不再需要WithViewStorevar body: some View { Form { TextField(Text, text: $store.text) Toggle(isOn: $store.isOn) } }替换 ViewStore.binding$store.tab.sending(...)ViewStore上还有一种派生绑定方式样板代码更多但涉及的工具更少为绑定显式定义一个 action。例如在 Tab 型应用中设置当前 tabReducer struct Feature { struct State { var tab 0 } enum Action { case tabChanged(Int) } var body: some ReducerOfSelf { 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持有 storeBindable var store: StoreOfFeature然后直接用$store.tab.sending(\.tabChanged)从 store 绑定派生绑定sending(_:)定义于 Sources/ComposableArchitecture/Observation/BindingObservation.swiftTabView(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 StoreOfFeature { 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: StoreOfFeature var body: some View { WithViewStore( store, observe: { $0 }, send: Feature.Action.view // ) { viewStore in Button(Login) { viewStore.send(.loginButtonTapped) } } } }这样既能省去.view(...)包装又保证了视图只能发送view动作。例如视图无法发送loginResponseviewStore.send(.loginResponse(false)) // Type Feature.Action.View has no member loginResponse1.7 中这个模式依然可行只需两处小改动。第一让你的View动作枚举遵循ViewAction协议Reducer struct Feature { // ... enum Action: ViewAction { // // ... } // ... }第二在视图上用ViewAction(for:)宏指定驱动该视图的 reducer。宏会为视图注入send方法让你直接发送视图动作而不再走store.send(_:)ViewAction(for: Feature.self) struct FeatureView: View { let store: StoreOfFeature 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: StoreOfFeature属性否则给出 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.swiftSwiftUI 集成AlertObservation.swift、NavigationStackObservation.swift、BindingObservation.swift宏测试用例ObservableStateMacroTests.swift、PresentsMacroTests.swift、ViewActionMacroTests.swift旧 API 的废弃声明Internal/Deprecations.swift赞分享前端移动开发【免费下载链接】swift-composable-architectureA library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.项目地址https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture点击查看免费下载相关推荐swift-composable-architecture SwiftUI 弃用 API 迁移指南从 ViewStore 走向 ObservableStateswift 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前端移动开发上一篇天勤量化TqSdk量化回测引擎全解析从策略验证到历史行情模拟实践指南下一篇mpv媒体播放器重新定义命令行视频播放体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考