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.

Haptika v SwiftUI: SensoryFeedback (2026)

Aktualizované: 5. júla 2026

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.
  • SensoryFeedback pokrýva 12 preddefinovaných patternov (impact, selection, success, warning, error, start, stop, alignment, decrease, increase, levelChange, path).
  • 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.

PatternPoužitieOdporúč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 zoznamu0.6 až 0.75
.impact(weight: .heavy)Primárna akcia formulára, drop v drag&drop0.7 až 0.85
.impact(flexibility: .soft)Sheet dismiss, mäkký „puk"0.5
.impact(flexibility: .rigid)Chyba layoutu, kolízia hraníc0.7
.selectionPicker, wheel, slider s krokmifixná
.successDokončená akcia (odoslanie, uloženie)fixná
.warningValidácia formulára, sub-critical stavfixná
.errorNeúspech siete, zamietnutá platbafixná
.start / .stopZačiatok/koniec dlhšej gesta (recording)fixná
.alignmentSnap k mriežke, zarovnanie textového rámufixná
.increase / .decreaseZoom step, hlasitosť, quantity pickerfixná

Kedy siahnuť po Core Haptics namiesto SensoryFeedback

Core Haptics je low-level framework s CHHapticEngine, ktorý používam v troch scenároch:

  1. 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.
  2. Dynamické parametre počas prehrávania. Napríklad slider, kde intenzita rastie s pozíciou palca. Potrebujem CHHapticDynamicParameter, ktorý meniem cez sendParameters(_:atTime:).
  3. 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):

func makeTap() throws -> CHHapticPattern {
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.75)
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.55)
    let event = CHHapticEvent(
        eventType: .hapticTransient,
        parameters: [intensity, sharpness],
        relativeTime: 0
    )
    return try CHHapticPattern(events: [event], parameters: [])
}

Kontinuálna udalosť s postupným útlmom cez parametrickú krivku:

func makeRumble(duration: TimeInterval = 0.8) throws -> CHHapticPattern {
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6)
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
    let event = CHHapticEvent(
        eventType: .hapticContinuous,
        parameters: [intensity, sharpness],
        relativeTime: 0,
        duration: duration
    )
    // Krivka zníži intenzitu z 1.0 na 0.0 za trvanie eventu, realistický útlm.
    let decay = CHHapticParameterCurve(
        parameterID: .hapticIntensityControl,
        controlPoints: [
            .init(relativeTime: 0,        value: 1.0),
            .init(relativeTime: duration, value: 0.0)
        ],
        relativeTime: 0
    )
    return try CHHapticPattern(events: [event], parameterCurves: [decay])
}

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:

guard let url = Bundle.main.url(forResource: "Success", withExtension: "ahap") else { return }
try engine?.playPattern(from: url)

Príklad minimálneho AHAP so single-tap na 0.2 sekundy so slabnúcou intenzitou:

{
  "Version": 1.0,
  "Pattern": [
    {
      "Event": {
        "Time": 0.0,
        "EventType": "HapticContinuous",
        "EventDuration": 0.2,
        "EventParameters": [
          { "ParameterID": "HapticIntensity", "ParameterValue": 0.7 },
          { "ParameterID": "HapticSharpness", "ParameterValue": 0.5 }
        ]
      }
    },
    {
      "ParameterCurve": {
        "ParameterID": "HapticIntensityControl",
        "Time": 0.0,
        "ParameterCurveControlPoints": [
          { "Time": 0.0, "ParameterValue": 1.0 },
          { "Time": 0.2, "ParameterValue": 0.0 }
        ]
      }
    }
  ]
}

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:

import WatchKit

WKInterfaceDevice.current().play(.notification) // .click, .success, .start, .stop, .navigationLinkTap, .directionUp/Down

.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.

Diana Kowalski
O Autorovi Diana Kowalski

Mobile UX engineer translating design intent into pixel-perfect SwiftUI. Has strong opinions about haptics.