【问题标题】:How to pass one SwiftUI View as a variable to another View struct如何将一个 SwiftUI 视图作为变量传递给另一个视图结构
【发布时间】: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 给出了正确的答案,现在一切都按预期工作。泛型是进一步查找的重要关键字。

标签: swift generics view protocols swiftui


【解决方案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)
          }
      }
      
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多