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

SwiftGenericsViewProtocolsSwiftui

Swift Problem Overview


I'm implementing a very custom NavigationLink called MenuItem and would like to reuse it across the project. It's a struct that conforms to View and implements var body : some View which contains a NavigationLink. I need to somehow store the view that shall be presented by NavigationLink in the body of MenuItem but have yet failed to do so.

I have defined destinationView in MenuItem's body as some View and tried two initializers:

This seemed too easy:

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

--> Error: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.

2nd try:

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

--> Error: Cannot assign value of type 'V' to type 'some View'.

Final try:

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

--> Error: Cannot assign value of type 'View' to type 'some View'.

I hope someone can help me. There must be a way if NavigationLink can accept some View as an argument. Thanks ;D

Swift Solutions


Solution 1 - Swift

To sum up everything I read here and the solution which worked for me:

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

This not only allows you to put simple Views inside, but also, thanks to @ViewBuilder, use if-else and switch-case blocks:

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

Bonus: If you want a greedy container, which will claim all the possible space (in contrary to the container above which claims only the space needed for its subviews) here it is:

struct GreedyContainerView<Content: View>: View {
    @ViewBuilder let content: Content
    
    var body: some View {
        content
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

If you need an initializer in your view then you can use @ViewBuilder for the parameter too. Even for multiple parameters if you will:

init(@ViewBuilder content: () -> Content) {…}

Solution 2 - Swift

The way Apple does it is using function builders. There is a predefined one called ViewBuilder. Make it the last argument, or only argument, of your init method for MenuItem, like so:

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

Assign it to a property defined something like this:

let viewBuilder: () -> Content

Then, where you want to diplay your passed-in views, just call the function like this:

HStack {
    viewBuilder()
}

You will be able to use your new view like this:

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

This will let you pass up to 10 views and use if conditions etc. though if you want it to be more restrictive you will have to define your own function builder. I haven't done that so you will have to google that.

Solution 3 - Swift

You should make the generic parameter part of MenuItem:

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

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

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

Solution 4 - Swift

You can create your custom view like this:

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

Using:

struct ContentView: View {
    
    var body: some View {
        ENavigationView {
            Text("My Text")
        }
    }
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution 5 - Swift

You can pass a NavigationLink (or any other view widget) as a variable to a subview as follows:

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

Solution 6 - Swift

The accepted answer is nice and simple. The syntax got even cleaner with iOS 14 + macOS 11:

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

Then continue to use it like this:

ContainerView{
  ...
}

Solution 7 - Swift

I really struggled to make mine work for an extension of View. Full details about how to call it are seen here.

The extension for View (using generics) - remember to 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()
                }
            }
        }
    }
}

Solution 8 - Swift

Alternatively you can use a static function extension. For example, I make a titleBar extension to Text. This makes it very easy to reuse code.

In this case you can pass a @Viewbuilder wrapper with the view closure returning a custom type that conforms to view. For example:

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


Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlexView Question on Stackoverflow
Solution 1 - SwiftramzesenokView Answer on Stackoverflow
Solution 2 - SwiftNathan DayView Answer on Stackoverflow
Solution 3 - SwiftrraphaelView Answer on Stackoverflow
Solution 4 - SwiftMickael BelhassenView Answer on Stackoverflow
Solution 5 - SwiftYarmView Answer on Stackoverflow
Solution 6 - SwiftClifton LabrumView Answer on Stackoverflow
Solution 7 - SwiftGeorgeView Answer on Stackoverflow
Solution 8 - SwiftNic WanavitView Answer on Stackoverflow