Haptika v SwiftUI: Kompletný sprievodca SensoryFeedback API a Core Haptics v iOS 26
Ako pridať haptickú spätnú väzbu v SwiftUI cez SensoryFeedback a Core Haptics. Praktický sprievodca s hodnotami intenzity, AHAP súbormi a príkladmi z produkcie pre iOS 26.
Haptickú spätnú väzbu v SwiftUI najjednoduchšie pridáte modifikátorom .sensoryFeedback(_:trigger:), ktorý spustí systémový haptický pattern zakaždým, keď sa zmení sledovaná hodnota. Od iOS 17 je SensoryFeedback odporúčaný spôsob asi pre 90 % prípadov, zatiaľ čo Core Haptics s CHHapticEngine a AHAP súbormi zostáva potrebný, keď potrebujem vlastnú krivku intenzity, ostrosti alebo synchronizáciu so zvukom. V tomto sprievodcovi ukážem konkrétne hodnoty, ktoré používam v produkčných appkách, a presne kedy zvoliť ktoré API.
.sensoryFeedback(.impact(weight: .light, intensity: 0.6), trigger: value) je najkratšia cesta k správne naladenej haptike v SwiftUI od iOS 17.
Core Haptics používam iba vtedy, keď potrebujem kontinuálny signál dlhší ako 100 ms, dynamické parametre alebo synchronizáciu so zvukom.
Haptika NEbeží v Xcode Preview a nefunguje ani v simulátore. Testovanie prebieha vždy na fyzickom zariadení s Taptic Enginom (iPhone 8 a novší).
Na Apple Watch používam WKInterfaceDevice.current().play(_:) s vlastnou sadou WKHapticType hodnôt. SensoryFeedback funguje aj vo watchOS 10+.
Intenzita nad 0.8 pôsobí agresívne; pre potvrdzovacie interakcie sa držím v pásme 0.4 až 0.65.
Čo je SensoryFeedback API v SwiftUI
SensoryFeedback je typ zavedený v iOS 17, ktorý reprezentuje systémovo definovaný haptický pattern. Namiesto ručného vytvárania UIImpactFeedbackGenerator, jeho pripravovania (prepare()) a spustenia (impactOccurred()) stačí jeden modifikátor pripojený k pohľadu, ktorý reaguje na zmenu sledovanej hodnoty. Systém sa postará o životný cyklus generátora, jeho predpripravenie, a v prípade potreby ho odloží, ak zariadenie nemá Taptic Engine.
Praktický rozdiel je v tom, že SensoryFeedback je deklaratívny. Viažem haptiku na stav, nie na callback. Ak sa @State premenná zmení, haptika sa prehrá. To perfektne zapadá do SwiftUI dátového toku a odstraňuje časté chyby ako „haptika sa prehrá dvakrát" alebo „prehrá sa počas úvodnej animácie". V praxi to znamená menej riadkov kódu a menej edge-caseov, ktoré musím ošetriť ručne.
SensoryFeedback funguje na iOS 17+, iPadOS 17+, watchOS 10+, macOS 14+ (bez efektu, ale bez chyby) a visionOS 1+. Na starších systémoch musím ostať pri UIImpactFeedbackGenerator, alebo aplikovať dostupnostný check if #available(iOS 17.0, *). Úprimne, väčšina projektov, na ktorých teraz robím, už drží deployment target na iOS 17, takže sa k tomu vraciam čoraz menej.
Ako pridať haptickú spätnú väzbu do SwiftUI
Začnime tým najjednoduchším prípadom, haptikou pri stlačení tlačidla:
import SwiftUI
struct LikeButton: View {
@State private var isLiked = false
var body: some View {
Button {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
isLiked.toggle()
}
} label: {
Image(systemName: isLiked ? "heart.fill" : "heart")
.foregroundStyle(isLiked ? .pink : .gray)
.font(.system(size: 28, weight: .semibold))
}
// Prehrá sa VŽDY, keď sa isLiked zmení, vrátane toggle späť na false
.sensoryFeedback(.impact(weight: .light, intensity: 0.6), trigger: isLiked)
}
}
Ak potrebujem, aby sa haptika prehrala len pri konkrétnej hodnote (napríklad iba pri „like", nie pri „unlike"), použijem verziu s uzáverom, ktorý vráti voliteľný pattern:
.sensoryFeedback(trigger: isLiked) { oldValue, newValue in
// Vráti nil = žiadna haptika. Pattern = prehrá sa daný pattern.
guard newValue == true else { return nil }
return .success
}
Táto forma je moja obľúbená pre komplexnejšie interakcie, napríklad drag-to-refresh, kde chcem svetlý „tick" pri prekročení threshold-u a „success" po uvoľnení. Sledovaná hodnota môže byť čokoľvek, čo je Equatable: Int, String, custom enum, alebo aj celý @Observable model, ak implementuje rovnosť.
enum PullState: Equatable { case idle, threshold, refreshing, done }
@State private var pullState: PullState = .idle
var body: some View {
ScrollView { /* ... */ }
.sensoryFeedback(trigger: pullState) { _, new in
switch new {
case .threshold: return .impact(flexibility: .soft, intensity: 0.5)
case .done: return .success
default: return nil
}
}
}
Všetky patterny SensoryFeedback s odporúčanými hodnotami
SensoryFeedback ponúka 12 pomenovaných patternov. V tabuľke sú konkrétne kontexty, v ktorých ich používam v produkcii, a hodnoty parametrov, kde to dáva zmysel.
Pattern
Použitie
Odporúčaná intenzita
.impact(weight: .light)
Toggle, malá interakcia (like, hviezdička)
0.5 až 0.65
.impact(weight: .medium)
Prepínač karty, výber položky zoznamu
0.6 až 0.75
.impact(weight: .heavy)
Primárna akcia formulára, drop v drag&drop
0.7 až 0.85
.impact(flexibility: .soft)
Sheet dismiss, mäkký „puk"
0.5
.impact(flexibility: .rigid)
Chyba layoutu, kolízia hraníc
0.7
.selection
Picker, wheel, slider s krokmi
fixná
.success
Dokončená akcia (odoslanie, uloženie)
fixná
.warning
Validácia formulára, sub-critical stav
fixná
.error
Neúspech siete, zamietnutá platba
fixná
.start / .stop
Začiatok/koniec dlhšej gesta (recording)
fixná
.alignment
Snap k mriežke, zarovnanie textového rámu
fixná
.increase / .decrease
Zoom step, hlasitosť, quantity picker
fixná
Kedy siahnuť po Core Haptics namiesto SensoryFeedback
Core Haptics je low-level framework s CHHapticEngine, ktorý používam v troch scenároch:
Kontinuálny signál dlhší ako ~120 ms. SensoryFeedback prehráva krátke transienty. Ak simulujem vibrujúci telefón, zvonenie alebo drag-with-resistance efekt, musím dať motoru CHHapticEvent s eventType: .hapticContinuous a explicitnú dĺžku.
Dynamické parametre počas prehrávania. Napríklad slider, kde intenzita rastie s pozíciou palca. Potrebujem CHHapticDynamicParameter, ktorý meniem cez sendParameters(_:atTime:).
Synchronizácia so zvukom. AHAP súbory môžu obsahovať audioCustom udalosti presne zosynchronizované s haptickými udalosťami, čo je kritické pre hry a hudobné aplikácie.
Pre všetko ostatné je SensoryFeedback lepšia voľba. Kratší kód, menšia šanca na chyby s životným cyklom motora a integrácia s SwiftUI stavom. Na nepodporovaných zariadeniach (iPad bez Taptic Enginu, Mac) SensoryFeedback ticho nič neurobí, zatiaľ čo Core Haptics vyžaduje explicitný capability check.
Core Haptics v praxi: kontinuálne a transientné udalosti
Základný setup CHHapticEngine. Motor treba držať v pamäti (typicky v @Observable service triede) a reštartovať pri prerušeniach:
import CoreHaptics
@Observable
final class HapticEngine {
private var engine: CHHapticEngine?
init() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
do {
engine = try CHHapticEngine()
engine?.stoppedHandler = { [weak self] reason in
// Systém motor zastavil (napr. audio session interrupt).
// Naštartujeme lazy pri najbližšom play.
self?.engine = nil
}
engine?.resetHandler = { [weak self] in
try? self?.engine?.start()
}
try engine?.start()
} catch {
engine = nil
}
}
func play(_ pattern: CHHapticPattern) {
guard let engine else { return }
do {
let player = try engine.makePlayer(with: pattern)
try player.start(atTime: CHHapticTimeImmediate)
} catch {
// V produkcii logujeme, ale nikdy nepadáme kvôli haptike.
}
}
}
Transientná udalosť s konkrétnymi hodnotami intenzity a ostrosti (obidve v rozsahu 0.0 až 1.0):
Pri hodnotách sharpness platí, že 0.0 zodpovedá „buchnutiu dlaňou" (nízke, mäkké), zatiaľ čo 1.0 je „klepnutie kľúčom" (ostré, high-frequency). V mikrointerakciách sa najčastejšie pohybujem medzi 0.3 a 0.6. Vyššie hodnoty rezervujem pre chybové stavy, kde chcem, aby si používateľ všimol.
AHAP súbory: dizajn haptických patternov mimo kódu
AHAP (Apple Haptic Audio Pattern) je JSON formát, ktorý drží haptické patterny mimo Swift kódu. Ich hlavná výhoda je, že designéri môžu iterovať bez rebuildu appky. Súbor priložím k projektu ako resource a načítam za behu:
Pre komplexnejšie patterny odporúčam Apple's Haptic Composer. Hoci nie je oficiálne súčasťou Xcode 26, existujú open-source webové editory, ktoré generujú platný AHAP. Pri návrhu tempa som zistila, že sekvencie kratšie ako 60 ms medzi udalosťami vnímam ako „buzzy", zatiaľ čo rozostupy 90 až 140 ms dávajú príjemný rytmus podobný animácii s .spring(response: 0.4), ktorú často používam v prepojenom vizuálnom pohybe. Ak vás zaujíma dizajn samotných animácií, prečítajte si aj sprievodcu animáciami v SwiftUI, ktorý pokrýva pružiny a phase animátory hlbšie.
Haptika na Apple Watch a watchOS 10+
Na Apple Watch používam dve cesty. Prvá, jednoduchšia, je rovnaký .sensoryFeedback modifikátor. Od watchOS 10 funguje identicky ako na iOS a je vždy prvá voľba pre stavovú haptiku v SwiftUI.
Druhá je legacy API cez WKInterfaceDevice, ktoré ponúka watchOS-špecifické patterny neexistujúce na iOS:
.navigationLinkTap je moja tichá obľúbenkyňa. Je to najjemnejší dostupný pattern, ideálny pre ovládanie zoznamov korunkou. .directionUp a .directionDown využívam v timeri a pri crown-driven scroll snap-e, kde vizuálne indikujem prekročenie hraníc. Rovnaké hardvérové motory znamenajú, že hodnoty intenzity nastavené v Core Haptics sa vzťahujú aj na Watch, no menší motor v Series 9 a novších zariadeniach má nižšie horné pásmo. Pattern navrhnutý pre iPhone môže znieť „lacne" bez dolaďovania.
Migrácia z UIImpactFeedbackGenerator na SensoryFeedback
Ak vaša codebase je stále z čias UIKit, prechod je priamočiary. Porovnajte pôvodný imperatívny prístup s deklaratívnym SwiftUI ekvivalentom:
// Pred: UIKit / iOS 16 a nižšie
class OldViewController: UIViewController {
let generator = UIImpactFeedbackGenerator(style: .medium)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
generator.prepare() // Manuálne pripravenie, inak lag pri prvom prehratí
}
@IBAction func buttonTapped() {
generator.impactOccurred(intensity: 0.7)
}
}
// Po: SwiftUI + iOS 17
struct NewView: View {
@State private var tapCount = 0
var body: some View {
Button("Tap") { tapCount += 1 }
.sensoryFeedback(.impact(weight: .medium, intensity: 0.7), trigger: tapCount)
}
}
Systém pripraví generátor automaticky pri prvom vykreslení pohľadu, takže volanie prepare() mizne. Podobný benefit získate pri prechode aplikácie na moderný SwiftUI state management, o ktorom píšem v článku o @Observable a Observation frameworku.
Prístupnosť, Reduce Motion a haptika
Haptika je pomocný kanál pre používateľov s poruchami zraku alebo sluchu, ale zároveň môže byť rušivá pri neurodivergentných používateľoch. Rešpektujem tri systémové prepínače:
UIAccessibility.isReduceMotionEnabled. Ak je true, nepotláčam všetku haptiku, ale tlmím pattern-y typu .impact(weight: .heavy) na .light alebo .selection.
UIAccessibility.isVoiceOverRunning. VoiceOver má vlastnú haptickú signalizáciu; pri navigácii zoznamom NEpridávam vlastný .selection pattern, ktorý by kolidoval so systémovou.
Systémový prepínač Settings → Sounds & Haptics → System Haptics, ktorý spracuje SensoryFeedback automaticky. Ak je off, môj kód nemusí robiť nič.
Pre kompletný pohľad na prístupnosť v SwiftUI odporúčam prečítať sprievodcu prístupnosťou vo SwiftUI, ktorý pokrýva VoiceOver, Dynamic Type a accessibility traits do detailu. Apple's Human Interface Guidelines pre haptiku je ďalšie must-read: sú tam presné rytmické odporúčania, ktoré sa v praxi oplatí dodržať.
Časté chyby a ako ich odladiť
Za posledné dva roky som narazila na štyri opakujúce sa problémy, ktoré stoja za popísanie. Priznám sa, na prvý som padla pri prvom preview svojej aplikácie a márne som pol hodiny hľadala, kde je chyba.
1. „Haptika nefunguje v Xcode Preview"
Toto nie je chyba, je to očakávané správanie. Xcode Preview a iOS simulátor nemajú prístup k Taptic Enginu. Test vždy prebieha na fyzickom zariadení. Ak potrebujem QA log, obalím prehratie #if targetEnvironment(simulator) checkom a namiesto haptiky vypíšem cez Logger.
2. Pattern sa prehráva zdvojene
Najčastejšie preto, že rovnaká hodnota triggeru sa emituje viackrát počas jedného SwiftUI cyklu. Rieši sa buď debounce-om cez @State private var lastTriggerTime = 0.0, alebo prechodom na verziu trigger: closure, kde vraciam nil, ak od posledného spustenia neprešlo minimálne 80 ms.
3. CHHapticEngine sa zastaví po prerušení audia
Ak používateľ prijme hovor alebo Siri prevezme audio session, motor sa zastaví. Riešenie je nastaviť stoppedHandler a lazily reštartovať pri najbližšom prehratí patternu, ako som ukázala vyššie. Nikdy motor nereštartujem synchrónne v handleri; spôsobí to deadlock.
4. Nesprávne kombinácie weight a intensity
Pattern .impact(weight: .heavy, intensity: 1.0) je najagresívnejšia možnosť a v neinvazívnych príkladoch (like, favorite) pôsobí ako chyba appky. Pravidlo, ktoré si držím: intenzita nad 0.8 iba pri chybách alebo dokončení long-running task-u. Pre všetko ostatné 0.4 až 0.65.
Často kladené otázky
Funguje SensoryFeedback v iOS 16 a starších verziách?
Nie, .sensoryFeedback modifikátor je dostupný iba od iOS 17, iPadOS 17, watchOS 10 a macOS 14. Pre staršie systémy musíte použiť UIImpactFeedbackGenerator, UISelectionFeedbackGenerator alebo UINotificationFeedbackGenerator z UIKitu, prípadne obaliť SwiftUI kód do if #available(iOS 17.0, *) vetvy s fallbackom.
Aký je rozdiel medzi SensoryFeedback a Core Haptics?
SensoryFeedback je high-level SwiftUI API s 12 preddefinovanými patternami vhodnými pre 90 % UI interakcií. Core Haptics je low-level framework, ktorý umožňuje vytvárať vlastné patterny s presne definovanou intenzitou, ostrosťou, trvaním a parametrickými krivkami. Core Haptics používajte len ak potrebujete kontinuálny signál, dynamické parametre počas prehrávania alebo synchronizáciu so zvukom.
Ako otestovať haptiku v SwiftUI, keď simulátor nefunguje?
Haptickú spätnú väzbu je nutné testovať na fyzickom zariadení s Taptic Enginom, teda iPhone 8 a novší, Apple Watch všetky série, alebo iPad Pro s pripojenou Magic Keyboard. Xcode Preview a iOS simulátor haptiku nepodporujú a volania sa ticho ignorujú. Pre CI a snapshot testy môžete overiť aspoň to, že sa modifikátor aplikoval, cez ViewInspector alebo custom PreferenceKey.
Ktoré zariadenia podporujú Core Haptics?
Core Haptics vyžaduje Taptic Engine, ktorý je prítomný na iPhone 8 a novších, na všetkých Apple Watch modeloch a na iPad Pro s Magic Keyboard (cez trackpad). Podporu skontrolujte cez CHHapticEngine.capabilitiesForHardware().supportsHaptics. Na nepodporovaných zariadeniach kód nezhavaruje, ale prehratie patternu je no-op.
Ako sa dá vypnúť haptika pre používateľov, ktorí ju nechcú?
Systémový prepínač Settings → Sounds & Haptics → System Haptics rešpektuje SensoryFeedback automaticky. Pre vlastnú granulárnu kontrolu pridajte do svojej appky vlastné nastavenie (napr. cez @AppStorage("hapticsEnabled")) a obaľte prehrávanie podmienkou. Pre Core Haptics dodatočne kontrolujte UIAccessibility.isReduceMotionEnabled a znížte intenzitu alebo použite jemnejší pattern.
Naučte sa stavať Live Activities a Dynamic Island v iOS 26 cez ActivityKit, App Intents a APNs. Praktické ukážky kódu pre lock screen, StandBy a Smart Stack na watchOS 26.
Kompletný sprievodca prístupnosťou v SwiftUI pre iOS 26. Pokrýva VoiceOver, Dynamic Type, @ScaledMetric, accessibility traits, Reduce Motion, AccessibilityRotor a audio grafy v Swift Charts. S príkladmi kódu a praktickými tipmi z code review.
Praktický sprievodca tvorbou vlastných Swift makier. Od freestanding cez attached až po sedem rolí, SwiftSyntax, testovanie a debugovanie v Xcode 16.4.