12

I stumbled upon a weird behaviour for Buttons in SwiftUI in combination with a custom ButtonStyle.

My target was to create a custom ButtonStyle with some kind of 'push-back animation'. I used the following setup for this:

struct CustomButton<Content: View>: View {
    private let content: () -> Content

    init(content: @escaping () -> Content) {
        self.content = content
    }

    var body: some View {
        VStack {
            Button(action: { ... }) {
                content()
            }
            .buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
        }
    }
}

private struct PushBackButtonStyle: ButtonStyle {
    let pushBackScale: CGFloat

    func makeBody(configuration: Self.Configuration) -> some View {
        configuration
            .label
            .scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
    }
}

// Preview
struct Playground_Previews: PreviewProvider {
    static var previews: some View {
        CustomButton {
            VStack(spacing: 10) {
                HStack {
                    Text("Button Text").background(Color.orange)
                }

                Divider()

                HStack {
                    Text("Detail Text").background(Color.orange)
                }
            }
        }
        .background(Color.red)
    }
}

When I now try to touch on this button outside of the Text view, nothing will happen. No animation will be visible and the action block will not be called.

showcase

What I found out so far:

  • when you remove the .buttonStyle(...) it does work as expected (no custom animation of course)
  • or when you set a .background(Color.red)) on the VStack in the CustomButton it does also work as expected in combination with the .buttonStyle(...)

The question now is if anybody have a better idea of how to properly work around this issue or how to fix it?

2 Answers 2

26

Just add hit testing content shape in your custom button style, like below

Tested with Xcode 11.4 / iOS 13.4

demo

private struct PushBackButtonStyle: ButtonStyle {
    let pushBackScale: CGFloat

    func makeBody(configuration: Self.Configuration) -> some View {
        configuration
            .label
            .contentShape(Rectangle())     // << fix !!
            .scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I wasted an hour of my life trying to fix this. Thank you!
0

Simply use a .frame and it should work. To make it easily testable I have rewritten it like this:

struct CustomButton: View {

    var body: some View {
                    Button(action: {  }) {
        VStack(spacing: 10) {
            HStack {
                Text("Button Text").background(Color.orange)
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.orange)
            }

            Divider()

            HStack {
                Text("Detail Text").background(Color.orange)
                .frame(minWidth: 0, maxWidth: .infinity)
                .background(Color.orange)
            }
                        }
    }
        .buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
    }
}

private struct PushBackButtonStyle: ButtonStyle {
    let pushBackScale: CGFloat

    func makeBody(configuration: Self.Configuration) -> some View {
        configuration
            .label
            .scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
    }
}

I hope I could help. :-)

@Edit With video.

enter image description here

4 Comments

I see what you are thinking of. That might work in this use-case but in more complex use-cases where also a Spacer() is present, it does not work anymore as the Text can't have the full width / height.
See my minimal edited answer with a Spacer(). It does work as intented. Text in SwiftUI takes as much place as we allow it to take with ".infinity". You could also define a value of course.
But my original use-case was with a Stack and Spacer inside of the Button as you can see in my Playground_Previews. There I think it will not work.
Added this scenario in my answer and made a video to show you that it works. Nevertheless is Asperis answer good too of course.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.