Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 930
Alex Avatar asked Jul 08 '19 16:07

Alex


People also ask

Why does SwiftUI use structs for views?

First, there is an element of performance: structs are simpler and faster than classes. I say an element of performance because lots of people think this is the primary reason SwiftUI uses structs, when really it's just one part of the bigger picture.

How do you pass a function as a parameter in SwiftUI?

To pass a function as a parameter into another function in Swift, the accepted parameter type needs to describe a function. Now you can call this function by passing it an argument that is any function that takes two doubles and returns a double.

How do I transfer data from SwiftUI to UIKit?

First, use binding to your input view. And for action use closure to get action from SwiftUI to UIKit.

What is @ViewBuilder?

@ViewBuilder is a kind of result builder that's specifically designed to help create child views. Result builders create functions that build a result from a sequence of elements. SwiftUI uses this in its own native views, controls and components. It also uses this in the body to compose your views.


5 Answers

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) {…}
like image 123
ramzesenok Avatar answered Oct 19 '22 15:10

ramzesenok


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.

like image 34
Nathan Day Avatar answered Oct 19 '22 17:10

Nathan Day


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 {
        // ...
    }
}
like image 22
rraphael Avatar answered Oct 19 '22 17:10

rraphael


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()
    }
}
like image 12
Mickael Belhassen Avatar answered Oct 19 '22 17:10

Mickael Belhassen


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")
    }
}
like image 8
Yarm Avatar answered Oct 19 '22 15:10

Yarm