Reputation: 533
I'm trying to implement haptic feedback at the beginning of a tap for a button in SwiftUI. Therefore I'm trying to use simultaneousGesture, but I'm sill struggling; I can't manage to figure out when the tap begins.
Also there is no haptic feedback implemented for Swift UI, so I guess I would blend it in from UIKit?
I tried to implement the updating method of the TapGesture but it does not seem to do anything. This is what I've got so far.
struct HapticButton : View {
@GestureState var isDetectingTap = false
var body: some View {
let tap = TapGesture()
.updating($isDetectingTap) { (body, stateType, transaction) in
// nothing happens below
print(body)
print(stateType)
print(transaction)
}.onEnded { _ in
// this one works but it is to late
// I need to figure out the beginning of the tap
print("Button was tapped, will invoke haptic feedback, maybe with UIKit")
}
return Button(action: {
print("Action executed")
}) {
HStack {
Image("icon")
Text("Login")
}
}.simultaneousGesture(tap)
}
}
Upvotes: 52
Views: 39187
Reputation: 119292
From iOS 17, you can use a native SwiftUI view modifier called sensoryFeedback
like:
.sensoryFeedback(.success, trigger: <#YourTrigger#>)
It also has overloads to give you more control and you can return an impact like:
.sensoryFeedback(trigger: randomNumber) { old, new in
.impact(flexibility: .solid, intensity: <#amount#>)
}
Upvotes: 16
Reputation: 1
I think the button doesn’t have a property for detecting before the button is let go, so my idea is to create a View that acts as a button instead, so the haptic is delivered as soon as the button is tapped and not after you let go. Maybe try that! This also enables you to create an animation that is played as soon as the button is pressed, rather than after, and it also prevents that weird glitchy button thing. It’s a shame that Apple doesn’t support something as simple as this......
Upvotes: 0
Reputation: 6957
I've made a couple of extensions that let you use Button
or onTapGesture
just normally, adding some impact feedback style.
Example usage:
Button(.heavy) {
something()
} label: {
Text("Beam me up, Scotty!")
}
VStack {
Text("Yo yo yo")
Text("What uuup!")
}.onTapGesture(.heavy) { something() }
Here's the code, it's pretty self-explanatory, just a simple higher-order function which generates some feedback and then calls a given function. Then that function is basically bundled up nicely into a ViewModifier
for onTapGesture
, and an extra constructor for Button
.
import SwiftUI
func withFeedback(
_ style: UIImpactFeedbackGenerator.FeedbackStyle,
_ action: @escaping () -> Void
) -> () -> Void {
{ () in
let impact = UIImpactFeedbackGenerator(style: style)
impact.prepare()
impact.impactOccurred()
action()
}
}
struct HapticTapGestureViewModifier: ViewModifier {
var style: UIImpactFeedbackGenerator.FeedbackStyle
var action: () -> Void
func body(content: Content) -> some View {
content.onTapGesture(perform: withFeedback(self.style, self.action))
}
}
extension View {
func onTapGesture(
_ style: UIImpactFeedbackGenerator.FeedbackStyle,
perform action: @escaping () -> Void
) -> some View {
modifier(HapticTapGestureViewModifier(style: style, action: action))
}
}
extension Button {
init(
_ style: UIImpactFeedbackGenerator.FeedbackStyle,
action: @escaping () -> Void,
@ViewBuilder label: () -> Label
) {
self.init(action: withFeedback(style, action), label: label)
}
}
Upvotes: 3
Reputation: 2764
As I know, this is the most simplest way to get haptic feedback in SwiftUI
When tapping a button :
Button(action: {
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
}) {
Text("This is a Button")
}
You can change the intensity of the haptic feedback by replacing .medium
with .soft
, .light
, .heavy
, or .rigid
or when tapping anything else :
.onTapGesture {
let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
impactHeavy.impactOccurred()
}
If you want to make something like Haptic Touch, replace .onTapGesture
with .onLongPressGesture
like this
.onLongPressGesture {
let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
impactHeavy.impactOccurred()
}
Upvotes: 96
Reputation: 4858
You can also encapsulate the Haptic generator in a singleton:
import UIKit
class Haptics {
static let shared = Haptics()
private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
}
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) {
UINotificationFeedbackGenerator().notificationOccurred(feedbackType)
}
}
Callsite:
// As a button action
Button(action: {
// some action
Haptics.shared.play(.heavy)
}, label: {
Text("Button")
})
// As a tap gesture on a View
SomeView()
.onTapGesture(perform: {
Haptics.shared.play(.light)
})
// In a function
func someFunc() {
Haptics.shared.notify(.success)
}
Options:
Haptics.shared.play(.heavy)
Haptics.shared.play(.light)
Haptics.shared.play(.medium)
Haptics.shared.play(.rigid)
Haptics.shared.play(.soft)
Haptics.shared.notify(.error)
Haptics.shared.notify(.success)
Haptics.shared.notify(.warning)
Upvotes: 27
Reputation: 2907
I have a wrapper view that plays a haptic event, and then calls the action:
struct HapticButton: View {
let content: String
let action: () -> Void
init(_ content: String, _ action: @escaping () -> Void) {
self.content = content
self.action = action
}
var body: some View {
Button(content) {
HapticService.shared.play(event: .buttonTap)
self.action()
}
}
}
Used like:
HapticButton("Press me") { print("hello") } // plays haptic and prints
Upvotes: -2
Reputation: 4605
pure SwiftUI solution below:
HStack {
Image("icon")
Text("Login")
}
.onTouchGesture(
touchBegan: { self.generateHapticFeedback = true },
touchEnd: { _ in self.generateHapticFeedback = false }
)
Just add this snippet somewhere in your project:
struct TouchGestureViewModifier: ViewModifier {
let touchBegan: () -> Void
let touchEnd: (Bool) -> Void
@State private var hasBegun = false
@State private var hasEnded = false
private func isTooFar(_ translation: CGSize) -> Bool {
let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
return distance >= 20.0
}
func body(content: Content) -> some View {
content.gesture(DragGesture(minimumDistance: 0)
.onChanged { event in
guard !self.hasEnded else { return }
if self.hasBegun == false {
self.hasBegun = true
self.touchBegan()
} else if self.isTooFar(event.translation) {
self.hasEnded = true
self.touchEnd(false)
}
}
.onEnded { event in
if !self.hasEnded {
let success = !self.isTooFar(event.translation)
self.touchEnd(success)
}
self.hasBegun = false
self.hasEnded = false
})
}
}
extension View {
func onTouchGesture(touchBegan: @escaping () -> Void = {},
touchEnd: @escaping (Bool) -> Void = { _ in }) -> some View {
modifier(TouchGestureViewModifier(touchBegan: touchBegan, touchEnd: touchEnd))
}
}
Upvotes: 3
Reputation: 14310
You can use UIFeedbackGenerator like this:
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.error)
Or, as you're using SwiftUI, you'll be able to use CoreHaptics like this:
let engine = try CHHapticEngine()
try engine.start()
let hapticEvent = CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness), CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity),
], relativeTime: 0)
let audioEvent = CHHapticEvent(eventType: .audioContinuous, parameters: [
CHHapticEventParameter(parameterID: .audioVolume, value: volume),
CHHapticEventParameter(parameterID: .decayTime, value: decay),
CHHapticEventParameter(parameterID: .sustained, value: 0),
], relativeTime: 0)
let pattern = try CHHapticPattern(events: [hapticEvent, audioEvent], parameters: [])
let hapticPlayer = try engine.makePlayer(with: pattern)
try hapticPlayer?.start(atTime: CHHapticTimeImmediate)
Upvotes: 1