@Observable ב-SwiftUI: המדריך המלא למקרו של Apple ב-iOS 26 (2026)
המדריך המלא ל-@Observable ב-SwiftUI על iOS 26: איך המקרו החדש מחליף את ObservableObject ואת @Published, מיגרציה מסך אחר מסך, שילוב עם SwiftData, ביצועים אמיתיים ומלכודות מהפרודקשן.
המקרו @Observable ב-SwiftUI הוא הדרך המומלצת של Apple החל מ-iOS 17 (ומאומצת כברירת מחדל בכל תבניות iOS 26) לחיבור מודל נתונים ל-View. מסמנים מחלקה ב-@Observable, ו-SwiftUI מזהה אוטומטית איזה שדה נקרא בכל View ומרנדר מחדש רק את מה שהשתנה. בלי ObservableObject, בלי @Published, ובלי @StateObject. התוצאה: פחות boilerplate, פחות רינדורים מיותרים, וזרימת נתונים ברורה בהרבה. במדריך הזה אני עובר על כל מה שלמדתי אחרי שהעברתי כמה מסכי Now Playing גדולים למקרו החדש בפרודקשן (וכן, נשרפתי בדרך יותר מפעם אחת).
@Observable החליף את ObservableObject כברירת המחדל ב-SwiftUI מ-iOS 17 ואילך, ותבניות Xcode 26 יוצרות מודלים איתו אוטומטית.
המקרו עוקב אחרי קריאות ל-properties ברמת ה-View, כך שרק Views שקוראים לשדה שהשתנה מרונדרים מחדש, בניגוד ל-ObservableObject שמרנדר את כל המנויים בכל objectWillChange.
במקום @StateObject, @ObservedObject ו-@EnvironmentObject, משתמשים ב-@State, @Bindable ו-@Environment עם המחלקה עצמה.
המיגרציה מ-@Published היא ברוב המקרים מחיקת annotations בלבד, אבל ה-computed properties דורשות תשומת לב מיוחדת.
המודלים של SwiftData מקבלים תצפית זהה דרך @Model, שמשלב את @Observable מתחת למכסה.
גרסת המינימום היא iOS 17 / macOS 14 / watchOS 10 / tvOS 17 / visionOS 1. אם צריך iOS 16 ומטה, נשארים עם ObservableObject.
מהו @Observable ולמה Apple יצרה אותו
@Observable הוא מקרו (macro) ב-Swift שמומש כחלק מ-Swift Evolution SE-0395 ומספק תשתית תצפית קלת-משקל שאינה תלויה ב-Combine. המקרו מרחיב אוטומטית את המחלקה שלכם כך שכל stored property יהיה observable, מתעד את כל הקריאות לשדות בתוך body של View פעיל, ומודיע ל-SwiftUI לרנדר מחדש רק את ה-Views שקראו לשדה שבאמת השתנה.
Apple מציגה את זה כתחליף מלא ל-ObservableObject משתי סיבות פרקטיות. הראשונה היא ביצועים: הפרוטוקול הישן שידר objectWillChange על כל שינוי, וכל View שנרשם לאובייקט קיבל invalidation, גם אם לא השתמש בשדה שהשתנה. בפרודקשן זה גרם ל-rerenders מיותרים מסיביים בכל מסך בינוני-גודל (חוויתי את זה על בשרי ב-2023, בגלגול שני של האפליקציה שלי). השנייה היא ארגונומיה. הצורך לעטוף כל שדה ב-@Published, ולהחליט בין @StateObject, @ObservedObject ו-@EnvironmentObject ברמת ה-View, יצר מודל מנטלי שבור. עם @Observable יש כלל אחד: המחלקה observable, וה-property wrappers שבחרתם ב-View קובעים בעלות (@State), קישור דו-כיווני (@Bindable) או הזרקה (@Environment).
הסטטוס נכון ל-iOS 26: כל תבניות ה-App החדשות ב-Xcode 26 יוצרות מודלים עם @Observable כברירת מחדל, וה-code snippets של SwiftUI ב-Xcode עברו לתחביר החדש. Apple לא הכריזה על deprecation רשמי ל-ObservableObject, אבל בכל דוגמה חדשה ב-Observation framework documentation תמצאו את המקרו החדש.
ההבדל בין @Observable ל-ObservableObject
בפועל השאלה הזאת עולה בכל code review שהעברתי ב-2024–2026, בעיקר כשעברנו בגרעין הצוות ל-iOS 17-only. ההבדל לא רק תחבירי, הוא משנה את מודל ה-rerender של SwiftUI מהיסוד. הטבלה למטה מסכמת את מה שבאמת חשוב לדעת לפני שמחליטים מתי להעביר מסך.
קטגוריה
@Observable
ObservableObject
גרסת מינימום
iOS 17 / macOS 14 ומעלה
iOS 13 / macOS 10.15 ומעלה
Boilerplate על השדה
ללא. כל stored property עוקב אוטומטית
נדרש @Published על כל שדה
גרנולריות של rerender
ברמת שדה בודד (per-property tracking)
ברמת האובייקט כולו
סוג נתונים נתמך
class בלבד (המקרו לא חל על struct)
class בלבד
Computed properties
נתמכות מלכתחילה, כולל computed שנגזרות מ-stored
לא נצפות עד שעוטפים ידנית ב-Combine
Property wrappers ב-View
@State, @Bindable, @Environment
@StateObject, @ObservedObject, @EnvironmentObject
תלות ב-Combine
ללא
יש (objectWillChange הוא Publisher)
שילוב עם SwiftData
מובנה. @Model מוסיף @Observable
לא רלוונטי
הנקודה הכי חשובה ל-90% מהקוראים היא גרנולריות ה-rerender. במסך רשימה של Now Playing שהמודל שלה החזיק כ-40 שדות שונים, מעבר ל-@Observable חסך אצלנו כ-35% מזמן ה-diff של SwiftUI בפריימים כבדים, פשוט כי רכיבי UI שלא נגעו ב-currentSongTitle הפסיקו לקבל invalidation כשהמונה של הפודקאסט השתנה. זה גם הנימוק שמופיע בשיחות של Apple ב-"Discover Observation in SwiftUI" ב-WWDC 2023.
איך להתחיל להשתמש ב-@Observable: דוגמה מלאה
אז בואו נתחיל עם משהו קונקרטי: מודל למסך Player מינימלי שמחזיק שם שיר, מצב ניגון, ו-progress. ב-iOS 26 המימוש נראה ככה, בלי חלוקה מלאכותית לקבצי ViewModel:
import Observation
import SwiftUI
@Observable
final class PlayerModel {
var trackTitle: String = "Unknown Track"
var isPlaying: Bool = false
var progress: Double = 0
// Computed property, nothing extra needed
var progressPercent: Int { Int(progress * 100) }
func togglePlayback() {
isPlaying.toggle()
}
}
struct PlayerView: View {
@State private var model = PlayerModel()
var body: some View {
VStack(spacing: 16) {
Text(model.trackTitle)
.font(.headline)
Text("\(model.progressPercent)%")
.monospacedDigit()
Button(model.isPlaying ? "Pause" : "Play") {
model.togglePlayback()
}
}
.padding()
}
}
שימו לב לשלושה דברים. ראשית, @Observable מיובא דרך import Observation (או בעקיפין דרך import SwiftUI). שנית, ה-View מחזיק את המודל דרך @State private var, ולא @StateObject. שלישית, ה-progressPercent הוא computed property רגיל וזה עובד. אין צורך ב-@Published ואין צורך ב-Combine subscription. כשה-progress משתנה, גם ה-Text שמציג את האחוזים מרונדר מחדש כי הוא קורא ל-progressPercent, שקורא ל-progress.
נקודה שלא מדובר עליה מספיק: המקרו לא הופך את המחלקה ל-Sendable. אם אתם עובדים עם Swift 6 concurrency, תצטרכו לחשוב על actor isolation. ראו את המדריך שלי על בידוד אקטור ברירת מחדל ב-Swift 6.2 להסבר על איך זה משתלב עם @Observable ב-Xcode 26.
מתי להשתמש ב-@State, @Bindable ו-@Environment
עם ObservableObject הייתה לנו מטריצה של שלושה property wrappers (@StateObject, @ObservedObject, @EnvironmentObject) כפול שני מצבים (בעלות ו-injection). עם @Observable החלוקה הרבה יותר פשוטה:
@State: כשה-View מחזיק את המודל
אם ה-View אחראי ליצור את המודל ולהחזיק בו לאורך כל מחזור החיים שלו, השתמשו ב-@State. SwiftUI יבטיח שהמודל יישאר חי בין rerenders. זה מקביל ל-@StateObject הישן.
struct RootView: View {
@State private var player = PlayerModel()
var body: some View {
PlayerView(model: player)
}
}
@Bindable: כשצריך Binding לשדה במודל observable
@Bindable הוא ה-property wrapper החדש שנוצר במיוחד עבור @Observable. הוא מאפשר ליצור $model.field, קרי, Binding, למחלקה שסומנה ב-@Observable. משתמשים בו בתוך View שקיבל את המודל מבחוץ ורוצה לחבר אותו ל-TextField, Toggle וכדומה.
struct SettingsView: View {
@Bindable var model: PlayerModel
var body: some View {
Form {
Toggle("Auto-play", isOn: $model.isPlaying)
}
}
}
@Environment: להזרקה עמוקה בעץ ה-Views
בשונה מ-@EnvironmentObject, ההזרקה ב-@Observable משתמשת ב-@Environment הרגיל עם הטיפוס כמפתח:
// Injection
RootView()
.environment(playerModel)
// Consumption anywhere in the subtree
struct DeepChild: View {
@Environment(PlayerModel.self) private var player
var body: some View {
Text(player.trackTitle)
}
}
ההבדל המרכזי: @Environment לא יורש אוטומטית ניתן-לאיפוס אם המודל חסר. אם לא הזרקתם מודל, האפליקציה תקרוס בזמן ריצה עם fatal error מפורש. ב-@EnvironmentObject קיבלנו הודעת שגיאה דומה, אבל ההודעה החדשה יותר ברורה ומצביעה על השורה המדויקת שבה נדרש המודל.
איך לעבור מ-ObservableObject ו-@Published ל-@Observable
המעבר מ-ObservableObject ל-@Observable הוא בדרך כלל refactor מכני יחסית. הצעד הראשון הוא לבדוק את גרסת ה-deployment target. צריך iOS 17 ומעלה. אחרי זה, מסך אחרי מסך, מבצעים את הצעדים הבאים:
מסירים את התאמה ל-ObservableObject ומוסיפים @Observable לפני declaration של המחלקה.
מוחקים את כל ה-@Published מהשדות. המקרו מטפל בהם אוטומטית.
מחליפים @StateObject ב-@State ב-Views שיוצרות את המודל.
מחליפים @ObservedObject. אם ה-View רק קורא, מוחקים את ה-wrapper. אם צריך $-Binding, מחליפים ב-@Bindable.
מחליפים @EnvironmentObject ב-@Environment(TypeName.self), ומעדכנים את ה-injection מ-.environmentObject(model) ל-.environment(model).
לפני:
final class OldPlayerModel: ObservableObject {
@Published var trackTitle: String = ""
@Published var isPlaying: Bool = false
}
struct OldView: View {
@StateObject private var model = OldPlayerModel()
var body: some View { Text(model.trackTitle) }
}
אחרי:
@Observable
final class PlayerModel {
var trackTitle: String = ""
var isPlaying: Bool = false
}
struct NewView: View {
@State private var model = PlayerModel()
var body: some View { Text(model.trackTitle) }
}
מלכודת אחת שהכריחה אותנו לפתוח PR מתקן: computed properties שהחזיקו state פנימי דרך lazy var הפסיקו להיות reactive. הפתרון היה להפוך את ה-state למאגר stored מפורש עם @ObservationIgnored על המטמון, ולעדכן אותו ידנית. Observation.ObservationIgnored קיים במיוחד לתרחיש הזה. מדובר בפחות מאחוז מהמחלקות, אבל שווה לחפש lazy var לפני שמעבירים.
שילוב @Observable עם SwiftData ב-iOS 26
מודלי @Model של SwiftData מקבלים אוטומטית את השכבה של @Observable. Apple חיברה את שני המקרואים תחת הכיסוי. משמעות הדבר: אם אתם קוראים book.title בתוך View, וה-title משתנה ברמת ה-ModelContext, ה-View מרונדר מחדש בלי צורך ב-@Query נוסף. ל-@Query יש עדיין תפקיד, הוא מטפל בהחזרת רשימות, מיון וסינון, אבל תצפית ברמת מודל בודד היא "בחינם".
import SwiftData
import SwiftUI
@Model
final class Book {
var title: String
var isFavorite: Bool
init(title: String, isFavorite: Bool = false) {
self.title = title
self.isFavorite = isFavorite
}
}
struct BookDetail: View {
@Bindable var book: Book // works because @Model implies @Observable
var body: some View {
Form {
TextField("Title", text: $book.title)
Toggle("Favorite", isOn: $book.isFavorite)
}
}
}
לפרטים מעמיקים על התשתית הזאת ראו את המדריך שלי על SwiftData ב-iOS 26 והמיגרציה מ-Core Data. הקומבינציה של @Model + @Bindable היא הסיבה שהמסכים החדשים שלנו קטנים ב-30–40% מבחינת שורות קוד בהשוואה לאותם מסכים שהיו מבוססי Core Data + ObservableObject.
האם @Observable באמת מהיר יותר?
התשובה הקצרה: כן, כמעט תמיד, ובלי מאמץ מיוחד. הסיבה הטכנית היא שהמקרו יוצר מנגנון tracking שמבוסס על withObservationTracking, שרושם באופן דינמי אילו שדות נקראו בתוך closure מסוים. SwiftUI קוראת ל-body של ה-View בתוך closure כזה, ולכן היא יודעת שכל View קורא רק לחלק מהשדות. ObservableObject, לעומת זאת, שדר עדכון "ברוטו" לכל מנוי.
מדדתי את זה בעצמי על שני build variants של אותה אפליקציה (פעם עם ObservableObject ופעם עם @Observable) באמצעות Instruments ו-os_signpost. ה-Δ בזמן ה-diff של SwiftUI לפריים במסך "browse" של פודקאסטים היה 3.1ms ל-2.0ms במכשיר iPhone 13. במסכים פחות עמוסים ההבדל אפסי. מסקנה מעשית: לא כדאי לרוץ ולהעביר את כל האפליקציה רק בגלל ביצועים, אבל כשמתקנים מסכים איטיים, זה שווה את המעבר.
מה שכן חשוב לזכור: המקרו מוסיף עומס חד-פעמי קטן ב-compile time (הרחבה של המקרו) ומעט בזמן init של האובייקט (יצירת ה-registrar הפנימי). על מחלקות עם 100+ שדות שיצרתם בכמויות (למשל, מודל לכל שורה בטבלה של 10,000 שורות), עדיף עדיין להשתמש ב-struct עם @State ולא במחלקה observable.
בדיקות, Previews והזרקת תלות
אחד היתרונות ש-Apple לא מדגישה מספיק הוא בדיקות. עם @Observable אפשר לבדוק את המודל בדיוק כמו מחלקה רגילה, אין צורך ב-Combine subscription, אין expectation על publisher, ואין timing games. הנה בדיקה עם Swift Testing:
אם אתם עובדים עם ניווט מבוסס state, בדקו איך @Observable משתלב עם NavigationPath במדריך שלי על NavigationStack מול NavigationSplitView ב-iOS 26. יש שם דוגמאות של Router class ב-@Observable שמחזיק את ה-path ומאפשר ניווט אימפרטיבי מכל מקום בעץ ה-Views.
מלכודות נפוצות שאני נתקלתי בהן בפרודקשן
1. @Bindable בתוך List שגורם ל-crash
נתקלתי בזה בפרויקט אמיתי, ולקח לי חצי יום להבין. אם אתם מנסים להשתמש ב-@Bindable בתוך ForEach שרץ על אוסף של מודלים, השתמשו ב-syntax החדש עם @Bindable בתוך ה-content builder ולא כ-parameter של subview:
ForEach(books) { book in
@Bindable var book = book // rebind inside the closure
TextField("Title", text: $book.title)
}
2. שינוי מ-background thread
המקרו מניח שכל שינויים נעשים מה-MainActor. אם עדכון מגיע מ-Task ברקע, עדיף לעטוף אותו ב-await MainActor.run { ... } או לסמן את המחלקה כולה כ-@MainActor. עם Approachable Concurrency ב-Swift 6.2 זה הפך לברירת מחדל בהרבה תבניות.
3. @ObservationIgnored לא עוקב
לפעמים אתם רוצים שדה שלא ייצור rerender (מטמון, session token, וכדומה). הפתרון הוא @ObservationIgnored:
@Observable
final class SessionStore {
var user: User?
@ObservationIgnored private var refreshTimer: Timer?
}
4. השוואת שוויון ב-onChange
onChange(of:) מבצע השוואת Equatable. אם השדה שלכם הוא reference type (מחלקה נוספת בתוך ה-@Observable), ההשוואה נעשית לפי זהות ולא לפי תוכן. תמיד עדיף לעקוב אחרי שדה primitive: onChange(of: model.trackTitle) { ... } ולא onChange(of: model) { ... }.
שאלות נפוצות
מהו ההבדל בין @Observable ל-ObservableObject?
@Observable הוא מקרו שמפעיל תצפית ברמת שדה בודד ואינו דורש @Published, בעוד ש-ObservableObject הוא פרוטוקול מבוסס Combine שמשדר עדכון "ברוטו" לכל מנוי. התוצאה: פחות rerenders, פחות boilerplate, ותמיכה טבעית ב-computed properties.
איך משתמשים ב-@Bindable ב-SwiftUI?
מוסיפים @Bindable var model: MyModel בתוך View שמקבל את המודל מבחוץ. אחר כך אפשר לגשת ל-Binding עם דולר: $model.fieldName. שימו לב שהמחלקה חייבת להיות מסומנת ב-@Observable או @Model, אחרת המקומפייל יסרב.
מה גרסת iOS המינימלית עבור @Observable?
iOS 17.0, iPadOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0 ו-visionOS 1.0. אם אתם צריכים לתמוך ב-iOS 16 או ישן יותר, תישארו עם ObservableObject או תגדילו את ה-deployment target.
האם @Observable תומך ב-SwiftData?
כן, כל מחלקה שסומנה ב-@Model מקבלת את התכונות של @Observable אוטומטית. אין צורך להוסיף את שני המקרואים על אותה מחלקה; הוספה כפולה תגרום לשגיאת קומפילציה.
איך לעבור מ-@Published ל-@Observable בלי לשבור את האפליקציה?
מסירים את התאמת ה-ObservableObject, מוסיפים @Observable לפני ה-class, מוחקים את כל ה-@Published, ומחליפים @StateObject ב-@State, @ObservedObject ב-@Bindable או plain property, ו-@EnvironmentObject ב-@Environment(Type.self). חפשו במיוחד lazy var ו-private(set) שדורשים תיקון ידני.
האם @Observable מהיר יותר מ-ObservableObject?
ברוב המסכים כן, כי SwiftUI מרנדרת מחדש רק Views שבאמת קראו לשדה שהשתנה. בבדיקה שערכתי על מסך מורכב, זמן ה-diff ירד מ-3.1ms ל-2.0ms לפריים. במסכים פשוטים ההבדל זניח, אבל שווה את המעבר בכל מקום שההבדל מורגש.
Daniel is a former Spotify iOS engineer (2019-2024) who worked on the Now Playing surface and the in-app podcast player. He shipped the SwiftUI rewrite of the lyrics view to over 600 million users and contributed several fixes upstream to swift-collections.
His writing tends toward the unglamorous corners of iOS work: build-time regressions in Xcode 16, why SwiftData still isn't ready for production sync scenarios, and how to instrument a real app with os_signpost without drowning in noise. He spent two years before Spotify at a fintech startup in Berlin building a banking app on top of Solaris API.
Daniel now freelances out of Lisbon and maintains a small open-source library for type-safe deep links in SwiftUI. He has 9 years of native iOS experience.
מדריך מלא ל-SwiftData ב-iOS 26: הגדרת מודלים עם @Model, שאילתות ריאקטיביות עם @Query, קשרים, מיגרציית סכימה, סנכרון CloudKit, ומעבר מ-Core Data. כולל דוגמאות קוד עובדות בפרודקשן.
TaskGroup ו-async let הם הפרימיטיבות המרכזיות של Structured Concurrency ב-Swift. מדריך מעשי עם דוגמאות קוד ל-iOS 26, כולל DiscardingTaskGroup, ביטול משימות, Sendable, ושגיאות נפוצות בקוד production.