Актуализирано: 13 септември 2026 г.
За да добавите хаптична обратна връзка в SwiftUI, използвайте модификатора .sensoryFeedback(_:trigger:). Работи от iOS 17 нагоре и покрива около 90% от типичните UX случаи с един ред код. За персонализирани патерни (например текстура на плат, вибрация с нарастващ интензитет или синхронизация със звук) преминете към Core Haptics и CHHapticEngine. В това ръководство давам конкретни стойности за intensity, sharpness и response, които съм извадил от продукционни SwiftUI приложения през 2026 г.
SensoryFeedback срещу Core Haptics: коя да изберете?
Първото решение, което вземам, когато дизайнер ме моли за „леко потупване при потвърждение“, е дали ще стигна със SensoryFeedback, или ще ми трябва CHHapticEngine. Практически правилото ми е просто: ако мога да опиша ефекта с една от седемте семантични думи на Apple (success, warning, error, selection, impact, increase, decrease), използвам SensoryFeedback. Ако имам нужда от прогресивен интензитет, синхронизация с аудио, ритмичен патерн или ефект, който да опише текстура (например преминаване през тик-марки на плъзгач с усещане за „зъбци“), скачам на Core Haptics.
| Характеристика | SensoryFeedback (SwiftUI) | Core Haptics (CHHapticEngine) |
|---|---|---|
| Минимална версия | iOS 17 / watchOS 10 / tvOS 17 | iOS 13 (iPhone 8+) |
| Стил на API | Декларативен модификатор | Императивен engine + събития |
| Семантични предефинирани | 13 case-а (.success, .selection, .impact...) | Няма, вие описвате всичко |
| Персонализирани патерни | Не | Да (transient + continuous) |
| Синхронизация с аудио | Не | Да (audio + haptic pattern player) |
| Ред код за „success“ | 1 ред | ~30 реда |
| Тестване в симулатора | Само визуален индикатор | Само визуален индикатор |
| Разход на батерия | Пренебрежим | Забележим при дълги continuous |
За контекст: в последното приложение, което пуснах в продукция, около 40 от 45 хаптик задействания се справиха със SensoryFeedback. Останалите 5 (текстурата при плъзгане в custom карта, ефектът при разгъване на pull-quote, и синхронизацията с преходни звуци) изискваха Core Haptics с AHAP файлове.
Как да добавя haptic feedback в SwiftUI?
Най-краткият път е модификаторът .sensoryFeedback(_:trigger:). Той се задейства, когато стойността на trigger се промени, така избягвате императивно „извикай това сега“ и оставяте SwiftUI да свърже събитието с промяна на състоянието.
import SwiftUI
struct FavoriteButton: View {
@State private var isFavorited = false
var body: some View {
Button {
isFavorited.toggle()
} label: {
Image(systemName: isFavorited ? "heart.fill" : "heart")
.foregroundStyle(isFavorited ? .pink : .secondary)
}
// Задейства се при всяко превключване на isFavorited.
.sensoryFeedback(.impact(weight: .light, intensity: 0.7),
trigger: isFavorited)
}
}
Ако искате хаптиката да се задейства само при определен преход (например „от false към true“), използвайте overload-а с closure:
.sensoryFeedback(trigger: isFavorited) { oldValue, newValue in
// Тик само при добавяне в любими, не при премахване.
guard !oldValue, newValue else { return nil }
return .success
}
Всички варианти на SensoryFeedback
В iOS 26 наборът от семантични случаи е:
.success,.warning,.error: за потвърждения и грешки..selection: за плъзгачи, picker-и и промяна на избора..increase,.decrease: за stepper-и и промяна на числена стойност..start,.stop: за начало и край на дълго действие..alignment,.levelChange: за snap към решетка или „прекрачване на праг“..pathComplete: за приключване на многостъпков поток..impact(weight: .light | .medium | .heavy, intensity: 0...1): общото „тик“..impact(flexibility: .soft | .solid | .rigid, intensity: 0...1): когато характерът на удара е по-важен от „теглото“.
Условно задействане с trigger closures
Ако едно и също view покрива няколко състояния, вместо да добавяте по един модификатор за всеки случай, върнете различни feedback стойности от closure-а:
enum SubmissionState { case idle, submitting, succeeded, failed }
@State private var state: SubmissionState = .idle
.sensoryFeedback(trigger: state) { _, newState in
switch newState {
case .succeeded: return .success
case .failed: return .error
case .submitting: return .impact(weight: .light, intensity: 0.4)
case .idle: return nil
}
}
Връщането на nil е ключово. Така казвате на SwiftUI, че тази промяна не заслужава хаптика, вместо да задействате празен pulse.
Core Haptics: персонализирани патерни с CHHapticEngine
Когато семантичните случаи не са достатъчни (например ви трябва плъзгач, който „тика“ при преминаване през всеки 10% с постепенно нарастващ интензитет), преминете към CHHapticEngine. Той работи с два типа събития: transient (кратък удар, под 100 ms) и continuous (постоянна вибрация с продължителност до 30 секунди).
import CoreHaptics
final class HapticsController {
private var engine: CHHapticEngine?
init?() {
// 1. Проверете за хардуерна поддръжка (iPhone 8+, не работи на iPad).
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
return nil
}
do {
engine = try CHHapticEngine()
// 2. Автоматично рестартиране, ако системата спре engine-а.
engine?.resetHandler = { [weak self] in
try? self?.engine?.start()
}
engine?.stoppedHandler = { reason in
print("Engine stopped: \(reason.rawValue)")
}
try engine?.start()
} catch {
return nil
}
}
func playTick(intensity: Float, sharpness: Float) {
guard let engine else { return }
let event = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity),
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness)
],
relativeTime: 0
)
do {
let pattern = try CHHapticPattern(events: [event], parameters: [])
let player = try engine.makePlayer(with: pattern)
try player.start(atTime: CHHapticTimeImmediate)
} catch {
print("Failed to play tick: \(error)")
}
}
}
Двете оси, които променят характера на удара, са:
- Intensity (0.0 – 1.0): колко силен е ефектът. Под 0.3 е едвам доловим; над 0.85 започва да звучи натрапчиво.
- Sharpness (0.0 – 1.0): 0.0 е мек, тъп „бум“; 1.0 е остро, метално „тик“. За премахване на елемент използвам 0.8; за „мек“ drag drop използвам 0.15.
Continuous събития и параметрични криви
За да опишете нарастващ мотор (например „зареждане на действие“ при задържане), използвайте hapticContinuous плюс CHHapticParameterCurve, който променя интензитета във времето:
let continuous = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 0,
duration: 1.5
)
let ramp = CHHapticParameterCurve(
parameterID: .hapticIntensityControl,
controlPoints: [
.init(relativeTime: 0.0, value: 0.3),
.init(relativeTime: 1.5, value: 1.0)
],
relativeTime: 0
)
let pattern = try CHHapticPattern(events: [continuous], parameterCurves: [ramp])
AHAP файлове: декларативно описание на патерна
Ако предпочитате да опишете патерна веднъж и да го преизползвате (или още по-добре, дизайнерът ви да го редактира без Swift), използвайте формат AHAP (Apple Haptic and Audio Pattern). Това е обикновен JSON, който заредете от bundle-а:
{
"Version": 1.0,
"Pattern": [
{ "Event": {
"Time": 0.0,
"EventType": "HapticTransient",
"EventParameters": [
{ "ParameterID": "HapticIntensity", "ParameterValue": 0.9 },
{ "ParameterID": "HapticSharpness", "ParameterValue": 0.4 }
]
}},
{ "Event": {
"Time": 0.12,
"EventType": "HapticTransient",
"EventParameters": [
{ "ParameterID": "HapticIntensity", "ParameterValue": 0.6 },
{ "ParameterID": "HapticSharpness", "ParameterValue": 0.7 }
]
}}
]
}
guard let url = Bundle.main.url(forResource: "PurchaseConfirmed", withExtension: "ahap") else { return }
try engine?.playPattern(from: url)
За детайлите на формата вижте официалната AHAP документация на Apple. Там са описани всички поддържани полета, включително аудио събитията, които могат да се синхронизират с хаптиките.
Хаптики за конкретни UX сценарии
Ето рецептите, които използвам най-често. Числата не са произволни. Тествал съм ги на iPhone 15 Pro и iPhone 17 с реални потребители, следейки как реагират след 30 повторения.
Форми и валидация
При успешно изпращане на форма: .success. При грешка на поле: .error, но само веднъж. Ако валидирате на всеки keystroke, ще подлудите потребителя. Ключът е да задействате хаптиката на trigger: submitAttempted, а не на trigger: isValid.
Плъзгачи и селектори
За snap-ване към стъпка използвайте .selection. За плъзгач без стъпки, но с психологически „центрирана“ стойност (например при изравняване с 0), използвайте .alignment. За детайли как да интегрирате това с плавни анимации, вижте нашето ръководство за SwiftUI анимации с PhaseAnimator и KeyframeAnimator. Хаптиките и spring анимациите се проектират заедно, не поотделно.
Pull-to-refresh и swipe actions
Стандартният refreshable модификатор в SwiftUI вече играе .impact(weight: .medium) автоматично при достигане на прага. Не го дублирайте. При swipe-to-delete добавете .impact(flexibility: .rigid, intensity: 0.8) точно в момента, в който бутонът се разкрие напълно.
Успешни и неуспешни действия при плащане
За плащания винаги избирам .success, дори когато е изкушаващо да измисля „custom победен акорд“. Потребителите са тренирани от Apple Pay да разпознават този патерн, и той им казва „парите ти са в безопасност“ по-бързо, отколкото визуалния tick.
Хаптики на Apple Watch с WatchKit
Apple Watch използва отделен, много по-ограничен API. Няма Core Haptics с патерни, само предефинирани случаи през WKInterfaceDevice:
import WatchKit
WKInterfaceDevice.current().play(.success)
WKInterfaceDevice.current().play(.notification)
WKInterfaceDevice.current().play(.retry)
WKInterfaceDevice.current().play(.directionUp)
WKInterfaceDevice.current().play(.directionDown)
От watchOS 10 нагоре обаче SensoryFeedback в SwiftUI също работи и се превежда автоматично към съответните WatchKit случаи. Практически това означава, че ако споделяте SwiftUI код между iOS и watchOS target, същите .sensoryFeedback модификатори продължават да работят. Поредната причина да предпочетете декларативния подход.
Хаптики и достъпност: не забравяйте Reduce Motion
Хаптиките попадат в същата категория, в която попадат и анимациите. За някои потребители те са дразнител или дори тригер за гадене. iOS централно уважава настройката „Настройки → Достъпност → Тактилна обратна връзка“, но добър UX инженер прави повече: следи UIAccessibility.isReduceMotionEnabled и предоставя in-app превключвател.
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@AppStorage("hapticsEnabled") private var hapticsEnabled = true
.sensoryFeedback(trigger: state) { _, newState in
guard hapticsEnabled, !reduceMotion else { return nil }
return newState == .succeeded ? .success : nil
}
Ако искате пълно ръководство по темата, включително как да интегрирате VoiceOver и Dynamic Type около същите състояния, прочетете нашето ръководство за SwiftUI Accessibility. Достъпността и хаптиките са част от един и същ дизайн-разговор, никога не ги проектирайте изолирано.
Тестване на хаптики: симулатор срещу устройство
Xcode симулаторът от версия 15.4 нагоре показва визуален индикатор за задействана хаптика (малка кръгла иконка в горния десен ъгъл на прозореца), но не пренася реални вибрации. Единственият надежден начин да тествате е на устройство, и по-точно на същото устройство, на което ще инсталира потребителят, защото Taptic Engine-ите на iPhone SE, iPhone 15 Pro и iPhone 17 Pro Max са физически различни и произвеждат различно „чувство“ за същите параметри.
Моят workflow: първо ги пиша с числа „по слух“, после ги проверявам на три референтни устройства: iPhone SE (най-слабият Taptic Engine), iPhone 15 (среден) и iPhone 17 Pro (най-точен). Ако едно и също intensity=0.7 звучи „лениво“ на SE и „агресивно“ на Pro, слизам на 0.55 и печеля устойчивост между устройствата. За препоръки по интеграция със системните жестове погледнете и SwiftUI ръководството за iOS 26. Част от новите Liquid Glass ефекти носят и подразбиращи се хаптики, които не трябва да дублирате.
Често срещани грешки при работа с haptics
След преглед на десетки code review-та, ето грешките, които виждам да се повтарят:
- Извикване на хаптики от background thread. И SensoryFeedback, и
CHHapticEngine.makePlayerтрябва да се използват от главната опашка, иначе engine-ът тихо ще игнорира събитието. - Един
CHHapticEngineза целия app life-cycle. Не създавайте нов engine за всяка хаптика, защото струва около 15 ms инициализация. Дръжте един singleton, стартиран при launch. - Задействане на хаптики по
onAppear. Потребителят не разбира защо телефонът вибрира при отваряне на екран. Ако наистина искате приветствие, ползвайтеtask {}с delay 0.3 s. - Хаптики без визуален feedback. Ако бутонът не показва анимация, но телефонът вибрира, потребителят се обърква. Правило: хаптика без анимация е бъг.
- Дублиране на системни хаптики.
refreshable,DatePickerиSliderвече свирят собствени хаптики, не добавяйте свои отгоре. - Игнориране на
CHHapticEngine.capabilitiesForHardware(). На iPad и на iPhone 7 и по-стари Core Haptics хвърля грешка при инициализация. Винаги проверявайте.
За най-актуалните препоръки на Apple по „кога да задействате хаптика“ вижте Playing Haptics в Human Interface Guidelines. Задължително четиво преди дизайн ревю.
Често задавани въпроси
Работят ли хаптиките на всички iPhone модели?
Не. Core Haptics с CHHapticEngine изисква Taptic Engine от второ поколение, тоест iPhone 8 или по-нов. iPad-ите нямат Taptic Engine и CHHapticEngine.capabilitiesForHardware().supportsHaptics винаги връща false. Простите UIKit-стил хаптики през UINotificationFeedbackGenerator работят и на по-стари устройства, но с ограничен репертоар.
Каква е разликата между SensoryFeedback и UIImpactFeedbackGenerator?
SensoryFeedback е декларативен SwiftUI модификатор от iOS 17, който се задейства при промяна на trigger. UIImpactFeedbackGenerator е императивен UIKit клас, който извиквате ръчно с .impactOccurred(). За нови SwiftUI проекти винаги предпочитайте SensoryFeedback, защото той се държи по-предвидимо със state management-а и работи и на watchOS.
Как да тествам haptics в Xcode симулатора?
Симулаторът не пренася истински вибрации. Той само показва малък визуален индикатор, когато се задейства хаптика. За реално тестване използвайте физическо устройство и, ако е възможно, три различни модела (например iPhone SE, iPhone 15 и iPhone 17 Pro), за да проверите как параметрите се превеждат на различни Taptic Engine-и.
Как да създам персонализирани хаптик патерни?
Използвайте Core Haptics с CHHapticEngine. Или конструирайте патерна програмно с масив от CHHapticEvent обекти (transient и continuous), или го опишете в AHAP JSON файл и го заредете с engine.playPattern(from: url). AHAP е предпочитан, защото дизайнерът ви може да го редактира без Swift знания.
Защо моята хаптика не се задейства при първо стартиране на приложението?
Най-често защото CHHapticEngine не е стартиран още, или защото е бил спрян при преминаване във фон и не е рестартиран. Имплементирайте engine.resetHandler и engine.stoppedHandler, които го рестартират автоматично, и извиквайте try engine.start() при scenePhase == .active.




