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

问题描述 投票:0回答:10

我正在实现一个名为 MenuItem

very
自定义 NavigationLink,并希望在整个项目中重用它。它是一个符合
View
并实现
var body : some View
的结构体,其中包含
NavigationLink
。 我需要以某种方式将
NavigationLink
呈现的视图存储在
MenuItem
的主体中,但尚未这样做。

我已在

destinationView
的主体中将
MenuItem
定义为
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”类型的值分配给“某个视图”类型。

最后的尝试:

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 可以接受某些 View 作为参数,则必须有一种方法。 谢谢;D

swift generics view protocols swiftui
10个回答
127
投票

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

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

36
投票

Apple 的做法是使用函数构建器。有一个预定义的称为

ViewBuilder
。使其成为
init
MenuItem
方法的最后一个参数或唯一的参数,如下所示:

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

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

let viewBuilder: () -> Content

然后,在您想要显示传入视图的地方,只需调用如下函数即可:

HStack {
    viewBuilder()
}

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

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

这将允许您传递最多 10 个视图并使用

if
条件等。但如果您希望它更具限制性,则必须定义自己的函数构建器。我还没有这样做,所以你必须谷歌一下。


28
投票

您应该将通用参数作为

MenuItem
的一部分:

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

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

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

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

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

10
投票

接受的答案既好又简单。 iOS 14 + macOS 11,语法变得更加清晰:

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

然后继续这样使用:

ContainerView{
  ...
}

6
投票

我真的很难让我的工作延长

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

3
投票

您也可以使用静态函数扩展。例如,我将 titleBar 扩展为 Text。这使得重用代码变得非常容易。

在这种情况下,您可以传递一个 @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)
    }
}



1
投票

如果有人试图将两个不同的视图传递给其他视图,并且由于此错误而无法做到这一点:

无法生成表达诊断;请提交错误报告...

因为我们正在使用,您传递的第一个视图,该视图将存储其类型,并期望您传递的第二个视图具有相同的类型,这样,如果您想传递文本和图像,您可以将无法。

解决方案很简单,添加另一个内容视图,并以不同的方式命名。

示例:

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

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

}

© www.soinside.com 2019 - 2024. All rights reserved.