Уявіть: додаток з 50 екранами, кожен ViewController тягне мережевий шар, базу даних і логіку навігації. Нова фіча додається за тиждень, але кожна зміна ламає старі тести. Кодова база зростає, а швидкість розробки падає. Ми бачили це в 30+ проектах — від банків до стрімінгу. Рішення — Clean Architecture, підхід, який жорстко розділяє відповідальність між шарами та робить код передбачуваним.
Clean Architecture, популяризована Робертом Мартіном, пропонує три концентричні шари: Domain (ядро бізнес-логіки), Data (реалізації репозиторіїв та джерел даних) та Presentation (UI та ViewModel). Залежності спрямовані всередину: Domain не знає про Data та Presentation. Це дозволяє тестувати бізнес-логіку без запуску симулятора та змінювати UI, не торкаючись ядра. Впровадження цього підходу прискорює додавання нових фіч в 3 рази та скорочує час регресійного тестування на 80% — цифри з нашої практики.
Ми налаштовували Clean Architecture для 30+ комерційних проектів: банківські додатки, стрімінгові сервіси, маркетплейси. Клієнти отримують архітектуру, яка прискорює розробку та знижує вартість підтримки. Нижче розберемо, як це працює на практиці.
Як Clean Architecture вирішує проблеми iOS-розробки?
У класичному трактуванні Боба Мартіна три кільця: Entities → Use Cases → Interface Adapters. В iOS це відображається наступним чином.
Domain-шар — ядро. Тут Entity-моделі: чисті Swift-структури без імпорту Foundation, лише бізнес-дані. Поряд — UseCase-протоколи та їх реалізації. Наприклад, FetchUserProfileUseCase приймає UserRepository через ін'єкцію залежностей та повертає AnyPublisher<UserProfile, DomainError>. Жодного URLSession, жодного CoreData. Цей шар компілюється та тестується ізольовано.
protocol UserRepository { func fetchProfile(id: String) -> AnyPublisher<UserProfile, DomainError> } final class FetchUserProfileUseCase { private let repository: UserRepository init(repository: UserRepository) { self.repository = repository } func execute(id: String) -> AnyPublisher<UserProfile, DomainError> { repository.fetchProfile(id: id) } } Data-шар — реалізації репозиторіїв. UserRepositoryImpl працює з URLSession або Alamofire, маппить DTO → доменну модель, обробляє мережеві помилки. CoreDataUserCache реалізує той самий протокол для локального кешу. Вибір джерела даних — у UserRepositoryImpl через стратегію або в DI-контейнері.
Presentation-шар — тут живуть ViewModel/Presenter. У зв'язці з SwiftUI зручний ObservableObject-ViewModel: він викликає UseCase, трансформує результат у @Published-стан та публікує його. ViewController або SwiftUI View займається виключно рендерингом.
Навігація та DI
| Шар | Відповідальність | Залежності |
|---|---|---|
| Domain | Бізнес-логіка, Entity, UseCase | Ні від кого |
| Data | Реалізація репозиторіїв, маппінг | Domain |
| Presentation | ViewModel, View, Coordinator | Domain |
Типова проблема — ViewController створює наступний ViewController та робить push. Рішення — Coordinator. ViewModel тримає слабке посилання на ProfileCoordinator. Конкретний ProfileCoordinatorImpl знає про UINavigationController та про наступні екрани. ViewModel — ні.
protocol ProfileCoordinator: AnyObject { func showEditProfile(user: UserProfile) func showOrders(userId: String) } Використовуємо ініціалізаторну ін'єкцію в ланцюжку: SceneDelegate створює AppCoordinator, той — конкретні репозиторії та UseCase-и, передає їх у ViewModel через init. Можна підключити Swinject або Needle, але для більшості проектів вистачає ручної збірки в CompositionRoot. Service Locator — анти-патерн: приховує залежності та ламає тести.
Чому Clean Architecture краще стандартного MVC?
У MVC ViewController часто стає «божественним об'єктом», відповідальним за все. Clean Architecture дає 3-кратне прискорення додавання нових фіч за рахунок чітких меж: зміни в UI не зачіпають бізнес-логіку. Час збірки тестів domain-шару скорочується на 80% — секунди замість хвилин. Ось приклад тесту:
final class FetchUserProfileUseCaseTests: XCTestCase { func test_execute_returnsProfile() { let mock = MockUserRepository(result: .success(.stub())) let sut = FetchUserProfileUseCase(repository: mock) var received: UserProfile? _ = sut.execute(id: "123").sink( receiveCompletion: { _ in }, receiveValue: { received = $0 } ) XCTAssertEqual(received?.id, "123") } } Як впровадити Clean Architecture в iOS-проект: покрокова інструкція
- Проведіть аудит поточної архітектури та виявіть точки перетину шарів.
- Створіть Swift Package для кожного шару: Domain, Data, Presentation.
- Визначте протоколи репозиторіїв у Domain, реалізуйте їх у Data.
- Налаштуйте CompositionRoot — центральний клас для ручної ініціалізації залежностей.
- Напишіть юніт-тести для ключових UseCase.
Часті помилки при впровадженні
- Занадто тонкі UseCase.
GetUsernameUseCase, який робитьreturn user.name— марний. - Domain-моделі з Codable. DTO та маппінг мають бути в Data-шарі.
- ViewModel знає про конкретний репозиторій. Тільки протокол + ініціалізаторна ін'єкція.
Уникнувши цих помилок, ви отримаєте архітектуру, яку легко підтримувати та розширювати. Ми гарантуємо якість результату на основі досвіду 5+ років. Зв'яжіться з нами для консультації щодо впровадження — оцінимо ваш проект і запропонуємо оптимальне рішення. Замовте налаштування Clean Architecture і прискорте розробку в рази.
Що входить у налаштування?
| Етап | Дії | Результат |
|---|---|---|
| Аудит | Аналіз поточної архітектури, виявлення залежностей | План міграції |
| Модулі | Створення Swift Package-модулів Domain, Data, Presentation | Чиста структура проекту |
| DI | Реалізація CompositionRoot або DI-контейнера | Управління залежностями |
| Фічі | Налаштування 2–3 фіча-модулів за зразком | Приклад для команди |
| Тести | Написання базових юніт-тестів domain-шару | Тестове покриття |
Терміни та як замовити
Налаштування з нуля на новому проекті: 3–5 днів. Рефакторинг існуючого проекту з міграцією 10–15 модулів: 2–4 тижні. Вартість розраховується після аналізу коду. Отримайте консультацію — ми оцінимо ваш проект і дамо гарантію на архітектуру. Зв'яжіться з нами, щоб обговорити деталі.







