Observation для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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, достаточно:

  1. Установить Xcode 15 или новее.
  2. В настройках проекта убедиться, что минимальная версия развертывания (Deployment Target) установлена на iOS 17, macOS 14, tvOS 17 или watchOS 10.
  3. Импортировать фреймворк в файле, где вы планируете его использовать: 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                    

Рекомендаций