Настройка архітектури Clean Architecture для iOS-застосунку
Коли кодова база iOS-застосунку виростає до 50–70 екранів, ViewController відповідає і за сітьові запити, і за трансформацію даних, і за навігацію. Тести писати некуди — залежності захардкожені. Новий розробник відкриває ProfileViewController.swift на 1200 рядків та закриває ноутбук.
Clean Architecture вирішує це через розділення на концентричні шари з жорсткою напрямком залежностей: внутрішні шари нічого не знають про зовнішні.
Як устроєна 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 займається виключно рендерингом.
Навігація: Coordinator або Router
Типова проблема — ViewController створює наступний ViewController та робить push. Це порушує Clean Architecture: presentation-слой знає про конкретні типи інших екранів. Рішення — Coordinator:
protocol ProfileCoordinator: AnyObject {
func showEditProfile(user: UserProfile)
func showOrders(userId: String)
}
ViewModel тримає слабку ссилку на ProfileCoordinator. Конкретний ProfileCoordinatorImpl знає про UINavigationController та про наступні екрани. ViewModel — ні.
DI: чиста ін'єкція без Service Locator
Service Locator (глобальний DIContainer.shared.resolve()) — анти-паттерн: скриває залежності та ломає тести. Використовуємо інініціалізаційну ін'єкцію в ланцюзі: SceneDelegate створює AppCoordinator, той — конкретні репозиторії та UseCase-и, передає їх у ViewModel через init. Можна підключити Swinject або Needle, але для більшості проектів вручна сборка в CompositionRoot достатньо.
Тестованість — основний виграш
Domain-слой тестується через XCTest без залежності від UIKit: створюємо MockUserRepository, підставляємо в UseCase, перевіряємо логіку. Ніяких XCTestExpectation для мережі, ніяких моків URLSession.
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")
}
}
Час збирання тестів domain-слоя — секунди, не хвилини. Це змінює культуру розробки в команді.
Типові помилки при впровадженні
Занадто тонкі UseCase. GetUsernameUseCase, який робить return user.name — бессмисловий слой. UseCase виправданий, коли інкапсулює нетривіальну логіку або оркеструє кілька репозиторіїв.
Domain-моделі з Codable. Додати Codable до доменної Entity означає протічку Data-слоя всередину. DTO — в Data-слое, маппінг — там же.
ViewModel знає про конкретний репозиторій. Якщо у ViewModel написано let repo = UserRepositoryImpl(...) — ін'єкції залежностей немає. Тільки протокол + інініціалізаційна ін'єкція.
Що входить у настройку
Аудит поточної архітектури (якщо проект існує): визначаємо, що виносяться в Domain, що залишається в Presentation, які залежності потрібно інвертувати.
Створення базової структури модулів: Domain, Data, Presentation — окремі Swift Package або targets в одному Xcode-проекті. Настройка залежностей між targets: Data залежить від Domain, Presentation залежить від Domain, не від Data.
Реалізація CompositionRoot / DI-контейнера. Настройка перших 2–3 фича-модулів як образця для команди.
Написання базових тестів domain-слоя як образця.
Терміни
Настройка архітектури з нуля на новому проекті (структура + DI + перший модуль): 3–5 днів. Рефакторинг існуючого проекту з міграцією 10–15 модулів: 2–4 тижні залежно від обсягу. Вартість розраховується після аналізу поточного кода та архітектурних рішень.







