Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mvvm-example

Сопровождение к докладу «MVVM, про которую не пишут в туториалах» — пример модуля на MVVM-архитектуре iOS-приложений hh.ru, который можно открыть, запустить и потрогать.

Это упрощённая реконструкция, а не наш продакшн-код. Настоящий базовый слой не в опенсорсе, поэтому здесь он воспроизведён мини-слоем поверх нативных iOS 17 API: та же механика, те же имена, малая доля объёма. Отличия перечислены ниже.

Скелет модуля — схема из доклада:

Скелет модуля: Screen собирает Router, ViewModel и View; ViewModel держит State; View читает State и возвращает ViewBody, по которому ViewController собирает экран

Состав:

  • Sources/Module — мини-слой, повторяющий механику базового слоя (в монорепозитории — фича Module): 17 файлов, ~600 строк.
  • Sources/Example — экран из доклада «Почему скрываете?»: модуль из 6 файлов и упрощённые компоненты вместо дизайн-системы.
  • Tests/ModuleTests — тесты на утверждения доклада о механизме обновления экрана.

Как запустить

Нужен Xcode 15.3 или новее (iOS 17+). App-таргета нет — экран запускается превью:

  1. Открыть Package.swift в Xcode.
  2. Открыть Sources/Example/Preview/BlacklistedReasonPreview.swift.
  3. Показать Canvas (⌥⌘↩) — экран откроется шторкой, как в приложении.

Проект в Xcode: модуль из шести файлов и экран в Canvas

Тесты: схема mvvm-example-Package, ⌘U.

Что говорится в докладе — и где это в коде

Утверждение Где смотреть
Состояние экрана — одна структура; фокус поля ввода лежит рядом с бизнес-полями ApplicantBlacklistedReasonState.swift
Вью — функция от состояния: читает состояние, возвращает описание экрана из трёх слотов ApplicantBlacklistedReasonView.swift, ModuleViewBody.swift
Экран обновляется сам: базовый слой записывает, какие свойства вью прочитала, и перерисовывает только при их изменении ModuleViewManager.swift + тест test_changeOfPropertyNotReadByView_causesNoViewBodyUpdate
Несколько изменений за один цикл RunLoop схлопываются в одну перерисовку наблюдатель beforeWaiting в ModuleViewManager.swift + тест test_fiveChangesInOneRunLoopCycle_causeSingleViewBodyUpdate
Зависимости записываются заново при каждом пересчёте — экран может начать зависеть от новых свойств тест test_dependenciesAreRecordedAgain_onEveryViewBodyUpdate
Двусторонняя связь: TextArea(value: $model.state.anotherReasonText) — ни одной строки синхронизации ApplicantBlacklistedReasonView.swift, ViewBindable.swift + тест test_viewBinding_writesThroughToState_andTriggersViewBodyUpdate
Роутер — строчка объявления и список возможностей, подключаемых протоколами ApplicantBlacklistedReasonRouter.swift, ScreenCancelling.swift
Шесть файлов модуля Sources/Example/Modules/BlacklistedReason/
Базовый слой на UIKit, компоненты на SwiftUI ModuleViewController.swift — контроллер собирает экран по описанию, слоты рендерит SwiftUI

Ни одной подписки руками: в модуле нет ни Combine, ни делегатов, ни renderState — перерисовку ведёт базовый слой.

Чем это отличается от продакшн-кода

  • Объём. Настоящий базовый слой — 70 файлов, ~4300 строк: контейнерные модули (Pages/Stack/Tabs), вложенные сабмодули, аналитика показа экрана, жизненный цикл из пятнадцати методов, свой список поверх UICollectionView с быстрым диффом. Здесь — только механика, о которой рассказывает доклад.
  • Наблюдение. В монорепозитории — Perception, бэкпорт Observation на старые iOS (@Perceptible, withPerceptionTracking). Здесь — нативный Observation iOS 17 (@Observable, withObservationTracking). Механика та же.
  • Вёрстка. В монорепозитории компоненты — дизайн-система Magritte, написанная инструментами Levitan (декларативный слой над UIKit и SwiftUI); контент экрана — надстройка над UICollectionView. Здесь слоты описания рендерятся SwiftUI через UIHostingController, дизайн-система не воспроизводится. @ViewBindable и ViewBinding в проде определены в Levitan.
  • Навигация. В монорепозитории — Nivelir (DSL для навигации по экранам) и фабрики экранов через DI. Здесь роутер держит слабую ссылку на контроллер, ScreenCancelling закрывает экран модально, ScreenFactory пустая.
  • Вью-модель примера — без аналитики и сервисов: в проде onSendTap отмечает вакансию скрытой через сервис и трекает события.

Структура

Sources/
  Module/                       # мини-слой: механика базового слоя
    ModuleScreen.swift          # точка сборки модуля
    ModuleView.swift            # вью — функция от состояния
    ModuleViewModel.swift       # база вью-модели
    ModuleViewManager.swift     # трекинг чтений + коалесценция перерисовок
    ModuleViewController.swift  # собирает экран по описанию
    ViewBody/                   # описание экрана: три слота и настройки
    Binding/                    # @ViewBindable и ViewBinding
    Routing/                    # ModuleRouter и ScreenCancelling
  Example/
    Modules/BlacklistedReason/  # модуль экрана — 6 файлов
    Models/                     # доменная модель причин
    Components/                 # упрощённые компоненты вместо дизайн-системы
    DI/                         # ScreenFactory
    Preview/                    # запуск экрана в превью
Tests/
  ModuleTests/                  # тесты механизма обновления

About

Пример MVVM-модуля из доклада «MVVM, про которую не пишут в туториалах»

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages