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

Магия SwiftUI или о Function builders

Магия SwiftUI или о Function builders - 1

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

var body: some View {
        VStack {
            Text("Placeholder1")
            Text("Placeholder2")
            // ... тут вьюшки с 3 по 10 . . .
            Text("Placeholder11")
        }
    }

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

Function Builder

Для начала стоит понять, как такой синтаксис стал вообще доступен. В основе столь непривычного нам декларативного создания элементов лежит механизм 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.

ViewBuilder

Теперь вернемся к 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