¿Qué habilita el DSL de SwiftUI?
(2)
Algo similar se describe en el video de Swift WWDC en la sección sobre DSL (comienza en ~ 31: 15). El compilador interpreta el atributo y lo traduce en código relacionado:
Parece que el nuevo marco
SwiftUI
Apple utiliza un
nuevo tipo de sintaxis
que construye efectivamente una tupla, pero tiene otra sintaxis:
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World") // No comma, no separator ?!
Text("Hello World!")
}
}
Tratando de
descifrar lo que
realmente es esta sintaxis
, descubrí que el inicializador
VStack
usado aquí toma un cierre del tipo
() -> Content
como el segundo parámetro, donde
Content
es un parámetro genérico conforme a la
View
que se infiere a través del cierre .
Para averiguar a qué tipo de
Content
se deduce, cambié el código ligeramente, manteniendo su funcionalidad:
var body: some View {
let test = VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
return test
}
Con esto, la
test
revela como de tipo
VStack<TupleView<(Text, Text)>>
, lo que significa que el
Content
es de tipo
TupleView<Text, Text>
.
Al buscar
TupleView
, descubrí que es un tipo de envoltura que se origina en
SwiftUI
y que solo se puede inicializar al pasar la tupla que debe envolver.
Pregunta
Ahora me pregunto cómo en el mundo las dos instancias de
Text
en este ejemplo se convierten a
TupleView<(Text, Text)>
.
¿Se ha pirateado esto en
SwiftUI
y, por lo tanto,
no
es
válida la sintaxis de Swift?
TupleView
es un tipo
SwiftUI
compatible con esta suposición.
¿O es esta
sintaxis Swift válida?
Si es así, ¿cómo se puede
usar fuera de
SwiftUI
?
Como dice Martin
, si miras la documentación para el
VStack
de
VStack
developer.apple.com/documentation/swiftui/vstack/3278367-init
, puedes ver que el parámetro
content:
tiene el atributo
@ViewBuilder
:
init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
@ViewBuilder content: () -> Content)
Este atributo se refiere al tipo de developer.apple.com/documentation/swiftui/viewbuilder , que si observa la interfaz generada, se ve así:
@_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
}
El atributo
@_functionBuilder
es parte de una característica no oficial llamada "
constructores de funciones
", que se ha desarrollado
en Swift evolution aquí
, e implementado especialmente para la versión de Swift que se envía con Xcode 11, lo que permite su uso en SwiftUI.
Marcar un tipo
@_functionBuilder
permite utilizarlo como un atributo personalizado en varias declaraciones, como funciones, propiedades computadas y, en este caso, parámetros del tipo de función.
Dichas declaraciones anotadas utilizan el generador de funciones para transformar bloques de código:
- Para funciones anotadas, el bloque de código que se transforma es la implementación.
- Para las propiedades computadas anotadas, el bloque de código que se transforma es el captador.
- Para los parámetros anotados del tipo de función, el bloque de código que se transforma es cualquier expresión de cierre que se le pasa (si corresponde).
La forma en que un generador de funciones transforma el código se define por su implementación de
métodos de
buildBlock
como
buildBlock
, que toma un conjunto de expresiones y las consolida en un solo valor.
Por ejemplo,
ViewBuilder
implementa
buildBlock
para 1 a 10
View
los parámetros conformes, consolidando múltiples vistas en un único
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
// ...
}
Esto permite que un conjunto de expresiones de vista dentro de un cierre pasado al
VStack
de
VStack
se transforme en una llamada a
buildBlock
que toma la misma cantidad de argumentos.
Por ejemplo:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
}
}
se transforma en una llamada a
buildBlock(_:_:)
:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
}
}
}
dando como
resultado el tipo de resultado opaco que
some View
satisfacen con
TupleView<(Text, Text)>
.
Notará que
ViewBuilder
solo define
buildBlock
hasta 10 parámetros, por lo que si intentamos definir 11 subvistas:
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!")
}
}
obtenemos un error del compilador, ya que no hay un método de construcción para manejar este bloque de código (tenga en cuenta que debido a que esta característica sigue siendo un trabajo en curso, los mensajes de error no serán muy útiles).
En realidad, no creo que la gente se tope con esta restricción con
ForEach
frecuencia, por ejemplo, el ejemplo anterior se serviría mejor utilizando la vista
ForEach
:
var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
Text("Hello world /(i)")
}
}
}
Sin embargo, si necesita más de 10 vistas definidas estáticamente, puede solucionar fácilmente esta restricción utilizando la vista de
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
también implementa otros métodos de
ViewBuilder
funciones, tales como:
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
}
Esto le da la capacidad de manejar si las declaraciones:
var body: some View {
VStack(alignment: .leading) {
if .random() {
Text("Hello World!")
} else {
Text("Goodbye World!")
}
Text("Something else")
}
}
que se transforma en:
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")
)
}
}
(emitiendo llamadas redundantes de 1 argumento a
ViewBuilder.buildBlock
para mayor claridad).