Creating a button in iOS is simple but if you want a more responsive button like the ones Apple Uses in their app store, you need to get creative with gestures.
Feel free to reproduce and implement this gist however you see fit. https://gist.github.com/willconno/dd891db6764a82bbc6e9633e9ec7b67c
UILongPressGestureRecognizer
In order to be notified when the user first presses on the screen you must look into the state variable of UILongPressGestureRecognizer. By default though, there is a delay before UIGestureRecognizer.State.began is called. You can easily reduce this though by settings delay to 0.
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(onLongPressRecognised(_:)))
longPressGesture.minimumPressDuration = 0
Delegate
The above code works great in a simple interface but if you nest these buttons in a UIScrollView the gesture absorbs all touches and prevents scrolling. A simple workaround is not to block other UIGesturesRecognizer‘s (which UIScrollView uses under-the-hood to manage scrolling) by overriding the delegate on the UILongPressGestureRecognizer we created above and implementing the following delegate method:
class Button: UIView, UIGestureRecognizerDelegate {
longPressGesture.delegate = self
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
By returning true we’re allowing any touches beneath our UILongPressGestureRecognizer to still be called.
Smoothing Animations
We’ve got our button animating now as soon as the user begins a touch down, we’re not stopping our screen from scrolling but now our we can’t stop our button .ended notification from being called. To fix this we simply guard against the function being called after the user begins to scroll:
var safeArea: CGFloat { return 5.0 }
@objc func onLongPressRecognised(_ sender: UILongPressGestureRecognizer) {
switch sender.state {
case .began:
self.initialPos = sender.location(in: parent)
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.allowUserInteraction], animations: {
self.animatedView?.transform = .init(scaleX: 0.95, y: 0.95)
})
case .changed:
let loc = sender.location(in: parent)
if loc.x < (initialPos.x - safeArea)
|| loc.x > (initialPos.x + safeArea)
|| loc.y < (initialPos.y - safeArea)
|| loc.y > (initialPos.y + safeArea) {
self.isCancelled = true
sender.state = .ended
}
case .ended:
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.allowUserInteraction], animations: {
self.animatedView?.transform = .identity
})
if isCancelled {
isCancelled = false
} else {
.. // do stuff
}
}}
You can see I’ve used a safe area here to make the cancellation of this gesture less sensitive and allow a better user experience.
Conclusion
Smooth button animations create an entertaining, interactive user experience. Although Apple loves to use these animations in their apps, they don’t make it readily known to developers how they can implement similar features. UILongPressGestureRecognizer is a very flexible class with a lot of room for experimentation and creativity in creating a rich user interface.