Observation для Swift: полное руководство с примерами
1. Что это и зачем нужна
Observation — это новый фреймворк, представленный Apple на WWDC 2023, который полностью переосмысливает механизм наблюдения за данными в Swift. Он пришёл на смену устаревшей модели на основе @Published и ObservableObject из Combine, предлагая более производительный, потокобезопасный и интуитивно понятный подход. В основе Observation лежит протокол Observable и макрос @Observable, которые позволяют отслеживать изменения на уровне отдельных свойств, а не всего объекта.
Зачем это нужно? В современной разработке под iOS, macOS, watchOS и tvOS основным фреймворком для построения интерфейсов является SwiftUI. Для эффективной работы SwiftUI требуется механизм, который сообщает ему об изменениях в данных, чтобы перерисовать только те части интерфейса, которые действительно изменились. Старый подход с ObservableObject перерисовывал весь View целиком при изменении любого свойства, что приводило к избыточным вычислениям и снижению производительности. Observation решает эту проблему, обеспечивая точное отслеживание изменений на уровне отдельных свойств.
Ключевые преимущества: Observation является частью стандартной библиотеки Swift (начиная с iOS 17, macOS 14, tvOS 17, watchOS 10), не требует дополнительных зависимостей, полностью потокобезопасен (изменения можно вносить из любого потока, а уведомления доставляются на главном потоке) и тесно интегрирован с SwiftUI через макрос @Bindable и новый модификатор .onChange(of:).
2. Установка
Observation — это встроенный фреймворк Apple. Он является частью стандартной библиотеки Swift, начиная с версии Swift 5.9 и Xcode 15. Никаких дополнительных установок через менеджеры пакетов (CocoaPods, Swift Package Manager, Carthage) не требуется.
Чтобы начать использовать Observation, достаточно:
- Установить Xcode 15 или новее.
- В настройках проекта убедиться, что минимальная версия развертывания (Deployment Target) установлена на iOS 17, macOS 14, tvOS 17 или watchOS 10.
- Импортировать фреймворк в файле, где вы планируете его использовать:
import Observation.
Если вы используете SwiftUI, импортировать Observation отдельно не обязательно — он автоматически доступен при импорте SwiftUI.
3. Быстрый старт — минимальный рабочий пример
Создадим простую модель данных и View, которое её отображает.
import SwiftUI
// 1. Создаём наблюдаемый класс
@Observable
class UserSettings {
var username: String = "Гость"
var isLoggedIn: Bool = false
}
// 2. Используем в View
struct ContentView: View {
// @State для управления жизненным циклом
@State private var settings = UserSettings()
var body: some View {
VStack(spacing: 20) {
Text("Пользователь: \(settings.username)")
.font(.title)
TextField("Введите имя", text: $settings.username)
.textFieldStyle(.roundedBorder)
.padding()
Toggle("Войти", isOn: $settings.isLoggedIn)
.padding()
if settings.isLoggedIn {
Text("Вы вошли как \(settings.username)")
.foregroundColor(.green)
} else {
Text("Вы не авторизованы")
.foregroundColor(.red)
}
}
.padding()
}
}
В этом примере макрос @Observable автоматически преобразует класс UserSettings так, что SwiftUI может отслеживать изменения каждого его свойства. Когда пользователь вводит текст в TextField или переключает Toggle, SwiftUI обновляет только те части интерфейса, которые зависят от изменившегося свойства.
4. Основные методы, функции и классы
4.1. Макрос @Observable
Сигнатура: @attached(member, names: named(_$id), named(_$willModify), named(_$didModify)) @attached(memberAttribute) @attached(extension, conformances: Observable) macro Observable()
Что делает: Преобразует класс в наблюдаемый объект, автоматически генерируя код для отслеживания изменений всех хранимых свойств. Это основной строительный блок Observation.
@Observable
class Counter {
var count: Int = 0
var label: String = "Счётчик"
}
let counter = Counter()
print(counter.count) // 0
counter.count = 5
// SwiftUI автоматически получит уведомление об изменении count
4.2. Протокол Observable
Сигнатура: protocol Observable: AnyObject
Что делает: Базовый протокол, которому автоматически удовлетворяют классы, помеченные макросом @Observable. Определяет интерфейс для регистрации наблюдателей и уведомления об изменениях.
// Явная реализация протокола (редко требуется)
final class ManualObservable: Observable {
// Реализация методов протокола
}
// Обычно используется через @Observable
@Observable
class MyModel: Observable {
var value: Int = 0
}
4.3. Функция withObservationTracking
Сигнатура: func withObservationTracking<T>(_ apply: () -> T, onChange: @escaping () -> Void) -> T
Что делает: Позволяет вручную подписаться на изменения свойств наблюдаемого объекта. Выполняет замыкание apply и регистрирует все обращения к свойствам Observable объектов внутри него. При изменении любого из этих свойств вызывается замыкание onChange.
@Observable
class Temperature {
var celsius: Double = 20.0
}
let temp = Temperature()
// Подписываемся на изменения
let initialValue = withObservationTracking {
temp.celsius // Возвращает текущее значение
} onChange: {
print("Температура изменилась!")
}
// Через некоторое время...
temp.celsius = 25.0 // Вызовет print("Температура изменилась!")
4.4. Макрос @Bindable
Сигнатура: @propertyWrapper @dynamicMemberLookup struct Bindable<Value> where Value: Observable
Что делает: Создаёт Binding (привязку) к свойствам Observable объекта. Используется в SwiftUI для двустороннего связывания, когда нужно передать Binding в дочерний View или использовать с элементами управления, требующими $.
@Observable
class User {
var name: String = "Анна"
}
struct ParentView: View {
@State private var user = User()
var body: some View {
ChildView(user: user)
}
}
struct ChildView: View {
@Bindable var user: User // Создаёт Bindable обёртку
var body: some View {
TextField("Имя", text: $user.name) // $user.name — это Binding
}
}
4.5. Свойство @ObservationIgnored
Сигнатура: @attached(accessor) macro ObservationIgnored()
Что делает: Исключает свойство из механизма наблюдения. Изменения этого свойства не будут вызывать обновление UI. Полезно для вычисляемых свойств, констант или свойств, которые не должны влиять на интерфейс.
@Observable
class DataManager {
var items: [String] = []
@ObservationIgnored var cache: [String: Data] = [:] // Не наблюдаем
@ObservationIgnored let dateFormatter = DateFormatter() // Константа
func loadData() {
// Загрузка данных...
cache["key"] = Data()
// Изменение cache не вызовет обновление UI
}
}
4.6. Модификатор .onChange(of:) (новая версия)
Сигнатура: func onChange<Value>(of value: Value, initial: Bool = false, _ action: @escaping (Value, Value) -> Void) -> some View where Value : Equatable
Что делает: Выполняет замыкание при изменении указанного значения. В отличие от старой версии из Combine, работает с любыми Equatable значениями, включая свойства Observable объектов. Предоставляет старое и новое значения.
@Observable
class SearchViewModel {
var query: String = ""
var results: [String] = []
}
struct SearchView: View {
@State private var viewModel = SearchViewModel()
var body: some View {
TextField("Поиск", text: $viewModel.query)
.onChange(of: viewModel.query) { oldValue, newValue in
print("Поисковый запрос изменился с '\(oldValue)' на '\(newValue)'")
// Выполняем поиск
viewModel.results = performSearch(query: newValue)
}
}
func performSearch(query: String) -> [String] {
// Имитация поиска
return query.isEmpty ? [] : ["Результат 1", "Результат 2"]
}
}
4.7. Функция observe (из библиотеки _ObservationInternals)
Сигнатура: func observe<T>(_ apply: () -> T, onChange: @escaping () -> Void) -> ObservationToken
Что делает: Низкоуровневая функция для ручного наблюдения. Возвращает токен, который можно использовать для отмены подписки. Обычно используется внутри SwiftUI, но может быть полезна для создания собственных механизмов наблюдения.
import Observation
@Observable
class Clock {
var time: Date = Date()
}
let clock = Clock()
let token = observe {
_ = clock.time
} onChange: {
print("Время изменилось: \(clock.time)")
}
// Отмена наблюдения
token.cancel()
4.8. Протокол MutableCollection и наблюдение за коллекциями
Сигнатура: protocol MutableCollection where Self: Observable
Что делает: Observation автоматически поддерживает наблюдение за изменениями в коллекциях (массивы, словари, множества). При добавлении, удалении или изменении элементов коллекции SwiftUI получает уведомление.
@Observable
class TodoList {
var items: [String] = ["Купить молоко", "Позвонить маме"]
}
struct TodoListView: View {
@State private var todoList = TodoList()
var body: some View {
List {
ForEach(todoList.items, id: \.self) { item in
Text(item)
}
Button("Добавить") {
todoList.items.append("Новая задача")
// UI обновится автоматически
}
}
}
}
5. Реальный практический пример (100-200 строк кода)
Создадим приложение для управления задачами (To-Do List) с использованием Observation, которое демонстрирует все ключевые возможности фреймворка.
import SwiftUI
// MARK: - Модель данных
@Observable
class TaskItem: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool
@ObservationIgnored var createdAt = Date() // Не влияет на UI
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
@Observable
class TaskStore {
var tasks: [TaskItem] = []
var filterOption: FilterOption = .all
@ObservationIgnored private var lastSaveDate: Date?
enum FilterOption: String, CaseIterable {
case all = "Все"
case active = "Активные"
case completed = "Завершённые"
}
var filteredTasks: [TaskItem] {
switch filterOption {
case .all:
return tasks
case .active:
return tasks.filter { !$0.isCompleted }
case .completed:
return tasks.filter { $0.isCompleted }
}
}
func addTask(title: String) {
let task = TaskItem(title: title)
tasks.append(task)
print("Добавлена задача: \(title)")
}
func toggleTask(_ task: TaskItem) {
task.isCompleted.toggle()
print("Задача '\(task.title)' теперь \(task.isCompleted ? "завершена" : "активна")")
}
func removeTask(_ task: TaskItem) {
tasks.removeAll { $0.id == task.id }
print("Удалена задача: \(task.title)")
}
}
// MARK: - ViewModel для управления состоянием
@Observable
class TaskViewModel {
var store = TaskStore()
var newTaskTitle: String = ""
var showAlert = false
var alertMessage = ""
func addNewTask() {
let trimmedTitle = newTaskTitle.trimmingCharacters(in: .whitespaces)
guard !trimmedTitle.isEmpty else {
alertMessage = "Название задачи не может быть пустым"
showAlert = true
return
}
store.addTask(title: trimmedTitle)
newTaskTitle = ""
}
}
// MARK: - SwiftUI Views
struct TaskRowView: View {
@Bindable var task: TaskItem
var