- 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
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.
App Tracking Transparency in SwiftUI
A practical guide to App Tracking Transparency in iOS, covering the Info.plist usage description, the ATTrackingManager API, authorization statuses, and when to actually prompt the user from SwiftUI.
Deep Linking and Universal Links Setup in iOS
How to set up custom URL schemes and Universal Links in iOS, handle incoming URLs in SwiftUI, and route users to the right screen.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.