Хабрахабр

Магия SwiftUI

Вы пробовали добавить в VStack больше 10 вьюх?

var body: some View }

Да, я тоже сначала был удивлен и погрузился в изучение форума Swift и гитхаба. Я попробовал — это не компилируется. Но подождите, давайте разберемся почему. Результатом моего изучения стало — "все равно не компилируется ¯\_(ツ)_/¯ ".

Function Builder

В основе столь непривычного нам декларативного создания элементов лежит механизм Function Builder.
На гитхабе в swift-evolution есть proposal от John McCall и Doug Gregor — Function builders (Proposal: SE-XXXX), в котором они подробно описывают о том, какая проблема перед ними стояла, почему было решено использовать именно Functions Builder и что это вообще такое. Для начала стоит понять, как такой синтаксис стал вообще доступен.

Итак, что это?

Сложно описать это в двух словах, но если коротко — это механизм, который позволяет в теле кложуры перечислить аргументы, некое содержимое, и выдать из всего этого общий результат.
Цитата из Proposal: SE-XXXX:

Эта "сборка" контролируется билдером функции, который является кастомным атрибутом. Основная идея в том, что мы берем результат выражения, включая вложенные выражения вроде if и switch, и формируем их в один результат, который становится возвращаемым значением текущей функции.

Оригинал

The way this collection is performed is controlled by afunction builder, which is just a new kind of custom-attribute type; the basic idea is that we take the «ignored» expression results of a block of statements — including in nested positions like the bodies ofifandswitchstatements — and build them into a single result value that becomes the return value of the current function.

Такой механизм позволяет писать декларативный древовидный код, без лишних символов пунктуации:

let myBody = body { let chapter = spellOutChapter ? "Chapter" : "" div { if useChapterTitles { h1(chapter + "1. Loomings.") } p { "Call me Ishmael. Some years ago" } p { "There is now your insular city" } }
}

Этим атрибутом помечается некоторый билдер, он может быть структурой. Доступно это благодаря новому атрибуту @_functionBuilder. Далее этот билдер используется сам, в качестве пользовательского атрибута в различных ситуациях.
Чуть ниже я покажу как это работает и как организовать такой код. У этого билдера реализуется ряд конкретных методов.

Зачем это?

При этом они отмечают, что их решение — это не универсальный DSL.
Это решение нацеленно на конкретный ряд проблем, в числе которых описывать линейные и древовидные структуры, такие как XML, JSON, иерархии View и т.д. Таким образом Apple хотят сделать поддержу встроенного domain-specific language DSL.
John McCall и Doug Gregor главными аргументами приводят то, что такой код намного легче читать и писать — это упрощает синтаксис, делает его более лаконичным и, как следствие, код становится более поддерживаемым.

Как с этим работать?

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

// 1. Создаем Builder
@_functionBuilder struct MyBuilder { static func buildBlock(_ atrs: String...) -> String { return atrs.reduce("", + ) }
}

// 2. Добавляем его атрибутом перед кложурой в каком либо методе
func stringsReduce(@MyBuilder block: () -> String) -> String { return block()
}

// 3. Используем в клиентском коде
let result = stringsReduce { "1" "2"
} print(result) // "12"

Под капотом это будет отрабатывать так:

let result = stringsReduce { return MyBuilder.build("1", "2")
}

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

static func buildBlock(_ <*atrs*>: <*String*>...) -> <*String*>

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

stringsReduce { if .random() { // рандомное значение Bool "one string" } else { "another one" } "fixed string"
}

Для поддержки такого синтаксиса в билдере нужно реализовать методы buildEither(first:/second:)

static func buildEither(first: String) -> String { return first
} static func buildEither(second: String) -> String { return second
}

Реакция сообщества

1, то есть пулл-реквест c этой фичей еще не влит, но тем не менее Apple уже добавили ее в XCode 11 beta. Забавно то, что этого еще нет в Swift 5. А на Function builders → Pitches → Swift Forums можно посмотреть реакцию комьюнити на этот proposal.

ViewBuilder

Теперь вернемся к VStack и посмотрим документацию его инициализатора init(alignment:spacing:content:).
Выглядит он следующим образом:

init(alignment: HorizontalAlignment = .center, spacing: ? = nil, @ViewBuilder content: () -> Content)

И перед кложурой контент стоит пользовательский атрибут @ViewBuilder
Объявлен он следующим образом:

@_functionBuilder public struct ViewBuilder { /// Builds an empty view from an block containing no statements, `{ }`. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through /// unmodified. public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

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

А если полистать документацию еще ниже, то там видно множество статических методов buildBlock, отличающихся количеством аргументов.

Это значит, что код вида

var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") Text("Placeholder3") }
}

под капотом преобразуется в такой

var body: some View { VStack(alignment: .leading) { ViewBuilder.buildBlock(Text("Placeholder1"), Text("Placeholder2"), Text("Placeholder3")) } }

отрабатывает метод билдера buildBlock(::_:). Т.е.

Из всего этого списка метод с максимальным количеством аргументов — это этот парень buildBlock(:::::::::🙂 (10 аргументов):

extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}

Но такого метода нет: отсюда и ошибка компиляции.
Это актуально для всех коллекций, использующих в инициализаторе кложуру с атрибутом @ViewBuilder: V|H|Z-Stack, List, Group и прочие, внутри которых вы можете объявить больше одной вьюшки перечислением.
И это грустно. И соответственно, возвращаясь к изначальному примеру, когда вы пытаетесь поднять VStack и одиннадцать вьюшек внутри, компилятор пытается найти метод ViewBuilder'a buildBlock, у которого 11 аргументов на входе.

MEM (простите, так и не нашел достойного мема)

Как быть?

Мы можем обходить это ограничение используя ForEach

struct TestView : View { var body: some View { VStack { ForEach(texts) { i in Text(«\(i)») } } } var texts: [Int] { var result: [Int] = [] for i in 0...150 { result.append(i) } return result }
}

Или же вложенностью коллекций:

var body: some View { VStack { VStack { Text("Placeholder_1") Text("Placeholder_2") // И Еще 8 } Group { Text("11") Text("12") // И Еще 8 } } }

Но какое оно это будущее?
В Swift уже есть Variadic parameters. Но такие решения выглядят как костыли и остается лишь надежда на светлое будущее. Например известный каждому метод print позволяет написать как print(1, 2), так и print(1, 2, 3, 4) и это без излишних перегрузок метода. Это возможность метода принимать на вход аргументы перечислением.

print(items: Any...)

Variadic generics позволяют абстрагироваться от множества generic типов, например как-то так: Но этой фичи языка недостаточно, так как метод buildBlock принимает на вход разные generic аргументы.
Добавление Variadic generics решило бы эту проблему.

static func buildBlock<…Component>(Component...) -> TupleView<(Component...)> where Component: View

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть