Search code examples
swiftgenericsviewprotocolsswiftui

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


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


Solution

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

    struct ContainerView<Content: View>: View {
        @ViewBuilder let 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) {
        self.content = content()
    }