Русский
Русский
English
Статистика
Реклама

Перевод Построители результатов в Swift описание и примеры кода

Перевод подготовлен в рамках набора на курс "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: изображения, текст, звук

Источник: habr.com
К списку статей
Опубликовано: 05.05.2021 12:08:12
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании otus

Программирование

Разработка под ios

Swift

Ml

Coreml

Createml

Ios-разработка

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru