Irbisa · Шпаргалка

Архитектура iOS

MVC, MVVM, VIPER, Coordinators, TCA, Clean Swift

Middle-разработчик · 24 вопроса

  1. 01

    Что такое MVC в UIKit и почему он так часто превращается в Massive View Controller?

    В MVC от Apple контроллер связывает модель и view, но UIViewController ещё и владеет жизненным циклом своей view, поэтому вёрстка, состояние, сеть и навигация оседают в одном классе, который нельзя протестировать без view.

  2. 02

    Что такое внедрение зависимостей и как оно делает view model тестируемой?

    Внедрение зависимостей (dependency injection) означает, что объект получает свои зависимости снаружи, а не создаёт и не ищет их сам.

    protocol UserAPI: Sendable {  func user(id: String) async throws -> User} @MainActor @Observablefinal class ProfileViewModel {…
  3. 03

    Что такое MVVM, какую проблему он решает и как выглядит в UIKit и в SwiftUI?

    MVVM выносит логику представления и состояние экрана из view во view model, которая отдаёт готовые к показу значения; view только привязывается к ним и передаёт события пользователя, поэтому логику можно покрыть юнит-тестами без всякого UI.

    @MainActor @Observablefinal class ProfileViewModel {            // no import UIKit or SwiftUI  private(set) var name = ""  private let api: ProfileAPI  init(api: ProfileAPI) { self.api = api }…
  4. 04

    Найдите баг: если быстро набрать "iphone", в списке иногда остаются результаты для "iph".

    @MainActor @Observablefinal class SearchViewModel {  private(set) var results: [Product] = []  private let api: any ProductAPI & Sendable  init(api: any ProductAPI & Sendable) { self.api = api }   func queryChanged(_ query: String) {    Task {      let found = (try? await api.search(query)) ?? []      results = found    }  }}

    Каждое нажатие клавиши запускает свой Task, и старые никто не отменяет, поэтому побеждает ответ, который пришёл последним: медленный ответ для "iph" приходит после быстрого для "iphone" и перезаписывает results устаревшими данными.

  5. 05

    Делегат, замыкание, NotificationCenter, KVO, Combine или AsyncSequence: как выбрать способ общения двух объектов?

    Выбирайте по форме связи: несколько сообщений одному известному собеседнику отправляют через делегат, один результат возвращают async-вызовом, а многих неизвестных слушателей оповещают нотификацией или наблюдаемым значением.

    // Delegate: long-lived, several messages, one known counterpartprotocol BarcodeScannerDelegate: AnyObject {  func scanner(_ scanner: BarcodeScanner, didFind code: String)  func scannerDidFail(_ scanner: BarcodeScanner, error: Error)}final class BarcodeScanner {…
  6. 06

    Какие классические паттерны проектирования используют UIKit и Foundation и где их видно?

    В UIKit и Foundation собран каталог классических паттернов: делегат и target-action для колбэков, наблюдатель через NotificationCenter и KVO, синглтоны для общих сервисов приложения, компоновщик для дерева view и цепочка обязанностей для responder chain.

  7. 07

    Что такое принципы SOLID, с примером из iOS для каждого?

    SOLID объединяет пять правил, которые держат типы маленькими и заменяемыми: одна причина для изменения, расширение без правки, подтип соблюдает контракт, маленькие протоколы, зависимость от абстракций.

  8. 08

    Что такое паттерн Coordinator и когда он оправдан?

    Координатором называют объект, который владеет навигацией флоу: экраны сообщают о событиях вроде «логин прошёл», а координатор решает, что показать дальше, поэтому view controller-ы никогда не пушат и не презентуют друг друга.

    @MainActor protocol Coordinator: AnyObject {   // conforming classes inherit the isolation  func start()} final class AppCoordinator: Coordinator {  private var children: [Coordinator] = []…
  9. 09

    После каждого завершённого оформления заказа его CheckoutCoordinator остаётся в памяти. Что не так в этом коде?

    @MainActorfinal class AppCoordinator {  private var children: [AnyObject] = []  private let nav: UINavigationController  init(nav: UINavigationController) { self.nav = nav }   func showCheckout() {    let checkout = CheckoutCoordinator(nav: nav)    checkout.onFinish = {      self.nav.popToRootViewController(animated: true)    }    children.append(checkout)    checkout.start()  }}

    Дочерний координатор добавляется в children, но никогда оттуда не удаляется, поэтому родитель держит каждое завершённое оформление вместе с его view controller-ами и черновиком.

  10. 10

    Пуш-уведомление должно открыть конкретный комментарий к конкретному посту. Где живёт это состояние навигации?

    В одном наблюдаемом объекте-роутере выше навигационного контейнера: он хранит типизированный массив path, который меняют обработчик ссылок, обработчик уведомлений и сам UI.

    enum Route: Hashable {  case post(id: String)  case comment(postID: String, commentID: String)} @Observable…
  11. 11

    Что такое паттерн Repository и зачем держать DTO, доменные модели и состояние экрана отдельными типами?

    Репозиторий представляет собой протокол на доменное понятие, который прячет, откуда пришли данные (сеть, база, кэш), и отдаёт доменные типы.

    // The wire format: pessimistic, matches the server exactlystruct PostDTO: Decodable {  let id: Int  let author_name: String?  let created_at: String  let status: String            // never an enum here…
  12. 12

    Когда DI-контейнер вроде Swinject или Factory оправдан по сравнению с обычным внедрением через конструктор?

    Редко: внедрение через конструктор из composition root проверяется компилятором и достаточно для большинства приложений.

    // wrong: a runtime registry; a missing line fails only when the code runslet container = Container()container.register(AuthService.self) { _ in LiveAuthService() }let auth = container.resolve(AuthService.self)!   // crash in production // right: the initialiser states the requirement; the compiler enforces it…
  13. 13

    Найдите баг: testFreshInstallShowsOnboarding проходит отдельно на чистом симуляторе, но падает после прогона всего класса, и дальше падает всегда.

    @MainActor final class OnboardingViewModel {  private let defaults: UserDefaults  init(defaults: UserDefaults = .standard) { self.defaults = defaults }  var showsOnboarding: Bool { !defaults.bool(forKey: "onboardingDone") }  func finish() { defaults.set(true, forKey: "onboardingDone") }}@MainActor final class OnboardingTests: XCTestCase {  func testFreshInstallShowsOnboarding() {    XCTAssertTrue(OnboardingViewModel().showsOnboarding)  }  func testFinishHidesOnboarding() {    let model = OnboardingViewModel()    model.finish()    XCTAssertFalse(model.showsOnboarding)  }}

    Оба теста используют UserDefaults.standard по умолчанию, а он сохраняется на диск.

  14. 14

    Как VIPER и Clean Swift (VIP) делят экран и что даёт эта церемония?

    Оба делят экран на объекты с одной обязанностью, связанные протоколами (VIPER на View, Interactor, Presenter, Entity и Router, Clean Swift на односторонний цикл ViewController → Interactor → Presenter), и дают тестируемые швы и единообразную форму ценой примерно пяти файлов на экран.

    // Clean Swift scene: VC -> Interactor -> Presenter -> VC, one direction only@MainActor protocol OrderBusinessLogic { func fetchOrder(request: Order.Fetch.Request) }protocol OrderPresentationLogic { func presentOrder(response: Order.Fetch.Response) }protocol OrderDisplayLogic: AnyObject { func displayOrder(viewModel: Order.Fetch.ViewModel) }@MainActor protocol OrderDataStore { var order: OrderEntity? { get } }protocol OrderLoading { func load(id: String) async throws -> OrderEntity }…
  15. 15

    Что такое feature flags и как с их помощью безопасно выкатить рискованное изменение?

    Feature flag работает как переключатель во время выполнения, который решает, активен ли код, уже лежащий в бинарнике.

  16. 16

    Сравните MVC, MVP, MVVM, VIPER и TCA: как выбрать архитектуру для фичи или команды?

    Они различаются тем, где живёт логика, не относящаяся к view, и насколько строго течут данные: MVC держит её в контроллере, MVP и MVVM выносят в тестируемый объект, VIPER дробит дальше, TCA проводит каждое изменение через reducer.

  17. 17

    Что такое однонаправленный поток данных и какие проблемы решает с его помощью TCA?

    Однонаправленный поток данных (unidirectional data flow) означает, что состояние меняется только в одну сторону: view отправляет action, reducer вычисляет новое состояние, view его отрисовывает.

    import ComposableArchitecture struct NumberFactClient { var fetch: @Sendable (Int) async throws -> String }extension NumberFactClient: DependencyKey {  static let liveValue = NumberFactClient { "\($0) is a number" }}…
  18. 18

    Что такое Clean Architecture в iOS и когда слой use case окупается?

    Clean Architecture раскладывает код по кольцам (в центре домен, вокруг данные и представление), и все зависимости направлены внутрь, поэтому бизнес-правила ничего не знают об URLSession, SwiftData или SwiftUI и тестируются отдельно.

    // Domain: plain Swift, no framework importsstruct Article: Identifiable, Equatable, Sendable {  let id: String  let title: String}…
  19. 19

    Как структурировать большое iOS-приложение (по фичам или по слоям) и где проводить границы модулей?

    Сначала делите по фичам, а внутри фичи уже по слоям: локальный Swift-пакет на каждую фичу поверх общих модулей Core и DesignSystem и тонкий app-таргет, который только связывает их между собой.

    // swift-tools-version: 6.0import PackageDescription let package = Package(  name: "Profile",  platforms: [.iOS(.v17)],…
  20. 20

    Оформление заказа из трёх шагов делит один черновик, и пользователи жалуются, что новый заказ открывается с адресом из предыдущего. Где должен жить этот черновик?

    В объекте, которым владеет сам флоу: он создаётся при старте флоу и освобождается при его завершении.

    // wrong: app-wide lifetime, so the next checkout inherits the last onefinal class CheckoutDraft { static let shared = CheckoutDraft() } // right: the draft's lifetime is the flow's lifetime@Observablefinal class CheckoutDraft {…
  21. 21

    Вы добавляете SwiftUI в десятилетнее UIKit-приложение. Где проводить границу?

    Внедряйте SwiftUI целыми экранами через UIHostingController, чтобы шов проходил по границе навигации, а не внутри вью.

    // UIKit pushes a SwiftUI screen; the coordinator still owns navigation@MainActorfinal class SettingsCoordinator {  private let nav: UINavigationController  private let session: SessionStore          // @Observable, shared by both worlds…
  22. 22

    В приложении 300 тысяч строк на view controller-ах, а фичи останавливать нельзя. Как прийти к тестируемой архитектуре?

    Рефакторьте по швам, которых вы и так касаетесь: новое пишется в целевой архитектуре, а старые экраны меняются только когда в них приходит задача.

    // before: untestable: the dependency is fetched, not receivedfinal class FeedViewController: UIViewController {  private var posts: [Post] = []   func load() {    Task { posts = (try? await NetworkManager.shared.feed()) ?? [] }…
  23. 23

    Как бы вы спроектировали конвейер загрузки и кэширования картинок для прокручиваемой ленты?

    По слоям: кэш в памяти для декодированных и уменьшенных картинок, кэш на диске для исходных байтов и загрузчик, который склеивает одинаковые запросы, декодирует вне главного потока и отменяет работу для ячеек, ушедших с экрана.

  24. 24

    Как бы вы спроектировали offline-first синхронизацию для приложения заметок, включая разрешение конфликтов?

    Сделайте локальную базу источником истины: UI читает и пишет только локально, каждое изменение дополнительно ставится в очередь исходящих (outbox), а движок синхронизации, когда может, отправляет outbox и забирает изменения с сервера, разрешая конфликты по заранее выбранному правилу.