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

我正在实现一个名为 MenuItem非常自定义 NavigationLink,并希望在整个项目中重用它。它是一个符合 View并实现包含 NavigationLinkvar body : some View的结构。 我需要以某种方式存储的意见,将由 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...
}
}

错误: Protocol‘ 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”类型。

我希望有人可以帮助我。如果导航链接可以接受一些视图作为一个论点,必须有一个方法。 谢谢 D

49934 次浏览

You should make the generic parameter part of MenuItem:

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


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


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

The way Apple does it is using function builders. There is a predefined one called ViewBuilder. Make it the last argument, or only argument, of your init method for MenuItem, like so:

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

Assign it to a property defined something like this:

let viewBuilder: () -> Content

Then, where you want to diplay your passed-in views, just call the function like this:

HStack {
viewBuilder()
}

You will be able to use your new view like this:

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

This will let you pass up to 10 views and use if conditions etc. though if you want it to be more restrictive you will have to define your own function builder. I haven't done that so you will have to google that.

You can create your custom view like this:

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")
}
}
}

Using:

struct ContentView: View {


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


}


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

You can pass a NavigationLink (or any other view widget) as a variable to a subview as follows:

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")
}
}

I really struggled to make mine work for an extension of View. Full details about how to call it are seen here.

The extension for View (using generics) - remember to 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()
}
}
}
}
}

To sum up everything I read here and the solution which worked for me:

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

var body: some View {
content
}
}

This not only allows you to put simple Views inside, but also, thanks to @ViewBuilder, use if-else and switch-case blocks:

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")
}
}
}
}

Bonus: If you want a greedy container, which will claim all the possible space (in contrary to the container above which claims only the space needed for its subviews) here it is:

struct GreedyContainerView<Content: View>: View {
@ViewBuilder let content: Content
    

var body: some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

If you need an initializer in your view then you can use @ViewBuilder for the parameter too. Even for multiple parameters if you will:

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

The accepted answer is nice and simple. The syntax got even cleaner with iOS 14 + macOS 11:

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

var body: some View {
content
}
}

Then continue to use it like this:

ContainerView{
...
}

Alternatively you can use a static function extension. For example, I make a titleBar extension to Text. This makes it very easy to reuse code.

In this case you can pass a @Viewbuilder wrapper with the view closure returning a custom type that conforms to view. For example:

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




If anyone is trying to pass two different views to other view, and can't do it because of this error:

Failed to produce diagnostic for expression; please submit a bug report...

Because we are using <Content: View>, the first view you passed, the view is going to store its type, and expect the second view you are passing be the same type, this way, if you want to pass a Text and an Image, you will not be able to.

The solution is simple, add another content view, and name it differently.

Example:

struct Collapsible<Title: View, Content: View>: View {
@State var title: () -> Title
@State var content: () -> Content


@State private var collapsed: Bool = true


var body: some View {
VStack {
Button(
action: { self.collapsed.toggle() },
label: {
HStack {
self.title()
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
        

VStack {
self.content()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none)
.clipped()
.animation(.easeOut)
.transition(.slide)
}
}

}

Calling this View:

Collapsible {
Text("Collapsible")
} content: {
ForEach(1..<5) { index in
Text("\(index) test")
}
}