Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
8 подробных ответов
01Почему Apple-овский MVC превращается в Massive View Controller и как разгрузить вью-контроллер, не меняя архитектуру?
junior
Короткий ответ: Потому что в UIKit вью-контроллер по умолчанию — точка сборки всего: жизненный цикл вью, лейаут, data source таблицы, навигация и сетевые вызовы. Разгружается он выносом обязанностей в отдельные объекты — менять архитектуру для этого не обязательно.
Подробно:
- Диагноз — MVC у Apple не плох сам по себе; проблема в том, что «C» удобно принимает любой код, и без дисциплины вью-контроллер растёт до тысяч строк.
- Что выносить:
| Обязанность | Куда выносится |
|---|---|
UITableViewDataSource / delegate |
отдельный объект-датасорс |
| Кусок экрана со своей логикой | child view controller |
| Сеть, кэш, аналитика | простые сервисные объекты |
| Конфигурация сабвью | сами вью (configure(with:)) |
- Итог — вью-контроллер остаётся дирижёром: связывает сервисы, вью и навигацию, но не делает всю работу сам.
⚠️ Частая ошибка: отвечать «MVC — плохая архитектура, поэтому все ушли в MVVM». Плоха недисциплинированная реализация: без выноса обязанностей тот же перегруз повторится и с ViewModel.
02MVVM на iOS: что куда переезжает, как связать View и ViewModel и в чём честная критика паттерна?
middle
Короткий ответ: ViewModel забирает у вью-контроллера презентационную логику и состояние экрана: форматирование, флаги загрузки, реакцию на действия. VM не импортирует UIKit, поэтому тестируется как обычный объект. View лишь подписывается на VM и рисует её состояние.
Подробно:
View (VC / SwiftUI) ──действия──► ViewModel ──запросы──► Model
View ◄──биндинг (состояние)───── ViewModel ◄───данные─── Model
Варианты биндинга:
| Механизм | Где уместен |
|---|---|
| Замыкания / delegate | UIKit без реактивщины, минимум зависимостей |
Combine @Published |
UIKit + sink, SwiftUI через ObservableObject |
@Observable (iOS 17+) |
SwiftUI: гранулярные обновления без церемоний |
Честная критика:
- VM разбухают — massive view controller превращается в massive view model, если не выносить сервисы.
- Навигация бездомна — MVVM не отвечает, кто показывает следующий экран; обычно сверху добавляют координаторы.
- Церемония — на простых экранах биндинги дают больше кода, чем пользы.
⚠️ Частая ошибка: импортировать UIKit в ViewModel «на минутку». Как только там появляется UIColor или UIView, теряется главное преимущество — дешёвая тестируемость.
03VIPER и Clean Architecture: какие слои, куда смотрят зависимости и когда это overkill?
middle
Короткий ответ: VIPER — iOS-вариант Clean Architecture: View, Interactor, Presenter, Entity, Router — по модулю на экран. Ключевое правило — зависимости направлены внутрь, к бизнес-логике: Interactor и Entity не знают ни про UIKit, ни про то, как их показывают.
Подробно:
внешний слой внутренний слой
┌────────┐ ┌───────────┐ ┌────────────┐ ┌────────┐
│ View │◄─►│ Presenter │◄─►│ Interactor │──►│ Entity │
└────────┘ └─────┬─────┘ └────────────┘ └────────┘
▼
┌────────┐ зависимости ──► внутрь:
│ Router │ UI знает о логике, не наоборот
└────────┘
- Роли — View тупая: показывает и передаёт события; Presenter форматирует; Interactor — бизнес-логика без фреймворков; Router — навигация.
- Выигрыш — каждый слой тестируем изолированно; в большой мультикомандной кодовой базе с кодогенерацией модулей все экраны устроены одинаково.
- Цена — 4–5 файлов и стопка протоколов на каждый экран, медленный онбординг, простое изменение размазано по слоям. Для приложения на три экрана — overkill.
⚠️ Частая ошибка: фанатизм в обе стороны — «VIPER всегда» и «VIPER мёртв». Зрелый ответ привязывает выбор к размеру команды и кодовой базы, а не к моде.
04Координаторы: какую проблему они решают и кто кем владеет?
middle
Короткий ответ: Координатор выносит навигацию из вью-контроллеров: VC сообщает «пользователь выбрал товар», а какой экран показать дальше — решает координатор. Вью-контроллер перестаёт знать своих соседей и переиспользуется в любом флоу.
Подробно:
final class ProfileCoordinator: Coordinator {
var childCoordinators: [Coordinator] = []
var onFinish: (() -> Void)? // сигнал родителю
func start() { /* показать первый экран флоу */ }
func showSettings() {
let child = SettingsCoordinator(...)
childCoordinators.append(child) // родитель владеет ребёнком
child.onFinish = { [weak self, weak child] in
self?.childCoordinators
.removeAll { $0 === child } // точка освобождения
}
child.start()
}
}
- Владение — строго сверху вниз — родительский координатор держит детей в массиве
childCoordinators; ребёнок ссылается на родителя только слабо (или не ссылается вовсе). - Завершение — ребёнок сигналит через замыкание или delegate, родитель удаляет его из массива — только тогда флоу освобождается.
- Бонус — навигация становится тестируемой и меняется в одном месте: A/B-флоу, deeplink — без правок вью-контроллеров.
⚠️ Частая ошибка: классическая утечка — забыть удалить завершившийся дочерний координатор из childCoordinators: весь флоу вместе с его экранами остаётся жить в памяти.
05Однонаправленный поток данных в стиле TCA: что такое State, Action и Reducer, что это даёт и чего стоит?
senior
Короткий ответ: Вся правда экрана — одно значение State; изменить его можно только отправив Action; единственное место мутаций — чистая функция Reducer: (inout State, Action) → Effect. Побочные эффекты изолированы в Effect и возвращают результат новым экшеном.
Подробно:
┌─────────── Action ──────────┐
│ │
┌────▼────┐ мутирует ┌─────────┴───┐
│ Reducer │─────────────►│ State │
└────┬────┘ └──────┬──────┘
│ Effect (async) │ рендер
┌────▼────┐ ┌────▼───┐
│ Мир │ │ View │──► новые Action
└─────────┘ └────────┘
Что выигрываешь:
- Тестируемость — reducer чистый: подал State + Action, сравнил результат; никаких моков UIKit.
- Предсказуемость — одна точка мутаций; история экшенов даёт time-travel-отладку.
- Композиция — фича собирается из мелких reducer'ов.
Чем платишь: кривая обучения, boilerplate на экшены и внимание к производительности — большой State и частые экшены требуют аккуратного скоупинга. Контраст с MVVM: там состояние размазано по свойствам множества VM и мутируется откуда угодно.
⚠️ Частая ошибка: делать сетевой вызов прямо в reducer. Reducer обязан оставаться чистым — всё асинхронное живёт в Effect.
06Dependency injection на iOS: инициализатор, свойство или контейнер — и как DI включает тестирование?
middle
Короткий ответ: По умолчанию — инъекция через инициализатор: зависимость видна в сигнатуре, объект не существует без неё, и всё проверяет компилятор. Зависимость объявляется протоколом — в тестах вместо реальной подставляется двойник.
Подробно:
protocol ProfileService {
func loadProfile() async throws -> Profile
}
final class ProfileViewModel {
private let service: ProfileService
init(service: ProfileService) { // init-инъекция
self.service = service
}
}
// В тестах — двойник вместо сети:
struct MockProfileService: ProfileService {
var result: Profile
func loadProfile() async throws -> Profile { result }
}
- Инициализатор — дефолт: compile-time-гарантия, явный граф зависимостей, невозможно забыть зависимость.
- Свойство — когда init недоступен (сториборды); цена — объект какое-то время живёт полусобранным.
- Контейнер / service locator — удобен на большом графе, но зависимости исчезают из сигнатур: ошибка всплывает в рантайме, а не при сборке. SwiftUI Environment — тот же компромисс, но скоуп честно ограничен поддеревом вью.
⚠️ Частая ошибка: дергать синглтоны Service.shared внутри методов. Это скрытые зависимости: тест не может их подменить, а реальный граф связей невидим.
07Модуляризация через SPM: как распилить монолитное приложение и что дают границы модулей?
senior
Короткий ответ: Монолит режется на core-модули (Networking, DesignSystem, Models) и фичевые модули; фичи не импортируют друг друга — общаются через протоколы-интерфейсы. Взамен: быстрые инкрементальные сборки, тесты по модулю и границы владения, которые проверяет компилятор.
Подробно:
App (композиция + DI-граф)
/ | \
FeatureFeed FeatureProfile FeatureAuth
│ фичи не импортируют друг друга — │
│ только протоколы-интерфейсы │
\ | /
Networking · DesignSystem · Models ← core-слой
- Правило — фича не импортирует фичу: если Feed должен открыть Profile, он зовёт протокол, а реализацию подставляет App на композиции.
- Что покупают границы — инкрементальная сборка пересобирает только изменённый модуль; тесты гоняются на модуле без всего приложения;
internalпо умолчанию делает публичный API модуля осознанным; у каждого модуля есть владелец. - С чего начать — сначала листовые утилиты без зависимостей (Models, Extensions), затем core-сервисы, и только потом вертикали фич.
⚠️ Частая ошибка: резать по горизонтали на «слои» (все ViewModels в одном модуле). Границы должны идти по фичам — иначе любой экран тянет за собой весь граф.
08«Какую архитектуру вы выберете для нового приложения среднего размера и почему?» — что хочет услышать интервьюер?
concept
Короткий ответ: Не название, а рассуждение: выбор зависит от команды, стека и сложности навигации. Защищаемый дефолт — MVVM + координаторы на UIKit или SwiftUI + @Observable-модели + роутер, с DI через инициализаторы с первого дня.
Подробно:
| Контекст | Разумный выбор |
|---|---|
| Маленькая команда, SwiftUI | @Observable-модели + роутер, без церемоний |
| UIKit, сложные флоу | MVVM + координаторы |
| Много команд, один продукт | Clean/VIPER-подобные слои + модули SPM |
| State-heavy экран (редактор, плеер) | UDF/TCA локально, не на всё приложение |
- Критерии, которые стоит назвать вслух — размер и опыт команды, требования к тестируемости, сложность навигации, доля SwiftUI vs UIKit.
- Мета-ответ — консистентность и границы важнее акронима: одинаково устроенные экраны, зависимости через протоколы, бизнес-логика вне вью. Команда, которая одинаково пишет «средний» MVVM, обгонит команду с идеальной, но чужой всем архитектурой.
- Красный флаг для интервьюера — «всегда X»: догма вместо инженерного выбора.
⚠️ Частая ошибка: продавать самую модную архитектуру, которую команда не удержит. Архитектура, которой следуют все, лучше идеальной, которой следуют трое.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.