Настройка архитектуры 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 недели в зависимости от объёма. Стоимость рассчитывается после анализа текущего кода и архитектурных решений.







