2 Expert @ViewBuilder Tips to Make High Reusability Layouts in SwiftUI

Introduction

@ViewBuilder is a custom parameter attribute that constructs views from closures since iOS 13 (since macOS 10.15). Base on @ViewBuilder, SwiftUI uses a declarative syntax so you can simply state what your user interface should do. Therefore, building custom views with @ViewBuilder is professional and productive to improve and reduce your user interface code. The tips below can be helpful to do better.

1. Declare Different Placeholder Types for Each @ViewBuilder Parameters in a Custom View

Usually, a custom view arranges its children in different ways, just like people wear shoes on their feet and take a hat on their head. Tom is below, let’s take a red hat and boot with teeth for him.

Tom Changes to Beautiful Tom

Define two placeholder types HeadContent and FeetContent in the struct declaration, and both of them implement protocol View.

struct Tom<HeadContent: View, FeetContent: View>: View {
    
    var body: some View {
        // ...
    }
}

Declare headContent and feetContent in Tom, to hold functions that are used for hat and boots creation next.

struct Tom<HeadContent: View, FeetContent: View>: View {
    
    var headContent: () -> HeadContent
    var feetContent: () -> FeetContent

    var body: some View {
        // ...
    }
}    

Add two escaping @ViewBuilder parameters in Tom’s constructor that accept different functions next.

init(@ViewBuilder headContent: @escaping () -> HeadContent,
     @ViewBuilder feetContent: @escaping () -> FeetContent) {
    self.headContent = headContent
    self.feetContent = feetContent
}

Draw Tom, with headContent and feetContent over him by overlay modifier.

var body: some View {
        
    Image("boy")
        .resizable()
        .overlay(VStack {
            headContent()
                .frame(maxHeight: 200)
            Spacer()
            feetContent()
        })
}

Take the red hat and wear the boot with teeth for him.

struct ContentView: View {
    
    var body: some View {
        
        Tom {
            Image("hat")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .offset(x: 10, y: -50)
        } feetContent: {
            HStack(spacing: 20) {        
                Image("boot")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxWidth: 120)
                    .rotation3DEffect(
                        .degrees(180),
                        axis: (x: 0.0, y: 1.0, z: 0.0)
                    )
                    
                Image("boot")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxWidth: 120)
            }
            .offset(x: 8)
        }
    }
}

Check out Tom project from Github for full source code.

2. Mark the @ViewBuilder Parameter of a Constructor with @escaping

The @ViewBuilder parameter of a constructor should be an escaping closure called after the constructor returns. As the Apple Developer said, “SwiftUI reads the value of this property any time it needs to update the view, which can happen repeatedly during the life of the view, typically in response to user input or system events. The value that the view returns is an element that SwiftUI draws onscreen”(Declare a Custom View). Therefore, @ViewBuilder parameters should be stored in the init and used in the body.

Conclusion

In summary, SwiftUI starts a new chapter of user interface programming with @ViewBuilder, which makes user interface code less and clearer than before. Then remember the two tips we mentioned above to make your code professional and productive. Firstly, use different placeholder types for each @ViewBuilder parameter when there are multiple @ViewBuilder parameters in constructors. Finally, mark your @ViewBuilder parameters with @escaping, storing them in the init, and using them in the body for performance consideration. Review and optimize your code now!

Leave a Reply

Your email address will not be published. Required fields are marked *