可选的@ViewBuilder 闭包

sup*_*cio 8 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")
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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!()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但我收到此错误:

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

谢谢。

Asp*_*eri 16

考虑到buildIf的特征ViewBuilder的以下方法是可能的,其允许保持ViewBuilderinit(即优选的)

经测试适用于 Xcode 11.2 / iOS 13.2

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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

所以像这个一样工作

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

和这个

struct TopAndBottomView_Previews: PreviewProvider {
    static var previews: some View {
        TopAndBottomView(topContent: {
            Text("TOP")
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 从 Swift 5.7、Xcode 14 开始,这不起作用。您需要使用 EmptyView() 而不是 nil/Optional。所以让它 () -&gt; ContentView = { EmptyView() }。否则父视图无法推断内容类型。 (8认同)
  • 如果 topContent 和 BottomContent 是不同的视图类型,您将如何执行此操作?我创建了一个新的通用属性,但是当使用默认的“nil”参数时,任何调用者都无法推断内容类型。 (3认同)

Mat*_*hew 10

@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") })
Run Code Online (Sandbox Code Playgroud)