- PVSM.RU - https://www.pvsm.ru -

Вы пробовали добавить в VStack больше 10 вьюх?
var body: some View {
VStack {
Text("Placeholder1")
Text("Placeholder2")
// ... тут вьюшки с 3 по 10 . . .
Text("Placeholder11")
}
}
Я попробовал — это не компилируется. Да, я тоже сначала был удивлен и погрузился в изучение форума Swift и гитхаба. Результатом моего изучения стало — "все равно не компилируется ¯_(ツ)_/¯ ". Но подождите, давайте разберемся почему.
Для начала стоит понять, как такой синтаксис стал вообще доступен. В основе столь непривычного нам декларативного создания элементов лежит механизм Function Builder.
На гитхабе в swift-evolution [1] есть proposal от John McCall и Doug Gregor — Function builders (Proposal: SE-XXXX) [2], в котором они подробно описывают о том, какая проблема перед ними стояла, почему было решено использовать именно Functions Builder и что это вообще такое.
Сложно описать это в двух словах, но если коротко — это механизм, который позволяет в теле кложуры перечислить аргументы, некое содержимое, и выдать из всего этого общий результат.
Цитата из Proposal: SE-XXXX [2]:
Основная идея в том, что мы берем результат выражения, включая вложенные выражения вроде if и switch, и формируем их в один результат, который становится возвращаемым значением текущей функции. Эта "сборка" контролируется билдером функции, который является кастомным атрибутом.
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. The way this collection is performed is controlled by afunction builder, which is just a new kind of custom-attribute type;
Такой механизм позволяет писать декларативный древовидный код, без лишних символов пунктуации:
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. Этим атрибутом помечается некоторый билдер, он может быть структурой. У этого билдера реализуется ряд конкретных методов. Далее этот билдер используется сам, в качестве пользовательского атрибута в различных ситуациях.
Чуть ниже я покажу как это работает и как организовать такой код.
Таким образом Apple хотят сделать поддержу встроенного domain-specific language DSL [3].
John McCall и Doug Gregor главными аргументами приводят то, что такой код намного легче читать и писать — это упрощает синтаксис, делает его более лаконичным и, как следствие, код становится более поддерживаемым. При этом они отмечают, что их решение — это не универсальный DSL.
Это решение нацеленно на конкретный ряд проблем, в числе которых описывать линейные и древовидные структуры, такие как XML, JSON, иерархии View и т.д.
Вы можете создать свой 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")
}
Важно, что в реализации билдера методы должны быть именно статическими, с конкретными именами и с конкретным видом параметров из этого списка [4]. Можно лишь изменять тип и имя входного параметра.
static func buildBlock(_ <*atrs*>: <*String*>...) -> <*String*>
Именно конкретные имена методов будут искаться в билдере и подставляться на этапе компиляции. И если метод не будет найден — случится ошибка компиляции.
И это магия. Когда вы будете реализовывать билдер, компилятор не подскажет вам совершенно ничего. Не скажет о доступных методах, не поможет автокомплитом. Лишь когда вы напишете клиентский код, который не сможет обработаться этим билдером, вы получите невнятную ошибку.
Пока единственное решение, которое я нашел, это руководствоваться списком методов [4].
Так зачем нужны другие методы? Ну например, чтобы поддержать такой код 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
}
Забавно то, что этого еще нет в Swift 5.1, то есть пулл-реквест [5] c этой фичей еще не влит, но тем не менее Apple уже добавили ее в XCode 11 beta. А на Function builders → Pitches → Swift Forums [6] можно посмотреть реакцию комьюнити на этот proposal.
Теперь вернемся к VStack и посмотрим документацию его инициализатора init(alignment:spacing:content:) [7].
Выглядит он следующим образом:
init(alignment: HorizontalAlignment = .center, spacing: ? = nil, @ViewBuilder content: () -> Content)
И перед кложурой контент стоит пользовательский атрибут @ViewBuilder [8]
Объявлен он следующим образом:
@_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(::_:) [9].
Из всего этого списка метод с максимальным количеством аргументов — это этот парень buildBlock(::::::::::) [10] (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
}
И соответственно, возвращаясь к изначальному примеру, когда вы пытаетесь поднять VStack и одиннадцать вьюшек внутри, компилятор пытается найти метод ViewBuilder'a buildBlock, у которого 11 аргументов на входе. Но такого метода нет: отсюда и ошибка компиляции.
Это актуально для всех коллекций, использующих в инициализаторе кложуру с атрибутом @ViewBuilder: V|H|Z-Stack, List, Group и прочие, внутри которых вы можете объявить больше одной вьюшки перечислением.
И это грустно.
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 [11]. Это возможность метода принимать на вход аргументы перечислением. Например известный каждому метод print позволяет написать как print(1, 2), так и print(1, 2, 3, 4) и это без излишних перегрузок метода.
print(items: Any...)
Но этой фичи языка недостаточно, так как метод buildBlock принимает на вход разные generic аргументы.
Добавление Variadic generics [12] решило бы эту проблему. Variadic generics позволяют абстрагироваться от множества generic типов, например как-то так:
static func buildBlock<…Component>(Component...) -> TupleView<(Component...)> where Component: View
И Apple просто обязаны добавить это. В этот механизм сейчас все упирается. И мне кажется, что его просто не успели допилить к WWDC 2019 (но это лишь домыслы).
Автор: Алексей Зверев
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/320569
Ссылки в тексте:
[1] swift-evolution: https://github.com/apple/swift-evolution
[2] Function builders (Proposal: SE-XXXX): https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md
[3] DSL: https://en.wikipedia.org/wiki/Domain-specific_language
[4] с конкретным видом параметров из этого списка: https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md#function-building-methods
[5] пулл-реквест: https://github.com/apple/swift/pull/25221
[6] Function builders → Pitches → Swift Forums: https://forums.swift.org/t/function-builders/25167/16
[7] init(alignment:spacing:content:): https://developer.apple.com/documentation/swiftui/vstack/3278367-init
[8] @ViewBuilder: https://developer.apple.com/documentation/swiftui/viewbuilder
[9] buildBlock(::_:): https://developer.apple.com/documentation/swiftui/viewbuilder/3278686-buildblock
[10] buildBlock(::::::::::) : https://developer.apple.com/documentation/swiftui/viewbuilder/3278693-buildblock
[11] Variadic parameters: https://docs.swift.org/swift-book/LanguageGuide/Functions.html
[12] Variadic generics: https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#variadic-generics
[13] Источник: https://habr.com/ru/post/455760/?utm_source=habrahabr&utm_medium=rss&utm_campaign=455760
Нажмите здесь для печати.