default value for @ViewBuilder closure?

Viewed 953

Can we give @ViewBuilder closure parameter a default value?

This question arose when I was doing some experiments:

import SwiftUI
import PlaygroundSupport

// MyView
struct MyView<S:View, T:View>: View {
    
    let groove: S
    let bar   : T
    let p     : CGFloat = 10  // padding
    
    // ⭐️ no default values
    init(@ViewBuilder groove: () -> S, @ViewBuilder bar: () -> T) {
        self.groove = groove()
        self.bar    = bar()
    }
    
    var body: some View {
        ZStack {
            groove
            bar.padding(p)
        }.frame(height: 80)
    }
}

// content view
struct ContentView: View {
    var body: some View {
        // using MyView
        MyView(groove: { 
            Gradient.down(.gray, .white) // ⭐️ my custom LinearGradient extension
        }, bar: { 
            Gradient.right(.purple, .yellow, .pink) // ⭐️ my custom extension
        })
    }
}

PlaygroundPage.current.setLiveView(ContentView())

(⭐️ my custom Gradient extension is mentioned here.)

and the result was quite good:

enter image description here

When I tried to go a little further and gave those @ViewBuilder closures default values, everything went bad:

import SwiftUI
import PlaygroundSupport

struct MyView<S:View, T:View>: View {
    
    let groove: S
    let bar   : T
    let p     : CGFloat = 10  // padding
    
    // ⭐️ try to give @ViewBuilder closures default values
    init(
        @ViewBuilder 
        groove: () -> S = { Gradient.down(.gray, .white) } as! () -> S, 
        @ViewBuilder 
        bar: () -> T = { Gradient.right(.purple, .yellow, .pink) } as! () -> T
    ) {
        self.groove = groove()
        self.bar = bar()
    }
    
    var body: some View {
        ZStack {
            groove
            bar.padding(p)
        }.frame(height: 80)
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            // ❌ can't infer `T`
            MyView(groove: { 
                Gradient.down(.gray, .white)
            })
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

Type parameter T can not be inferred from the code above. Any ideas?

----[edited]----

I've made some efforts to my question, and here is what I've got so far:

import SwiftUI
import PlaygroundSupport

// default values for @ViewBuilder parameters
@ViewBuilder var defaultGroove: some View {
    Gradient.down(.gray, .white)
}

@ViewBuilder var defaultBar: some View {
    Gradient.right(.purple, .yellow, .pink)
}

// MyView
struct MyView<S:View, T:View>: View {
    
    let groove: S
    let bar   : T
    
    // ⭐️ try to give @ViewBuilder parameters default value
    init(
        @ViewBuilder groove: () -> S = { defaultGroove as! S }, 
        @ViewBuilder bar   : () -> T = { defaultBar as! T }
    ) {
        self.groove = groove()
        self.bar = bar()
    }
    
    var body: some View {
        ZStack {
            groove
            bar.padding(10)
        }.frame(height: 80)
    }
}

// Content View
struct ContentView: View {
    var body: some View {
        VStack {
            
            // `bar` omitted
            MyView<LinearGradient, LinearGradient>(groove: { 
                Gradient.bottomRight(.white, .gray, .white)
            })
            // `groove` omitted
            MyView<LinearGradient, Color>(bar: { 
                Color.pink
            }) 
            // both omitted
            MyView<LinearGradient, LinearGradient>()
            
            // ❌ can't infer `S`
            // ⭐️ it would be perfect if `S` can be inferred.
//              MyView(bar: { 
//                  Gradient.right(.purple, .white)
//              })
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

and the result is:

my view too

It would be perfect if both parameters could be inferred automatically.

----[ edited again ]----

According to @Asperi's previous advise, I've made my third attempt:

import SwiftUI
import PlaygroundSupport

// default values for @ViewBuilder parameters

@ViewBuilder var defaultGroove: some View {
    Gradient.down(.gray, .white)
}

@ViewBuilder var defaultBar: some View {
    Gradient.right(.purple, .yellow, .pink)
}

// MyView
struct MyView<S:View, T:View>: View {
    
    let groove: S
    let bar   : T
    
    // default value for @ViewBuilder parameters
    init(
        @ViewBuilder groove: () -> S = { defaultGroove as! S }, 
        @ViewBuilder bar: () -> T = { defaultBar as! T }
    ) {
        self.groove = groove()
        self.bar = bar()
    }
    
    var body: some View {
        ZStack {
            groove
            bar.padding(10)
        }.frame(height: 80)
    }
}

// ⭐️ conditional extensions for convenience inits

extension MyView where T == LinearGradient {
    /// MyView(groove:)
    init(@ViewBuilder groove: () -> S){
        self.init(
            groove: groove, 
            bar   : { defaultBar as! T }
        )
    }
}

extension MyView where S == LinearGradient {
    /// MyView(bar:)
    init(@ViewBuilder bar: () -> T){
        self.init(
            groove: { defaultGroove as! S }, 
            bar   : bar
        )
    }
}

extension MyView where S == LinearGradient, T == LinearGradient {
    /// MyView()
    init(){
        self.init(
            groove: { defaultGroove as! S }, 
            bar   : { defaultBar    as! T }
        )
    }
}

// Content View
struct ContentView: View {
    var body: some View {
        VStack {
            
            // ⭐️ `S`, `T` are both inferred in the following cases
            
            MyView(groove: { 
                Gradient.bottomRight(.white, .yellow, .green)
            }, bar: {
                Color(white: 0.8)
                    .shadow(color: .black, radius: 3, x: 3, y: 3)
                    .shadow(color: .white, radius: 3, x: -3, y: -3)
            })
            
            // `bar` omitted
            MyView(groove: { 
                Gradient.right(.red, .purple)
            })
            
            // `groove` omitted
            MyView(bar: { 
                Gradient.right(.purple, .white)
                    .shadow(color: .black, radius: 3, x: 0, y: 2)
            })
            
            // `groove`, `bar` both omitted 
            MyView()
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

and the result is:

my view 3

I've implemented all the needed extensions for those convenience intializers, it works but if we could find a way to avoid those extensions in the first place, it would be perfect.

Is it possible?

1 Answers

Here is a demo of possible approach (you can substitute your Gradient type) - you need extension with default init for specialised types.

Tested as worked with Xcode 12 / iOS 14

extension MyView where S == Text, T == Button<Text> {
    init() {
        self.init(groove: { Text("Hello") },
            bar: { Button("World", action: { }) })
    }
}

and then you can just use MyView() which generates your default groove and bar.

Related