I've had a look around for a good solution to this, but they all have their downsides. An approach I have very often seen (here) revolves around creating a custom ViewModifier
with a DragGesture
on the button content that recognizes user touch and release. There are many issues with this solution, mainly that the gesture is given priority over any other gesture. This makes it impossible to use the buttons in a ScrollView
.
From iOS 13.0+, Apple provides a ButtonStyle
protocol primarily made for swiftly styling buttons. The protocol exposes an isPressed
variable that perfectly reflects the button state. By this I mean that when you press and hold a button and drag your finger outside its view, the variable changes from true
to false
just as you would expect. This is not the case with the previous solutions I linked to.
The solution to the problem is to first define a new ButtonStyle with a variable binding isPressed
:
struct IsPressedRegisterStyle : ButtonStyle {
@Binding var isPressed : Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: {newVal in
isPressed = newVal
})
}
}
And then use the style with this:
struct IconButton : View {
@State var width: CGFloat? = nil
@State var height: CGFloat? = nil
let action : () -> Void
@State private var isPressed : Bool = false
var body : some View {
Image(isPressed ? "someImage" : "someOtherImage")
.renderingMode(.original)
.resizable()
.scaledToFit()
.frame(width: width, height: height)
.overlay{
GeometryReader{proxy in
Button(
action:{
action()
},
label: {
Spacer()
.frame(width: proxy.size.width, height: proxy.size.height)
.contentShape(Rectangle())
})
.buttonStyle(IsPressedRegisterStyle(isPressed: $isPressed))
}
}
}
}
Note the use of the custom ButtonStyle
at the bottom of its definition.
Just a quick walkthrough of what exactly happens here:
- We create an
Image
and overlay a Button
that spans its entire area using a spacer.
- Pressing the image triggers the overlaying button.
- The custom
ButtonStyle
we apply exposes the internal isPressed
variable of the button to our IconButton
view.
- We set the button image according to the exposed value.
You instantiate a button like this:
// arbitrary values for width and height
IconButton(width: 200, height: 70){
print("Button Pressed")
}
I have not tested this code for too much, so please let me know if there are any downsides to using this.
Cheers!