Pass a SwiftUI view that has its own arguments as a variable to another view struct
Asked Answered
R

2

6

I'll try to outline my case here, I have a NavigationLink I'm wanting to turn into it's own structure so I can re-use it. The Label inside the NavigationLink is the same for all the cases I'm using, just different text and images. I'm trying to make that new struct that contains the NavigationLink have an argument I use for the destination View of the NavigationLink.

I found this link that got me most of the way, but I just don't seem to be able to get it the last mile.

How to pass one SwiftUI View as a variable to another View struct

Here is the re-usable NavigationLink struct I made:

struct MainMenuButtonView<Content: View>: View {
    
    var imageName: String
    var title: String
    var description: String
    var content: Content
    
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        VStack {
            NavigationLink(destination: content) {
                Image(imageName)
                    .resizable()
                    .frame(width: 100, height: 100)
                Text(title)
                    .font(.title)
                Text(description)
                    .foregroundColor(Color(UIColor.systemGray2))
                    .multilineTextAlignment(.center)
                    .font(.footnote)
                    .frame(width: 175)
            }
            .buttonStyle(PlainButtonStyle())
        }
    }
}

I currently don't get any errors on that part, but that doesn't mean there isn't something wrong with it.

And here is where I'm using it at, currently, I just shown one, but I'll have more once I get it working.

struct MainMenuView: View {
    
    var body: some View {
        NavigationView {
            MainMenuButtonView(imageName: "List Icon",
                               title: "Lists",
                               description: "Auto generated shopping lists by store",
                               content: TestMainView(testText: "Lists"))
            }
            .buttonStyle(PlainButtonStyle())
            .navigationBarTitle(Text("Main Menu"))
        }
    }
}

When I leave it as above, it tells me that there is an extra argument and that 'Contect' can't be inferred. Xcode does offer a fix, and it ends up looking like this after I do the fix

MainMenuButtonView<Content: View>(imageName: "List Icon",

But then I get an error that it cannot find 'Content' in scope. I know the main difference between my question and the example I linked above is my View I'm passing in also has arguments. I'm not sure if I'm also supposed to put all the arguments in the callout within the <>.

Thank you for any help you can give.

Randallrandan answered 18/1, 2021 at 21:7 Comment(0)
R
10

You need to correct the init in the MainMenuButtonView:

struct MainMenuButtonView<Content: View>: View {
    var imageName: String
    var title: String
    var description: String
    var content: () -> Content // change to closure

    // add all parameters in the init
    init(imageName: String, title: String, description: String, @ViewBuilder content: @escaping () -> Content) {
        self.imageName = imageName // assign all the parameters, not only `content`
        self.title = title
        self.description = description
        self.content = content
    }

    var body: some View {
        VStack {
            NavigationLink(destination: content()) { // use `content()`
                Image(imageName)
                    .resizable()
                    .frame(width: 100, height: 100)
                Text(title)
                    .font(.title)
                Text(description)
                    .foregroundColor(Color(UIColor.systemGray2))
                    .multilineTextAlignment(.center)
                    .font(.footnote)
                    .frame(width: 175)
            }
            .buttonStyle(PlainButtonStyle())
        }
    }
}

Also, you need to pass a closure to the content parameter (as you indicated in the init):

struct MainMenuView: View {
    var body: some View {
        NavigationView {
            MainMenuButtonView(
                imageName: "List Icon",
                title: "Lists",
                description: "Auto generated shopping lists by store",
                content: { TestMainView(testText: "Lists") } // pass as a closure
            )
            .navigationBarTitle(Text("Main Menu"))
        }
    }
}
Reckless answered 18/1, 2021 at 21:34 Comment(3)
Worked like a charm, Thank you! I just realized I still had the NavigationLink in the MainMenuView as well, so anyone reading, you don't need it in both places.Randallrandan
@JasonBrady Right, I didn't notice - I removed the extra NavigationLink from my answer.Reckless
I did the same on mine too.Randallrandan
C
1

If you want to make the builder empty or nil, you can do it like this:

struct ExampleView: View {
     content: (() -> any View)?
     init() {
          self.content = nil
     }
     init(@ViewBuilder content: @escaping () -> any View) {
          self.content = content
     }
     public var body: some View {
          ...
          if let content {
             AnyView(content())
          }
          ...
     }
}

And you can use it like this:

var body: some View {
    ExampleView()
    ExampleView {
        Color.blue
    }
}
Carollcarolle answered 31/5, 2023 at 19:16 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.