Въведение: Защо Observation Framework променя всичко
С пускането на iOS 17 и Swift 5.9, Apple представи Observation Framework — един фундаментално нов подход към управлението на състоянието в SwiftUI. И не, това не е поредното малко подобрение. Това е цялостно преосмисляне на начина, по който данните протичат между моделите и потребителския интерфейс.
Ако сте работили със SwiftUI от 2019 г. насам, вероятно знаете болката.
Старият подход с протокола ObservableObject и Combine имаше доста сериозни ограничения. Всеки път, когато дори едно @Published свойство се промени, всички изгледи, наблюдаващи обекта, получаваха известие да се обновят — без значение дали реално използваха промененото свойство. Резултатът? Излишни преизчертавания, лоша производителност и разочаровани програмисти.
Observation Framework решава точно този проблем с гранулирано проследяване (fine-grained tracking). SwiftUI вече знае точно кои свойства даден изглед чете и обновява само него, когато конкретно тези свойства се променят. Производителността скача значително, а API-то е далеч по-просто.
В това ръководство ще минем през всички аспекти на Observation Framework — от макрото @Observable и вътрешния му механизъм, през @State, @Bindable и @Environment, до миграция от старата система и практически примери. Хайде да започнем.
Какво е Observation Framework
Observation Framework беше представен на WWDC23 и е базиран на Swift Evolution предложението SE-0395 (Observation). Това е самостоятелен Swift фреймуърк, който не е обвързан единствено със SwiftUI — може да се използва във всякакъв Swift контекст — но честно казано, именно в SwiftUI той наистина блести.
Ядрото на фреймуърка е макрото @Observable, което замества протокола ObservableObject и property wrapper-а @Published. Вместо да маркирате всяко свойство поотделно с @Published, просто анотирате целия клас с @Observable и фреймуъркът автоматично се грижи за проследяването.
Основните компоненти на новата система са:
@Observable— макро, което прави клас наблюдаем с автоматично проследяване на всички свойства@State— вече се използва за притежаване и управление на жизнения цикъл на Observable обекти (замества@StateObject)@Bindable— създава обвързвания (bindings) към свойствата на Observable обект (замества част от ролята на@ObservedObject)@Environment— инжектира Observable обекти чрез средата (замества@EnvironmentObject)@ObservationIgnored— изключва конкретни свойства от проследяване
Това опростяване означава, че трябва да помните по-малко property wrapper-и и е по-трудно да объркате нещо. Важно е да отбележим, че @Observable работи само с класове, не със структури. Причината е, че проследяването на промени изисква референтна семантика — Swift трябва да знае кога обектът се мутира на място, а не когато се създава ново копие.
Какво генерира макрото под повърхността
Когато Swift компилаторът обработи макрото @Observable, той генерира доста код зад кулисите. За да разберем защо Observation Framework е толкова ефективен, нека надникнем какво всъщност се случва:
// Това, което пишете:
@Observable
class UserProfile {
var name: String = ""
var age: Int = 0
}
// Какво компилаторът генерира (опростено):
class UserProfile: Observable {
private var _name: String = ""
var name: String {
get {
access(keyPath: \.name)
return _name
}
set {
withMutation(keyPath: \.name) {
_name = newValue
}
}
}
private var _age: Int = 0
var age: Int {
get {
access(keyPath: \.age)
return _age
}
set {
withMutation(keyPath: \.age) {
_age = newValue
}
}
}
internal let _$observationRegistrar = ObservationRegistrar()
}
Ключовият момент тук е, че access(keyPath:) се извиква при четене, а withMutation(keyPath:) — при запис. Така SwiftUI знае точно кое свойство е прочетено от кой изглед и изпраща известие за преизчертаване само когато конкретно това свойство се промени. Елегантно, нали?
@Observable срещу ObservableObject
За да разберем напълно предимствата на новия подход, нека сравним двата модела едно до друго. Нищо не говори по-ясно от конкретен код.
Старият подход с ObservableObject
import SwiftUI
import Combine
// Модел — стар подход
class ShoppingCart: ObservableObject {
@Published var items: [CartItem] = []
@Published var couponCode: String = ""
@Published var isLoading: Bool = false
var totalPrice: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
func addItem(_ item: CartItem) {
items.append(item)
}
func removeLast() {
items.removeLast()
}
}
struct CartItem: Identifiable {
let id = UUID()
var name: String
var price: Double
var quantity: Int
}
// Изглед — стар подход
struct CartView: View {
@StateObject private var cart = ShoppingCart()
var body: some View {
VStack {
Text("Общо: \(cart.totalPrice, format: .currency(code: "BGN"))")
// Промяна на couponCode ще преизчертае ЦЕЛИЯ изглед,
// въпреки че Text-ът не използва couponCode
}
}
}
// Дъщерен изглед — стар подход
struct CartItemsList: View {
@ObservedObject var cart: ShoppingCart
var body: some View {
List(cart.items) { item in
Text("\(item.name) — \(item.price, format: .currency(code: "BGN"))")
}
}
}
// Среда — стар подход
struct CartEnvironmentView: View {
@EnvironmentObject var cart: ShoppingCart
var body: some View {
Text("Продукти: \(cart.items.count)")
}
}
Новият подход с @Observable
import SwiftUI
import Observation
// Модел — нов подход
@Observable
class ShoppingCart {
var items: [CartItem] = []
var couponCode: String = ""
var isLoading: Bool = false
var totalPrice: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
func addItem(_ item: CartItem) {
items.append(item)
}
func removeLast() {
items.removeLast()
}
}
struct CartItem: Identifiable {
let id = UUID()
var name: String
var price: Double
var quantity: Int
}
// Изглед — нов подход
struct CartView: View {
@State private var cart = ShoppingCart()
var body: some View {
VStack {
Text("Общо: \(cart.totalPrice, format: .currency(code: "BGN"))")
// Промяна на couponCode НЯМА да преизчертае този изглед,
// защото Text-ът не чете couponCode
}
}
}
// Дъщерен изглед — нов подход
struct CartItemsList: View {
var cart: ShoppingCart // Просто обикновено свойство!
var body: some View {
List(cart.items) { item in
Text("\(item.name) — \(item.price, format: .currency(code: "BGN"))")
}
}
}
// Среда — нов подход
struct CartEnvironmentView: View {
@Environment(ShoppingCart.self) var cart
var body: some View {
Text("Продукти: \(cart.items.count)")
}
}
Забележете няколко ключови разлики. Първо, няма @Published — всички съхранявани свойства се проследяват автоматично. Второ, @StateObject е заменен с обикновен @State. Трето (и това е любимата ми част), @ObservedObject изобщо не е необходим — можете просто да подадете обекта като обикновено свойство. И четвърто, @EnvironmentObject е заменен с @Environment(ShoppingCart.self).
Нови property wrapper-и в SwiftUI
С Observation Framework ролята на property wrapper-ите в SwiftUI се промени значително. Нека разгледаме всеки от тях по-подробно.
@State с @Observable обекти
В стария модел, @State беше запазен за прости стойностни типове (Int, String, Bool), а @StateObject се използваше за референтни типове. Сега @State поема и двете роли, което честно казано доста опростява нещата.
@Observable
class FormData {
var username: String = ""
var email: String = ""
var acceptedTerms: Bool = false
var isValid: Bool {
!username.isEmpty && email.contains("@") && acceptedTerms
}
}
struct RegistrationForm: View {
@State private var formData = FormData()
var body: some View {
Form {
TextField("Потребителско име", text: $formData.username)
TextField("Имейл", text: $formData.email)
Toggle("Приемам условията", isOn: $formData.acceptedTerms)
Button("Регистрация") {
submitForm()
}
.disabled(!formData.isValid)
}
}
private func submitForm() {
// Изпращане на данните
}
}
Когато използвате @State с @Observable обект, SwiftUI притежава и управлява жизнения цикъл на обекта. Обектът оцелява между преизчертаванията на изгледа — точно както правеше @StateObject преди.
@Bindable — за обвързвания без притежание
@Bindable е нов property wrapper, представен специално за Observation Framework. Използва се, когато не притежавате обекта (тоест не го създавате с @State), но имате нужда от $ синтаксиса за създаване на обвързвания.
@Observable
class UserSettings {
var darkMode: Bool = false
var fontSize: Double = 14.0
var notificationsEnabled: Bool = true
}
// Родителски изглед притежава обекта
struct SettingsScreen: View {
@State private var settings = UserSettings()
var body: some View {
NavigationStack {
SettingsDetailView(settings: settings)
.navigationTitle("Настройки")
}
}
}
// Дъщерен изглед — получава обекта, но не го притежава
struct SettingsDetailView: View {
@Bindable var settings: UserSettings
var body: some View {
Form {
Toggle("Тъмен режим", isOn: $settings.darkMode)
Slider(value: $settings.fontSize, in: 10...24, step: 1) {
Text("Размер на шрифта: \(Int(settings.fontSize))")
}
Toggle("Известия", isOn: $settings.notificationsEnabled)
}
}
}
Без @Bindable, бихте получили грешка при компилация при опит да използвате $settings.darkMode. Правилото е просто: ако трябва само да четете — обикновено свойство е достатъчно; ако трябва да пишете с $ — слагате @Bindable.
@Environment с @Observable обекти
Синтаксисът за инжектиране на Observable обекти чрез средата също се промени. Вместо @EnvironmentObject, сега се използва @Environment с типа на обекта.
@Observable
class AuthManager {
var isLoggedIn: Bool = false
var currentUser: String? = nil
func login(username: String) {
currentUser = username
isLoggedIn = true
}
func logout() {
currentUser = nil
isLoggedIn = false
}
}
// Коренен изглед — инжектиране в средата
struct MyApp: App {
@State private var authManager = AuthManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(authManager) // Не .environmentObject()!
}
}
}
// Дъщерен изглед — получаване от средата
struct ProfileView: View {
@Environment(AuthManager.self) var auth
var body: some View {
if auth.isLoggedIn {
Text("Здравей, \(auth.currentUser ?? "Потребител")!")
Button("Изход") { auth.logout() }
} else {
Text("Моля, влезте в профила си.")
}
}
}
Обърнете внимание — използваме .environment(authManager) вместо .environmentObject(authManager). По-чисто е и типът на обекта служи като ключ. Малък детайл, но прави кода по-четим.
Миграция от ObservableObject към @Observable
Добрата новина е, че миграцията от стария модел към новия е относително директна. Лошата — трябва внимание към детайлите. Ето стъпка по стъпка как става.
Стъпка 1: Промяна на модела
// ПРЕДИ:
class TaskStore: ObservableObject {
@Published var tasks: [Task] = []
@Published var selectedFilter: Filter = .all
@Published var searchText: String = ""
}
// СЛЕД:
import Observation
@Observable
class TaskStore {
var tasks: [Task] = []
var selectedFilter: Filter = .all
var searchText: String = ""
}
Премахвате съответствието с ObservableObject, добавяте макрото @Observable и махате всички @Published анотации. Не забравяйте import Observation.
Стъпка 2: Обновяване на изгледите
// ПРЕДИ:
struct TaskListView: View {
@StateObject private var store = TaskStore()
var body: some View {
List(store.tasks) { task in
TaskRow(task: task)
}
}
}
// СЛЕД:
struct TaskListView: View {
@State private var store = TaskStore()
var body: some View {
List(store.tasks) { task in
TaskRow(task: task)
}
}
}
Стъпка 3: Обновяване на дъщерни изгледи
// ПРЕДИ:
struct TaskFilterView: View {
@ObservedObject var store: TaskStore
var body: some View {
Picker("Филтър", selection: $store.selectedFilter) {
ForEach(Filter.allCases, id: \.self) { filter in
Text(filter.rawValue).tag(filter)
}
}
}
}
// СЛЕД:
struct TaskFilterView: View {
@Bindable var store: TaskStore // @Bindable, защото имаме нужда от $
var body: some View {
Picker("Филтър", selection: $store.selectedFilter) {
ForEach(Filter.allCases, id: \.self) { filter in
Text(filter.rawValue).tag(filter)
}
}
}
}
Стъпка 4: Обновяване на средата
// ПРЕДИ:
ContentView()
.environmentObject(store)
// СЛЕД:
ContentView()
.environment(store)
// ПРЕДИ:
@EnvironmentObject var store: TaskStore
// СЛЕД:
@Environment(TaskStore.self) var store
Обобщение на замените
ObservableObject→@Observable@Published→ премахва се (автоматично)@StateObject→@State@ObservedObject→ обикновено свойство или@Bindable(ако трябват bindings)@EnvironmentObject→@Environment(ТипаНаОбекта.self).environmentObject()→.environment()
Минималната версия за деплойване е iOS 17+, така че ако поддържате по-стари версии, ще трябва да използвате #available проверки или условна компилация. На практика много екипи вече са минали изцяло на iOS 17+, но ако не сте сред тях — бъдете готови за малко допълнителна работа.
Производителност: Гранулирано проследяване
Тук идва най-готината част. Гранулираното проследяване е може би най-значимото предимство на Observation Framework. Нека видим конкретен пример.
@Observable
class AppState {
var userName: String = "Иван"
var notificationCount: Int = 0
var theme: String = "light"
var isLoading: Bool = false
}
struct HeaderView: View {
var state: AppState
var body: some View {
// Този изглед чете САМО userName
Text("Здравей, \(state.userName)!")
.font(.title)
}
}
struct BadgeView: View {
var state: AppState
var body: some View {
// Този изглед чете САМО notificationCount
if state.notificationCount > 0 {
Text("\(state.notificationCount)")
.badge(state.notificationCount)
}
}
}
struct ThemeView: View {
var state: AppState
var body: some View {
// Този изглед чете САМО theme
Text("Тема: \(state.theme)")
}
}
struct DashboardView: View {
@State private var state = AppState()
var body: some View {
VStack {
HeaderView(state: state)
BadgeView(state: state)
ThemeView(state: state)
Button("Увеличи известията") {
state.notificationCount += 1
// Със стария ObservableObject: HeaderView, BadgeView И ThemeView
// всички щяха да се преизчертаят.
// С @Observable: САМО BadgeView се преизчертава,
// защото само той чете notificationCount.
}
}
}
}
Тази разлика е огромна при по-сложни приложения. При ObservableObject, промяна на което и да е @Published свойство изпращаше известие чрез objectWillChange publisher-а на Combine, което караше всички наблюдаващи изгледи да се преизчертаят. С @Observable, SwiftUI записва кои свойства са прочетени по време на изпълнението на body и реагира само на промени в тези конкретни свойства.
На практика това означава:
- По-малко CPU цикли за ненужни преизчертавания
- По-плавни анимации и по-отзивчив интерфейс
- По-дълъг живот на батерията (особено при приложения с чести промени на данните)
- Не ви се налага ръчно да разделяте модели на по-малки части заради производителността
Последното е важно. Преди бяхме принудени да разбиваме view model-ите на по-малки парчета, само за да избегнем излишни преизчертавания. Сега това просто не е нужно.
@ObservationIgnored и @ObservationTracked
Въпреки че @Observable автоматично проследява всички съхранявани свойства, понякога имате нужда от по-фин контрол.
@ObservationIgnored
@ObservationIgnored маркира свойства, които не трябва да предизвикват обновяване на изгледи при промяна. Кога е полезно?
- За кешове и временни данни, които не влияят на UI-я
- За тежки обекти като мрежови клиенти или менажери на файлове
- За вътрешно състояние, което не е визуално значимо
import Observation
@Observable
class MediaPlayer {
var currentTrack: String = "Няма избрана песен"
var isPlaying: Bool = false
var volume: Double = 0.5
// Тези свойства НЕ предизвикват преизчертаване на изгледи
@ObservationIgnored
var audioBuffer: [Float] = []
@ObservationIgnored
var analyticsData: [String: Any] = [:]
@ObservationIgnored
private var internalTimer: Timer?
@ObservationIgnored
let networkClient = NetworkClient()
func play() {
isPlaying = true // Ще предизвика обновяване
analyticsData["lastPlayed"] = Date() // Няма да предизвика обновяване
}
func updateBuffer(with samples: [Float]) {
audioBuffer = samples // Няма да предизвика обновяване
}
}
@ObservationTracked
@ObservationTracked е обратното на @ObservationIgnored и честно казано рядко ще го ползвате директно. Макрото @Observable автоматично анотира всяко свойство с @ObservationTracked зад кулисите, така че няма нужда да го правите ръчно.
@Observable
class ExampleModel {
// Тези две декларации са еквивалентни:
var name: String = "" // Автоматично @ObservationTracked
@ObservationTracked var age: Int = 0 // Изрично @ObservationTracked (излишно)
@ObservationIgnored var cache: [String: Data] = [:] // Не се проследява
}
withObservationTracking извън SwiftUI
Observation Framework не е ограничен до SwiftUI — можете да го използвате навсякъде в Swift кода си чрез функцията withObservationTracking. Това е особено полезно, ако работите с UIKit или просто искате да реагирате на промени от произволен контекст.
import Observation
@Observable
class DataStore {
var items: [String] = []
var lastUpdated: Date = Date()
}
let store = DataStore()
// Наблюдаване на промени извън SwiftUI
withObservationTracking {
// Този блок записва кои свойства се четат
print("Брой елементи: \(store.items.count)")
} onChange: {
// Извиква се, когато НЯКОЕ от прочетените свойства се промени
print("Данните се промениха!")
}
Критично важно: Callback-ът onChange се извиква само веднъж. След първата промяна наблюдението спира. Ако искате да продължите да наблюдавате, трябва да извикате withObservationTracking отново — рекурсивно.
import Observation
@Observable
class SensorData {
var temperature: Double = 20.0
var humidity: Double = 50.0
}
func observeContinuously(data: SensorData) {
withObservationTracking {
print("Температура: \(data.temperature)°C, Влажност: \(data.humidity)%")
} onChange: {
// Планираме ново наблюдение на главната опашка
DispatchQueue.main.async {
observeContinuously(data: data)
}
}
}
let sensorData = SensorData()
observeContinuously(data: sensorData)
// По-късно, всяка промяна ще бъде засечена:
sensorData.temperature = 22.5 // Ще отпечата новите стойности и ще се регистрира отново
Това е полезно за сценарии като логване, синхронизация с бекенд или обновяване на UIKit изгледи. В SwiftUI не е нужно да правите това ръчно — фреймуъркът го прави автоматично зад кулисите.
Често срещани грешки и капани
При работа с Observation Framework има няколко капана, в които е лесно да попаднете. Ето най-важните (от личен опит включително).
1. Използване на @StateObject вместо @State
Ако вече ползвате @Observable, не бъркайте стария @StateObject с новия подход. Те просто не работят заедно.
@Observable
class ViewModel {
var data: [String] = []
}
// ГРЕШНО: @StateObject не работи с @Observable
struct MyView: View {
// @StateObject private var vm = ViewModel() // Грешка при компилация!
// ПРАВИЛНО:
@State private var vm = ViewModel()
var body: some View {
List(vm.data, id: \.self) { item in
Text(item)
}
}
}
2. Забравяне на @Bindable, когато трябват обвързвания
Класическа грешка. Подавате @Observable обект на дъщерен изглед без @Bindable и после се чудите защо $ синтаксисът не работи.
@Observable
class Settings {
var volume: Double = 0.5
}
struct VolumeControl: View {
// ГРЕШНО: Няма да компилира с $settings.volume
// var settings: Settings
// ПРАВИЛНО: @Bindable позволява $ синтаксис
@Bindable var settings: Settings
var body: some View {
Slider(value: $settings.volume, in: 0...1)
}
}
3. Опит за използване на @Observable със структури
@Observable работи само с класове. Структурите просто няма как да го поддържат — имат стойностна семантика.
// ГРЕШНО: Структурите не поддържат @Observable
// @Observable
// struct UserData {
// var name: String = ""
// }
// ПРАВИЛНО: Използвайте клас
@Observable
class UserData {
var name: String = ""
}
4. @Environment с грешен синтаксис
Новият синтаксис за @Environment използва типа, не key path. Лесно е да се объркате, особено ако дълго сте работили със стария API.
@Observable
class ThemeManager {
var isDark: Bool = false
}
// ГРЕШНО (стар синтаксис):
// @EnvironmentObject var theme: ThemeManager
// ПРАВИЛНО (нов синтаксис):
// @Environment(ThemeManager.self) var theme
5. Четене на свойства извън body
Тук е важно да знаете: SwiftUI проследява достъпа до свойства само по време на изпълнението на body. Ако прочетете свойство в onAppear или друг callback, то няма да бъде проследено за автоматично обновяване.
@Observable
class Counter {
var count: Int = 0
}
struct CounterView: View {
var counter: Counter
var body: some View {
VStack {
// ДОБРЕ: Четене в body — ще се проследи
Text("Брояч: \(counter.count)")
Button("Увеличи") {
counter.count += 1
}
}
.onAppear {
// Четенето тук НЕ регистрира проследяване
// Но това обикновено не е проблем, защото onAppear
// се изпълнява за странични ефекти, а не за визуализация
print("Текущ брояч: \(counter.count)")
}
}
}
6. Съвместимост със Swift 6.2 и Approachable Concurrency
В Swift 6.2 с функцията Approachable Concurrency, @Observable класовете работят безпроблемно с @MainActor по подразбиране. Тъй като SwiftUI изгледите вече са изолирани към главния актьор, вашите Observable модели също се изпълняват на главния актьор — което елиминира повечето главоболия с конкурентността.
// В Swift 6.2 с Approachable Concurrency,
// @Observable класовете по подразбиране са @MainActor
@Observable
class ProfileViewModel {
var name: String = ""
var isLoading: Bool = false
func loadProfile() async {
isLoading = true
// Мрежовото извикване се изпълнява на фонова нишка,
// но обновяването на свойствата се връща на MainActor
let profile = await fetchProfile()
name = profile.name
isLoading = false
}
private func fetchProfile() async -> (name: String, id: Int) {
try? await Task.sleep(for: .seconds(1))
return (name: "Иван Петров", id: 1)
}
}
Практически пример: Приложение за задачи
Време е да обединим всичко в цялостен практически пример — приложение за управление на задачи, което използва @Observable, @State, @Bindable и @Environment. Това е нещо, което реално бихте могли да използвате като отправна точка за собствен проект.
Модел на данните
import Foundation
import Observation
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool = false
var priority: Priority = .medium
var createdAt: Date = Date()
enum Priority: String, CaseIterable {
case low = "Нисък"
case medium = "Среден"
case high = "Висок"
}
}
@Observable
class TodoStore {
var items: [TodoItem] = []
var searchText: String = ""
var selectedPriority: TodoItem.Priority? = nil
// Изчислено свойство — автоматично се обновява
var filteredItems: [TodoItem] {
items.filter { item in
let matchesSearch = searchText.isEmpty ||
item.title.localizedCaseInsensitiveContains(searchText)
let matchesPriority = selectedPriority == nil ||
item.priority == selectedPriority
return matchesSearch && matchesPriority
}
}
var completedCount: Int {
items.filter(\.isCompleted).count
}
var pendingCount: Int {
items.count - completedCount
}
func addItem(title: String, priority: TodoItem.Priority = .medium) {
let newItem = TodoItem(title: title, priority: priority)
items.append(newItem)
}
func toggleItem(_ item: TodoItem) {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items[index].isCompleted.toggle()
}
}
func deleteItems(at offsets: IndexSet) {
let sortedItems = filteredItems
for offset in offsets {
if let index = items.firstIndex(where: { $0.id == sortedItems[offset].id }) {
items.remove(at: index)
}
}
}
}
Главен изглед на приложението
import SwiftUI
@main
struct TodoApp: App {
@State private var store = TodoStore()
var body: some Scene {
WindowGroup {
TodoListView()
.environment(store)
}
}
}
Списък със задачи
struct TodoListView: View {
@Environment(TodoStore.self) var store
@State private var showingAddSheet = false
var body: some View {
@Bindable var store = store
NavigationStack {
VStack(spacing: 0) {
// Статистика
HStack {
Label("\(store.pendingCount) чакащи", systemImage: "circle")
Spacer()
Label("\(store.completedCount) завършени", systemImage: "checkmark.circle.fill")
}
.font(.subheadline)
.foregroundStyle(.secondary)
.padding()
// Списък
List {
ForEach(store.filteredItems) { item in
TodoRowView(item: item) {
store.toggleItem(item)
}
}
.onDelete { offsets in
store.deleteItems(at: offsets)
}
}
}
.navigationTitle("Моите задачи")
.searchable(text: $store.searchText, prompt: "Търсене...")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
showingAddSheet = true
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .topBarLeading) {
Menu("Филтър") {
Button("Всички") { store.selectedPriority = nil }
ForEach(TodoItem.Priority.allCases, id: \.self) { priority in
Button(priority.rawValue) {
store.selectedPriority = priority
}
}
}
}
}
.sheet(isPresented: $showingAddSheet) {
AddTodoView()
.environment(store)
}
}
}
}
Ред за задача
struct TodoRowView: View {
let item: TodoItem
let onToggle: () -> Void
var body: some View {
HStack {
Button(action: onToggle) {
Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(item.isCompleted ? .green : .gray)
.font(.title3)
}
.buttonStyle(.plain)
VStack(alignment: .leading, spacing: 4) {
Text(item.title)
.strikethrough(item.isCompleted)
.foregroundStyle(item.isCompleted ? .secondary : .primary)
HStack {
Text(item.priority.rawValue)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(priorityColor.opacity(0.2))
.foregroundStyle(priorityColor)
.clipShape(Capsule())
Text(item.createdAt, style: .date)
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
}
.padding(.vertical, 4)
}
private var priorityColor: Color {
switch item.priority {
case .low: return .blue
case .medium: return .orange
case .high: return .red
}
}
}
Добавяне на нова задача
struct AddTodoView: View {
@Environment(TodoStore.self) var store
@Environment(\.dismiss) var dismiss
@State private var title = ""
@State private var priority: TodoItem.Priority = .medium
var body: some View {
NavigationStack {
Form {
Section("Заглавие") {
TextField("Какво трябва да направите?", text: $title)
}
Section("Приоритет") {
Picker("Приоритет", selection: $priority) {
ForEach(TodoItem.Priority.allCases, id: \.self) { p in
Text(p.rawValue).tag(p)
}
}
.pickerStyle(.segmented)
}
}
.navigationTitle("Нова задача")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Отказ") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Добави") {
store.addItem(title: title, priority: priority)
dismiss()
}
.disabled(title.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
}
}
}
Този пример демонстрира пълния набор от инструменти: @Observable за модела, @State за притежаване на обекта, @Environment за инжектиране в дъщерни изгледи, @Bindable за създаване на обвързвания, и обикновени свойства за четене. Ако тръгвате от нулата с Observation Framework, това е солидна основа.
Често задавани въпроси (FAQ)
Мога ли да използвам @Observable с iOS 16 или по-стари версии?
За съжаление, не. Observation Framework изисква минимум iOS 17, macOS 14, watchOS 10 или tvOS 17. Ако поддържате по-стари версии, трябва да продължите с ObservableObject или да приложите условна компилация с if #available(iOS 17, *). На практика доста екипи поддържат и двата подхода паралелно по време на прехода.
@Observable работи ли със структури?
Не. Макрото @Observable работи само с класове. Структурите имат стойностна семантика — при всяка промяна се създава ново копие — а Observation Framework изисква референтна семантика за проследяване на мутациите. Ако имате нужда от наблюдаем стойностен тип, просто използвайте @State с обикновена структура — SwiftUI си има собствен механизъм за това.
Какво се случва, ако забравя @Bindable?
Получавате грешка при компилация: "Cannot find '$variableName' in scope." Решението е просто — добавете @Bindable пред свойството. Ако изгледът само чете данни, @Bindable не е нужен — той е необходим само когато трябва да записвате обратно чрез bindings.
Мога ли да смесвам ObservableObject и @Observable в едно приложение?
Да, абсолютно. Двата подхода могат да съществуват едновременно, което е полезно при постепенна миграция. Можете да конвертирате модели един по един. Само внимавайте — @StateObject работи единствено с ObservableObject, а @State за обекти — с @Observable. Не ги смесвайте за един и същ тип.
withObservationTracking извиква ли onChange повече от веднъж?
Не. Callback-ът onChange се извиква само веднъж — при първата промяна на което и да е проследено свойство. След това наблюдението спира. За непрекъснато наблюдение, трябва да извикате withObservationTracking отново в onChange блока. SwiftUI прави това автоматично при всяко преизчертаване, така че в контекста на SwiftUI не е нужно да се притеснявате за това.