Перейти к содержанию
Мобильная разработка

8 вопросов по теме «iOS: Архитектура» на собеседовании

В этом материале — 8 вопросов из русской колоды RecallDeck по теме «iOS: Архитектура». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

7 мин чтения8 подробных ответовПроверено 24 августа 2026
Главная мысль

Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.

Вопросы и ответы

8 подробных ответов

01

Почему Apple-овский MVC превращается в Massive View Controller и как разгрузить вью-контроллер, не меняя архитектуру?

Короткий ответ: Потому что в UIKit вью-контроллер по умолчанию — точка сборки всего: жизненный цикл вью, лейаут, data source таблицы, навигация и сетевые вызовы. Разгружается он выносом обязанностей в отдельные объекты — менять архитектуру для этого не обязательно.

Подробно:

  1. Диагноз — MVC у Apple не плох сам по себе; проблема в том, что «C» удобно принимает любой код, и без дисциплины вью-контроллер растёт до тысяч строк.
  2. Что выносить:
Обязанность Куда выносится
UITableViewDataSource / delegate отдельный объект-датасорс
Кусок экрана со своей логикой child view controller
Сеть, кэш, аналитика простые сервисные объекты
Конфигурация сабвью сами вью (configure(with:))
  1. Итог — вью-контроллер остаётся дирижёром: связывает сервисы, вью и навигацию, но не делает всю работу сам.

⚠️ Частая ошибка: отвечать «MVC — плохая архитектура, поэтому все ушли в MVVM». Плоха недисциплинированная реализация: без выноса обязанностей тот же перегруз повторится и с ViewModel.

02

MVVM на iOS: что куда переезжает, как связать View и ViewModel и в чём честная критика паттерна?

Короткий ответ: 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: гранулярные обновления без церемоний

Честная критика:

  1. VM разбухают — massive view controller превращается в massive view model, если не выносить сервисы.
  2. Навигация бездомна — MVVM не отвечает, кто показывает следующий экран; обычно сверху добавляют координаторы.
  3. Церемония — на простых экранах биндинги дают больше кода, чем пользы.

⚠️ Частая ошибка: импортировать UIKit в ViewModel «на минутку». Как только там появляется UIColor или UIView, теряется главное преимущество — дешёвая тестируемость.

03

VIPER и Clean Architecture: какие слои, куда смотрят зависимости и когда это overkill?

Короткий ответ: VIPER — iOS-вариант Clean Architecture: View, Interactor, Presenter, Entity, Router — по модулю на экран. Ключевое правило — зависимости направлены внутрь, к бизнес-логике: Interactor и Entity не знают ни про UIKit, ни про то, как их показывают.

Подробно:

 внешний слой                       внутренний слой
┌────────┐   ┌───────────┐   ┌────────────┐   ┌────────┐
│  View  │◄─►│ Presenter │◄─►│ Interactor │──►│ Entity │
└────────┘   └─────┬─────┘   └────────────┘   └────────┘

              ┌────────┐   зависимости ──► внутрь:
              │ Router │   UI знает о логике, не наоборот
              └────────┘
  1. Роли — View тупая: показывает и передаёт события; Presenter форматирует; Interactor — бизнес-логика без фреймворков; Router — навигация.
  2. Выигрыш — каждый слой тестируем изолированно; в большой мультикомандной кодовой базе с кодогенерацией модулей все экраны устроены одинаково.
  3. Цена — 4–5 файлов и стопка протоколов на каждый экран, медленный онбординг, простое изменение размазано по слоям. Для приложения на три экрана — overkill.

⚠️ Частая ошибка: фанатизм в обе стороны — «VIPER всегда» и «VIPER мёртв». Зрелый ответ привязывает выбор к размеру команды и кодовой базы, а не к моде.

04

Координаторы: какую проблему они решают и кто кем владеет?

Короткий ответ: Координатор выносит навигацию из вью-контроллеров: 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()
    }
}
  1. Владение — строго сверху вниз — родительский координатор держит детей в массиве childCoordinators; ребёнок ссылается на родителя только слабо (или не ссылается вовсе).
  2. Завершение — ребёнок сигналит через замыкание или delegate, родитель удаляет его из массива — только тогда флоу освобождается.
  3. Бонус — навигация становится тестируемой и меняется в одном месте: A/B-флоу, deeplink — без правок вью-контроллеров.

⚠️ Частая ошибка: классическая утечка — забыть удалить завершившийся дочерний координатор из childCoordinators: весь флоу вместе с его экранами остаётся жить в памяти.

05

Однонаправленный поток данных в стиле TCA: что такое State, Action и Reducer, что это даёт и чего стоит?

Короткий ответ: Вся правда экрана — одно значение State; изменить его можно только отправив Action; единственное место мутаций — чистая функция Reducer: (inout State, Action) → Effect. Побочные эффекты изолированы в Effect и возвращают результат новым экшеном.

