SensoryFeedback הוא ה-view modifier ההצהרתי של SwiftUI להפעלת רטט והפטיקה על iPhone, Apple Watch ו-iPad עם Magic Keyboard, שהוצג ב-iOS 17 ונעשה לסטנדרט המומלץ החל מ-iOS 26. במקום להחזיק instance של UIImpactFeedbackGenerator ולנהל את מחזור החיים שלו ידנית, אתם קושרים סוג הפטיקה לערך state שמופעל ב-onChange. כשה-value משתנה, SwiftUI מפעיל את הפידבק במקום בטיימינג הנכון של מחזור הרנדור, בדיוק באותו רגע שבו האנימציה מגיעה ל-settle. במאמר הזה אני עוברת על כל סוגי ה-SensoryFeedback, על תבניות ה-trigger שהוכיחו את עצמן בשטח, ועל האופן שבו אני מסנכרנת אותם עם spring(response: 0.4, dampingFraction: 0.75). זה הערך שאני חוזרת אליו כמעט בכל אינטראקציית מגע.
SensoryFeedback החליף את UIImpactFeedbackGenerator כ-API המומלץ להפטיקה ב-SwiftUI מ-iOS 17 ואילך, עם שדרוגי דיוק ב-iOS 26.
ה-modifier מקבל trigger מסוג Equatable. כל שינוי בערך מפעיל את ההפטיקה, כולל חתימות טריגר לפי מעבר בין ערכים ספציפיים.
סוגי ההפטיקה החדשים ב-iOS 26 כוללים .impact(flexibility:intensity:) עם שליטה עדינה יותר מאשר .light/.medium/.heavy.
סנכרון עם אנימציית spring דורש לוודא שה-state שמפעיל את ההפטיקה משתנה באותו tick של הרנדור, אחרת ההפטיקה תרגיש מנותקת ממה שהעין רואה.
watchOS 26 חושף את אותו modifier עם מיפוי אוטומטי ל-WKHapticType, אז אפליקציות multi-platform חוסכות תרגום ידני.
Core Haptics עדיין נדרש לפידבק מותאם אישית עם עקומות עוצמה, אבל ל-95% מהמקרים SensoryFeedback מספיק ונכון יותר.
מה זה SensoryFeedback ואיך הוא עובד
SensoryFeedback הוא struct שמייצג פידבק חושי בודד (impact, selection, success, warning, error ועוד), יחד עם ה-view modifier .sensoryFeedback(_:trigger:) שמחבר אותו ל-lifecycle של ה-view. הרעיון המרכזי הוא שההפטיקה היא side effect של שינוי state, לא של פעולת משתמש. במקום להגיד "כשהמשתמש לוחץ, הפעל רטט", אתם מגדירים "כשהערך הזה משתנה, הפעל רטט", וזה מאפשר להפטיקה להתלוות בצורה נכונה גם לשינויי state שמגיעים מ-network response, מ-Combine pipeline, או מ-animation completion.
ההבדל בפועל מרגיש קטן אבל הוא עמוק. ב-UIKit הישן, הייתי צריכה להחזיק UIImpactFeedbackGenerator כ-property, לקרוא prepare() מראש כדי להוריד latency, ולוודא שאני משחררת אותו במקום הנכון. עם SensoryFeedback, SwiftUI מנהל את כל זה עבורי: הוא מכין את ה-engine בפעם הראשונה שה-modifier מופיע, שומר אותו בין renders, ומשחרר אותו כשה-view יוצא מהעץ. פחות boilerplate, פחות באגים של haptics שלא מרגישים.
מבחינת חוויית משתמש, ההפטיקה הנכונה היא הפטיקה שהמשתמש לא שם לב אליה כשהיא קורית, ומרגיש שמשהו חסר כשהיא לא. אני מגדירה לעצמי כלל פשוט: אם אתם צריכים להסביר למשתמש שיש הפטיקה, ההפטיקה שלכם חזקה מדי או במקום הלא נכון.
סוגי ההפטיקה הזמינים ב-iOS 26
iOS 26 כולל תשעה סוגי הפטיקה מובנים ב-SensoryFeedback, כל אחד עם character שונה ותפקיד סמנטי ברור. להשתמש בסוג הלא נכון זה כמו לבחור פונט לא נכון: טכנית זה עובד, אבל זה לא מרגיש כמו iOS. הנה הרשימה עם ההמלצות שלי מהשטח.
סוג
אינטנסיביות
מתי להשתמש
.selection
קלה מאוד
picker, slider עם נקודות snap, ניווט בין tabs
.impact(weight: .light)
קלה
toggle של UI element קטן, בחירת פריט ברשימה
.impact(weight: .medium)
בינונית
פתיחת sheet, הצגת alert, drag-and-drop drop
.impact(weight: .heavy)
חזקה
לחיצה על CTA משמעותי, אישור פעולה חשובה
.impact(flexibility:intensity:)
מותאמת (iOS 26)
אנימציית spring עם משך מותאם, לסנכרן intensity לפי amplitude
.success
שלישייה עולה
שמירה מוצלחת, תשלום עבר, פעולה הושלמה
.warning
זוגית
אזהרה שדורשת תשומת לב אבל לא שגיאה
.error
שלישייה יורדת
כשל בפעולה, ולידציה נכשלה, network error
.increase/.decrease
מיקרו
stepper, שינוי ערך numeric קטן
הטעות הכי נפוצה שאני רואה (כמעט בכל code review) היא שימוש ב-.impact(weight: .heavy) בכל מקום. זה עייף את המשתמש תוך דקה. אני מקצה .heavy רק לאירועים שקורים פעם ב-session: checkout, delete confirmation, unlock. לכל השאר, .light או .selection.
החידוש הגדול ב-iOS 26 הוא .impact(flexibility: .rigid | .solid | .soft, intensity: 0.0...1.0). ה-flexibility משנה את ה-character של הפעימה עצמה. .rigid נשמע כמו לחיצה על כפתור מכני, ו-.soft כמו אצבע שנוגעת בכרית. אני משתמשת ב-.soft באינטראקציות עם onboarding, וב-.rigid עם כפתורי action בר-קבע.
איך להוסיף הפטיקה ב-SwiftUI בשלושה שלבים
הדוגמה המינימלית הבאה מדגימה שימוש נכון ב-sensoryFeedback: state boolean שמייצג בחירה של פריט, ו-trigger שמופעל כשהערך משתנה. ה-modifier ממוקם על ה-view שמכיל את האינטראקציה, לא על כפתור ספציפי.
שימו לב לשלושה דברים. הראשון, שה-trigger הוא isFavorite עצמו ולא ה-action של הכפתור. זה קריטי: אם ה-toggle נכשל מסיבה כלשהי (validation, network), ההפטיקה לא תופעל. השני, ה-spring(response: 0.4, dampingFraction: 0.75). הערך הזה נותן bounce מורגש שנגמר תוך כ-350ms, שזה בערך המרחק שבו ההפטיקה מסתיימת. שלישית, ה-scaleEffect הוא visual anchor להפטיקה: העין רואה תזוזה, האצבע מרגישה pulse, וזה נקרא ל-brain כאירוע יחיד.
לתרחישים מורכבים יותר, אתם יכולים לספק trigger משוכלל שמפעיל הפטיקה רק כשמעבר ספציפי קורה.
@State private var itemCount = 0
var body: some View {
Stepper("פריטים: \(itemCount)", value: $itemCount, in: 0...10)
.sensoryFeedback(trigger: itemCount) { oldValue, newValue in
if newValue > oldValue {
return .increase
} else if newValue < oldValue {
return .decrease
} else {
return nil
}
}
}
הצורה הזו, closure שמחזירה SensoryFeedback?, מאפשרת לבחור סוג הפטיקה דינמית לפי סוג השינוי, או לוותר על ההפטיקה כשלא רלוונטית (return nil).
תבניות trigger מתקדמות לשליטה עדינה
ה-signature הבסיסית trigger: T עובדת מצוין ל-95% מהמקרים, אבל ה-API מציע חתימות נוספות שכדאי להכיר. הנה הרביעייה שאני משתמשת בהן בקוד production.
.sensoryFeedback(_:trigger:). הבסיסי, מפעיל בכל שינוי של הערך.
.sensoryFeedback(_:trigger:condition:). מפעיל רק כש-condition(oldValue, newValue) מחזיר true. שימושי לפילטרים.
.sensoryFeedback(trigger:_:) עם closure. מחזיר SensoryFeedback? לפי הערכים, כמו בדוגמה הקודמת.
.sensoryFeedback(_:trigger:) where T: Equatable & Sendable. הגרסה החדשה ב-iOS 26 שתומכת ב-trigger cross-actor.
דוגמה טובה ל-condition: pull-to-refresh שאמור להפעיל הפטיקה רק ברגע שה-refresh יצא לפועל, לא בכל update של scroll position.
הכלל שאני שומרת עליו: trigger פשוט תמיד עדיף על מורכב. אם אתם מוצאים את עצמכם בונים condition בת ארבע שורות, סימן שה-state שלכם צריך רפקטור. כנראה שיש שם ערך נגזר שאתם צריכים לחשב פעם אחת ולעשות לו bind. פידבק חושי הוא נגזרת של state, לא של אירועים.
סנכרון הפטיקה עם אנימציות spring
כאן מתחילה האמנות. הפטיקה שמגיעה 100ms מוקדם או מאוחר מהאנימציה שהיא אמורה ללוות, מרגישה כמו באג, גם אם המשתמש לא יכול להצביע על הבעיה. הבחירות שלי:
Impact פשוט עם spring(response: 0.4, dampingFraction: 0.75). ההפטיקה מופעלת מיידית כשה-state משתנה, ה-spring מסיים בערך תוך 350ms, וזה מרגיש כמו יחידה אחת.
אנימציית ניווט עם spring(response: 0.55, dampingFraction: 0.85). אני משתמשת ב-.selection, שהיא קלה מספיק כדי לא להיגמר לפני שהמעבר הוויזואלי גומר.
Alert / sheet presentation עם spring(response: 0.5, dampingFraction: 0.9). הבחירה שלי היא .impact(weight: .medium), מופעל על isPresented.
הטעות הגדולה שראיתי בקוד: להפעיל הפטיקה בסוף האנימציה עם animation(...).onChange { ... }. זה מרגיש רע כי המוח מצפה לפידבק פיזי ברגע שה-motion מתחיל, לא כשהוא נעצר. הפטיקה היא ה-attack, לא ה-decay. אם אתם צריכים marker לסוף אנימציה, השתמשו ב-.impact(flexibility: .soft, intensity: 0.4): כמעט לא מורגש, רק sub-conscious confirmation.
אני ממליצה לקרוא את המדריך שלנו על TaskGroup ו-async let ב-Swift Concurrency אם אתם מפעילים הפטיקה בעקבות פעולות רשת מקבילות. התיאום בין task completion ל-main actor state update הוא בדיוק המקום שבו הפטיקה מנותקת. עוד הקשר רלוונטי הוא בחירת NavigationStack מול NavigationSplitView, כי ב-NavigationSplitView על iPad ההפטיקה על ניווט אמורה להיות שקטה יותר. היד רחוקה יותר מהמסך.
המקבילה ב-SwiftUI עם SensoryFeedback הרבה יותר קצרה, ואין צורך ב-prepare() כי SwiftUI מטפל בזה אוטומטית.
// SwiftUI — modern approach
struct MyView: View {
@State private var tapCount = 0
var body: some View {
Button("לחץ עלי") { tapCount += 1 }
.sensoryFeedback(.impact(weight: .medium), trigger: tapCount)
}
}
הנה שולחן ההשוואה המהיר:
שיקול
UIImpactFeedbackGenerator
SensoryFeedback
גישה
imperative, קוראים method
declarative, קושרים ל-state
ניהול מחזור חיים
ידני (prepare, retain)
אוטומטי (SwiftUI)
Latency
נמוך רק אחרי prepare()
נמוך תמיד
שילוב עם SwiftUI
דורש UIViewRepresentable או Wrapper
נטיבי
watchOS / macOS
לא זמין
זמין (עם fallback)
Cross-actor safety
MainActor bound
מטופל אוטומטית
למי שיש הרבה legacy UIKit, לפי התיעוד הרשמי של Apple ל-sensoryFeedback, ניתן להשתמש ב-modifier גם על view שמוצג בתוך UIHostingController, כך שאפילו שילוב הדרגתי עובד. אני בונה SwiftUI wrapper רק לכפתור ההפטיקה עצמו ומטמיעה אותו ב-UIKit, וזה חוסך overhead ומשמר את ההתנהגות המודרנית. (הגישה הזו עבדה לי בפרויקט אחרון שבו היו לנו 40 מסכי UIKit ורק 3 מסכי SwiftUI חדשים.)
SensoryFeedback ב-watchOS 26 ובפלטפורמות אחרות
אחד היתרונות הגדולים של המעבר ל-SensoryFeedback הוא שהקוד שלכם עובד ללא שינוי על watchOS 26. ה-modifier ממפה אוטומטית את סוגי ההפטיקה ל-WKHapticType המתאים על ה-Apple Watch.
SensoryFeedback (iOS)
WKHapticType (watchOS)
.success
.success
.warning
.notification
.error
.failure
.selection
.click
.impact(weight: .light)
.click
.impact(weight: .heavy)
.start
.increase/.decrease
.directionUp/.directionDown
ב-macOS, ההפטיקה עובדת רק כשה-Magic Trackpad או Force Touch trackpad מחובר. SwiftUI מבצע detection ומדלג בשקט אם אין מנוע פיזי. ב-tvOS, ה-modifier לא עושה כלום, וזו התנהגות מכוונת, לא באג.
למי שבונה עבור vision Pro (visionOS 26), SensoryFeedback ממופה ל-spatial audio cue קצר, כי אין vibrating hardware. Apple מציינת את זה ב-מפגש WWDC 2026 על haptics ב-SwiftUI, ולדעתי זו החלטת עיצוב מבריקה: היא שומרת על העקביות הסמנטית של הקוד בלי לדרוש מכל מפתח לחשוב על כל פלטפורמה.
מתי לעבור ל-Core Haptics במקום
SensoryFeedback מכסה את הצרכים של רוב האפליקציות, אבל יש מקרים שבהם אתם צריכים לרדת ל-Core Haptics ולתכנת את הפעימות ידנית. הכללים שלי:
עברו ל-Core Haptics אם: אתם צריכים pattern שמשך יותר מ-300ms, אתם רוצים סנכרון מדויק עם audio, אתם בונים משחק שדורש התאמת רעש לגרפיקה, או שאתם צריכים לשלוט ב-transient sharpness וב-continuous intensity בו-זמנית.
הישארו עם SensoryFeedback אם: אתם רק מסמנים אירועים UI (בחירה, הצלחה, שגיאה, לחיצה), אתם רוצים behavior עקבי עם שאר iOS, או שאתם עובדים על multi-platform ולא רוצים לכתוב fallbacks.
Core Haptics דורש CHHapticEngine, ניהול lifecycle, טעינת AHAP files או בניית patterns פרוגרמטית. זו התחייבות של כמה מאות שורות קוד. שווה את זה רק אם ההפטיקה היא חלק ממוצר החוויה עצמו, לא רק סימון של אירועים.
שאלות נפוצות
מה ההבדל בין SensoryFeedback ל-UIImpactFeedbackGenerator?
SensoryFeedback הוא API הצהרתי שמותאם ל-SwiftUI ומחובר ל-state, בעוד ש-UIImpactFeedbackGenerator הוא API imperative של UIKit שדורש קריאה מפורשת. SensoryFeedback גם מנהל את ה-engine lifecycle אוטומטית ותומך ב-watchOS ו-macOS ללא קוד נוסף.
איך מפעילים רטט ב-SwiftUI ל-iOS 26?
הוסיפו את ה-modifier .sensoryFeedback(.impact(weight: .light), trigger: someState) על ה-view הרלוונטי. ה-someState חייב להיות Equatable, ובכל פעם שהערך שלו משתנה, SwiftUI מפעיל את ההפטיקה במקום הנכון של lifecycle הרנדור.
האם SensoryFeedback עובד ב-iPad וב-Mac?
ב-iPad, כן, גם דרך Magic Keyboard עם trackpad. ב-Mac, רק כשמחובר Force Touch trackpad, אחרת ה-modifier מדלג בשקט. ב-Apple Vision Pro הוא ממופה ל-spatial audio cue קצר במקום רטט פיזי.
איך מסנכרנים את ההפטיקה עם אנימציית spring?
ההפטיקה צריכה להתרחש ברגע ההתחלה של האנימציה (ה-attack), לא בסופה. עדכנו את ה-state trigger בתוך withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) { ... }, כך ההפטיקה והאנימציה מתחילות באותו רגע רנדור והמוח קורא להן כאירוע יחיד.
מתי כדאי להשתמש ב-Core Haptics במקום SensoryFeedback?
עברו ל-Core Haptics כאשר אתם צריכים patterns שמשכם יותר מ-300ms, סנכרון מדויק עם audio, או שליטה עדינה ב-transient sharpness וב-continuous intensity. עבור סימון אירועי UI פשוטים, הישארו עם SensoryFeedback.
המדריך המלא ל-@Observable ב-SwiftUI על iOS 26: איך המקרו החדש מחליף את ObservableObject ואת @Published, מיגרציה מסך אחר מסך, שילוב עם SwiftData, ביצועים אמיתיים ומלכודות מהפרודקשן.
מדריך מלא ל-SwiftData ב-iOS 26: הגדרת מודלים עם @Model, שאילתות ריאקטיביות עם @Query, קשרים, מיגרציית סכימה, סנכרון CloudKit, ומעבר מ-Core Data. כולל דוגמאות קוד עובדות בפרודקשן.
TaskGroup ו-async let הם הפרימיטיבות המרכזיות של Structured Concurrency ב-Swift. מדריך מעשי עם דוגמאות קוד ל-iOS 26, כולל DiscardingTaskGroup, ביטול משימות, Sendable, ושגיאות נפוצות בקוד production.