告别崩溃与卡顿:Swift Composable Architecture避坑指南
告别崩溃与卡顿:Swift Composable Architecture避坑指南
你是否在使用Swift Composable Architecture(SCA)开发时遇到过状态不一致导致的UI异常?或者因为Effect管理不当引发的内存泄漏?本文将揭示SCA开发中最常见的6个陷阱,并通过真实案例演示如何写出健壮、可维护的架构代码。
陷阱1:未处理的异步Effect导致测试失败
症状:测试中出现"An effect returned for this action is still running"错误,或实际状态与预期不符。
错误示范:
case .loadDataButtonTapped:
return .run { send in
try await Task.sleep(for: .seconds(1)) // 直接使用Task.sleep
await send(.dataLoaded)
}
问题根源:直接使用Task.sleep等系统API会导致测试必须等待真实时间流逝,且无法控制异步行为。在测试文档中明确指出,这会使测试缓慢且不可靠。
解决方案:使用可控依赖项如ContinuousClock:
@Reducer
struct Feature {
@Dependency(\.continuousClock) var clock // 注入可控时钟
var body: some Reducer<State, Action> {
Reduce { state, action in
case .loadDataButtonTapped:
return .run { send in
try await self.clock.sleep(for: .seconds(1)) // 使用依赖时钟
await send(.dataLoaded)
}
}
}
}
测试时可替换为立即执行的时钟:
let store = TestStore(initialState: Feature.State()) {
Feature()
} withDependencies: {
$0.continuousClock = ImmediateClock() // 测试用即时时钟
}
陷阱2:状态观察不正确导致UI不更新
症状:状态已更新但UI未刷新,或出现多余的重渲染。
错误示范:
// 错误:未使用@ObservableState宏
struct State: Equatable {
var count = 0
}
// 错误:直接访问store.state而非使用绑定
Text("\(store.state.count)")
问题根源:SCA通过@ObservableState宏实现状态观察,未正确标记的状态无法触发视图更新。根据运行时警告,这种情况会触发"State is not observable"警告。
解决方案:正确使用观察宏和视图绑定:
@Reducer
struct Feature {
@ObservableState // 正确标记可观察状态
struct State: Equatable {
var count = 0
}
// ...
}
// 视图中使用绑定
Text("\(store.count)") // 直接访问store属性而非store.state
陷阱3:导航状态管理混乱
症状:页面跳转后状态异常,或返回时数据未保留。
错误示范:
// 错误:使用Bool管理导航状态
struct State: Equatable {
var isDetailPresented = false
var selectedItem: Item?
}
// 错误:手动控制导航呈现
Button("Show Detail") {
store.state.isDetailPresented = true // 直接修改状态
}
.sheet(isPresented: $store.state.isDetailPresented) {
DetailView(store: store)
}
问题根源:SCA提供了专门的导航状态管理工具。如导航文档所述,直接操作Bool值会导致状态不同步和测试困难。
解决方案:使用Presents宏和栈导航:
@Reducer
struct Feature {
@ObservableState
struct State: Equatable {
@Presents var detail: DetailFeature.State? // 自动管理导航状态
}
enum Action {
case showDetailTapped
case detail(PresentationAction<DetailFeature.Action>)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
case .showDetailTapped:
state.detail = DetailFeature.State() // 正确触发导航
return .none
}
.ifLet(\.$detail, action: /Action.detail) { // 关联子Feature
DetailFeature()
}
}
}
陷阱4:依赖注入不当导致测试困难
症状:无法模拟网络请求或系统API,测试依赖外部环境。
错误示范:
// 错误:直接在Reducer中使用URLSession
case .fetchData:
return .run { send in
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com")!)
// ...
}
问题根源:硬编码依赖使测试无法隔离。入门文档强调,依赖注入是SCA可测试性的核心。
解决方案:定义依赖协议并注入:
// 定义依赖协议
struct APIClient {
var fetchData: () async throws -> Data
}
// 注册依赖
extension APIClient: DependencyKey {
static let liveValue = APIClient(
fetchData: {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com")!)
return data
}
)
}
extension DependencyValues {
var apiClient: APIClient {
get { self[APIClient.self] }
set { self[APIClient.self] = newValue }
}
}
// 使用依赖
@Reducer
struct Feature {
@Dependency(\.apiClient) var apiClient // 注入依赖
var body: some Reducer<State, Action> {
Reduce { state, action in
case .fetchData:
return .run { send in
let data = try await self.apiClient.fetchData() // 使用依赖
// ...
}
}
}
}
测试时替换为模拟实现:
let store = TestStore(initialState: Feature.State()) {
Feature()
} withDependencies: {
$0.apiClient = APIClient(
fetchData: { Data("{\"mock\": true}".utf8) } // 模拟返回
)
}
陷阱5:过度使用全局状态
症状:应用状态膨胀,组件复用困难,测试变得复杂。
错误示范:
// 错误:单一巨大的AppState包含所有功能状态
struct AppState: Equatable {
var user: User?
var posts: [Post] = []
var comments: [Comment] = []
var settings: Settings = .init()
// ... 20+更多字段
}
问题根源:未遵循组件化设计原则。SCA示例中的Todos应用和SyncUps应用都采用了功能拆分的状态设计。
解决方案:按功能拆分状态并组合:
// 正确:每个功能独立状态
struct UserFeature: Reducer {
@ObservableState struct State: Equatable { /* 用户相关状态 */ }
enum Action { /* 用户相关操作 */ }
}
struct PostsFeature: Reducer {
@ObservableState struct State: Equatable { /* 帖子相关状态 */ }
enum Action { /* 帖子相关操作 */ }
}
// 组合状态
struct AppState: Equatable {
var user: UserFeature.State?
var posts: PostsFeature.State = .init()
}
struct AppFeature: Reducer {
var body: some Reducer<AppState, AppAction> {
Scope(state: \.user, action: /AppAction.user) {
UserFeature()
}
Scope(state: \.posts, action: /AppAction.posts) {
PostsFeature()
}
}
}
陷阱6:测试中忽略Effect处理
症状:测试通过但实际运行时出现异常,或测试不稳定。
错误示范:
// 错误:只发送action但不处理返回的effect
func testFetchData() async {
let store = TestStore(initialState: Feature.State()) { Feature() }
await store.send(.fetchData) // 发送action后未处理effect
// 缺少store.receive(...)调用
}
问题根源:SCA的测试文档强调,必须完整处理所有Effect才能确保测试的准确性。TestStore会严格检查所有状态变化和Effect完成情况。
解决方案:完整测试流程,包括接收Effect结果:
func testFetchData() async {
let store = TestStore(initialState: Feature.State()) {
Feature()
} withDependencies: {
$0.apiClient.fetchData = { Data("{}".utf8) }
}
await store.send(.fetchData) // 发送action
// 必须接收并验证effect结果
await store.receive(\.dataLoaded) {
$0.isLoading = false
$0.data = "Mock data"
}
}
最佳实践总结
- 依赖管理:始终通过
@Dependency注入外部依赖,避免直接使用系统API - 状态设计:遵循单一职责原则,拆分过大的状态对象
- 导航管理:使用
@Presents和StackState等专用导航工具 - 测试完整:使用
TestStore时确保处理所有发送和接收的Action - 性能优化:通过
ifLet、forEach等Reducer作用域控制状态更新范围
通过避免这些常见陷阱,你的SCA应用将更加健壮、可测试且易于维护。更多最佳实践可参考官方CaseStudies中的实现,特别是性能优化文档中关于状态最小化和依赖隔离的建议。
更多推荐


所有评论(0)