首页 > 解决方案 > @ViewBuilder 闭包的默认值?

问题描述

我们可以给@ViewBuilder闭包参数一个默认值吗?

当我做一些实验时出现了这个问题:

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())

(⭐️这里Gradient提到了我的自定义扩展。)

结果非常好:

在此处输入图像描述

当我尝试更进一步并为这些@ViewBuilder闭包提供默认值时,一切都变糟了:

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())

T无法从上面的代码中推断出类型参数。有任何想法吗?

----[编辑]----

我已经为我的问题做出了一些努力,这就是我到目前为止所得到的:

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())

结果是:

我的看法也是

如果两个参数都可以自动推断出来就完美了。

----[再次编辑]----

根据@Asperi之前的建议,我进行了第三次尝试:

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())

结果是:

我的看法 3

我已经为这些便利初始化器实现了所有需要的扩展,它可以工作,但是如果我们能找到一种方法来避免这些扩展,那将是完美的。

可能吗?

标签: swiftui

解决方案


这是一个可能方法的演示(您可以替换您的Gradient类型) - 您需要使用默认 init 进行扩展以用于特殊类型。

经测试可与 Xcode 12 / iOS 14 一起使用

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

然后你可以使用MyView()它来生成你的默认凹槽和酒吧。


推荐阅读