Wygląda na to, że nowa SwiftUI
struktura Apple wykorzystuje nowy rodzaj składni, która skutecznie buduje krotkę, ale ma inną składnię:
var body: some View {
VStack(alignment: .leading) {
Text("Hello, World") // No comma, no separator ?!
Text("Hello World!")
}
}
Próbując wyjaśnić, czym naprawdę jest ta składnia , dowiedziałem się, że VStack
użyty tutaj inicjator przyjmuje zamknięcie typu () -> Content
jako drugi parametr, gdzie Content
jest to ogólny parametr zgodny z View
tym, który jest wywnioskowany przez zamknięcie. Aby dowiedzieć się, do jakiego typu Content
jest wywnioskowany, nieznacznie zmieniłem kod, zachowując jego funkcjonalność:
var body: some View {
let test = VStack(alignment: .leading) {
Text("Hello, World")
Text("Hello World!")
}
return test
}
Dzięki temu test
okazuje się być typem VStack<TupleView<(Text, Text)>>
, co oznacza, że Content
jest typu TupleView<Text, Text>
. Patrząc w górę TupleView
, stwierdziłem, że jest to typ otoki pochodzący od SwiftUI
samego siebie, który można zainicjować tylko przez przekazanie krotki, którą ma zawijać.
Pytanie
Teraz zastanawiam się, jak na świecie te dwa Text
wystąpienia w tym przykładzie są konwertowane na TupleView<(Text, Text)>
. Czy to się włamało, SwiftUI
a zatem jest nieprawidłową zwykłą składnią Swift? TupleView
bycie SwiftUI
typem wspiera to założenie. A może jest to poprawna składnia Swift? Jeśli tak, jak można go używać na zewnątrz SwiftUI
?
@ViewBuilder
developer.apple.com/documentation/swiftui/viewbuilder .Odpowiedzi:
Jak mówi Martin , jeśli spojrzysz na dokumentację dla
VStack
'sinit(alignment:spacing:content:)
, zobaczysz, żecontent:
parametr ma atrybut@ViewBuilder
:init(alignment: HorizontalAlignment = .center, spacing: Length? = nil, @ViewBuilder content: () -> Content)
Ten atrybut odnosi się do
ViewBuilder
typu, który patrząc na wygenerowany interfejs wygląda następująco:@_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
Atrybut jest częścią nieoficjalną funkcję o nazwie „ budowniczych funkcyjne ”, który został dwuspadowym o Swift ewolucji tutaj i realizowane specjalnie dla wersji Swift że statki z Xcode 11, co pozwala na stosowanie go w SwiftUI.Oznaczenie typu
@_functionBuilder
pozwala na użycie go jako atrybutu niestandardowego w różnych deklaracjach, takich jak funkcje, obliczone właściwości i, w tym przypadku, parametry typu funkcji. Takie deklaracje z adnotacjami używają konstruktora funkcji do przekształcania bloków kodu:Sposób, w jaki konstruktor funkcji przekształca kod, jest definiowany przez implementację metod konstruktora, takich jak
buildBlock
, która pobiera zestaw wyrażeń i konsoliduje je w jedną wartość.Na przykład
ViewBuilder
implementujebuildBlock
od 1 do 10View
zgodnych parametrów, konsolidując wiele widoków w jedenTupleView
:@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 // ... }
Pozwala to
VStack
na przekształcenie zestawu wyrażeń widoku w zamknięciu przekazanym do inicjalizatora w wywołanie,buildBlock
które pobiera taką samą liczbę argumentów. Na przykład:struct ContentView : View { var body: some View { VStack(alignment: .leading) { Text("Hello, World") Text("Hello World!") } } }
zmienia się w połączenie z
buildBlock(_:_:)
:struct ContentView : View { var body: some View { VStack(alignment: .leading) { ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!")) } } }
w wyniku czego nieprzezroczysty typ wyniku
some View
zostanie spełniony przezTupleView<(Text, Text)>
.Zauważysz, że
ViewBuilder
definiuje tylkobuildBlock
do 10 parametrów, więc jeśli spróbujemy zdefiniować 11 podglądów podrzędnych: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!") } }
otrzymujemy błąd kompilatora, ponieważ nie ma metody konstruktora do obsługi tego bloku kodu (zwróć uwagę, że ponieważ ta funkcja jest nadal w toku, komunikaty o błędach wokół niej nie będą tak pomocne).
W rzeczywistości nie sądzę, aby ludzie często napotykali na to ograniczenie, na przykład powyższy przykład byłby lepszy przy użyciu
ForEach
widoku:var body: some View { VStack(alignment: .leading) { ForEach(0 ..< 20) { i in Text("Hello world \(i)") } } }
Jeśli jednak potrzebujesz więcej niż 10 statycznie zdefiniowanych widoków, możesz łatwo obejść to ograniczenie za pomocą
Group
widoku: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
implementuje również inne metody konstruktora funkcji, takie jak: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 }
Daje to możliwość obsługi instrukcji if:
var body: some View { VStack(alignment: .leading) { if .random() { Text("Hello World!") } else { Text("Goodbye World!") } Text("Something else") } }
który zostaje przekształcony w:
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") ) } }
(emitowanie zbędnych 1-argumentowych wezwań do
ViewBuilder.buildBlock
jasności).źródło
ViewBuilder
definiuje tylkobuildBlock
do 10 parametrów - czy to oznacza, żevar body: some View
nie może mieć więcej niż 11 podglądów podrzędnych?ForEach
widok. Możesz jednak użyćGroup
widoku, aby obejść to ograniczenie, zredagowałem moją odpowiedź, aby to pokazać.Analogiczna rzecz jest opisana w filmie Co nowego w Swift WWDC w sekcji o DSL (zaczyna się od ~ 31:15). Atrybut jest interpretowany przez kompilator i tłumaczony na powiązany kod:
źródło