BLoC в SwiftUI

С выходом Apple SwiftUI и растущей популярностью Flutter и Jetpack Compose кажется, что пользовательский интерфейс для наших мобильных приложений в будущем станет в основном декларативным. Пока я трепался, я наткнулся на интересную архитектурную шаблон, известный в сообществе Flutter под названием шаблон BloC, и мне он понравился в первую очередь потому, что мне понравилась концепция документирования всех возможных состояний представления и событий как типов. Поэтому я хотел посмотреть, как это будет выглядеть адаптированным в SwiftUI. Текст ниже показывает мою адаптацию к этому шаблону в SwiftUI.

Базовый обзор BLoC

Основная цель архитектуры BLoC - отделить бизнес-логику от пользовательского интерфейса. Он делает это, выступая в роли посредника между нашим представлением и источником данных, предоставляя компонент Б использования Логического C. Поскольку это шаблон, разработанный для удовлетворения потребностей декларативного программирования пользовательского интерфейса, вполне естественно, что он взаимодействует с представлением посредством потоков. Каждый раз, когда происходит новое событие, оно опускается в BLoC, и каждый раз, когда происходит обновление состояния представления, BLoC передает его в представление.

Основную суть можно увидеть на диаграмме ниже:

Обзор приложения-витрины

Чтобы продемонстрировать принятие этой архитектуры, я сделал небольшое демонстрационное приложение, которое пытается адаптировать архитектуру BLoC в SwiftUI (полный исходный код можно найти по адресу https://github.com/BozidarK93/SwiftUI-BLoC-Pattern) . На гифке ниже показано базовое использование приложения.

Итак, как мы видим, само приложение позволяет пользователю вводить название города и получать пивоварни в указанном районе города (спасибо https://www.openbrewerydb.org за предоставление этого классного API).

Уровень данных

Сначала давайте начнем с получения данных, необходимых для представления. Как видно выше, мы стремимся отобразить список пивоварен в определенной области. Итак, давайте начнем с создания простой модели под названием Brewery:

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

А теперь давайте создадим реальную реализацию, в которой мы будем вызывать наш API. Обратите внимание, что основное внимание приложения уделяется демонстрации использования шаблона BloC, поэтому подход сетевого вызова является довольно примитивным. Но здесь мы могли бы использовать некоторую реализацию клиента API. На эту тему есть несколько полезных статей, например: https://medium.com/better-programming/upgrade-your-swift-api-client-with-combine-4897d6e408a0

Просмотр состояний

Теперь, когда у нас есть данные, перейдем к слою просмотра. Первое, что нам нужно сделать, это определить все возможные состояния, в которых может отображаться представление. Что мне нравится в этом шаблоне, так это то, что он позволяет нам документировать все возможные состояния представления как тип, который упрощает понимание пользовательского интерфейса и всего состояний, в которых он может отображаться. С другой стороны, это также может быть "спагетти", и если мы не будем осторожны, мы можем закончить и просто сбрасывать множество состояний представления в один тип и тем самым создавать путаницу.

Что круто в Swift, так это то, что он позволяет нашим перечислениям иметь связанные значения, поэтому мы можем просто использовать перечисления напрямую при определении состояний нашего представления, передавая любые данные представления в качестве связанного значения. Еще одна хорошая вещь заключается в том, что при использовании этого подхода мы вынуждены определять представление для каждого состояния, поскольку переключение перечислений является исчерпывающим.

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

Просмотр событий

Теперь, когда мы определили состояния представления, давайте также определим события представления, на которые будет реагировать пользовательский интерфейс. Итак, в нашем пользовательском интерфейсе могут произойти два события. Пользователь может указать название города, после которого мы будем вызывать наш API, и другое событие, которое может произойти, - это проверка того, изменилось ли текстовое поле. Имея это в виду, наши события будут представлены следующим образом:

BLoC

После определения состояний представления и событий мы можем создать наш BLoC. Сначала давайте начнем с создания наших потоков просмотра. Сначала нам нужно определить наш класс блока как ObservableObject, это позволит нам отображать представление всякий раз, когда на его издателях происходит изменение. Поскольку мы хотим публиковать изменения при изменении состояния представления, мы создаем состояние @Publisher. Что касается событий, давайте создадим наблюдателя свойств для нашего перечисления событий, чтобы при каждом изменении пользовательского интерфейса мы выполняли соответствующее действие.

Теперь давайте реализуем наш метод handleViewEvent, который реализует соответствующую логику представления. Мы хотим обработать два ранее определенных события: когда город отправлен, мы хотим получить данные и установить соответствующие состояния. Еще одно событие, которое нам нужно обработать, - это каждый раз, когда вводится текст, мы проверяем, является ли ввод числовым, чтобы обеспечить базовую проверку на стороне клиента.

Объявление представления для разных состояний

Теперь, когда мы написали нашу логику представления и получили соответствующие данные, мы можем объявить фактический пользовательский интерфейс для каждого состояния.

Как мы видим, мы определяем наш блок как @StateObject, чтобы всякий раз, когда происходит изменение в нашем издателе состояния, мы могли визуализировать представление с соответствующими состояниями представления. В нашем теле у нас просто есть оператор switch, который проходит через все возможные состояния, и мы можем объявить представление, которое мы хотим для каждого состояния.

Превью

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

Тестирование снимков

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

Модульное тестирование

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

Вот и все. Если вы хотите увидеть полный исходный код и попытаться немного поиграться с ним, вы можете проверить репо и сообщить мне, что вы думаете, в комментариях =): https://github.com/BozidarK93/SwiftUI- BLoC-шаблон