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

105

我正在实现一个非常自定义的导航链接,称为MenuItem,并希望在整个项目中重复使用它。这是一个符合View协议并实现var body : some View的结构体,其中包含一个NavigationLink

我需要以某种方式存储将由NavigationLink呈现的视图,但尚未成功实现。

我已在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可以接受一些视图作为参数,那肯定有办法的。 谢谢 ;D


我很难“看到”你的问题。让我知道我哪里错了。你有一个名为MenuItem的视图......它是另一个视图的一部分,后者是NavigationLink的目标吗?就这样吗?如果是这样,为什么不只创建一个MainMenu视图,它具有MenuItem视图并且是你的NavigationLink的目的地?编辑:你能用"具体的"话来给出你想要做的例子吗?我认为让我感到困惑的是什么?(好问题,顺便说一句。只是我认为我不明白你实际想要的输出是什么。) - user7014451
嘿@dfd!感谢回复;D 我已经更新了第一段,以更好地反映我试图创建一个名为“MenuItem”的替代NavigationLink的内容。 @rraphael给出了正确的答案,现在一切都按预期工作。泛型是进一步查找的重要关键字。 - Alex
10个回答

137
总结我在这里阅读的一切和对我有效的解决方案:
struct ContainerView<Content: View>: View {
    @ViewBuilder let content: Content
    
    var body: some View {
        content
    }
}

这不仅允许您在其中放置简单的“View”,而且还可以借助“@ViewBuilder”使用“if-else”和“switch-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")
            }
        }
    }
}

奖励: 如果你想要一个贪婪的容器,它将占用所有可能的空间(与上面的容器相反,后者只占用其子视图所需的空间),在这里:

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

如果您的视图需要一个初始化程序,您可以将参数也设置为@ViewBuilder。即使对于多个参数也适用。
init(@ViewBuilder content: () -> Content) {…}

完美的解决方案在这里。只是想指出,我也使用 ContentView 而不是 Content 在 MacOS 上使其工作。 - LasaleFamine
4
谔谔,Content只是一个通用属性的名称,它不应该会导致某些功能的有效或无效。我刚刚检查了这个解决方案,在macOS上完全没有任何更改就可以正常工作。 - ramzesenok
1
正是我所需要的,解释得非常好 - 谢谢!+1 - LilaQ
谢谢。但是在切换位置参数时如何保持每个视图的滚动位置?我的意思是类似于TabView内容的东西。 - Binh Ho
抱歉,@BinhHo,我不太确定你的意思。能否请你详细说明一下? - ramzesenok
显示剩余2条评论

36

苹果公司的做法是使用函数构建器。有一个预定义的函数构建器称为ViewBuilder。将其作为您的MenuIteminit方法的最后或唯一参数,如下所示:

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

将其分配给类似于此定义的属性:

let viewBuilder: () -> Content

然后,在您想要显示传入的视图的位置,只需像这样调用函数:

HStack {
    viewBuilder()
}

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

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

这将允许您通过最多10个视图并使用if条件等,但如果您希望它更加严格,您将不得不定义自己的函数构建器。我没有做过那样的事情,所以您需要去谷歌一下。


我在这里遇到了一个问题,MenuItem 中有一个 ForEach,它由 ObservedObject 的数组驱动。当 ObservedObject 中的数组更新时,ForEach 不会重新渲染其内容。然而,当相同的代码不放置在 MenuItem 内容中时,它确实会重新渲染。有什么想法吗?对于 MenuItem 中的所有内容都是一样的,当 ObservedObject 的内容更新时,它们都不会重新渲染。 - Othyn
MenuItem 的内容是一个闭包,因此其中的数组是一个副本,您可以尝试更改 MenuItem 的定义,使其将 ObservedObject 数组作为参数,并在调用 viewBuilder 时传递它。 - Nathan Day
1
为使此功能正常工作,您需要将 Content 添加为通用类型,请参见rraphael的答案。 结构体 MenuItemContent:View: View { } - Brett

28

你应该将泛型参数作为 MenuItem 的一部分:

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

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

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

4
非常感谢!我对泛型还不熟悉,所以很高兴看到它实际上并不难。因此,我们基本上是在说:“嘿,有一个包含符合 View 协议的对象的 MenuItem。 我们将称其类型为 ContentdestinationView 是这个类型的,我们需要在 init() 中使用它。” 注意:如果您稍后阅读此内容,则可以删除 init() 因为它已经自动生成了;D - Alex
1
这里的唯一问题是,当ObservedObject刷新Content时,destinationView似乎没有得到更新。有什么想法吗? - Othyn

14

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

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

在ContentView中,我有一个ObservedObject,它动态更新ForEach的数组内容。但是,当我将该ForEach放置在ContentView内的ENavigationView Content中时,当ObservedObject更新时,该ForEach不再重新渲染。有什么想法吗?这对于ContentView中的所有ENavigationView内容都是相同的。 - Othyn
谢谢,这是一个简单明了的例子。通过视图构建器传递给视图的绑定按预期更新。 - Rocket Garden

11

被接受的答案很好也很简单。语法在iOS 14 + macOS 11中变得更加简洁:

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

然后继续像这样使用:

ContainerView{
  ...
}

11
您可以将 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")
    }
}

8

我曾经很费劲地让自己的代码作为 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()
                }
            }
        }
    }
}

2
非常出色。这是一种优雅的视图定制实现。它还将隐藏导航栏集中在一个地方,而不是在所有视图中重复,并且在此情况下进行自定义,例如隐藏列表中的披露指示器。 - Tao-Nhan Nguyen

3

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

在这种情况下,你可以传递一个使用 @Viewbuilder 包装的视图闭包,返回符合 view 协议的自定义类型。例如:

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



2
如果有人试图将两个不同的视图传递给其他视图,并因此错误而无法实现:无法生成表达式的诊断信息;请提交错误报告......
因为我们正在使用<Content: View>,所以您传递的第一个视图将存储其类型,并期望您传递的第二个视图是相同类型的,这样,如果您想要传递一个文本和一个图像,则无法实现。
解决方案很简单,添加另一个内容视图,并命名不同。
例如:
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)
    }
}

调用此视图:

使用方法:

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

1
请注意,擦除视图类型可能会导致性能损失,因为SwiftUI会丢失有关其内部结构的信息。为了解决您的问题,您可以引入第二个泛型类型,并将其限制为View(例如Content2),然后在第二个视图上使用它。 - Alex
你是对的Alex,我会更新我的答案并提供两种类型。 - Alessandro Pace

0

2个视图的语法

struct PopOver<Content, PopView> : View where Content: View, PopView: View {
var isShowing: Bool
@ViewBuilder var content: () -> Content
@ViewBuilder var popover: () -> PopView

var body: some View {
    ZStack(alignment: .center) {
        self
            .content()
            .disabled(isShowing)
            .blur(radius: isShowing ? 3 : 0)
        
        ZStack {
            self.popover()
        }
        .frame(width: 112, height: 112)
        .opacity(isShowing ? 1 : 0)
        .disabled(!isShowing)
        
    }
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接