Read more
Disclaimer: I don't work on the SwiftUI team and have no idea if, when, or how they might choose to adopt buildPartialBlock
. I'm just discussing designs that I believe, based on publicly-available information, they could adopt if this proposal were accepted. Also, everything you see here is untested.
For reference, here's a rough approximation of how SwiftUI is currently implementing buildBlock
, reconstructed from its module interface (but with cleaner formatting and without a bunch of keyword/attribute/extension clutter):
@resultBuilder struct ViewBuilder {
static func buildBlock() -> EmptyView {
EmptyView()
}
static func buildBlock<Content>(_ content: Content) -> Content where Content: View {
content
}
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
return .init((c0, c1))
}
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
return .init((c0, c1, c2))
}
static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0: View, C1: View, C2: View, C3: View {
return .init((c0, c1, c2, c3))
}
static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0: View, C1: View, C2: View, C3: View, C4: View {
return .init((c0, c1, c2, c3, c4))
}
static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View {
return .init((c0, c1, c2, c3, c4, c5))
}
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View {
return .init((c0, c1, c2, c3, c4, c5, c6))
}
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View {
return .init((c0, c1, c2, c3, c4, c5, c6, c7))
}
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View {
return .init((c0, c1, c2, c3, c4, c5, c6, c7, c8))
}
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 {
return .init((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9))
}
// ...other builder methods omitted...
}
I see two ways SwiftUI could adopt buildPartialBlock
. First, there's the simple way which gives slightly different results:
@resultBuilder struct ViewBuilder {
// Still need this to allow empty blocks:
static func buildBlock() -> EmptyView {
EmptyView()
}
static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View {
content
}
static func buildPartialBlock<C0, C1>(accumulated c0: C0, next c1: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
return .init((c0, c1))
}
// ...other builder methods omitted...
}
The difference between the original implementation and this simple one is that, given this view:
struct MyView: View {
var body: some View {
View1()
View2()
View3()
}
}
The builder implementation with multiple buildBlock
overloads will produce a TupleView<(View1, View2, View3)>
, whereas the implementation with a single buildPartialBlock
will produce a TupleView<(TupleView<(View1, View2)>, View3)>
instead. Even though these types are different, though, they would actually both work fine. This is probably what I would have taught in my talk if buildPartialBlock
had been available.
(If buildPartialBlock
had been available from the beginning, I imagine SwiftUI would have implemented a much simpler PairView<C0: View, C1: View>
instead of the TupleView<Tuple>
they use now, which I believe leans on some runtime magic to extract the views from the tuple. But this is basically fanfiction about an alternate universe.)
If this difference in types was unacceptable, ViewBuilder
could take a second approach instead. It could retain the same overload set it had before, but change most of the buildBlock
methods to buildPartialBlock
. This would expand functionality while keeping the same types for existing code:
@resultBuilder struct ViewBuilder {
// Still need this to allow empty blocks:
static func buildBlock() -> EmptyView {
EmptyView()
}
static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View {
content
}
static func buildPartialBlock<C0, C1>(accumulator: C0, next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
return .init((accumulator, next))
}
static func buildPartialBlock<C0, C1, C2>(accumulator: TupleView<(C0, C1)>, next: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
return .init((accumulator.value.0, accumulator.value.1, next))
}
static func buildPartialBlock<C0, C1, C2, C3>(accumulator: TupleView<(C0, C1, C2)>, next: C3) -> TupleView<(C0, C1, C2, C3)> where C0: View, C1: View, C2: View, C3: View {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, next))
}
static func buildPartialBlock<C0, C1, C2, C3, C4>(accumulator: TupleView<(C0, C1, C2, C3)>, next: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0: View, C1: View, C2: View, C3: View, C4: View {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, accumulator.value.3, next))
}
static func buildPartialBlock<C0, C1, C2, C3, C4, C5>(accumulator: TupleView<(C0, C1, C2, C3, C4)>, next: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, accumulator.value.3, accumulator.value.4, next))
}
static func buildPartialBlock<C0, C1, C2, C3, C4, C5, C6>(accumulator: TupleView<(C0, C1, C2, C3, C4, C5)>, next: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, accumulator.value.3, accumulator.value.4, accumulator.value.5, next))
}
static func buildPartialBlock<C0, C1, C2, C3, C4, C5, C6, C7>(accumulator: TupleView<(C0, C1, C2, C3, C4, C5, C6)>, next: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, accumulator.value.3, accumulator.value.4, accumulator.value.5, accumulator.value.6, next))
}
static func buildPartialBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(accumulator: TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)>, next: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, accumulator.value.3, accumulator.value.4, accumulator.value.5, accumulator.value.6, accumulator.value.7, next))
}
static func buildPartialBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(accumulator: TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)>, next: 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 {
return .init((accumulator.value.0, accumulator.value.1, accumulator.value.2, accumulator.value.3, accumulator.value.4, accumulator.value.5, accumulator.value.6, accumulator.value.7, accumulator.value.8, next))
}
// ...other builder methods omitted...
}
Both the currently shipping implementation and this more complex replacement would produce TupleView<(View1, View2, View3)>
for the three-subview view we looked at before. The difference is that, given a view with over ten subviews (since that's the limit of the largest overload):
struct MyView: View {
var body: some View {
View1()
View2()
View3()
View4()
View5()
View6()
View7()
View8()
View9()
View10()
View11()
View12()
}
}
With the original builder, the compiler just gives up and emits an error (I believe it complains about the buildBlock
call having too many arguments). But the builder with an overloaded buildPartialBlock(accumulator:next:)
would neatly group the views into tuples of ten before wrapping them in another nested tuple, producing a result like TupleView<(TupleView<(View1, View2, View3, View4, View5, View6, View7, View8, View9, View10)>, View11, View12)>
. (Or at least it could do that--it might take a little tweaking. Like I said, I haven't tested this.)