mallow
mallow

Reputation: 2856

SwiftUI - reusable components with links to other views as parameters

I would like to create reusable components in my app.

I have searched for similar problem. But I have only found much more complex examples.

Let's try this simple example - a button that could open different Views based on passed parameter. I have 2 views that I will open as a sheet:

FirstView.swift

import SwiftUI

struct FirstView: View {
    var body: some View {
        Text("First view")
    }
}

SecondView.swift

struct SecondView: View {
    var body: some View {
        Text("Second view")
    }
}

ButtonView.swift This is a view I would like to use as a reusable component in my design system.

import SwiftUI

struct ButtonView: View {

    @State private var showModal: Bool = false
    
    // This works
    var text: String
    // Here I am getting an error:
    // Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
    var link: View
    
    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                self.showModal = true
            }) {
                Text(text)
                    .padding(20)
                    .foregroundColor(Color.white)
            }.sheet(isPresented: self.$showModal) {
                link
            }
            .background(Color.blue)
        }
    }
}

struct ButtonView_Previews: PreviewProvider {
    static var previews: some View {
        ButtonView(text: "TEST", link: FirstView())
    }
}

ContentView.swift Here I am trying to use the same button component, but with different labels and links.

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack {
            ButtonView(text: "first", link: FirstView())
                .padding()
            ButtonView(text: "second", link: SecondView())
                .padding()
        }
    }
}

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

Passing String parameters works. Labels are different. But I cannot make it work with links to different Views. I am getting an error:

Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements

Upvotes: 2

Views: 2734

Answers (1)

Shivani Bajaj
Shivani Bajaj

Reputation: 1046

Keeping First view and Second View as the same, use the following for the ButtonView:

struct ButtonView<Content : View>: View {

    @State private var showModal: Bool = false
    
    var text: String
 
    // This is the generic content parameter
    let content: Content
    
    init(text: String, @ViewBuilder contentBuilder: () -> Content){
        self.text = text
        self.content = contentBuilder()
    }
    
    var body: some View {
        VStack {
            Spacer()
            Button(action: {
                self.showModal = true
            }) {
                Text(text)
                    .padding(20)
                    .foregroundColor(Color.white)
            }.sheet(isPresented: self.$showModal) {
                content
            }
            .background(Color.blue)
        }
    }
}

Here the generic parameter named content is used to receive any view and the initializer is used with the @ViewBuilder property wrapper to build the view.Now use it in the following way in ContentView struct:

struct ContentView: View {
    var body: some View {
        HStack {
            ButtonView(text: "First") {
                FirstView()
            }
            ButtonView(text: "Second") {
                SecondView()
            }
        }
    }
}

It will work like a charm :)

Also if you want to keep preview for ButtonView and don't want it to crash then add the preview as:

struct ButtonView_Previews: PreviewProvider {     
    static var previews: some View {         
       ButtonView(text: "First") {             
              FirstView()
        }
     }
 }

Upvotes: 2

Related Questions