- Published on
- 4 min read Beginner
> Sensory Feedback and Haptics in SwiftUI
// What_You_Will_Learn
- Add haptic feedback to SwiftUI views with the sensoryFeedback modifier
- Choose the right feedback type for different user interactions
- Trigger haptics based on value changes and user actions
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
TextEditor Horizontal Padding in SwiftUI: contentMargins
Learn how to add horizontal padding inside a TextEditor without affecting the scrollbar using the contentMargins modifier introduced in iOS 17.
WWDC 2026 Predictions Report Card: What Apple Actually Shipped
Before WWDC 2026 I wrote down predictions and never hit publish. Four months later, with iOS 27 out and iPhone Duo days away, it's time to grade them against what Apple actually shipped.
iPhone Duo Split View: Is Your Window Left or Right?
When two windows share iPhone Duo's inner display, neither one is told which side it's on. I built a proof of concept in Xcode 27.1 and found the fold's ReservedRegion answers it, once you know its frame is clipped to your window.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.