SwiftUI SensoryFeedback: Vodič za haptički feedback u iOS 26
Praktični vodič za SwiftUI SensoryFeedback API u iOS 26: sve varijante, kada preći na Core Haptics, tempiranje uz spring animacije i pravila za motion design.
SensoryFeedback je SwiftUI modifikator koji pokreće haptički odgovor uređaja kad se promijeni vrijednost koju promatrate, u praksi jedna linija koda umjesto ručnog rukovanja s UIImpactFeedbackGenerator. Uveden je u iOS 17, a u iOS 26 dolazi s finijim tempiranjem i boljom integracijom s @Observable stanjem. U ovom vodiču pokazujem kako ga koristim u produkciji, koje varijante postoje, kad prelazim na Core Haptics te kako kalibrirati intenzitet tako da haptika "sjeda" na pokret, a ne guši interakciju.
SensoryFeedback modifikator se okida promjenom praćene vrijednosti; trigger je stanje, a ne akcija.
Za jedinstvene uzorke koristite Core Haptics s CHHapticEngine. SensoryFeedback pokriva 90% slučajeva bez toga.
Haptika ne radi u iOS simulatoru; testirajte na fizičkom uređaju s Taptic Engine (iPhone 7 i noviji).
Apple Watch koristi zaseban WKInterfaceDevice.play(_:) API. SensoryFeedback ipak radi na watchOS 10+ za osnovne varijante.
Poštujte postavku "Smanji pokret" i procijenite kad je haptika suvišna. Spring animacija s response 0.4 i haptika istovremeno mogu prezasititi doživljaj.
Što je SensoryFeedback i kako funkcionira
SensoryFeedback je deklarativni SwiftUI API koji povezuje vibrotaktilnu reakciju uređaja s promjenom u pogledu. Za razliku od UIKit pristupa, gdje eksplicitno stvarate generator, pripremate ga i pozivate impactOccurred(), u SwiftUI-ju samo pripnete modifikator .sensoryFeedback(_:trigger:) na pogled i navedete koju vrijednost promatrati. Sistem sam upravlja životnim ciklusom Taptic Enginea, pa više nema onog klasičnog "generator ostaje prazan jer nije bio pripremljen na vrijeme".
API je uveden u iOS 17 (WWDC 2023) i od tada je proširen dvama važnim mehanizmima. iOS 18 je dodao .sensoryFeedback(trigger:_:) zatvorku koja vraća SensoryFeedback? ovisno o razlici stare i nove vrijednosti, savršeno za scenarije "reagiraj drugačije ako je vrijednost porasla ili pala". iOS 26 dorađuje raspored okidanja tako da se haptika ne aktivira ako je view u prijelazu (transition), što uklanja neugodne dvostruke pulseve pri push navigaciji. Službena Apple dokumentacija pokriva sve preopterećene inicijalizatore.
Pod haubom, SensoryFeedback koristi isti Taptic Engine kao Core Haptics, ali s prethodno definiranim uzorcima koje je Apple kalibrirao za dosljedan osjećaj kroz cijeli sustav. To znači da .success u vašoj aplikaciji zvuči i osjeća se identično kao .success u Wallet aplikaciji, što je velika prednost za usability. Za usporedbu, u sličnom duhu deklarativnog API-ja pišemo i interaktivne grafikone u Swift Charts.
Kako dodati haptički feedback u SwiftUI
Osnovna upotreba traje tri retka. Sve što trebate je stanje koje se mijenja i modifikator koji ga promatra.
import SwiftUI
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 : .secondary)
.contentTransition(.symbolEffect(.replace))
}
// Haptika se okine svaki put kad `isLiked` promijeni vrijednost.
.sensoryFeedback(.success, trigger: isLiked)
}
}
Ključna razlika u odnosu na UIKit je što ne pozivate haptiku iz akcije gumba. Umjesto toga, akcija mijenja stanje, a SwiftUI okine haptiku kad detektira razliku. To je važno jer vam omogućuje reagirati na promjene iz bilo kojeg izvora, bilo iz animacije, dolaznog podatka s mreže ili tajmera, bez duplog koda.
Ako želite drugačiji feedback ovisno o smjeru promjene, koristite closure preopterećenje uvedeno u iOS 18:
@State private var stepCount = 0
var body: some View {
Stepper("Koraci: \(stepCount)", value: $stepCount, in: 0...100)
.sensoryFeedback(trigger: stepCount) { oldValue, newValue in
newValue > oldValue ? .increase : .decrease
}
}
Vratite li nil, haptika se ne okida (dobro za scenarije poput "prvo učitavanje ne treba pulsirati"). Za pogled kartice s dinamičkim brojačima, kombiniram to s @Observable makrom tako da haptički okidač živi na razini view modela, a ne u lokalnom @State.
Sve varijante SensoryFeedback API-ja
API definira 11 semantičkih varijanti, a svaka ima jasnu ulogu. Nemojte izmišljati hijerarhiju; Apple ju je već složio. Evo pregleda:
Varijanta
Osjećaj
Kada koristiti
.success
Dva blaga pulsa, uzlazni
Uspješna radnja (spremljeno, poslano)
.warning
Dva srednja pulsa
Upozorenje, potrebna pažnja korisnika
.error
Tri jaka pulsa
Neuspjeh, blokirana radnja
.selection
Jedan lagani "klik"
Prelazak fokusa u picker-u, izbor
.impact(.light/.medium/.heavy)
Jedan udarac, varijabilne težine
Simulacija fizičkog kontakta
.impact(weight:intensity:)
Fino podesivi udarac
Prilagođeni intenzitet 0.0 do 1.0
.increase / .decrease
Uzlazni / silazni ton
Stepper, slider s koracima
.start / .stop
Početni / završni marker
Snimanje, tajmer, gesta
.alignment
Škljocaj poravnanja
Uređivač koji se snapa na grid
.levelChange
Promjena razine
Prelazak između kategorija
Moj pragmatičan pristup: za većinu aplikacija dovoljne su .selection i .success. .impact(weight:) koristite tek kad simulirate fizičku interakciju, primjerice slider s magnetnim točkama ili karticu koja se "sudari" s rubom ekrana. Sve ostalo su specijalni slučajevi koje treba pažljivo opravdati u dizajn reviewu.
SensoryFeedback vs Core Haptics: kada koristiti što
Kratki odgovor: SensoryFeedback za svakodnevne interakcije, Core Haptics za igre, glazbene aplikacije i sve što treba jedinstven haptički potpis. SensoryFeedback ne može stvoriti kontinuirani "buzz" niti sinkronizirati vibraciju s audio zapisom. Ako trebate jedno od toga, prelazite razinu niže.
Prednost Core Haptics je potpuna kontrola nad intensity (0.0 do 1.0) i sharpness (0.0 mekano, 1.0 oštro), plus kontinuirani .hapticContinuous događaji do 30 sekundi. Nedostatak: sami morate paziti na engine.stoppedHandler i restart engine-a kad iOS suspendira aplikaciju. Iskreno, tu sam se opekao jednom u produkciji kad je engine tiho umro nakon telefonskog poziva, i od tada uvijek registriram handler prvog dana. Core Haptics dokumentacija ima detaljan overview životnog ciklusa engine-a. Za dubinske primjere pattern editora vidi Human Interface Guidelines za haptiku.
Tempiranje haptike u odnosu na motion design
Ovdje je najvažnija stvar koju vidim u recenzijama koda: haptika se okida trenutno, dok spring animacija ima trajanje. Ako u jednom event handleru pokrenete isPressed = true koji istovremeno okida haptički .selection i vizualnu spring animaciju s response: 0.4, korisnik osjeti udar, a onda gleda kako se view smiruje 400 ms. Djeluje razdvojeno.
Moje pravilo: usklađujte haptiku s vrhom pokreta, a ne s njegovim početkom. Za scale-down efekt gumba koristim spring(response: 0.3, dampingFraction: 0.7) i haptičkim .selection okidam tek kad se scale vrati na 1.0. To zahtijeva razdvajanje trigger stanja:
struct PressButton: View {
@State private var isPressed = false
@State private var didComplete = false
let action: () -> Void
var body: some View {
Rectangle()
.fill(.tint)
.frame(width: 200, height: 60)
.scaleEffect(isPressed ? 0.95 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isPressed)
.onTapGesture {
isPressed = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
isPressed = false
didComplete.toggle() // haptika ide tek sad
action()
}
}
.sensoryFeedback(.impact(weight: .light, intensity: 0.6), trigger: didComplete)
}
}
Za intenzitet: 0.6 je moj default za "primarna radnja". 1.0 ostavljam za rijetke, značajne trenutke (uspješna transakcija, otključavanje sadržaja). Ako sve okidate na 1.0, korisnik će brzo isključiti haptiku u postavkama. Slično razmišljanje o navigacijskim tranzicijama pokriva vodič za SwiftUI NavigationStack u iOS 26.
Haptički feedback na Apple Watch
Na watchOS-u SensoryFeedback API radi od watchOS 10 nadalje, ali s ograničenim setom varijanti. Dostupne su .success, .failure, .warning, .click, .start, .stop. Za sve što nije pokriveno, koristite izvorni WKInterfaceDevice.current().play(_:):
Sat je haptički bogatiji od telefona jer je Taptic Engine bliži zapešću i korisnici osjete čak i najfinije uzorke. Zbog toga sam na watchOS-u još konzervativniji: .click za svaki tap na crown je previše. Ograničavam haptiku na trenutke gdje je zaista informativna (dolazak podataka, kraj tajmera, potvrda plaćanja). U Complications i Live Activities nikad ne okidam haptiku iz pozadinskog rada jer sat trenutno budi ekran i to je izuzetno naporno korisniku.
Kako testirati haptičke učinke
iOS Simulator ne emitira haptiku, što je najveća "gotcha" za novije developere. Testirate isključivo na fizičkom uređaju. Za bržu iteraciju držim iPhone spojen kroz Xcode i koristim ovaj checklist:
Isključite tihi način rada i provjerite da je "Haptics" uključena u Settings → Sounds & Haptics.
Testirajte s uređajem u ruci, ne na stolu. Na stolu vibracija zvuči glasno i pojačava dojam.
Testirajte i s isključenim zvukom. Haptika mora nositi značenje sama, bez audio pojačanja.
Uključite Accessibility → Motion → Prefer Cross-Fade Transitions i provjerite da haptički okidači i dalje daju smisao bez animacije.
Za unit testove haptički poziv se ne može izravno verificirati jer je API vezan uz UIKit lifecycle. Umjesto toga, apstrahirajte poziv kroz protokol i injectajte mock u testovima:
protocol HapticProviding {
func trigger(_ feedback: SensoryFeedback)
}
struct SystemHaptics: HapticProviding {
func trigger(_ feedback: SensoryFeedback) {
// Za direktne pozive iz view modela — fallback preko UIImpactFeedbackGenerator.
// U pogledima uvijek preferirajte deklarativni .sensoryFeedback modifikator.
}
}
final class MockHaptics: HapticProviding {
var triggeredEvents: [SensoryFeedback] = []
func trigger(_ feedback: SensoryFeedback) {
triggeredEvents.append(feedback)
}
}
Pristupačnost, baterija i najbolje prakse
Postavka "Vibracija" u Settings → Accessibility → Touch je master switch. Ako je korisnik isključi, cijeli Taptic Engine šuti i vaši SensoryFeedback pozivi tiho nestaju. To je namjerno i ne trebate to zaobići. Nikad ne oslanjajte kritičnu informaciju samo na haptiku (npr. "poruka je poslana ako je vibriralo"). Uvijek dupla: vizualni i haptički signal.
Za bateriju: Taptic Engine je iznenađujuće efikasan. Standardni .selection troši oko 0.5 mAh po pozivu, što je zanemarivo osim ako ne okidate stotine puta u sekundi (što ne biste smjeli iz UX razloga). Kontinuirani .hapticContinuous preko Core Haptics je red veličine skuplji, pa ga koristite razumno.
U jednoj od zadnjih aplikacija koje sam radio, orkestraciju haptike na razini cijelog sustava rješavam asinkronim patternom koji dobro pokriva Swift concurrency vodič. Actor koji drži Core Haptics engine i serijalizira pozive kroz aplikaciju sprječava sudare između modula, i to je bila najveća pojedinačna pobjeda za stabilnost haptike.
Često postavljana pitanja
Zašto haptika ne radi u iOS simulatoru?
Simulator nema pristup Taptic Engine hardveru pa se svaki haptički poziv tiho ignorira. Testirajte isključivo na fizičkom iPhone 7 ili novijem uređaju. Xcode konzola ne prijavljuje grešku, jednostavno se ne dogodi ništa.
Koja je razlika između SensoryFeedback i UIImpactFeedbackGenerator?
UIImpactFeedbackGenerator je imperativni UIKit API. Instancirate generator, pripremate ga s prepare() i pozivate impactOccurred(). SensoryFeedback je deklarativni SwiftUI modifikator koji sam upravlja pripremom i okida se promjenom stanja. Interno koriste isti Taptic Engine.
Radi li SensoryFeedback na iPadu?
Ne. iPad modeli nemaju Taptic Engine i haptički pozivi neće emitirati vibraciju. API se ipak neće srušiti; jednostavno će ostati bez učinka. Za iPad razmišljajte o alternativnim signalima (zvuk, vizualna animacija).
Kako dodati vlastite haptičke uzorke?
Za jedinstvene uzorke koristite Core Haptics framework s CHHapticEngine i CHHapticPattern. Možete definirati uzorke u kodu ili učitati iz .ahap JSON datoteka izvezenih iz Apple Haptic Composer alata. SensoryFeedback ne podržava prilagođene uzorke.
Utječe li haptika na trajanje baterije?
Za tipičnu upotrebu utjecaj je zanemariv. Jedan .selection poziv troši oko 0.5 mAh. Kontinuirani haptički događaji kroz Core Haptics troše značajno više. Ne okidajte haptiku više od 5 do 10 puta u sekundi jer to nije samo skupo nego i loše UX.
Vodič za izradu Live Activities i Dynamic Island u iOS 26 pomoću ActivityKit okvira. Postavljanje Widget ekstenzije, ActivityAttributes, sva tri stanja Dynamic Islanda, App Intents i testiranje u Xcode 26 simulatoru.
Praktičan vodič kroz TipKit u iOS 26: kako izraditi savjete, definirati pravila, koristiti TipGroup, sinkronizirati stanje preko iClouda i lokalizirati poruke uz radne SwiftUI primjere.
App Intents u iOS 26 omogućuje da jedan Swift tip radi kao Siri naredba, interaktivni widget i Apple Intelligence akcija. Kompletan vodič s primjerima za Swift 6.