如何将一个 SwiftUI View 作为变量传递给另一个 View 结构

How to pass one SwiftUI View as a variable to another View struct

我正在实施一个名为 MenuItem 非常 的自定义 NavigationLink,并希望在整个项目中重复使用它。它是一个符合 View 并实现 var body : some View 的结构,其中包含一个 NavigationLink。 我需要以某种方式将 NavigationLink 应呈现的视图存储在 MenuItem 的正文中,但尚未这样做。

我在 MenuItem 的正文中将 destinationView 定义为 some View 并尝试了两个初始化程序:

这似乎太简单了:

struct MenuItem: View {
    private var destinationView: some View

    init(destinationView: View) {
        self.destinationView = destinationView
    }

    var body : some View {
        // Here I'm passing destinationView to NavigationLink...
    }
}

-->错误:协议'View'只能用作泛型约束,因为它有Self或关联类型要求。

第二次尝试:

struct MenuItem: View {
    private var destinationView: some View

    init<V>(destinationView: V) where V: View {
        self.destinationView = destinationView
    }

    var body : some View {
        // Here I'm passing destinationView to NavigationLink...
    }
}

--> 错误: 无法将类型 'V' 的值分配给类型 'some View'.

最终尝试:

struct MenuItem: View {
    private var destinationView: some View

    init<V>(destinationView: V) where V: View {
        self.destinationView = destinationView as View
    }

    var body : some View {
        // Here I'm passing destinationView to NavigationLink...
    }
}

--> 错误: 无法将类型 'View' 的值分配给类型 'some View'.

我希望有人能帮助我。如果 NavigationLink 可以接受一些 View 作为参数,则必须有一种方法。 谢谢 ;D

您应该将通用参数设为 MenuItem 的一部分:

struct MenuItem<Content: View>: View {
    private var destinationView: Content

    init(destinationView: Content) {
        self.destinationView = destinationView
    }

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

Apple 的做法是使用函数生成器。有一个预定义的 ViewBuilder。将它设为 MenuIteminit 方法的最后一个参数,或唯一的参数,如下所示:

..., @ViewBuilder builder: @escaping () -> Content)

将其分配给 属性 定义如下:

let viewBuilder: () -> Content

然后,在您想显示 passed-in 视图的地方,只需像这样调用函数:

HStack {
    viewBuilder()
}

您将能够像这样使用您的新视图:

MenuItem {
   Image("myImage")
   Text("My Text")
}

这将允许您传递最多 10 个视图并使用 if 条件等。但如果您希望它更具限制性,则必须定义您自己的函数构建器。我还没有这样做,所以你将不得不 google 那。

您可以像这样创建自定义视图:

struct ENavigationView<Content: View>: View {

    let viewBuilder: () -> Content

    var body: some View {
        NavigationView {
            VStack {
                viewBuilder()
                    .navigationBarTitle("My App")
            }
        }
    }

}

struct ENavigationView_Previews: PreviewProvider {
    static var previews: some View {
        ENavigationView {
            Text("Preview")
        }
    }
}

使用:

struct ContentView: View {

