Синхронізація станів між кількома екранами та складна навігація в 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 для вашого проекту. Отримайте консультацію наших інженерів — оцінимо складність та терміни. Зв'яжіться з нами через форму на сайті.







