Синхронізація станів між кількома екранами та складна навігація в SwiftUI-додатку часто призводять до важковловимих багів. Ви витрачаєте години на відладку, а зміни в одному модулі ламають інші. The Composable Architecture (TCA) від Point-Free — The Composable Architecture — пропонує строгий односпрямований потік даних з детермінованим тестуванням. Ми впроваджуємо TCA в проекти на SwiftUI з iOS 17+ та Swift 5.9, налаштовуємо композицію редьюсерів та ін'єкцію залежностей через DependencyValues. Результат — передбачуваний код, який тестується без симулятора.
Налаштування TCA під ключ включає аналіз вашої архітектури, встановлення через SPM та навчання команди. Зв'яжіться з нами для оцінки проекту — ми покажемо, як TCA підходить саме вашому додатку.
Як TCA вирішує проблему глобального стану?
TCA від Point-Free — не просто ще один спосіб розташувати папки в Xcode. Це строгий односпрямований потік даних, де стан всього додатку змінюється тільки через Reducer, а кожна зміна тестується детерміновано. Якщо ви працюєте з SwiftUI, складною навігацією та великою командою — TCA дає інструментарій, якого немає у MVVM.
Store, State, Action, Reducer, Effect — п'ять китів TCA.
State — структура, що описує все, що потрібно екрану. Action — enum з асоційованими значеннями, що описує все, що може статися. Reducer — чиста функція (State, Action) -> Effect<Action>. Effect — обгортка над асинхронною роботою (мережа, таймери, 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
}
}
}
}View не містить логіки: store.send(.loadProfile(id: userId)) та store.user — вся взаємодія через Store.
Порівняння TCA та MVVM
| Критерій | TCA | MVVM |
|---|---|---|
| Потік даних | Односпрямований, централізований стан | Двонаправлений, розмазаний по ViewModel |
| Тестованість | Детермінована через TestStore | Вимагає моків та XCTestExpectation |
| Композиція | Вбудована через Scope та StackState | Ручна, через координатори |
| Робота в команді | Ізольовані модулі | Ризик конфліктів на рівні ViewModel |
| Асинхронність | Effect + Task | Combine, async/await вручну |
Таблиця спрощує вибір: TCA окупається при команді від 3 осіб та вимозі тестового покриття >80%.
Як тестувати TCA-редьюсери?
TCA надає TestStore, який перевіряє кожну зміну State у відповідь на Action. Якщо стан змінився не так, як описано — тест впаде. Це прибирає хибнопозитивні тести та гарантує, що регресії по стану неможливі. Завдяки такому підходу тестове покриття бізнес-логіки досягає 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")
}
}TestStore вимагає явно описати кожну зміну стану. Якщо щось змінилося, але не описано — тест падає. Це дороге тестування писати, але воно повністю виключає регресії по стану та скорочує час регресійного тестування на 40%.
Чому TCA краще синглтонів для залежностей?
Синглтони на кшталт URLSession.shared роблять код крихким для тестів. TCA замінює їх на DependencyValues — явне оголошення залежностей з можливістю підміни. У тестах: withDependencies { $0.userClient = .mock } { ... }. Жодного протоколу-заглушки, жодних 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
- Інтеграція DependencyValues для мережевого шару, зберігання даних тощо
- Міграція 3-5 екранів з демонстрацією команді
- Навчання: 2-3 сесії розбору живого коду
- Документація по стандартах та кодингу
Процес налаштування: терміни та результати
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз | 1-2 дні | Карта модулів та залежностей |
| Прототип | 3-4 дні | Працюючий екран з тестами |
| Міграція | 3-6 тижнів | 10-20 екранів на TCA |
| Навчання | 2-3 сесії | Команда пише нові модулі самостійно |
Терміни та вартість
Налаштування TCA з нуля займає від 5 до 8 днів. Міграція існуючого проекту — від 3 до 6 тижнів. Вартість розраховується індивідуально після аналізу обсягу та поточної архітектури. Ми маємо досвід більш ніж 50 проектів на TCA — від соціальних мереж до fintech. Гарантуємо стабільність архітектури та повну тестованість.
Приклад інтеграції глибоких посилань (deep linking) з TCA
Deep linking через Universal Links легко інтегрується: action в редьюсері обробляє URL, змінює стан навігації. TCA не накладає обмежень — використовуєте стандартні механізми iOS.
Замовте налаштування TCA для вашого проекту. Отримайте консультацію наших інженерів — оцінимо складність та терміни. Зв'яжіться з нами через форму на сайті.







