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

Posted

技术标签:

【中文标题】如何将一个 SwiftUI 视图作为变量传递给另一个视图结构【英文标题】:How to pass one SwiftUI View as a variable to another View struct 【发布时间】:2019-11-18 04:45:13 【问题描述】:

我正在实现一个名为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...
    

--> 错误: 协议“视图”只能用作通用约束,因为它具有自身或关联的类型要求。

第二次尝试:

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”的值分配给类型“某些视图”。

最后的尝试:

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...
    

--> 错误: 无法将类型“视图”的值分配给类型“某些视图”。

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

【问题讨论】:

我很难“可视化”您的问题。让我知道我错在哪里。您有一个名为MenuItem 的视图...它是另一个视图的一部分,它是NavigationLink 的目的地?这就是全部?如果是这样,为什么不创建一个具有MenuItem 视图并且是NavigationLink 目标的MainMenu 视图?编辑:你能举一个“具体”的例子来说明你试图用文字做什么吗?我想让我感到困惑的是? (顺便问下好问题。我只是不明白你真正想要输出什么。) 嘿@dfd!感谢您的回复;D 我已经更新了第一段,以更好地反映我正在尝试做的事情,即创建 NavigationLink 的替代方案 MenuItem。 @rraphael 给出了正确的答案,现在一切都按预期工作。泛型是进一步查找的重要关键字。 【参考方案1】:

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

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

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

struct GreedyContainerView<Content: View>: View 
    @ViewBuilder let content: Content
    
    var body: some View 
        Color.clear
            .overlay(content)
    

【讨论】:

这里的完美解决方案。只是要指出我在 MacOS 上也使用ContentView 而不是Content 谢谢 :) 嗯,Content 只是一个通用属性的名称,它不应该导致某些东西工作或不工作。我刚刚检查了这个解决方案,它在 macOS 上完美运行,无需任何更改 正是我想要的,完美的解释 - 谢谢! +1【参考方案2】:

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

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

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

let viewBuilder: () -> Content

然后,你想在哪里显示你传入的视图,只需像这样调用函数:

HStack 
    viewBuilder()

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

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

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

【讨论】:

我有一个错误,MenuItem 中有一个由 ObservedObject 的数组驱动的 ForEach。当 ObservedObject 中的数组更新时,ForEach 不会重新渲染其内容。而当未放置在 MenuItem 内容中时,相同的代码会执行此操作。有任何想法吗? MenuItem 中的所有内容都是一样的,当 ObservedObject 内容更新时,它们都不会重新渲染。 MenuItem 的内容是一个闭包,因此其中的数组是一个副本,您可以尝试更改 MenuItem 的定义,以便它将您的 ObservedObject 数组作为参数,然后在您使用时传递它调用你的 viewBuilder。 为了使其工作,您需要将 Content 添加为通用类型,请参阅 rraphael 的回答。 struct MenuItem: 查看 【参考方案3】:

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

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

    init(destinationView: Content) 
        self.destinationView = destinationView
    

    var body : some View 
        // ...
    

【讨论】:

非常感谢!我对泛型还很陌生,所以很高兴看到它实际上并不难。所以我们基本上是在说“嘿,有一个MenuItem 包含一个符合View 的对象。我们将其类型称为ContentdestinationView 就是这种类型,我们在init() 期间需要它”。稍后阅读本文的任何人请注意:您可以删除 init(),因为它为您合成;D 唯一的问题是,当 ObservedObject 刷新内容时,destinationView 似乎没有更新。有什么想法吗?【参考方案4】:

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

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 内容中时,当 ObservedObject 更新时,该 ForEach 不再重新呈现。有任何想法吗?这对于 ContentView 中的 ENavigationView 中的所有内容都是相同的。 谢谢,这是一个简单的例子。传入视图构建器构建的视图的绑定按预期更新。【参考方案5】:

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

【讨论】:

【参考方案6】:

我真的很难让我的工作扩展View。有关如何调用它的完整详细信息,请参阅here。

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

【讨论】:

优秀。这是视图自定义的优雅实现。它还联合在一个地方隐藏导航栏,而不是在所有视图中重复它,并进行自定义,例如在这种情况下隐藏披露指示器,当封装在列表中时。【参考方案7】:

接受的答案既好又简单。 iOS 14 + macOS 11 使语法更加简洁:

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

然后继续这样使用:

ContainerView
  ...

【讨论】:

【参考方案8】:

您也可以使用静态函数扩展。例如,我对 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)
    



【讨论】:

以上是关于如何将一个 SwiftUI 视图作为变量传递给另一个视图结构的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 如何将@Published 分配给另一个@Published

如何将数据从子视图传递到父视图到 SwiftUI 中的另一个子视图?

如何忽略触摸事件并将它们传递给另一个子视图的 UIControl 对象?

Swift UI 将绑定传递给另一个视图以通过 init 发出警报

QT信号和槽将变量传递给另一种形式

将变量从 UIHostingController 传递到 SWIFTUI 视图