Tüm Yazılar
KategoriArchitecture
Okuma Süresi
23 dk
Yayın Tarihi
...
Kelime Sayısı
1.455kelime

Kahveni hazırla - bu içerikli bir makale!

Point-Free'nin TCA framework'ü ile SwiftUI'da state management, side effects ve navigation'ı derinlemesine öğrenin. Reducer composition, dependency injection ve testing dahil.

The Composable Architecture (TCA): SwiftUI için Modern Mimari

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

  1. TCA Nedir ve Neden Kullanmalı?
  2. Temel Kavramlar: State, Action, Reducer
  3. Store ve ViewStore
  4. Side Effects ve Dependencies
  5. Reducer Composition
  6. Navigation ve Routing
  7. Testing ile TCA
  8. TCA vs MVVM vs VIPER
  9. Migration Guide
  10. 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 ComposableArchitecture
2 
3// 1. State - Ekranın tüm durumu
4@ObservableState
5struct CounterFeature: Reducer {
6 struct State: Equatable {
7 var count = 0
8 var isLoading = false
9 var fact: String?
10 }
11 
12 // 2. Action - Kullanıcı eylemleri ve sistem event'leri
13 enum Action {
14 case incrementButtonTapped
15 case decrementButtonTapped
16 case factButtonTapped
17 case factResponse(String)
18 }
19 
20 // 3. Dependencies
21 @Dependency(\.numberFact) var numberFact
22 
23 // 4. Reducer - State'i action'a göre güncelle
24 var body: some ReducerOf<Self> {
25 Reduce { state, action in
26 switch action {
27 case .incrementButtonTapped:
28 state.count += 1
29 state.fact = nil
30 return .none
31 
32 case .decrementButtonTapped:
33 state.count -= 1
34 state.fact = nil
35 return .none
36 
37 case .factButtonTapped:
38 state.isLoading = true
39 return .run { [count = state.count] send in
40 let fact = try await numberFact.fetch(count)
41 await send(.factResponse(fact))
42 }
43 
44 case .factResponse(let fact):
45 state.isLoading = false
46 state.fact = fact
47 return .none
48 }
49 }
50 }
51}
52 
53// 5. View
54struct 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 point
2@main
3struct 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ımla
2struct NumberFactClient {
3 var fetch: @Sendable (Int) async throws -> String
4}
5 
6extension NumberFactClient: DependencyKey {
7 static let liveValue = NumberFactClient(
8 fetch: { number in
9 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@ObservableState
2struct AppFeature: Reducer {
3 struct State: Equatable {
4 var tab = Tab.home
5 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 in
19 switch action {
20 case .tabChanged(let tab):
21 state.tab = tab
22 return .none
23 default:
24 return .none
25 }
26 }
27 // Child reducer'ları compose et
28 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@MainActor
2func 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 // Increment
10 await store.send(.incrementButtonTapped) {
11 $0.count = 1
12 }
13 
14 // Get fact
15 await store.send(.factButtonTapped) {
16 $0.isLoading = true
17 }
18 
19 // Fact response
20 await store.receive(.factResponse("1 is a nice number")) {
21 $0.isLoading = false
22 $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ı:

  1. ViewModel → Reducer: Her ViewModel'i bir Feature Reducer'a çevir
  2. @Published → State: Published property'leri State struct'a taşı
  3. Fonksiyonlar → Actions: ViewModel fonksiyonlarını Action enum case'lerine dönüştür
  4. async/await → Effect: Asenkron işlemleri Effect.run'a taşı
  5. Singleton → Dependency: Singleton kullanımlarını @Dependency'ye çevir

Production Best Practices {#best-practices}

  1. Feature modülleri oluştur — her ekran bir Feature Reducer
  2. Shared state için parent reducer kullan, global state'ten kaçın
  3. Effect cancellation ile gereksiz network call'ları iptal et
  4. TestStore ile her action/state geçişini test et
  5. 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@Reducer
2struct ContactsFeature {
3 @ObservableState
4 struct State: Equatable {
5 var contacts: [Contact] = []
6 @Presents var destination: Destination.State?
7 }
8 
9 enum Action {
10 case addButtonTapped
11 case destination(PresentationAction<Destination.Action>)
12 }
13 
14 @Reducer
15 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 in
23 switch action {
24 case .addButtonTapped:
25 state.destination = .addContact(AddContactFeature.State())
26 return .none
27 case .destination:
28 return .none
29 }
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/)

Etiketler

#tca#composable-architecture#swiftui#architecture#pointfree#state-management
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz