@Observable и фреймворк Observation

Когда состояние сложнее флажка — это целая модель с логикой — на сцену выходит макрос @Observable из фреймворка Observation, представленного в iOS 17.
Суть урока: @Observable — современная замена ObservableObject. Достаточно пометить класс макросом, и SwiftUI сам отследит, какие именно свойства читает каждое вью, перерисовывая только нужное.

@State хорош для простых данных. Но модель приложения — например, корзина с товарами и методами — это класс с логикой, который должны видеть несколько экранов. Раньше для этого использовали ObservableObject с @Published. С iOS 17 пришёл фреймворк Observation и макрос @Observable, который проще и быстрее:

import Observation

@Observable
class Cart {
    var items: [String] = []
    var total = 0

    func add(_ item: String, price: Int) {
        items.append(item)
        total += price
    }
}

Никаких @Published над каждым свойством — макрос делает класс наблюдаемым целиком. Во вью такую модель хранят через знакомый @State (для объекта, которым владеет вью):

struct CartView: View {
    @State private var cart = Cart()

    var body: some View {
        VStack {
            Text("Итого: \(cart.total)")     // следим за total
            Button("Добавить кофе") {
                cart.add("Кофе", price: 200)
            }
        }
    }
}

Ключевое преимущество перед старым подходом — точечное отслеживание. С ObservableObject изменение любого @Published-свойства перерисовывало все наблюдающие вью. С @Observable SwiftUI отслеживает, какие именно свойства читает каждое вью, и обновляет только те, что действительно зависят от изменения. Меньше лишних перерисовок — выше производительность.

Если дочернему вью нужно создавать binding к свойству наблюдаемой модели, используется @Bindable:

struct EditView: View {
    @Bindable var cart: Cart
    var body: some View {
        // $cart.total теперь доступно как binding
        Stepper("Сумма: \(cart.total)", value: $cart.total)
    }
}
ObservableObject (старое)        @Observable (iOS 17+)
 меняем любое @Published           меняем total
        |                                |
  перерисовка ВСЕХ                  перерисовка ТОЛЬКО вью,
  наблюдателей                     что читают total
   (избыточно)                       (точечно, быстрее)

Попробуй сам ▶ — запусти код прямо в браузере (Pyodide). Здесь нет Swift, но логика та же, что под капотом мобильного кода:

# Имитируем точечное отслеживание: фиксируем, какие поля читает вью.
class Cart:
    def __init__(self):
        self.items = []
        self.total = 0
    def add(self, item, price):
        self.items.append(item)
        self.total += price

class View:
    def __init__(self, cart, reads):
        self.cart = cart
        self.reads = reads      # какие поля важны этому вью
    def needs_redraw(self, changed_field):
        return changed_field in self.reads

cart = Cart()
total_view = View(cart, {'total'})
items_view = View(cart, {'items'})
cart.add('Кофе', 200)
print('total_view перерисовать?', total_view.needs_redraw('total'))   # True
print('items_view перерисовать?', items_view.needs_redraw('total'))   # False

Как работает под капотом

Макрос @Observable на этапе компиляции переписывает класс: оборачивает доступ к свойствам так, что чтение регистрируется в текущем контексте отрисовки, а запись уведомляет зависящие вью. SwiftUI ведёт учёт «вью X прочло свойство total», и при изменении total планирует перерисовку только X. Это и есть тонкое отслеживание зависимостей. Поскольку @Observable работает с классами (ссылочный тип), модель естественно разделяется между экранами.

Частые ошибки

  • Тянуть старый @StateObject/@Published. С iOS 17 предпочтителен @Observable и обычный @State.
  • Помечать @Observable структуру. Макрос рассчитан на классы (ссылочную семантику).
  • Использовать @State для модели, общей у многих экранов без передачи. Передавайте объект явно или через окружение.

Best practices

  • Для моделей с логикой используйте @Observable-классы, для локального состояния — @State со значением.
  • Читайте во вью только нужные свойства — так точечное отслеживание работает максимально эффективно.
  • Для bindings к свойствам модели применяйте @Bindable.

Итоги. @Observable из фреймворка Observation — современный, быстрый способ управлять сложным разделяемым состоянием. Он заменяет ObservableObject, убирает @Published и перерисовывает только то, что реально изменилось. Это рекомендованный подход для приложений на iOS 17 и новее.

Шире контекста

Появление фреймворка Observation и макроса @Observable в iOS 17 — это веха, заметно упростившая жизнь iOS-разработчикам. Старый подход с ObservableObject, @Published, @StateObject и @ObservedObject был многословным и грубым: любое изменение перерисовывало всех наблюдателей, даже тех, кому изменённое свойство было безразлично. Новый макрос делает класс наблюдаемым целиком, без аннотаций над каждым свойством, и даёт тонкое отслеживание зависимостей на уровне отдельных свойств. Это не только удобнее писать, но и заметно быстрее на сложных экранах. Если вы встретите в старых руководствах ObservableObject — знайте, что для проектов на iOS 17 и новее современный путь это @Observable плюс обычный @State для владения объектом. Этот же объект легко передавать вниз по дереву явно или через окружение, что делает его идеальным местом для бизнес-логики, отделённой от представления.

Проверьте себя
1. Чем @Observable превосходит старый ObservableObject?
AРаботает со структурами
BОтслеживает, какие именно свойства читает вью, и перерисовывает только нужное
CНе требует классов вообще
DПолностью отменяет @State
2. Какой property wrapper используют, чтобы создать binding к свойству @Observable-модели?
A@Binding
B@Published
C@Bindable
D@StateObject