Я пытаюсь объяснить архитектуру моего любимого приложения.

Идеал

Идеальная архитектура для интерактивного приложения — цикл реального времени. Приложение продолжает работать независимо от любого ввода со скоростью 60 кадров в секунду или более. В этом случае приложение становится простым REPL, и событие вообще не требуется, по крайней мере, на уровне ядра.

Эта архитектура подходит для некоторых приложений. Обычно (жесткие/твердые/мягкие) приложения реального времени, такие как игры. Если ваше приложение постоянно анимировано, лучше всего выбрать архитектуру REPL с постоянным циклом. В этом случае вы просто пишете REPL, и он становится FRP. FRP действительно прекрасен… во всяком случае, он просто недоступен без петли 60 кадров в секунду.

Но для большинства других приложений — особенно неигровых приложений для iOS — мы не можем свободно наслаждаться этой роскошью. Потому что мы должны экономить батарею и другие системные ресурсы, и мы не можем оправдать цикл 60 кадров в секунду даже в режиме ожидания. Там, где мы не можем постоянно запускать цикл со скоростью 60 кадров в секунду, мы не можем перехватывать и обрабатывать входные сигналы в режиме реального времени без событий. Источник всей боли в заднице.

Лучшее усилие

Есть много испытаний, чтобы преодолеть эти ограничения. MVC, MVVM, Viper, Flux, Redux, FRP, … Их идеи интересны, и все они заслуживают внимания. На мой взгляд, архитектура, подобная Flux/Redux или основанная на ней, наиболее надежна и лучше всего масштабируется. Я также использую некоторые подходы MVVM в некоторых компонентах представления. FRP интересен, но я не думаю, что он может охватывать базовую архитектуру iOS-приложения. То же самое для реактивного программирования. Это интересно и очень полезно, если вам нужно управлять очень сложным многопоточным вводом-выводом. Но я не вижу в них большой ценности для приложений iOS. Он не предлагает больше возможностей, чем хорошо сделанные причудливые библиотеки расширений.

Во-первых, есть центральный контроллер под названием Driver. Это верхний контейнер, обеспечивающий некоторую связь со средой UIKit. AppDelegate владеет объектом Driver между запуском приложения и его завершением.

Driver содержит DriverState и несколько объектов -Feature. Каждый объект -Feature отвечает за одну функцию. Все они являются компонентами ввода/вывода. Это означает, что все они принимают входящие сообщения и отправляют исходящие сообщения. Я называю их Control(входящее сообщение) и Note(исходящее сообщение). Driver наблюдает за всеми функциями своих заметок и выполняет правильную обработку всех сообщений заметок, а также снова отправляет элементы управления соответствующим функциям. На самом деле это REPL. Просто REPL с ручной итерацией цикла на входном сигнале вместо постоянного цикла 60 кадров в секунду.

Вот очень важное правило. REPL LOOP НЕ ДОЛЖЕН ПОВТОРЯТЬСЯ! Повторный вход — это самая большая ловушка, которую вы должны избегать.

Опять же, REPL LOOP НЕ ДОЛЖЕН ПОВТОРЯТЬСЯ!

Потому что, если повторится одна итерация REPL, это уже не будет единым циклом. По логике, может произойти какая-то странная ситуация, например, запись в память во время записи в память. Бессмысленно, но повторный вход может легко вызвать такую ​​​​проблему, и его следует избегать.

Обычно повторного входа можно легко избежать, если не запускать релейные сигналы в конце итерации REPL. Сигналы пожарной ретрансляции только сигналами завершения ввода-вывода или вводом конечного пользователя.

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

Точки

Вот точки.

  • Центральный контроллер Driver сохраняет центральный DriverState и несколько компонентов ввода/вывода.
  • DriverState — чистое значение. Ссылки нет.
  • DriverState собирать и хранить всю информацию о приложении. Поскольку вся информация находится в одном месте, легко принять решение и вывести новое состояние.
  • Driver управляет обменом сообщениями между компонентами ввода-вывода.
  • Компоненты ввода-вывода взаимодействуют только посредством сообщений.
  • DriverState можно передать как есть каждому компоненту ввода-вывода, если это необходимо.
  • Каждый компонент ввода/вывода определяет входящие/исходящие сообщения, называемые Control и Note.
  • Каждый компонент ввода/вывода принимает Control сообщений и отправляет Note сообщений.
  • Driver никогда не выполняйте асинхронную операцию. Вместо этого Driver отправьте правильные Control сообщения соответствующим компонентам ввода-вывода, чтобы заставить их работать.
  • Связь с каждым компонентом ввода-вывода всегда является двунаправленным потоком сигналов. Нет концепции запроса/ответа, потому что это слишком сложно, чтобы иметь дело с таймингами.
  • Компоненты ввода-вывода не взаимодействуют напрямую. Driver всегда обрабатывает свои Note сообщения и отправляет Control сообщений компонентам ввода/вывода.

Что насчет пользовательского интерфейса? Пользовательский интерфейс — это просто компонент ввода-вывода. Пользовательский интерфейс — это просто компонент ввода-вывода, который выполняет ввод-вывод между приложением и пользователем. Единственная разница в том, что это ввод-вывод для человека, а не для машины. Они размещены в объекте Driver рядом с другими компонентами ввода-вывода. Они принимают управляющее сообщение .render с DriverState. По сути, все компоненты пользовательского интерфейса должны всегда выполнять полный повторный рендеринг состояния приложения. Концепция здесь заключается в преобразовании состояния в состояние, и вы можете (и должны) оптимизировать рендеринг с помощью дополнительной контекстной информации.

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

МВВМ

Представление на уровне листа реализовано в MVVM. Все они определяют некоторое состояние как свою модель представления и визуализируют себя, визуализируя состояние.