SwiftUI'da state management karmaşıklaşınca, @State ile başlayan macera bir süre sonra kaosa dönebilir. The Composable Architecture (TCA), Brandon Williams ve Stephen Celis tarafından geliştirilen, unidirectional data flow prensibine dayanan bir mimari framework. Redux'tan ilham alır ama Swift'in tip sistemi ve async/await ile tam uyumludur.
💡 Hızlı Not: TCA, Point-Free tarafından geliştirilir ve açık kaynaklıdır. Bu rehber TCA 1.0+ (Observation API ile) kapsar.
İçindekiler
- TCA Nedir ve Neden Kullanmalı?
- Temel Kavramlar: State, Action, Reducer
- Store ve ViewStore
- Side Effects ve Dependencies
- Reducer Composition
- Navigation ve Routing
- Testing ile TCA
- TCA vs MVVM vs VIPER
- Migration Guide
- Production Best Practices
TCA Nedir ve Neden Kullanmalı? {#tca-nedir}
TCA, bir uygulamanın tüm state'ini tek bir truth kaynağında tutarak, state değişikliklerini öngörülebilir ve test edilebilir kılar.
TCA'nın Çözdüğü Sorunlar
Sorun | Geleneksel SwiftUI | TCA ile |
|---|---|---|
State dağınıklığı | @State, @Binding, @StateObject dağınık | Tek State struct |
Side effect yönetimi | Ad-hoc async/await | Effect type ile kontrollü |
Test edilebilirlik | UI test'e bağımlı | Unit test ile %100 coverage |
Navigation | Programmatic navigation karmaşık | Declarative, state-driven |
Dependency injection | Manual veya framework bağımlı | Built-in DI sistemi |
Composition | Module'ler arası bağımlılık | Reducer composition |
Temel Kavramlar: State, Action, Reducer {#temel-kavramlar}
swift
1import ComposableArchitecture2 3// 1. State - Ekranın tüm durumu4@ObservableState5struct CounterFeature: Reducer {6 struct State: Equatable {7 var count = 08 var isLoading = false9 var fact: String?10 }11 12 // 2. Action - Kullanıcı eylemleri ve sistem event'leri13 enum Action {14 case incrementButtonTapped15 case decrementButtonTapped16 case factButtonTapped17 case factResponse(String)18 }19 20 // 3. Dependencies21 @Dependency(\.numberFact) var numberFact22 23 // 4. Reducer - State'i action'a göre güncelle24 var body: some ReducerOf<Self> {25 Reduce { state, action in26 switch action {27 case .incrementButtonTapped:28 state.count += 129 state.fact = nil30 return .none31 32 case .decrementButtonTapped:33 state.count -= 134 state.fact = nil35 return .none36 37 case .factButtonTapped:38 state.isLoading = true39 return .run { [count = state.count] send in40 let fact = try await numberFact.fetch(count)41 await send(.factResponse(fact))42 }43 44 case .factResponse(let fact):45 state.isLoading = false46 state.fact = fact47 return .none48 }49 }50 }51}52 53// 5. View54struct CounterView: View {55 let store: StoreOf<CounterFeature>56 57 var body: some View {58 VStack(spacing: 16) {59 HStack {60 Button("-") { store.send(.decrementButtonTapped) }61 Text("\(store.count)")62 .font(.largeTitle)63 Button("+") { store.send(.incrementButtonTapped) }64 }65 66 Button("Get Fact") { store.send(.factButtonTapped) }67 .disabled(store.isLoading)68 69 if let fact = store.fact {70 Text(fact)71 .padding()72 }73 }74 }75}Store ve ViewStore {#store}
Store, state'i tutar ve action'ları reducer'a iletir. TCA 1.0+ ile @ObservableState macro'su sayesinde doğrudan store property'lerine erişebilirsin:
swift
1// App entry point2@main3struct MyApp: App {4 let store = Store(initialState: AppFeature.State()) {5 AppFeature()6 }7 8 var body: some Scene {9 WindowGroup {10 AppView(store: store)11 }12 }13}Side Effects ve Dependencies {#side-effects}
TCA'da side effect'ler (API call, timer, notification) Effect type ile yönetilir:
swift
1// Dependency tanımla2struct NumberFactClient {3 var fetch: @Sendable (Int) async throws -> String4}5 6extension NumberFactClient: DependencyKey {7 static let liveValue = NumberFactClient(8 fetch: { number in9 let (data, _) = try await URLSession.shared.data(10 from: URL(string: "http://numbersapi.com/\(number)")!11 )12 return String(data: data, encoding: .utf8) ?? ""13 }14 )15 16 static let testValue = NumberFactClient(17 fetch: { "\($0) is a great number!" }18 )19}20 21extension DependencyValues {22 var numberFact: NumberFactClient {23 get { self[NumberFactClient.self] }24 set { self[NumberFactClient.self] = newValue }25 }26}Reducer Composition {#composition}
Büyük reducer'ları küçük parçalara böl ve birleştir:
swift
1@ObservableState2struct AppFeature: Reducer {3 struct State: Equatable {4 var tab = Tab.home5 var home = HomeFeature.State()6 var profile = ProfileFeature.State()7 var settings = SettingsFeature.State()8 }9 10 enum Action {11 case tabChanged(Tab)12 case home(HomeFeature.Action)13 case profile(ProfileFeature.Action)14 case settings(SettingsFeature.Action)15 }16 17 var body: some ReducerOf<Self> {18 Reduce { state, action in19 switch action {20 case .tabChanged(let tab):21 state.tab = tab22 return .none23 default:24 return .none25 }26 }27 // Child reducer'ları compose et28 Scope(state: \.home, action: \.home) { HomeFeature() }29 Scope(state: \.profile, action: \.profile) { ProfileFeature() }30 Scope(state: \.settings, action: \.settings) { SettingsFeature() }31 }32}Testing ile TCA {#testing}
TCA'nın en güçlü yanı: tam test edilebilirlik:
swift
1@MainActor2func testCounter() async {3 let store = TestStore(initialState: CounterFeature.State()) {4 CounterFeature()5 } withDependencies: {6 $0.numberFact.fetch = { "\($0) is a nice number" }7 }8 9 // Increment10 await store.send(.incrementButtonTapped) {11 $0.count = 112 }13 14 // Get fact15 await store.send(.factButtonTapped) {16 $0.isLoading = true17 }18 19 // Fact response20 await store.receive(.factResponse("1 is a nice number")) {21 $0.isLoading = false22 $0.fact = "1 is a nice number"23 }24}TCA vs MVVM vs VIPER Karşılaştırma {#karsilastirma}
Özellik | TCA | MVVM | VIPER |
|---|---|---|---|
Complexity | Orta-Yüksek | Düşük-Orta | Yüksek |
Learning curve | Dik | Düşük | Çok dik |
Testability | Mükemmel | İyi | Mükemmel |
SwiftUI uyumu | Mükemmel | İyi | Zayıf |
State management | Built-in | Manual | Manual |
Side effects | Built-in (Effect) | Manual | Interactor'da |
Navigation | State-driven | Mixed | Router'da |
Team scale | 3-10 kişi | 1-5 kişi | 5-20 kişi |
Boilerplate | Orta | Düşük | Çok yüksek |
Migration Guide {#migration}
MVVM'den TCA'ya geçiş adımları:
- ViewModel → Reducer: Her ViewModel'i bir Feature Reducer'a çevir
- @Published → State: Published property'leri State struct'a taşı
- Fonksiyonlar → Actions: ViewModel fonksiyonlarını Action enum case'lerine dönüştür
- async/await → Effect: Asenkron işlemleri Effect.run'a taşı
- Singleton → Dependency: Singleton kullanımlarını @Dependency'ye çevir
Production Best Practices {#best-practices}
- Feature modülleri oluştur — her ekran bir Feature Reducer
- Shared state için parent reducer kullan, global state'ten kaçın
- Effect cancellation ile gereksiz network call'ları iptal et
- TestStore ile her action/state geçişini test et
- Dependency sistemi ile tüm external bağımlılıkları inject et
TCA'da Navigation Patterns
TCA 1.0+ ile gelen tree-based navigation, state-driven yaklaşım sunar:
swift
1@Reducer2struct ContactsFeature {3 @ObservableState4 struct State: Equatable {5 var contacts: [Contact] = []6 @Presents var destination: Destination.State?7 }8 9 enum Action {10 case addButtonTapped11 case destination(PresentationAction<Destination.Action>)12 }13 14 @Reducer15 enum Destination {16 case addContact(AddContactFeature)17 case editContact(EditContactFeature)18 case alert(AlertState<AlertAction>)19 }20 21 var body: some ReducerOf<Self> {22 Reduce { state, action in23 switch action {24 case .addButtonTapped:25 state.destination = .addContact(AddContactFeature.State())26 return .none27 case .destination:28 return .none29 }30 }31 .ifLet(\.$destination, action: \.destination)32 }33}Performance İpuçları
Sorun | Çözüm | Etki |
|---|---|---|
Gereksiz view rebuild | `WithPerceptionTracking` ile sarma | View rebuild %60 azalır |
Büyük state | `Scope` ile child state'e focus | Memory kullanımı düşer |
Effect leak | `Effect.cancel(id:)` kullan | Gereksiz network call'lar iptal |
Yavaş test | `TestStore` + `exhaustivity: .off` | Test süresi %80 azalır |
Compile time | Feature'ları ayrı module'lere böl | Incremental build hızlanır |
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var: **Kaynaklar:** - [TCA GitHub Repository](https://github.com/pointfreeco/swift-composable-architecture) - [Point-Free Episodes](https://www.pointfree.co/collections/composable-architecture) - [WWDC23: Discover Observation in SwiftUI](https://developer.apple.com/videos/play/wwdc2023/10149/)

