Использование EnvironmentValues для простого реагирования на события, генерируемые во всей иерархии представлений.

За последние 10 лет я провел много времени, отвечая на вопросы в StackOverflow, и вопрос, который я очень часто вижу, имеет привкус:

Как вызвать событие в классе A из функции класса B?

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

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

Введите цепочки ответчиков

Цепочка респондентов — это шаблон проектирования, в котором объекты респондентов образуют «цепочку». События генерируются в одном из «звеньев» или «узлов» цепочки, и узел определяет, может ли он обработать событие или нет. Если он не может обработать событие, оно отправляется следующему узлу в цепочке. Этот процесс продолжается до тех пор, пока узел не сможет обработать событие или не будет достигнут конец цепочки.

Отношение object-delegate является тривиальным примером этого шаблона. object создает событие, с которым не может справиться сам, поэтому событие отправляется следующему узлу в цепочке, то есть delegate. Более сложный пример — цепочка UIResponder.

Явные цепочки респондентов не слишком распространены в разработке iOS, вероятно, потому, что большинство основных строительных блоков UIKit, таких как UIViewController и его подклассы, очень легко построить без них.

Однако в SwiftUI каждый объект связан с корневым представлением через иерархию представлений, а это означает, что общая структура, необходимая для построения цепочки респондентов, уже создана, все, что нам нужно добавить, это респонденты и события.

Иерархия представления SwiftUI

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

Чтобы сделать вещи немного более наглядными, это иерархия, которая будет сгенерирована из этого простого представления. Обратите внимание, как модификаторы, такие какforegroundColor, создают узел, являющийся родителем изменяемого ими представления.

Среда SwiftUI

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

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

Создание ответчика

Ранее мы упоминали, что ответчик несет ответственность за определение того, может ли он обработать событие или нет. Наши представления будут регистрироваться как респонденты, регистрируя свои собственные eventClosure в среде.

Любые дочерние представления, вызывающие eventClosure, будут запускать обработчик нашего представления, если его не заменило промежуточное представление. Если наше представление определяет, что оно не может обработать событие, оно может использовать замыкание, прочитанное из среды, которое будет получено от родительского ответчика, чтобы позволить событию продолжиться по цепочке.

Поскольку это будет общий шаблон, мы создадим ViewModifier, который будет инкапсулировать эту функциональность.

Наше новое замыкание handler имеет возвращаемый тип Any?, потому что именно так мы собираемся определить, было ли событие обработано или нет. Если возвращенное значение равно nil, событие будет считаться обработанным, если возвращенное значение будет любым другим, это значение будет отправлено вверх по цепочке.

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

Отправка события

Инициировать событие намного проще, все, что нам нужно сделать, это прочитать eventClosure из среды и вызвать его с любым значением.

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

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

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

Масштабируемость и ремонтопригодность

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

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

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

Создание фреймворка

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

Эта структура берет эту концепцию и расширяет ее, добавляя определенные модификаторы для получения, обработки или преобразования события или ошибки, а также для перехвата ошибок.

  1. Получение события или ошибки позволяет решить, было ли событие обработано или нет.
  2. Обработка события или ошибки означает, что они будут использованы, никаких других вариантов.
  3. Модификаторы преобразования позволяют заменить полученное событие или ошибку другим значением.

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