Composable Generic SwiftUI View — Part 1

mein
5 min readNov 1, 2022

Writing composable SwiftUI views is important for view reusability and app scalability. However, the syntax is somewhat tricky when mixing @ViewBuilder, @escaping, closure and function type altogether.

Let’s try to understand all the combinations and learn different levels of abstraction starting from concrete view type all the way up to composable generic view.

Level 1: Concrete View Type

We could use stored property or computed property to compose the view.

struct MyView: View {

var storedPropertyView : Text = Text("view1")

var comptedPropertyView : Text {
Text("view2")
}

var storedPropertyViewInitWithClosure : Text = {
Text("view3")
}()

var body: some View {
Form{
storedPropertyView
comptedPropertyView
storedPropertyViewInitWithClosure
}
}
}

Level 1–1: Concrete View Type With Parameter

It needs to be function type to accept parameter. So we need to implement the view component in closure or func.

struct MyView: View {

var storedPropertyView : (String)->Text = {Text($0)}

var comptedPropertyView : (String)->Text {
{Text($0)}
}

func funcView(_ str: String)->Text {
Text(str)
}


var body: some View {
Form{
storedPropertyView("view1")
comptedPropertyView("view2")
funcView("view3")
}
}
}

Level 1–2: Initialize Concrete View Type With Parameter

struct MyView: View {

var storedPropertyView : (String)->Text

init(storedPropertyView: @escaping (String) -> Text) {
self.storedPropertyView = storedPropertyView
}
var body: some View {
Form{
storedPropertyView("view1")
}
}
}

Then assign value to the initializer. We could imagine varieties of syntax when assigning the value.

struct MyView_Previews: PreviewProvider {
static var parameter1 : (String)->Text = {Text($0)}
static var parameter2 : (String)->Text { {Text($0)} }
static func parameter3(_ str: String)->Text {
Text(str)
}
static func parameter4() -> (String)->Text {
{Text($0)}
}
static func parameter5(_ prefix: String) -> (String)->Text {
{Text(prefix+$0)}
}
static var previews: some View {
Group{
MyView(storedPropertyView: {Text($0)})
MyView(storedPropertyView: parameter1)
MyView(storedPropertyView: parameter2)
MyView(storedPropertyView: parameter3)
MyView(storedPropertyView: parameter4())
MyView(storedPropertyView: parameter5("prefix"))
}
}
}

Level 2: Mixed View Type

We start to introduce @ViewBuilder property wrapper into our view component.

struct MyView: View {

@ViewBuilder var viewBuilderComptedPropertyView : some View {
let flag = Int.random(in: 0...10)
if flag < 5 { Text("view1") }
else { Spacer() }
}

@ViewBuilder func viewBuilderFuncView() -> some View {
let flag = Int.random(in: 0...10)
if flag < 5 { Text("view2") }
else { Spacer() }
}

var body: some View {
Form{
viewBuilderComptedPropertyView
viewBuilderFuncView()
}
}
}

Noted:

  • After Swift 5.4, local declarations inside @ViewBuilder block is supported according to SE-0289 Result Builders.
  • Property wrapper @ViewBuilder can be applied to computed property and function, but can NOT be applied to stored property. Here is an erroneous example.

Result builder attribute ‘ViewBuilder’ can only be applied to a property if it defines a getter

  • Property wrapper @ViewBuilder can NOT be applied to function type either. Here is an erroneous example.

Level 2–1: Mixed View Type With Parameter

We can only use @ViewBuilder with function here.

struct MyView: View {

@ViewBuilder func funcView(_ flag: Bool) -> some View{
if flag { Text("view1") }
else { Spacer() }
}

var body: some View {
Form{
funcView(true)
}
}
}

As mentioned before, @ViewBuilder can not be applied to computed property of function type which means closure is not working here. Here is an erroneous example.

Level 2–2: Initialize Mixed View Type With Parameter

When we declare a stored property which accepts mixed view types, we need to use generic type, for example, the SubView in the following code is generic type.

struct MyView<SubView:View>: View {

var storedPropertyView : (Bool)->SubView

init(storedPropertyView: @escaping (Bool) -> SubView) {
self.storedPropertyView = storedPropertyView
}

var body: some View {
Form{
storedPropertyView(true)
}
}
}

Then we could assign value to the initializer.

struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(storedPropertyView: {Text($0.description)})
MyView(storedPropertyView: {_ in Spacer()})
}
}

But the following syntax would raise error because this closure handles mixed types of view.

There are two solutions for this:

  • Solution 1 : pass the @ViewBuilder result to the initializer.
  • Solution 2 : decorate the parameter of the initializer with property wrapper @ViewBuilder
struct MyView<SubView1:View, SubView2:View>: View {

var solution1 : (Bool)->SubView1
var solution2 : (Bool)->SubView2

init(solution1: @escaping (Bool) -> SubView1,
@ViewBuilder solution2: @escaping (Bool) -> SubView2
) {
self.solution1 = solution1
self.solution2 = solution2
}

var body: some View {
Form{
solution1(true)
solution2(true)
}
}
}
struct MyView_Previews: PreviewProvider {
@ViewBuilder static func funcView(_ flag: Bool)->some View {
if flag{ Text(flag.description) }
else { Spacer() }
}
static var previews: some View {
MyView(solution1: funcView,
solution2: {
if $0{ Text($0.description) }
else { Spacer() }
}
)
}
}

Level 3: Nested View

We could think of nested view as ‘the view with generic parameter’. I will write about this in the next article.

Conclusion:

As you may find out, it needs trial-and-error to make the syntax right. I am not sure whether all of them are explainable by some criteria or not. Or is there some implicit syntax rule?

Sometimes it is easier to learn by example than by syntax rule. That is why I feel the needs to write these examples down.

--

--