    var body: some View {
        ENavigationView {
            Text("My Text")
        }
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

您可以将 NavigationLink(或任何其他视图小部件)作为变量传递给子视图,如下所示:

import SwiftUI

struct ParentView: View {
    var body: some View {
        NavigationView{

            VStack(spacing: 8){

                ChildView(destinationView: Text("View1"), title: "1st")
                ChildView(destinationView: Text("View2"), title: "2nd")
                ChildView(destinationView: ThirdView(), title: "3rd")
                Spacer()
            }
            .padding(.all)
            .navigationBarTitle("NavigationLinks")
        }
    }
}

struct ChildView<Content: View>: View {
    var destinationView: Content
    var title: String

    init(destinationView: Content,  title: String) {
        self.destinationView = destinationView
        self.title = title
    }

    var body: some View {
        NavigationLink(destination: destinationView){
            Text("This item opens the \(title) view").foregroundColor(Color.black)
        }
    }
}

struct ThirdView: View {
    var body: some View {
        VStack(spacing: 8){

            ChildView(destinationView: Text("View1"), title: "1st")
            ChildView(destinationView: Text("View2"), title: "2nd")
            ChildView(destinationView: ThirdView(), title: "3rd")
            Spacer()
        }
        .padding(.all)
        .navigationBarTitle("NavigationLinks")
    }
}

为了 View 的扩展,我真的很努力地工作。有关如何调用它的完整详细信息,请参阅

View 的扩展(使用泛型)- 记得 import SwiftUI:

extension View {

    /// Navigate to a new view.
    /// - Parameters:
    ///   - view: View to navigate to.
    ///   - binding: Only navigates when this condition is `true`.
    func navigate<SomeView: View>(to view: SomeView, when binding: Binding<Bool>) -> some View {
        modifier(NavigateModifier(destination: view, binding: binding))
    }
}


// MARK: - NavigateModifier
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {

    // MARK: Private properties
    fileprivate let destination: SomeView
    @Binding fileprivate var binding: Bool


    // MARK: - View body
    fileprivate func body(content: Content) -> some View {
        NavigationView {
            ZStack {
                content
                    .navigationBarTitle("")
                    .navigationBarHidden(true)
                NavigationLink(destination: destination
                    .navigationBarTitle("")
                    .navigationBarHidden(true),
                               isActive: $binding) {
                    EmptyView()
                }
            }
        }
    }
}

总结一下我在这里阅读的所有内容以及对我有用的解决方案:

struct ContainerView<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        content
    }
}

这不仅允许您在里面放置简单的 Views,而且,感谢 @ViewBuilder,使用 if-elseswitch-case 块:

struct SimpleView: View {
    var body: some View {
        ContainerView {
            Text("SimpleView Text")
        }
    }
}

struct IfElseView: View {
    var flag = true
    
    var body: some View {
        ContainerView {
            if flag {
                Text("True text")
            } else {
                Text("False text")
            }
        }
    }
}

struct SwitchCaseView: View {
    var condition = 1
    
    var body: some View {
        ContainerView {
            switch condition {
            case 1:
                Text("One")
            case 2:
                Text("Two")
            default:
                Text("Default")
            }
        }
    }
}

奖金: 如果你想要一个贪婪的容器,它将要求所有可能的 space (与上面的容器相反,它只要求其子视图所需的 space )这里是:

struct GreedyContainerView<Content: View>: View {
    @ViewBuilder let content: Content
    
    var body: some View {
        content
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

如果您需要在视图中使用初始值设定项,那么您也可以使用 @ViewBuilder 作为参数。即使对于多个参数,如果您愿意:

init(@ViewBuilder content: () -> Content) {…}

接受的答案很简单。 iOS 14 + macOS 11:

语法变得更清晰
struct ContainerView<Content: View>: View {
  @ViewBuilder var content: Content
    
  var body: some View {
    content
  }
}

然后继续这样使用:

ContainerView{
  ...
}

或者,您可以使用静态函数扩展。例如,我对 Text 做了一个 titleBar 扩展。这使得重用代码变得非常容易。

在这种情况下,您可以传递一个带有视图闭包的@Viewbuilder 包装器,返回一个符合视图的自定义类型。例如:

import SwiftUI

extension Text{
    static func titleBar<Content:View>(
        titleString:String,
        @ViewBuilder customIcon: ()-> Content
    )->some View {
        HStack{
            customIcon()
            Spacer()
            Text(titleString)
                .font(.title)
            Spacer()
        }
        
    }
}

struct Text_Title_swift_Previews: PreviewProvider {
    static var previews: some View {
        Text.titleBar(titleString: "title",customIcon: {
            Image(systemName: "arrowshape.turn.up.backward")
        })
            .previewLayout(.sizeThatFits)
    }
}