- Published on
- 4 min read
> Sensory Feedback and Haptics in SwiftUI
Haptic feedback makes interactions feel more tangible. A subtle vibration when toggling a switch or completing an action helps users understand that something happened. iOS 17 added the sensoryFeedback modifier to SwiftUI, making haptics easy to add without dropping into UIKit.
Basic Usage
The sensoryFeedback modifier takes a feedback type and a trigger value. When the trigger changes, the feedback plays:
struct LikeButton: View {
@State private var isLiked = false
var body: some View {
Button {
isLiked.toggle()
} label: {
Image(systemName: isLiked ? "heart.fill" : "heart")
.foregroundStyle(isLiked ? .red : .gray)
.font(.title)
}
.sensoryFeedback(.selection, trigger: isLiked)
}
}
Every time isLiked changes, a selection haptic plays. The feedback type determines the vibration pattern.
Feedback Types
SwiftUI provides several predefined feedback styles:
// Light tap for selections
.sensoryFeedback(.selection, trigger: selectedItem)
// Confirm successful actions
.sensoryFeedback(.success, trigger: saveCompleted)
// Indicate something went wrong
.sensoryFeedback(.error, trigger: errorOccurred)
// Caution without being an error
.sensoryFeedback(.warning, trigger: approachingLimit)
// Values increasing
.sensoryFeedback(.increase, trigger: volume)
// Values decreasing
.sensoryFeedback(.decrease, trigger: volume)
// Beginning an action
.sensoryFeedback(.start, trigger: isRecording)
// Ending an action
.sensoryFeedback(.stop, trigger: isRecording)
// Snapping to a position
.sensoryFeedback(.alignment, trigger: alignedPosition)
// Discrete level changes
.sensoryFeedback(.levelChange, trigger: currentLevel)
Impact Feedback
For physical collision-style feedback, use .impact with weight and intensity:
struct DraggableCard: View {
@State private var offset = CGSize.zero
@State private var dropped = false
var body: some View {
RoundedRectangle(cornerRadius: 12)
.fill(.blue)
.frame(width: 100, height: 100)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation {
offset = .zero
}
dropped.toggle()
}
)
.sensoryFeedback(
.impact(weight: .medium, intensity: 0.7),
trigger: dropped
)
}
}
Weight options are .light, .medium, and .heavy. Intensity ranges from 0.0 to 1.0.
Conditional Feedback
You can choose feedback dynamically based on the old and new values:
struct SearchResults: View {
@State private var results: [String] = []
var body: some View {
List(results, id: \.self) { result in
Text(result)
}
.sensoryFeedback(trigger: results) { oldValue, newValue in
if newValue.isEmpty {
return .error
} else if newValue.count > oldValue.count {
return .success
} else {
return .selection
}
}
}
}
This plays error feedback when results become empty, success when new results arrive, and selection otherwise.
Conditional Triggering
Sometimes you only want feedback under certain conditions:
struct FormSubmit: View {
@State private var submitted = false
@State private var hasErrors = false
var body: some View {
Button("Submit") {
validateAndSubmit()
}
.sensoryFeedback(.success, trigger: submitted) { _, newValue in
newValue && !hasErrors
}
.sensoryFeedback(.error, trigger: submitted) { _, newValue in
newValue && hasErrors
}
}
func validateAndSubmit() {
// validation logic
submitted.toggle()
}
}
The closure returns a Bool indicating whether to play the feedback.
Practical Example
Here's a counter with different haptics for increment and decrement:
struct HapticCounter: View {
@State private var count = 0
@State private var lastAction: CounterAction?
enum CounterAction {
case increment, decrement, reset
}
var body: some View {
VStack(spacing: 24) {
Text("\(count)")
.font(.system(size: 72, weight: .bold, design: .rounded))
HStack(spacing: 16) {
Button {
count -= 1
lastAction = .decrement
} label: {
Image(systemName: "minus.circle.fill")
.font(.largeTitle)
}
Button {
count = 0
lastAction = .reset
} label: {
Image(systemName: "arrow.counterclockwise.circle.fill")
.font(.largeTitle)
}
Button {
count += 1
lastAction = .increment
} label: {
Image(systemName: "plus.circle.fill")
.font(.largeTitle)
}
}
}
.sensoryFeedback(trigger: lastAction) { _, newValue in
switch newValue {
case .increment:
return .increase
case .decrement:
return .decrease
case .reset:
return .impact(weight: .heavy)
case nil:
return nil
}
}
}
}
Increment plays an "increase" haptic, decrement plays "decrease," and reset gives a heavier impact.
Device Support
Haptic feedback only works on iPhones with a Taptic Engine. iPads, Macs, and the Simulator won't produce vibrations. Your app should work fine without haptics since they're supplementary feedback, not essential information.
Best Practices
Use haptics sparingly. Too much vibration becomes annoying and drains battery. Good candidates for haptics include confirming destructive actions, success or failure of important operations, toggle state changes, and snapping to positions in custom controls.
Avoid haptics for scrolling, typing, or other high-frequency interactions. Match the feedback intensity to the action's significance. A minor selection should feel lighter than completing a major task.
For more control over haptics, including custom patterns, you can still use UIFeedbackGenerator subclasses directly. But for most cases, sensoryFeedback covers what you need with less code.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
Adding Horizontal Padding to TextEditor in SwiftUI with contentMargins
Learn how to add horizontal padding inside a TextEditor without affecting the scrollbar using the contentMargins modifier introduced in iOS 17.
ControlWidgetButton for Control Center Widgets
Create interactive buttons for the Control Center using ControlWidgetButton and App Intents in iOS 18.
ViewThatFits for Adaptive Layouts in SwiftUI
Use ViewThatFits to automatically select the best layout variant based on available space, without writing manual size calculations.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.