Похоже, что новый фреймворк SwiftUI от Apple использует новый вид синтаксиса, который эффективно создает кортеж, но имеет другой синтаксис:
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World") // No comma, no separator ?!
Text("Hello World!")
}
}
Попытка понять, что на самом деле представляет собой этот синтаксис, я обнаружил, что используемый здесь инициализатор VStack принимает замыкание типа () -> Content
в качестве второго параметра, где Content — это общий параметр, соответствующий View, который выводится через замыкание. Чтобы узнать, к какому типу относится Content, я немного изменил код, сохранив его функциональность:
var body: some View {
let test = VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
return test
}
При этом test оказывается принадлежащим к типу VStack<TupleView<(Text, Text)>>, что означает, что Content относится к типу TupleView<Text, Text>. Глядя на TupleView, я обнаружил, что это тип оболочки, происходящий от самого SwiftUI, который можно инициализировать только путем передачи кортежа, который он должен обернуть.
Вопрос
Теперь мне интересно, как два экземпляра Text в этом примере преобразуются в TupleView<(Text, Text)>. Является ли это взломанным SwiftUI и, следовательно, неверный обычный синтаксис Swift?TupleView, являющимся типом SwiftUI, поддерживает это предположение. Или это допустимый синтаксис Swift? Если да, то как один использовать его снаружи SwiftUI?
Обсуждается на форуме Swift здесь forums.swift.org/t/pitch-introduce-custom-attributes/21335 и здесь forums.swift.org/t/pitch-static-custom-attributes-round-2/22938.





