SensoryFeedback στο SwiftUI: Πλήρης Οδηγός για Haptic Feedback σε iOS (2026)
Το SensoryFeedback είναι ο declarative τρόπος να παίξεις haptics στο SwiftUI. Οδηγός με όλους τους τύπους, trigger patterns, σύγκριση με Core Haptics και best practices από UX engineer.
Το SensoryFeedback είναι ο σύγχρονος declarative τρόπος πυροδότησης haptic feedback στο SwiftUI, διαθέσιμος από το iOS 17 και βελτιωμένος στο iOS 26. Δηλώνεις τι θέλεις (.success, .impact(weight: .medium), .selection) και ένα trigger value, και το system αναλαμβάνει engine lifecycle, throttling, και συμβατότητα με τις ρυθμίσεις accessibility του χρήστη. Δεν χρειάζεται πια να διαχειρίζεσαι manually τους UIImpactFeedbackGenerator όπως στο UIKit.
Το view modifier .sensoryFeedback(_:trigger:) προστέθηκε στο iOS 17 και είναι το πρώτο declarative haptics API του SwiftUI· καλύπτει τουλάχιστον 12 προκαθορισμένους τύπους.
Το trigger πυροδοτεί feedback κάθε φορά που η Equatable τιμή του αλλάζει· δεν χρειάζεται onChange ή manual generator.prepare().
Για complex custom patterns (π.χ. rumble effect σε game controller) χρειάζεσαι ακόμη Core Haptics με αρχεία AHAP. Το SensoryFeedback δεν αντικαθιστά το CHHapticEngine.
Το iOS 26 προσθέτει βελτιώσεις στο SensoryFeedback.Weight και συμπεριφορά batching όταν τρέχεις πολλαπλά feedbacks στο ίδιο runloop tick.
Δεν λειτουργεί σε iPad ή Mac· μόνο σε iPhone με Taptic Engine. Πρέπει πάντα να έχεις graceful degradation και σεβασμό στο Reduce Motion.
Χρήση κατά της UX λογικής (haptic σε κάθε tap, feedback σε λάθος σημείο) καταστρέφει το perceived quality, και οι κανόνες του Apple HIG είναι non-negotiable.
Τι είναι το SensoryFeedback στο SwiftUI;
Λοιπόν, ας ξεκινήσουμε από τα βασικά. Το SensoryFeedback είναι ένας τύπος του SwiftUI που περιγράφει ένα haptic pattern με σημασιολογικό όνομα (τι εκφράζει), όχι με φυσικές παραμέτρους (πώς δονείται). Το προσαρτάς σε ένα view με τον modifier .sensoryFeedback(_:trigger:), δίνοντας ένα Equatable trigger value. Κάθε φορά που το trigger αλλάζει, το system πυροδοτεί το ζητούμενο feedback, εφόσον η συσκευή το υποστηρίζει και ο χρήστης δεν έχει απενεργοποιήσει τη haptic engagement.
Η διαφορά από το UIKit είναι θεμελιώδης. Στο UIKit διαχειριζόσουν manually έναν UIImpactFeedbackGenerator: init, prepare(), impactOccurred(). Ξεχνούσες το prepare(); Το πρώτο tap έφτανε με 30-50ms καθυστέρηση επειδή το Taptic Engine έπρεπε να "ξυπνήσει". Το SwiftUI αυτοματοποιεί αυτό το lifecycle. Μόλις το view εμφανιστεί, ο runtime προθερμαίνει το engine.
Ένα ακόμη σημαντικό: το SensoryFeedback δεν είναι ένα vibration, είναι μια σημασιολογική δήλωση. Όταν λες .success, το system αποφασίζει τι ακριβώς παίζει (σήμερα ένα διπλό tap χαμηλής έντασης, αύριο ίσως κάτι διαφορετικό). Έτσι, ένα app που γράφτηκε το 2023 συνεχίζει να "ακούγεται" σωστά σε iOS 26, ακόμη κι αν η Apple αλλάξει τα underlying waveforms.
Πώς προσθέτω haptic feedback σε ένα κουμπί SwiftUI;
Ο ελάχιστος κώδικας είναι τρεις γραμμές. Δηλώνεις ένα state που αλλάζει όταν πατιέται το κουμπί, και το χρησιμοποιείς ως trigger στο .sensoryFeedback. Δεν χρειάζεσαι onTapGesture, onChange, ούτε UIImpactFeedbackGenerator.
Κάθε πάτημα αυξάνει το likeCount. Επειδή το trigger είναι Equatable και αλλάζει, το system πυροδοτεί ένα medium impact. Παρατήρησε τι δεν κάνουμε: δεν καλούμε engine.prepare(), δεν χειριζόμαστε το threading, δεν ελέγχουμε αν το device υποστηρίζει haptics. Το SwiftUI το κάνει.
Ειλικρινά, για πιο δουλεμένο UX σε ένα heart button θα χρησιμοποιούσα δύο feedbacks: ένα .selection στο touch-down και ένα .success ή .impact(weight: .heavy, intensity: 0.7) στο commit. Οι δικές μου τιμές σε production apps είναι weight .light για micro-confirmations, .medium για default taps, και .heavy ΜΟΝΟ για destructive ή irreversible actions. Το .rigid και το .soft είναι texture-based, οπότε τα κρατάω για ξεχωριστά moments όπως ένα toggle που "κλειδώνει" (rigid) ή ένα modal που "προσγειώνεται" (soft).
Τύποι SensoryFeedback: πότε να χρησιμοποιείς κάθε έναν
Το enum SensoryFeedback έχει πάνω από δώδεκα cases στο iOS 26. Οι περισσότεροι developers χρησιμοποιούν μόνο τρεις, πράγμα κρίμα, γιατί τα σωστά cases δίνουν στο app σου σεμαντικά σωστό feel. Ο παρακάτω πίνακας είναι ο δικός μου cheatsheet που μοιράζομαι με designers στο kickoff κάθε feature:
Ο κανόνας που έχω από τα projects μου: αν μπορείς να διαλέξεις μεταξύ ενός σημασιολογικού case (.success) και ενός impact, πάντα διάλεξε το σημασιολογικό. Ο λόγος είναι διπλός. Πρώτον, το system μπορεί στο μέλλον να αντικαταστήσει το underlying waveform (το iOS 17 άλλαξε ήδη το .success pattern σε σχέση με το iOS 16). Δεύτερον, οι VoiceOver χρήστες σε ορισμένες ρυθμίσεις παίρνουν διαφορετική audio feedback ανάλογα με το semantic case, κάτι που δεν συμβαίνει με raw impacts.
SensoryFeedback vs UIFeedbackGenerator vs Core Haptics
Η ερώτηση "ποιο API να χρησιμοποιήσω;" εξαρτάται από το πόσο custom είναι το pattern που θέλεις να παίξεις. Ο παρακάτω κανόνας των τριών επιπέδων σκέψης είναι αυτός που ακολουθώ:
Feature
SensoryFeedback (SwiftUI)
UIFeedbackGenerator (UIKit)
Core Haptics (CHHapticEngine)
Ελάχιστο iOS
17.0
10.0
13.0
Setup complexity
Καμία, 1 modifier
Manual generator lifecycle
Engine start/stop + AHAP file ή builder
Custom patterns
Όχι
Όχι (μόνο presets)
Ναι, πλήρες control σε transient/continuous events
Audio-haptic sync
Όχι
Όχι
Ναι, μέσω AHAP AudioCustom
Automatic prepare/warmup
Ναι
Όχι (πρέπει prepare())
Manual μέσω engine.start()
Declarative API
Ναι
Όχι
Όχι
Καλή για games
Όχι
Όχι
Ναι
Χρησιμοποιώ SensoryFeedback για το 90% των production UI. Χρησιμοποιώ Core Haptics για μοναδικά moments: onboarding "success" σε ένα fitness app όπου συγχρονίζεται με ήχο, ή ένα custom rumble σε racing game. Το UIFeedbackGenerator το κρατάω μόνο για legacy UIKit code που δεν έχει μεταναστεύσει· δεν θα το ξεκινούσα σε νέο project.
Για βαθύτερη κατανόηση των custom patterns, η επίσημη τεκμηρίωση του Core Haptics είναι απαραίτητη, ειδικά η ενότητα για AHAP JSON schemas. Το Apple HIG για haptics είναι επίσης essential reading πριν αποφασίσεις σε ποιο moment παίζεις τι.
Advanced trigger patterns και conditional feedback
Το trigger του .sensoryFeedback είναι το κρίσιμο κομμάτι που καθορίζει πότε παίζει το feedback. Έχεις τρεις overloads και ξεχωριστά semantics για τον καθένα.
Πυροδοτείται κάθε φορά που το selectedTab αλλάζει value. Αν είναι Int, String, ή οποιοδήποτε Equatable, ο runtime κρατάει το προηγούμενο value και το συγκρίνει. Ιδανικό για @State selection στο TabView, σε Picker, ή σε custom segmented control.
2. Conditional trigger με closure
.sensoryFeedback(.impact(weight: .heavy), trigger: dragOffset) { oldValue, newValue in
// Παίξε feedback μόνο όταν περνάμε το threshold snap-back
abs(oldValue) < 100 && abs(newValue) >= 100
}
Ο closure λαμβάνει την παλιά και τη νέα τιμή· επιστρέφει Bool. Το feedback παίζει μόνο όταν επιστρέψεις true. Αυτό είναι το pattern που χρησιμοποιώ σε κάθε gesture-driven interaction: pull-to-refresh όταν φτάσει το threshold, drag-to-dismiss όταν περάσει το commit point, snap-to-grid όταν αλλάξει cell.
3. Feedback που εξαρτάται από την κατάσταση
.sensoryFeedback(trigger: uploadResult) { _, result in
switch result {
case .success: return .success
case .failure: return .error
case .none: return nil
}
}
Ο closure αυτή τη φορά επιστρέφει SensoryFeedback?. Το nil σημαίνει "μη παίξεις τίποτα". Χρησιμοποιώ αυτό το pattern για ένα singular trigger point που παίζει διαφορετικό feedback ανάλογα με το outcome. Καθαρότερο από το να έχεις πέντε ξεχωριστούς modifiers για κάθε case.
Προσβασιμότητα, iPad και συμβατότητα συσκευών
Πρώτο και βασικό: το SensoryFeedback δεν παίζει σε iPad ή Mac. Ο Taptic Engine υπάρχει μόνο σε iPhone (και Apple Watch με ξεχωριστό API, το WKInterfaceDevice.play). Στο iPad το call σου γίνεται silent no-op χωρίς compilation error. Πρακτικά αυτό σημαίνει ότι το σχέδιο interaction σου δεν πρέπει ποτέ να βασίζεται στο haptic ως μοναδικό signal. Πρέπει πάντα να έχει visual ή auditory alternative, κάτι που είναι επίσης βασική αρχή WCAG.
Δεύτερο: το iOS σέβεται την προτίμηση χρήστη Settings → Sounds & Haptics → System Haptics. Αν είναι απενεργοποιημένο, όλα σου τα SensoryFeedback γίνονται silent, και αυτό είναι σωστό. Δεν χρειάζεται να το ελέγξεις εσύ. Επίσης το Reduce Motion ΔΕΝ επηρεάζει το haptics feedback στο iOS 17+ (αλλά το επηρεάζει σε παλιότερα iOS όταν χρησιμοποιείς Core Haptics με transient events). Αν θέλεις να σεβαστείς και τα δύο, μπορείς να ελέγξεις το @Environment(\.accessibilityReduceMotion):
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
Button("Save") { save() }
.sensoryFeedback(
reduceMotion ? .selection : .impact(weight: .medium),
trigger: saveCount
)
}
Ο δικός μου κανόνας: όταν είναι reduceMotion == true, downgrade σε πιο ήπιο feedback (.selection αντί για .impact), αλλά ΜΗΝ αφαιρέσεις εντελώς. Οι χρήστες με vestibular disorders συχνά βασίζονται στα haptics ως αντικαταστάτη του motion. Το πλήρες accessibility landscape καλύπτεται στον οδηγό μας για type-safe navigation με NavigationStack, όπου βλέπουμε πώς κάθε navigation event πρέπει να έχει discoverable feedback.
Best practices haptic design από UX engineer
Έχω γράψει haptic code σε τρία shipped apps και όσο περνάει ο καιρός καταλήγω πάντα στους ίδιους πέντε κανόνες. Δεν είναι κανόνες Apple, είναι δικοί μου, βγαλμένοι από usability sessions όπου παρακολουθώ ανθρώπους να πατούν κουμπιά και να μορφάζουν.
Κανόνας 1: Ένα haptic ανά discrete user action
Αν ο χρήστης πατάει ένα κουμπί και ξεκινάει μια αλυσίδα ενεργειών (validate → save → dismiss modal), ΜΟΝΟ ΕΝΑ haptic παίζει, στο τελικό outcome. Πολλαπλά haptics σε γρήγορη διαδοχή αισθάνονται σαν rumble motor και σβήνουν το nuance. Αν έχεις ουσιαστικά διαφορετικά stages, βάλε 200-400ms μεταξύ τους.
Κανόνας 2: Το haptic ακολουθεί το animation, δεν προηγείται
Αν έχεις spring animation με response: 0.4, dampingFraction: 0.7, το impact haptic πρέπει να παίξει στο settle του spring, όχι στο commit. Αλλιώς νιώθεις τη δόνηση σε άδειο οπτικό χώρο. Αυτό το κατάλαβα σχεδιάζοντας το Liquid Glass component system, όπου κάθε .smooth transition χρειάζεται συγχρονισμένο haptic tick.
Κανόνας 3: Selection είναι δωρεάν, impact κοστίζει
Το .selection έχει pattern αρκετά subtle ώστε ο χρήστης να μην το προσέξει συνειδητά· μπορείς να το παίξεις σε κάθε αλλαγή picker index. Το .impact(weight: .medium) αντίθετα είναι εμφανές· αν το παίξεις σε κάθε scroll tick, ο χρήστης θα σβήσει τα haptics στα Settings μέσα σε 30 δευτερόλεπτα.
Κανόνας 4: Έλεγξε σε πραγματικό device στα 40% brightness
Οι developers δοκιμάζουν haptics σε τραπέζι με το iPhone flat, μεγάλη οθόνη ανοιχτή, χωρίς περίπτωση. Οι χρήστες τα δοκιμάζουν σε τραμ, με silicone case, με κρύα χέρια. Το ίδιο .impact(.medium) νιώθεται τελείως διαφορετικά. Δοκίμαζε πάντα με case, σε φυσικές συνθήκες. Αυτόν τον κανόνα τον μαθαίνεις μία φορά, μετά τον κρατάς για πάντα.
Κανόνας 5: Documenta ό,τι παίζεις
Στο design system μου κρατάω ένα HapticTokens.swift που mapping ονομάτων UX (.confirmation, .destructiveCommit, .pickerStep) σε συγκεκριμένα SensoryFeedback cases. Έτσι όταν το design token αλλάξει, όλο το app αλλάζει μαζί. Αυτό είναι το ίδιο pattern που εφαρμόζουμε και για state management μέσω του @Observable macro, με centralized source of truth.
enum HapticTokens {
static let confirmation: SensoryFeedback = .success
static let destructiveCommit: SensoryFeedback = .impact(weight: .heavy, intensity: 0.9)
static let pickerStep: SensoryFeedback = .selection
static let toggleLock: SensoryFeedback = .impact(flexibility: .rigid, intensity: 0.6)
static let modalArrival: SensoryFeedback = .impact(flexibility: .soft, intensity: 0.5)
}
// Χρήση:
.sensoryFeedback(HapticTokens.confirmation, trigger: saveComplete)
Για τις πιο πρόσφατες αλλαγές σε APIs, παρακολούθησε το επίσημο SwiftUI sensoryFeedback documentation. Η Apple κρατάει τα version deltas ενημερωμένα ανά major iOS release.
Συχνές Ερωτήσεις
Πώς κάνω vibrate ένα iPhone σε SwiftUI χωρίς UIKit;
Χρησιμοποίησε τον modifier .sensoryFeedback(.impact(weight: .medium), trigger: yourStateValue). Δεν χρειάζεσαι UIImpactFeedbackGenerator ή import του UIKit· το SwiftUI χειρίζεται εσωτερικά το Taptic Engine από το iOS 17 και μετά.
Ποια είναι η διαφορά μεταξύ Core Haptics και sensoryFeedback;
Το SensoryFeedback παίζει προκαθορισμένα semantic patterns (success, impact, selection). Το Core Haptics σου δίνει πλήρες control για custom transient και continuous events, audio sync, και dynamic parameters, απαραίτητο για games ή unique brand moments αλλά overkill για standard UI.
Λειτουργεί το SensoryFeedback στο iPad;
Όχι. Το iPad δεν διαθέτει Taptic Engine, οπότε το call σου γίνεται silent no-op χωρίς να πετάει error. Ποτέ μη σχεδιάζεις interaction που εξαρτάται αποκλειστικά από haptic feedback· παρέχε πάντα visual ή auditory alternative.
Πρέπει να ελέγχω αν το device υποστηρίζει haptics πριν το χρησιμοποιήσω;
Όχι με το SensoryFeedback, το SwiftUI το χειρίζεται αυτόματα. Αν χρησιμοποιείς Core Haptics απευθείας, τότε έλεγξε CHHapticEngine.capabilitiesForHardware().supportsHaptics πριν το engine startup.
Γιατί καθυστερεί το πρώτο μου haptic feedback;
Στο UIKit αυτό συμβαίνει αν παρέλειψες το generator.prepare(). Στο SwiftUI SensoryFeedback το system προθερμαίνει το engine αυτόματα όταν το view εμφανίζεται· αν βλέπεις καθυστέρηση, συνήθως σημαίνει ότι το trigger σου αλλάζει πριν το view γίνει fully attached. Έλεγξε το timing του state mutation.
Μπορώ να χρησιμοποιήσω διαφορετικό haptic ανάλογα με το outcome μιας ενέργειας;
Ναι. Χρησιμοποίησε την overload .sensoryFeedback(trigger:_:) που δέχεται closure και επιστρέφει SensoryFeedback?. Επιστρέφοντας .success, .error, ή nil ανάλογα με το state, παίζεις σωστό feedback χωρίς να χρειάζεσαι πολλαπλούς modifiers.
Πλήρης οδηγός για το App Intents framework στο iOS 26: AppIntent, AppEntity, AppShortcut, ενσωμάτωση με Apple Intelligence, Interactive Widgets και migration από SiriKit με κώδικα Swift.
Πρακτικός οδηγός για type-safe navigation στο SwiftUI με NavigationStack και NavigationPath: deep linking, pop-to-root, state restoration και accessibility patterns για iOS 26 με έτοιμα παραδείγματα κώδικα.
Το @Observable macro του Swift αντικαθιστά το ObservableObject με ανά-ιδιότητα tracking, λιγότερο boilerplate και αισθητά καλύτερη απόδοση στο SwiftUI.