Перевод подготовлен в рамках набора на курс "iOS Developer. Professional".
Всех желающих приглашаем на открытый демо-урок Machine Learning в iOS с помощью CoreML и CreateML: изображения, текст, звук. На занятии обсудим:
1. Основные архитектуры нейронных сетей и их оптимизированные версии под мобильные устройства;
2. Возможности CoreML 3 и 4, обучение на iOS устройстве;
3. Самостоятельное обучение классификатора изображений с помощью CreateML и использование его с Vision;
4. Использование обученных моделей для работы с текстом и звуком в iOS.
Построители результатов (result builders) в Swift позволяют получать результирующее значение из последовательности компонентов выставленных друг за другом строительных блоков. Они появились в Swift5.4 и доступны в Xcode12.5 и более поздних версиях. Ранее эти средства были известны как function builders (построители функций). Вам, вероятно, уже приходилось использовать их при создании стеков представлений в SwiftUI.
Должен признаться: поначалу я думал, что это некая узкоспециализированная возможность Swift, которую я никогда не стану применять для организации своего кода. Однако стоило мне в ней разобраться и написать небольшое решение для создания ограничений представления в UIKit, как я обнаружил, что раньше просто не понимал всю мощь построителей результатов.
Что такое построители результатов?
Построитель результата можно рассматривать как встроенный
предметно-ориентированный язык (DSL), описывающий объединение неких
частей в окончательный результат. В простых объявлениях
представлений SwiftUI за кадром используется атрибут
@ViewBuilder
, который представляет собой реализацию
построителя результата:
struct ContentView: View { var body: some View { // This is inside a result builder VStack { Text("Hello World!") // VStack and Text are 'build blocks' } } }
Все дочерние представления (в данном случае VStack, содержащий
Text
) будут объединены в одно представление
View
. Другими словами, строительные блоки
View
встраиваются в результат View
. Это
важно понять, поскольку именно так работают построители
результатов.
Если рассмотреть объявление протокола View
в
SwiftUI, можно заметить, что переменная body
определяется с использованием атрибута
@ViewBuilder
:
@ViewBuilder var body: Self.Body { get }
Именно так можно использовать собственный построитель результата в качестве атрибута функции, переменной или сабскрипта.
Создание собственного построителя результата
Способ определения кастомного построителя результата я покажу на примере, который использовал сам. При разработке авторазметки посредством кода я обычно реализую логику следующего вида:
var constraints: [NSLayoutConstraint] = [ // Single constraint swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) ] // Boolean check if alignLogoTop { constraints.append(swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)) } else { constraints.append(swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor)) } // Unwrap an optional if let fixedLogoSize = fixedLogoSize { constraints.append(contentsOf: [ swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width), swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height) ]) } // Add a collection of constraints constraints.append(contentsOf: label.constraintsForAnchoringTo(boundsOf: view)) // Returns an array // Activate NSLayoutConstraint.activate(constraints)
Как видите, здесь довольно много условных ограничений. В сложных представлениях прочитать их все может быть весьма непросто.
В этом случае построители результатов это отличное решение. Они позволяют переписать приведенный выше пример кода следующим образом:
@AutolayoutBuilder var constraints: [NSLayoutConstraint] { swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) // Single constraint if alignLogoTop { swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) } else { swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor) // Single constraint } if let fixedLogoSize = fixedLogoSize { swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width) swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height) } label.constraintsForAnchoringTo(boundsOf: view) // Returns an array }
Здорово, не правда ли?
Итак, рассмотрим способ создания такого решения.
Определение построителя для авторазметки
Начинаем с определения собственной структуры
AutolayoutBuilder
и добавляем атрибут
@resultBuilder
, чтобы пометить ее как построитель
результата:
@resultBuilder struct AutolayoutBuilder { // .. Handle different cases, like unwrapping and collections }
Чтобы объединить все строительные блоки и получить результат, нам нужно настроить обработчики для каждого случая, в частности для обработки опционалов и коллекций. Но для начала реализуем обработку случая с единственным ограничением.
Это делается с помощью следующего метода:
@resultBuilder struct AutolayoutBuilder { static func buildBlock(_ components: NSLayoutConstraint...) -> [NSLayoutConstraint] { return components } }
Этот метод принимает на вход вариативный параметр components (тоесть параметр с переменным числом возможных значений). Это означает, что может существовать одно или несколько ограничений. Нам нужно вернуть коллекцию ограничений, то есть в этом случае мы можем напрямую вернуть входные компоненты.
Теперь мы можем определить коллекцию ограничений следующим образом:
@AutolayoutBuilder var constraints: [NSLayoutConstraint] { // Single constraint swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) }
Обработка коллекции строительных блоков
Следующим шагом будет обработка коллекции элементов как одного
элемента. В первом примере кода мы использовали удобный метод
constraintsForAnchoringTo(boundsOf:)
, который
возвращает множество ограничений в виде коллекции. Если бы мы
применили его в этом случае, мы получили бы следующую ошибку:
Описание ошибки отлично объясняет происходящее:
Cannot pass array of type [NSLayoutConstraint] as variadic arguments of type NSLayoutConstraint Невозможно передать массив типа [NSLayoutConstraint] как вариативные аргументы типа NSLayoutConstraint
Как ни странно, в Swift нельзя передавать массив в качестве вариативных параметров. Вместо этого нужно определить собственный метод для обработки коллекции в качестве входных компонентов. На первый взгляд может показаться, что нам нужен следующий доступный метод:
Список доступных методов в определении кастомного построителя результата.К сожалению, как указано в описании метода, он обеспечивает поддержку только циклов, которые объединяют несколько результатов в один. Мы здесь используем не итератор, а удобный метод для прямого возврата коллекции, поэтому нам потребуется написать еще немного собственного кода.
Можно решить эту проблему, определив новый протокол, который
реализуется как с использованием одного
NSLayoutConstraint
, так и с использованием коллекции
ограничений:
protocol LayoutGroup { var constraints: [NSLayoutConstraint] { get } } extension NSLayoutConstraint: LayoutGroup { var constraints: [NSLayoutConstraint] { [self] } } extension Array: LayoutGroup where Element == NSLayoutConstraint { var constraints: [NSLayoutConstraint] { self } }
Этот протокол позволит нам преобразовывать как отдельные
ограничения, так и коллекцию ограничений в массив ограничений.
Другими словами, мы можем объединить оба типа в один
[NSLayoutConstraint
].
Теперь мы можем переписать наш построитель результата так, чтобы
он принимал наш протокол LayoutGroup
:
@resultBuilder struct AutolayoutBuilder { static func buildBlock(_ components: LayoutGroup...) -> [NSLayoutConstraint] { return components.flatMap { $0.constraints } } }
Для получения единой коллекции ограничений здесь используется
метод flatMap
. Если вы не знаете, для чего нужен метод
flatMap
или почему мы использовали его вместо
compactMap
, почитайте мою статью
Методы compactMap и flatMap:в чем разница?
Наконец, мы можем обновить наше решение, чтобы задействовать новый обработчик коллекции строительных блоков:
@AutolayoutBuilder var constraints: [NSLayoutConstraint] { // Single constraint swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) label.constraintsForAnchoringTo(boundsOf: view) // Returns an array }
Разворачивание опционалов
Другой случай, который необходимо рассмотреть, это разворачивание опционалов. Этот механизм позволит добавлять ограничения, если существует определенное значение.
Добавим метод buildOptional(..)
к нашему
построителю результата:
@resultBuilder struct AutolayoutBuilder { static func buildBlock(_ components: LayoutGroup...) -> [NSLayoutConstraint] { return components.flatMap { $0.constraints } } static func buildOptional(_ component: [LayoutGroup]?) -> [NSLayoutConstraint] { return component?.flatMap { $0.constraints } ?? [] } }
Метод пытается преобразовать результат в коллекцию ограничений или возвращает пустую коллекцию, если данного значения не существует.
Теперь мы можем развернуть опционал в нашем определении строительных блоков:
@AutolayoutBuilder var constraints: [NSLayoutConstraint] { // Single constraint swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) label.constraintsForAnchoringTo(boundsOf: view) // Returns an array // Unwrapping an optional if let fixedLogoSize = fixedLogoSize { swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width) swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height) } }
Обработка условных операторов
Еще один распространенный случай условные операторы. В зависимости от логического значения может потребоваться добавить то или иное ограничение. Этот обработчик может обрабатывать первый или второй компонент в проверке условия:
@AutolayoutBuilder var constraints: [NSLayoutConstraint] { // Single constraint swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) label.constraintsForAnchoringTo(boundsOf: view) // Returns an array // Unwrapping an optional if let fixedLogoSize = fixedLogoSize { swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width) swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height) } // Conditional check if alignLogoTop { // Handle either the first component: swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) } else { // Or the second component: swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor) } }
В наш построитель результата надо добавить еще пару обработчиков строительных блоков:
@resultBuilder struct AutolayoutBuilder { static func buildBlock(_ components: LayoutGroup...) -> [NSLayoutConstraint] { return components.flatMap { $0.constraints } } static func buildOptional(_ component: [LayoutGroup]?) -> [NSLayoutConstraint] { return component?.flatMap { $0.constraints } ?? [] } static func buildEither(first component: [LayoutGroup]) -> [NSLayoutConstraint] { return component.flatMap { $0.constraints } } static func buildEither(second component: [LayoutGroup]) -> [NSLayoutConstraint] { return component.flatMap { $0.constraints } } }
В обоих обработчиках buildEither
для получения
ограничений и их возвращения в виде плоской структуры используется
все тот же протокол LayoutGroup
.
Это были последние два обработчика, необходимые для работы нашего примера. Ура!
Однако мы еще не закончили. Мы можем немного усовершенствовать этот код, используя построители результатов внутри функций.
Использование построителей результатов в качестве параметров функций
Отличный способ использовать построитель результата определить
его как параметр функции. Так мы действительно получим пользу от
нашего кастомного AutolayoutBuilder
.
Например, можно добавить такое расширение к
NSLayoutConstraint
, чтобы немного упростить активацию
ограничений:
extension NSLayoutConstraint { /// Activate the layouts defined in the result builder parameter `constraints`. static func activate(@AutolayoutBuilder constraints: () -> [NSLayoutConstraint]) { activate(constraints()) }
Применяться расширение будет вот так:
NSLayoutConstraint.activate { // Single constraint swiftLeeLogo.centerXAnchor.constraint(equalTo: view.centerXAnchor) label.constraintsForAnchoringTo(boundsOf: view) // Returns an array // Unwrapping an optional if let fixedLogoSize = fixedLogoSize { swiftLeeLogo.widthAnchor.constraint(equalToConstant: fixedLogoSize.width) swiftLeeLogo.heightAnchor.constraint(equalToConstant: fixedLogoSize.height) } // Conditional check if alignLogoTop { // Handle either the first component: swiftLeeLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) } else { // Or the second component: swiftLeeLogo.centerYAnchor.constraint(equalTo: view.centerYAnchor) } }
Теперь, реализовав данный метод, мы также можем создать удобный
метод в UIView
для непосредственного добавления
субпредставления с ограничениями:
protocol SubviewContaining { } extension UIView: SubviewContaining { } extension SubviewContaining where Self == UIView { /// Add a child subview and directly activate the given constraints. func addSubview<View: UIView>(_ view: View, @AutolayoutBuilder constraints: (Self, View) -> [NSLayoutConstraint]) { addSubview(view) NSLayoutConstraint.activate(constraints(self, view)) } }
Это можно использовать следующим образом:
let containerView = UIView() containerView.addSubview(label) { containerView, label in if label.numberOfLines == 1 { // Conditional constraints } // Or just use an array: label.constraintsForAnchoringTo(boundsOf: containerView) }
Поскольку мы используем дженерики, мы можем выполнять проверку
условий на основе входного типа UIView
. В этом случае
можно добавить различные ограничения, если метка label
будет содержать только одну строку текста.
Как разработать собственное решение с построителем результата?
Вы, наверняка, думаете: как же определить, будет ли построитель результата полезен в том или ином фрагменте кода?
Каждый раз, когда вам встречается фрагмент кода, состоящий из нескольких условных элементов и преобразуемый в одиночный элемент возвращаемого типа, вы можете задуматься о написании построителя результата. Однако делать это стоит лишь в случае, если вы уверены, что вам придется писать этот фрагмент часто.
Когда мы пишем ограничения для разработки авторазметки посредством кода, мы повторяем одни и те же инструкции по нескольку раз, поэтому в данном случае стоит написать кастомный построитель результата. Сами ограничения тоже состоят из множества строительных блоков, если рассматривать каждую коллекцию ограничений (одиночную или нет) как отдельный такой блок.
Наконец, я хотел бы сослаться на репозиторий с примерами построителей функций (которые теперь называются построителями результатов).
Заключение
Построители результатов это очень мощное дополнение к Swift5.4. Они позволяют писать код на собственном предметно-ориентированном языке, за счет чего можно усовершенствовать свой подход к написанию кода. Я надеюсь, что эта статья немного прояснит для вас понятие кастомных построителей результатов, которые помогают упрощать код на организационном уровне.
Узнать подробнее о курсе "iOS Developer. Professional"
Смотреть вебинар Machine Learning в iOS с помощью CoreML и CreateML: изображения, текст, звук