Как говорит Мартин, если вы посмотрите документацию для VStackinit(alignment:spacing:content:), вы увидите, что параметр content: имеет атрибут @ViewBuilder:
init(alignment: HorizontalAlignment = .center, spacing: Length? = 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
where Content : View
}Атрибут @_functionBuilder является частью неофициальной функции под названием «конструкторы функций», которая была разбился на эволюцию Swift здесь и реализована специально для версии Swift, которая поставляется с Xcode 11, что позволяет использовать ее в SwiftUI.
Пометка типа @_functionBuilder позволяет использовать его в качестве пользовательского атрибута в различных объявлениях, таких как функции, вычисляемые свойства и, в данном случае, параметры типа функции. Такие аннотированные объявления используют построитель функций для преобразования блоков кода:
Способ, которым построитель функций преобразует код, определяется его реализацией методы построения, такой как buildBlock, которая принимает набор выражений и объединяет их в одно значение.
Например, ViewBuilder реализует buildBlock от 1 до 10 View соответствующих параметров, объединяя несколько представлений в одно TupleView:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
/// 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
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1)
-> TupleView<(C0, C1)> where C0 : View, C1 : View
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
-> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
// ...
}
Это позволяет преобразовать набор выражений представления в замыкании, переданном инициализатору VStack, в вызов buildBlock, который принимает такое же количество аргументов. Например:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
}
}
превращается в вызов buildBlock(_:_:):
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
}
}
}
в результате чего непрозрачный тип результатаsome View удовлетворяется TupleView<(Text, Text)>.
Вы заметите, что ViewBuilder определяет только buildBlock до 10 параметров, поэтому, если мы попытаемся определить 11 подпредставлений:
var body: some View {
// error: Static member 'leading' cannot be used on instance of
// type 'HorizontalAlignment'
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
Text("Hello World!")
}
}
мы получаем ошибку компилятора, так как нет метода компоновщика для обработки этого блока кода (обратите внимание, что, поскольку эта функция все еще находится в стадии разработки, сообщения об ошибках вокруг нее не будут такими полезными).
На самом деле, я не верю, что люди будут сталкиваться с этим ограничением так часто, например, приведенный выше пример лучше использовать с представлением ForEach:
var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
Text("Hello world \(i)")
}
}
}
Однако, если вам нужно более 10 статически определенных представлений, вы можете легко обойти это ограничение, используя представление Group:
var body: some View {
VStack(alignment: .leading) {
Group {
Text("Hello world")
// ...
// up to 10 views
}
Group {
Text("Hello world")
// ...
// up to 10 more views
}
// ...
}
ViewBuilder также реализует другие методы построения функций, такие как:
extension ViewBuilder {
/// Provides support for "if" statements in multi-statement closures, producing
/// ConditionalContent for the "then" branch.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
/// Provides support for "if-else" statements in multi-statement closures,
/// producing ConditionalContent for the "else" branch.
public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
-> ConditionalContent<TrueContent, FalseContent>
where TrueContent : View, FalseContent : View
}
Это дает ему возможность обрабатывать операторы if:
var body: some View {
VStack(alignment: .leading) {
if .random() {
Text("Hello World!")
} else {
Text("Goodbye World!")
}
Text("Something else")
}
}
который превращается в:
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(
.random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
: ViewBuilder.buildEither(second: Text("Goodbye World!")),
Text("Something else")
)
}
}
(испуская избыточные вызовы с 1 аргументом для ViewBuilder.buildBlock для ясности).
ViewBuilder определяет только buildBlock до 10 параметров — значит ли это, что у var body: some View не может быть более 11 подвидов?
@LinusGeffarth На самом деле я не думаю, что люди будут сталкиваться с этим ограничением так часто, поскольку вместо этого они, вероятно, захотят использовать что-то вроде представления ForEach. Однако вы можете использовать представление Group, чтобы обойти это ограничение, я отредактировал свой ответ, чтобы показать это.
Спасибо, что дразнили это! Нашли ли вы какую-либо поддержку для повторно используемых пользовательских (суб-)представлений/групп или логики переносимого/параметризованного компоновщика? В противном случае я не вижу в этом полезности (читай: поддерживаемой) для чего-либо, кроме относительно простых макетов.
@MandisaW - вы можете группировать представления в свои собственные представления и использовать их повторно. Я не вижу в этом проблемы. На самом деле я сейчас на WWDC и разговаривал с одним из инженеров в лаборатории SwiftUI — он сказал, что это ограничение Swift прямо сейчас, и они выбрали 10 как разумное число. Как только вариативный дженерик будет введен в Swift, мы сможем иметь столько «подпредставлений», сколько захотим.
Спасибо за разъяснения. Насколько я понимаю, @_functionBuilder не может иметь 2 функции с одинаковым количеством параметров. Если это правда, то можем ли мы получить ошибку компиляции, если мы делаем это по ошибке, и если нет, то как он узнает, какие функции использовать для преобразования.
Отличная информация, спасибо. Но почему сигнатура init выглядит иначе, если вы просматриваете ее в XCode, там у нее нет атрибута Viewbuilder: @inlinable public init(alignment: HorizontalAlignment = .center, spacing: Length? = nil, content: () -> Content)
@Gusutafu Я считаю, что это ошибка компилятора - ASTPrinter еще не был обновлен для печати атрибутов для параметров.
Может быть, более интересно, в чем смысл методов buildEither? Похоже, вам нужно реализовать оба, и оба имеют один и тот же тип возвращаемого значения, почему бы каждому из них просто не возвращать рассматриваемый тип?
Хорошо, это облегчение. Это немного смутило меня сегодня, когда я пытался найти, где на самом деле упоминается строитель.
@Gusutafu Хороший вопрос о buildEither! Я полагать, чтобы помочь SwiftUI понять, когда ему нужно перерисовать контент. Например, это может сделать вывод из информации о типе, не нужно ли перерисовывать определенные виды. Используя ConditionalContent (который возвращает buildEither), предположительно SwiftUI проверяет, совпадает ли выбранная ветка. Если это так, то его не нужно перерисовывать, пока никакое другое состояние представления в этой ветке не было изменено.
В продолжение моего комментария об ошибке ASTPrinter, это будет исправлено на мастере после объединения PR построителей функций..
Ах, ладно, хотя у вас всегда есть возможность реализовать buildEither, двойной общий вариант был исключительно вещью SwiftUI. Может быть, похоже на то, почему возвращается «какой-то вид», я думаю, они предпочитают иметь все статические знания о том, какой конкретный вид, которые вы потеряли бы, если бы вернули экзистенциальный. Но ваш комментарий возвращает меня к моему основному вопросу о SwiftUI, как теперь может владеть представлением, когда какое-то состояние изменилось. Похоже, что хранилище State находится не в экземпляре State<T>, но даже если это какой-то синглтон, как View может наблюдать за его изменением?
Может помочь ссылка на github.com/apple/swift-evolution/blob/…, в которой подробно описаны принципы.
@Hamish У меня также есть связанный с этим вопрос: есть ли способ видеть, во что превратился любой данный код SwiftUI после прохождения через конструктор?
@matt К сожалению, я не знаю. Для последнего примера я сделал swiftc -dump-ast, чтобы увидеть преобразованный AST, но это довольно сложно прочитать для нетривиальных примеров (и даже для тривиальных примеров может потребоваться немного знаний о том, для чего предназначены разные узлы). Было бы неплохо, если бы когда-нибудь в будущем Swift мог поддерживать реконструкцию читаемого исходного кода из AST с проверкой типов, что также позволило бы пользователю видеть такие вещи, как синтезированные соответствия протоколов.
Начиная с Xcode 11.3 что-то изменилось в реализации ViewBuilders? Мой код перестает работать правильно. public init<A: View, B: View, C: View>(@ViewBuilder content: () -> TupleView<(A, B, C)>) { let views = content().value self.childs = [AnyView (views.0), AnyView(views.1), AnyView(views.2)] } приведенный выше код больше не работает, он должен явно вызывать ViewBuilder.buildBlock(), а @ViewBuilder вызывает конструктор для одного представления содержимого без Tuple
Аналогичная вещь описана в Что нового в Swift, видео WWDC в разделе про DSL (начало ~31:15). Атрибут интерпретируется компилятором и транслируется в соответствующий код:
@ViewBuilderdeveloper.apple.com/documentation/swiftui/viewbuilder.