可选的@ViewBuilder闭包

时间:2020-03-14 22:32:12

标签: ios swift swiftui

在SwiftUI中是否可以有一个可选的@ViewBuilder闭包?例如,假设我要开发一个自定义视图,该视图需要两个这样的视图构建器闭包:

import SwiftUI

struct TopAndBottomView<Content>: View where Content: View {
    let topContent: () -> Content
    let bottomContent: () -> Content

    init(@ViewBuilder topContent: @escaping () -> Content, @ViewBuilder bottomContent: @escaping () -> Content) {
        self.topContent = topContent
        self.bottomContent = bottomContent
    }

    var body: some View {
        VStack {
            topContent()
            Spacer()
            bottomContent()
        }
    }
}

struct TopAndBottomView_Previews: PreviewProvider {
    static var previews: some View {
        TopAndBottomView(topContent: {
            Text("TOP")
        }, bottomContent: {
            Text("BOTTOM")
        })
    }
}

但是我希望底视图是可选的。我尝试过:

struct TopAndBottomView<Content>: View where Content: View {
    let topContent: () -> Content
    let bottomContent: (() -> Content)?

    init(@ViewBuilder topContent: @escaping () -> Content, @ViewBuilder bottomContent: (() -> Content)? = nil) {
        self.topContent = topContent
        self.bottomContent = bottomContent
    }

    var body: some View {
        VStack {
            topContent()
            Spacer()
            if bottomContent != nil {
                bottomContent!()
            }
        }
    }
}

但我收到此错误:

函数构建器属性“ ViewBuilder”只能应用于 函数类型的参数。

谢谢。

4 个答案:

答案 0 :(得分:9)

考虑到buildIf的{​​{1}}功能,可以采用以下方法将ViewBuilder保留在ViewBuilder中(最好)

经过测试,可与Xcode 11.2 / iOS 13.2一起使用

init

因此就是这样

struct TopAndBottomView<Content>: View where Content: View {
    let topContent: () -> Content
    let bottomContent: () -> Content?

    init(@ViewBuilder topContent: @escaping () -> Content, 
         @ViewBuilder bottomContent: @escaping () -> Content? = { nil }) {
        self.topContent = topContent
        self.bottomContent = bottomContent
    }

    var body: some View {
        VStack {
            topContent()
            Spacer()
            bottomContent()
        }
    }
}

还有这个

struct TopAndBottomView_Previews: PreviewProvider {
    static var previews: some View {
        TopAndBottomView(topContent: {
            Text("TOP")
        }, bottomContent: {
            Text("BOTTOM")
        })
    }
}

答案 1 :(得分:6)

@JoeBayLD问:

如果topContent和bottomContent是不同的视图类型,您将如何做?我做了一个新的泛型属性,但是当使用默认的'nil'参数时,任何调用者都无法推断内容类型

您可以将两个ViewBuilder参数设置为非可选,然后通过扩展where BottomContent == EmptyView处理“无底内容”情况:

struct TopAndBottomView<TopContent: View, BottomContent: View>: View {
    let topContent: TopContent
    let bottomContent: BottomContent

    init(@ViewBuilder topContent: () -> TopContent,
         @ViewBuilder bottomContent: () -> BottomContent) {
        self.topContent = topContent()
        self.bottomContent = bottomContent()
    }

    var body: some View {
        VStack {
            topContent
            Spacer()
            bottomContent
        }
    }
}

extension TopAndBottomView where BottomContent == EmptyView {
    init(@ViewBuilder topContent: () -> TopContent) {
        self.init(topContent: topContent, bottomContent: { EmptyView() })
    }
}

// usage

TopAndBottomView(topContent: { Text("hello") })

TopAndBottomView(topContent: { Text("hello") }, bottomContent: { Text("world") })

答案 2 :(得分:2)

在Sundell的这个奇妙的post中,他建议我们构建一个自定义struct Unwrap,以解开可选值并将其转换为View,以下代码是他在那篇文章中所做的:

import SwiftUI

/// # Unwrap
/// unwraps a value (of type `Value`) and turns it 
/// into `some View` (== `Optional<Content>`).
struct Unwrap<Value, Content: View>: View {
    
    private let value  : Value?               // value to be unwrapped
    private let content: (Value) -> Content   // closure: turn `Value` into `Content`
    
    init(
        _ value: Value?,
         @ViewBuilder content: @escaping (Value) -> Content  // ⭐️ @ViewBuilder
    ) {
        self.value   = value
        self.content = content
    }
    
    var body: some View {   
        // map: (by the closure `content`)
        // nil (Optional<Value>.none)  -> nil (Optional<Content>.none)
        // Optional<Value>.some(Value) -> Optional<Content>.some(Content)
        value.map(content)  // Optional<Content>
    }
}

然后我写了一些代码来演示如何使用Unwrap来构建视图:

import SwiftUI

// MyView
struct MyView: View {
    
    @State private var isValue1Nil = false
    @State private var isValue2Nil = false
    
    var value1: Int? { isValue1Nil ? nil : 1}
    var value2: Int? { isValue2Nil ? nil : 2}
    
    var body: some View {
        VStack {
            
            // stack of `Unwrap`s
            VStack {
                // ⭐️ `Unwrap` used here.
                Unwrap(value1) {
                    Color.red.overlay(Text("\($0)"))
                }
                Unwrap(value2) {
                    Color.orange.overlay(Text("\($0)"))
                }
            }.border(Color.blue, width: 3)
                
            // toggles
            HStack {
                Toggle(isOn: $isValue1Nil) {
                    Text("value1 is nil")
                }
                Toggle(isOn: $isValue2Nil) {
                    Text("value2 is nil")
                }
                Spacer()
            }
                .padding()
                .overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
                
        } // VStack (container)
            .padding()
            .border(Color.gray, width: 3)
    }
}

结果如下:

optional views

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

或者,我们可以扩展View来完成这项工作:

// view.ifLet(_:then:)
extension View {
    @ViewBuilder func ifLet<Value, Content: View>(
        _ value: Value?, 
        @ViewBuilder then modifySelfWithValue: (Self, Value) -> Content 
    ) -> some View {
        if value != nil {
            modifySelfWithValue(self, value!)
        } else { self }
    }
}

以下是有关如何使用此扩展程序的另一个演示:

struct ContentView: View {
    
    @State private var isNil = false
    var value: Int? { isNil ? nil : 2 }
    
    var body: some View {
        VStack {
            
            Color.red.overlay(Text("1"))
                // ⭐️ view.ifLet(_:then:)
                .ifLet(value) { (thisView, value) in
                    // construct new view with `thisView` and `value`
                    VStack {
                        thisView
                        Color.orange.overlay(Text("\(value)"))
                    }
            } // view modified by `ifLet`
                .border(Color.blue, width: 3)
                
            // toggles
            Toggle(isOn: $isNil) { Text("value is nil") }
                .padding()
                .overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
            
        } // VStack (container)
            .padding()
            .border(Color.gray, width: 3).frame(height: 300)
    }
}

结果是:

optional view

答案 3 :(得分:0)

似乎您在初始化程序中不需要@ViewBuilder,因此可以使用:

struct TopAndBottomView<Content>: View where Content: View {
    let topContent: () -> Content
    let bottomContent: (() -> Content)?

    init(@ViewBuilder topContent: @escaping () -> Content, bottomContent: (() -> Content)?) {
        self.topContent = topContent
        self.bottomContent = bottomContent
    }

    var body: some View {
        VStack {
            topContent()
            Spacer()

            if bottomContent != nil {
                bottomContent!()
            }
        }
    }
}