Подробно:

        ┌─────────── Action ──────────┐
        │                             │
   ┌────▼────┐   мутирует   ┌─────────┴───┐
   │ Reducer │─────────────►│    State    │
   └────┬────┘              └──────┬──────┘
        │ Effect (async)           │ рендер
   ┌────▼────┐                ┌────▼───┐
   │   Мир   │                │  View  │──► новые Action
   └─────────┘                └────────┘

Что выигрываешь:

  1. Тестируемость — reducer чистый: подал State + Action, сравнил результат; никаких моков UIKit.
  2. Предсказуемость — одна точка мутаций; история экшенов даёт time-travel-отладку.
  3. Композиция — фича собирается из мелких reducer'ов.

Чем платишь: кривая обучения, boilerplate на экшены и внимание к производительности — большой State и частые экшены требуют аккуратного скоупинга. Контраст с MVVM: там состояние размазано по свойствам множества VM и мутируется откуда угодно.

⚠️ Частая ошибка: делать сетевой вызов прямо в reducer. Reducer обязан оставаться чистым — всё асинхронное живёт в Effect.

06

Dependency injection на iOS: инициализатор, свойство или контейнер — и как DI включает тестирование?

Короткий ответ: По умолчанию — инъекция через инициализатор: зависимость видна в сигнатуре, объект не существует без неё, и всё проверяет компилятор. Зависимость объявляется протоколом — в тестах вместо реальной подставляется двойник.

Подробно:

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 }
}
  1. Инициализатор — дефолт: compile-time-гарантия, явный граф зависимостей, невозможно забыть зависимость.
  2. Свойство — когда init недоступен (сториборды); цена — объект какое-то время живёт полусобранным.
  3. Контейнер / service locator — удобен на большом графе, но зависимости исчезают из сигнатур: ошибка всплывает в рантайме, а не при сборке. SwiftUI Environment — тот же компромисс, но скоуп честно ограничен поддеревом вью.

⚠️ Частая ошибка: дергать синглтоны Service.shared внутри методов. Это скрытые зависимости: тест не может их подменить, а реальный граф связей невидим.

07

Модуляризация через SPM: как распилить монолитное приложение и что дают границы модулей?

Короткий ответ: Монолит режется на core-модули (Networking, DesignSystem, Models) и фичевые модули; фичи не импортируют друг друга — общаются через протоколы-интерфейсы. Взамен: быстрые инкрементальные сборки, тесты по модулю и границы владения, которые проверяет компилятор.

Подробно:

              App (композиция + DI-граф)
             /           |            \
     FeatureFeed   FeatureProfile   FeatureAuth
        │  фичи не импортируют друг друга —  │
        │  только протоколы-интерфейсы       │
             \           |            /
      Networking · DesignSystem · Models   ← core-слой
  1. Правило — фича не импортирует фичу: если Feed должен открыть Profile, он зовёт протокол, а реализацию подставляет App на композиции.
  2. Что покупают границы — инкрементальная сборка пересобирает только изменённый модуль; тесты гоняются на модуле без всего приложения; internal по умолчанию делает публичный API модуля осознанным; у каждого модуля есть владелец.
  3. С чего начать — сначала листовые утилиты без зависимостей (Models, Extensions), затем core-сервисы, и только потом вертикали фич.

⚠️ Частая ошибка: резать по горизонтали на «слои» (все ViewModels в одном модуле). Границы должны идти по фичам — иначе любой экран тянет за собой весь граф.

08

«Какую архитектуру вы выберете для нового приложения среднего размера и почему?» — что хочет услышать интервьюер?

Короткий ответ: Не название, а рассуждение: выбор зависит от команды, стека и сложности навигации. Защищаемый дефолт — MVVM + координаторы на UIKit или SwiftUI + @Observable-модели + роутер, с DI через инициализаторы с первого дня.

Подробно:

Контекст Разумный выбор
Маленькая команда, SwiftUI @Observable-модели + роутер, без церемоний
UIKit, сложные флоу MVVM + координаторы
Много команд, один продукт Clean/VIPER-подобные слои + модули SPM
State-heavy экран (редактор, плеер) UDF/TCA локально, не на всё приложение
  1. Критерии, которые стоит назвать вслух — размер и опыт команды, требования к тестируемости, сложность навигации, доля SwiftUI vs UIKit.
  2. Мета-ответ — консистентность и границы важнее акронима: одинаково устроенные экраны, зависимости через протоколы, бизнес-логика вне вью. Команда, которая одинаково пишет «средний» MVVM, обгонит команду с идеальной, но чужой всем архитектурой.
  3. Красный флаг для интервьюера — «всегда X»: догма вместо инженерного выбора.

⚠️ Частая ошибка: продавать самую модную архитектуру, которую команда не удержит. Архитектура, которой следуют все, лучше идеальной, которой следуют трое.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Мобильная разработка8 мин

10 вопросов по теме «Android: Компоненты и жизненный цикл» на собеседовании

10 вопросов по теме «Android: Компоненты и жизненный цикл» с подробными ответами, примерами, компромиссами и частыми ошибками кандидатов на техническом интервью.

10 подробных ответов
Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS