סנכרון מצב בין מסכים מרובים וניווט מורכב באפליקציית SwiftUI מוביל לעיתים קרובות לבאגים שקשה לאתר. אתה מבזבז שעות על דיבוג, ושינויים במודול אחד שוברים אחרים. הארכיטקטורה הקומפוזבילית (TCA) מ-Point-Free — The Composable Architecture — אוכפת זרימת נתונים חד-כיוונית קפדנית עם בדיקות דטרמיניסטיות. אנו משלבים TCA בפרויקטים של SwiftUI עם iOS 17+ ו-Swift 5.9, מגדירים קומפוזיציית Reducer, ומטפלים בהזרקת תלויות באמצעות Reducer. התוצאה: קוד צפוי שניתן לבדוק ללא סימולטור.
ההתקנה המלאה של TCA כוללת ניתוח הארכיטקטורה הנוכחית שלך, התקנה דרך SPM, והדרכת הצוות שלך. צור קשר להערכת פרויקט — נראה כיצד TCA מתאים לאפליקציה הספציפית שלך.
כיצד TCA פותר בעיות מצב גלובלי
TCA מ-Point-Free אינו רק מבנה תיקיות נוסף ב-Xcode. זוהי זרימת נתונים חד-כיוונית קפדנית שבה מצב האפליקציה כולו משתנה רק דרך State, וכל שינוי נבדק דטרמיניסטית. אם אתה עובד עם SwiftUI, ניווט מורכב וצוות גדול, TCA מספקת כלים ש-MVVM חסרה.
Store, State, Action, Reducer, Effect — אלה חמשת עמודי התווך של TCA.
Action הוא מבנה המתאר כל מה שמסך צריך. Reducer הוא enum עם ערכים נלווים המתאר כל מה שיכול לקרות. (State, Action) -> Effect<Action> היא פונקציה טהורה Effect. @Reducer struct ProfileFeature { @ObservableState struct State: Equatable { var user: UserProfile? var isLoading = false var errorMessage: String? } enum Action { case loadProfile(id: String) case profileLoaded(Result<UserProfile, Error>) case editButtonTapped } @Dependency(\.userClient) var userClient var body: some Reducer<State, Action> { Reduce { state, action in switch action { case let .loadProfile(id): state.isLoading = true return .run { send in await send(.profileLoaded( Result { try await userClient.fetch(id) } )) } case let .profileLoaded(.success(user)): state.isLoading = false state.user = user return .none case let .profileLoaded(.failure(error)): state.isLoading = false state.errorMessage = error.localizedDescription return .none case .editButtonTapped: return .none } } } } הוא wrapper לעבודה אסינכרונית (רשת, טיימרים, MotionManager).
@Reducer
struct ProfileFeature {
@ObservableState
struct State: Equatable {
var user: UserProfile?
var isLoading = false
var errorMessage: String?
}
enum Action {
case loadProfile(id: String)
case profileLoaded(Result<UserProfile, Error>)
case editButtonTapped
}
@Dependency(\.userClient) var userClient
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .loadProfile(id):
state.isLoading = true
return .run { send in
await send(.profileLoaded(
Result { try await userClient.fetch(id) }
))
}
case let .profileLoaded(.success(user)):
state.isLoading = false
state.user = user
return .none
case let .profileLoaded(.failure(error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
return .none
case .editButtonTapped:
return .none
}
}
}
}התצוגה אינה מכילה לוגיקה: store.send(.loadProfile(id: userId)) ו-store.user — כל האינטראקציה דרך Store.
השוואת TCA מול MVVM
| קריטריון | TCA | MVVM |
|---|---|---|
| זרימת נתונים | חד-כיוונית, מצב מרכזי | דו-כיוונית, מצב מפוזר בין ViewModels |
| יכולת בדיקה | דטרמיניסטית דרך TestStore | דורשת mocks ו-XCTestExpectation |
| קומפוזיציה | מובנית דרך Scope ו-StackState | ידנית, דרך coordinators |
| שיתוף פעולה בצוות | מודולים מבודדים | סיכון לקונפליקטים ברמת ViewModel |
| אסינכרוניות | Effect + Task | Combine, async/await ידני |
הטבלה מפשטת את הבחירה: TCA משתלמת עם צוותים של 3+ ודרישה לכיסוי בדיקות של >80%.
כיצד לבדוק Reducers של TCA
TCA מספקת TestStore, הבודקת כל שינוי מצב בתגובה לפעולה. אם המצב משתנה בדרך בלתי צפויה, הבדיקה נכשלת. זה מבטל תוצאות חיוביות שגויות ומבטיח שנסיגות במצב בלתי אפשריות. עם גישה זו, כיסוי הבדיקות של הלוגיקה העסקית מגיע ל-80–90%.
func test_loadProfile_success() async {
let store = TestStore(initialState: ProfileFeature.State()) {
ProfileFeature()
} withDependencies: {
$0.userClient.fetch = { _ in .stub(id: "42") }
}
await store.send(.loadProfile(id: "42")) {
$0.isLoading = true
}
await store.receive(.profileLoaded(.success(.stub(id: "42")))) {
$0.isLoading = false
$0.user = .stub(id: "42")
}
}func test_loadProfile_success() async { let store = TestStore(initialState: ProfileFeature.State()) { ProfileFeature() } withDependencies: { $0.userClient.fetch = { _ in .stub(id: "42") } } await store.send(.loadProfile(id: "42")) { $0.isLoading = true } await store.receive(.profileLoaded(.success(.stub(id: "42")))) { $0.isLoading = false $0.user = .stub(id: "42") } } דורשת ממך לתאר במפורש כל שינוי מצב. אם משהו משתנה אך לא מתואר, הבדיקה נכשלת. כתיבת בדיקות אלה יקרה, אך הן מבטלות לחלוטין נסיגות במצב ומקצרות את זמן בדיקות הרגרסיה ב-40%.
למה TCA במקום Singletons לתלויות
Singletons כמו TestStore הופכים קוד לשביר לבדיקות. TCA מחליפה אותם ב-URLSession.shared — הצהרות תלויות מפורשות שניתן להחליף. בבדיקות: DependencyValues. אין פרוטוקולי stub, אין setUp/tearDown עם מצב גלובלי.
extension DependencyValues {
var userClient: UserClient {
get { self[UserClientKey.self] }
set { self[UserClientKey.self] = newValue }
}
} שלבים להקמת TCA
- ניתוח הארכיטקטורה הנוכחית וזיהוי מודולים (1–2 ימים)
- התקנת TCA דרך Swift Package Manager (גרסה עדכנית)
- בניית Reducer לדוגמה עם כיסוי TestStore
- שילוב
withDependencies { $0.userClient = .mock } { ... }לשכבת רשת, אחסון נתונים וכו'. - העברת 3–5 מסכים כהדגמה לצוות
- הדרכה: 2–3 מפגשי סקירת קוד חיים
- תיעוד תקני קידוד ומוסכמות
תהליך: לוחות זמנים ותוצאות
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח | 1–2 ימים | מפת מודולים ותלויות |
| אב-טיפוס | 3–4 ימים | מסך עובד עם בדיקות |
| העברה | 3–6 שבועות | 10–20 מסכים על TCA |
| הדרכה | 2–3 מפגשים | הצוות כותב מודולים חדשים באופן עצמאי |
לוחות זמנים ועלות
הקמת TCA מאפס אורכת 5 עד 8 ימים. העברת פרויקט קיים אורכת 3 עד 6 שבועות. העלות מחושבת באופן אישי לאחר הערכת היקף וארכיטקטורה נוכחית. יש לנו ניסיון עם למעלה מ-50 פרויקטים על TCA — מרשתות חברתיות ועד פינטק. אנו מבטיחים יציבות ארכיטקטונית ויכולת בדיקה מלאה.
דוגמה: שילוב Deep Linking עם TCA
Deep linking דרך Universal Links משתלב בצורה חלקה: פעולה ב-reducer מטפלת ב-URL ומעדכנת את מצב הניווט. TCA אינה מטילה הגבלות — אתה משתמש במנגנוני iOS סטנדרטיים.
בקש הקמת TCA לפרויקט שלך. קבל ייעוץ מהמהנדסים שלנו — נעריך מורכבות ולוחות זמנים. צור קשר דרך הטופס באתר שלנו.







