如何将一个 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
这不仅允许您将简单的View
s 放入其中,而且感谢@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
Color.clear
.overlay(content)
【讨论】:
这里的完美解决方案。只是要指出我在 MacOS 上也使用ContentView
而不是Content
。
谢谢 :) 嗯,Content
只是一个通用属性的名称,它不应该导致某些东西工作或不工作。我刚刚检查了这个解决方案,它在 macOS 上完美运行,无需任何更改
正是我想要的,完美的解释 - 谢谢! +1【参考方案2】:
Apple 的做法是使用函数构建器。有一个预定义的称为ViewBuilder
。将它作为 MenuItem
的 init
方法的最后一个参数或唯一参数,如下所示:
..., @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您应该将泛型参数作为MenuItem
的一部分:
struct MenuItem<Content: View>: View
private var destinationView: Content
init(destinationView: Content)
self.destinationView = destinationView
var body : some View
// ...
【讨论】:
非常感谢!我对泛型还很陌生,所以很高兴看到它实际上并不难。所以我们基本上是在说“嘿,有一个MenuItem
包含一个符合View
的对象。我们将其类型称为Content
。destinationView
就是这种类型,我们在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 对象?