bogen
bogen

Reputation: 10432

How to add a custom tap handler on a custom SwiftUI View?

List {
   ItemView(item: item)
       .myCustomTapHandler {
           print("ItemView was tapped, triggered from List!")         
       }
   }
}

struct ItemView: View {
    var body: some View {
    VStack {
        Button(action: {
           // this should fire myCustomTapHandler
        }) {
            Text("Hello world")
        }
        
    }
}

I have a custom ItemView with a simple button. I want to re create the same trailing closure syntax as .onTapGesture, with only triggering when you tap the Button. This will be named .myCustomTapHandler How to do this in SwiftUI?

Upvotes: 2

Views: 1961

Answers (2)

lorem ipsum
lorem ipsum

Reputation: 29474

What you are describing (The dot) is a ViewModifier

struct MyItemListView: View {
    var body: some View {
        List {
            //Using a ViewModifier you make the whole View tappable not just the button
            MyItemView(item: 2)
                .myCustomTapHandler{
                    print("ItemView has custom modifier that makes the whole ItemView tappable")
                }
        }
    }
}

struct MyCustomTapHandler: ViewModifier {
    var myCustomTapHandler: () -> Void
    func body(content: Content) -> some View {
        content
            //Add the onTap to the whole View
            .onTapGesture {
                myCustomTapHandler()
            }
    }
}
extension View {
    func myCustomTapHandler(myCustomTapHandler: @escaping () -> Void) -> some View {
        modifier(MyCustomTapHandler(myCustomTapHandler: myCustomTapHandler))
    }
}

The ViewModifier affects the entire View not just the Button.

But, unless you are doing something else it is just an onTapGesture.

This is likely not the best solution because with the Button in the ItemView you will have inconsistent results.

Sometimes the Button will get the tap and sometimes the ViewModifier will get the tap and given that the View is in a List it will likely make the whole tapping confusing because the List has properties that make the whole row tappable anyway vs just the Text of the `Button

If you want the Button to perform an action that is defined in the ListView you can pass it as a parameter.

This will likely give you the best results.

struct MyItemListView: View {
    
    var body: some View {
        VStack {
            //This passes the custom action to the button
            MyItemView(item: 1){
                print("Button needs to be tapped to trigger this")
            }
        }
    }
}
struct MyItemView: View {
    let item: Int
    var myCustomTapHandler: () -> Void
    
    var body: some View {
        VStack {
            Button(action: {
                myCustomTapHandler()
            }) {
                Text("Hello world")
            }
        }
    }
}

Upvotes: 1

Raja Kishan
Raja Kishan

Reputation: 18994

You can add like this.

struct ItemView: View {
    
    private var action: (() -> Void)? = nil

    
    var body: some View {
        VStack {
            Button(action: {
                action?()
            }) {
                Text("Hello world")
            }
            
        }
    }
    
    func myCustomTapHandler(onAction: @escaping () -> Void) -> Self {
        var view = self
        view.action = onAction
        return view
    }
}

Usage

struct ContentView: View {
    
    var body: some View {
        ItemView()
            .myCustomTapHandler {
                print("Hello word")
            }
    }
}


Another way is...

I don't think with dot property you will get action. You need closure inside the ItemView.

Like this

struct ItemView: View {
    
    var action: () -> Void
    
    var body: some View {
        VStack {
            Button(action: action) {
                Text("Hello world")
            }
            
        }
    }
}

Usage

List {
    ItemView {
        // Here you will get action
        print("ItemView was tapped, triggered from List!")
    }
}

Upvotes: 1

Related Questions