SwiftПрограммированиеiOS Developer

С помощью какой стратегии макрорасширения во время компиляции **Swift** реализует автоматический отслеживание зависимостей фреймворка **Observation**, и как этот механизм устраняет необходимость в ручном шаблоне **Combine**, требуемом для **ObservableObject**?

Проходите собеседования с ИИ помощником Hintsage

Ответ на вопрос

История вопроса

До Swift 5.9 реактивное программирование в SwiftUI полагалось на протокол ObservableObject в сочетании с фреймворком Combine. Разработчики вручную аннотировали свойства с помощью @Published, чтобы синтезировать публикации, или вызывали objectWillChange.send(), чтобы уведомить представления об изменениях. Эта схема страдала от грубых обновлений — любое изменение свойства вызывало полную повторную оценку тела представления — и требовала семантики ссылок, что препятствовало использованию структур для сложных моделей представлений. Фреймворк Observation был введён для обеспечения тонкой автоматической реактивности без явных деклараций публикаций.

Проблема

Основной задачей было обнаружение доступа и изменения свойств без явного шаблона при сохранении типобезопасности и высокой производительности. Традиционные решения требовали вручную наблюдать за ключевыми значениями (KVO), что является небезопасным и хрупким, или использования обёрточных типов, которые загромождали домен модели. Системе нужно было перехватывать операции чтения и записи, чтобы динамически регистрировать зависимости, при этом не нагружая выполнение свитков метода и не ограничивая архитектурные конструкции переключения идентичности, свойственные ObservableObject.

Решение

Swift использует макрос @Observable, который сочетает в себе возможности макросов для同чаений, членов и доступов. Во время компиляции макрос преобразует аннотированный класс, инжектируя частный экземпляр ObservationRegistrar. Затем он переписывает каждое сохранённое свойство в вычисляемые доступы, которые оборачивают чтения с помощью _$observationRegistrar.track(self, keyPath: ...) и записи с уведомлениями willSet/didSet. Это расширение автоматически синтезирует соблюдение протокола Observable и реализует требуемое вычисляемое свойство observationRegistrar. SwiftUI интегрируется с этим регистратором во время оценки тела представления, регистрируя себя как наблюдателя только для реально используемых свойств, тем самым достигая тонких обновлений без ручной настройки Combine.

@Observable class SettingsViewModel { var isDarkModeEnabled = false var notificationCount = 0 } // Концептуальное расширение компилятора: class SettingsViewModel { private let _$observationRegistrar = ObservationRegistrar() var isDarkModeEnabled: Bool { get { _$observationRegistrar.track(self, keyPath: \.isDarkModeEnabled) return _isDarkModeEnabled } set { _$observationRegistrar.willSet(self, keyPath: \.isDarkModeEnabled) let oldValue = _isDarkModeEnabled _isDarkModeEnabled = newValue _$observationRegistrar.didSet(self, keyPath: \.isDarkModeEnabled, oldValue: oldValue) } } private var _isDarkModeEnabled = false // ... идентичный паттерн для notificationCount }

Ситуация из жизни

Вы проектируете приложение SwiftUI для реального времени для финансовой панели, отображающей текущие цены акций, общие суммы пользовательского портфеля и ленты новостей. Модель представления содержит тридцать различных свойств, начиная от логических UI-флагов и заканчивая сложными структурами данных для графиков.

Изначально команда реализовала это, используя ObservableObject с обёртками @Published на каждом свойстве. Это вызвало сильное снижение производительности: когда обновилась одна цена акции, вся панель пересчитывалась, потому что ObservableObject уведомляет, что "что-то изменилось" во всем объекте, отсутствуя гранулярность. Код также был многословным, требуя повторяющихся объявлений @Published var и ручного хранения AnyCancellable, чтобы избежать утечек памяти в подписках.

Команда оценила три архитектурных подхода для решения проблем с производительностью и шаблоном.

Первый подход заключался в ручной оптимизации Combine. Они создавали бы индивидуальные экземпляры PassthroughSubject для каждого критического свойства и подписывались на конкретные обновления с помощью .onReceive. Преимущество заключалось в точном контроле над тем, какие компоненты UI обновляются. Однако недостатком было большое увеличение кода — тридцати субъектам требовалось тридцать подписок и подверженное ошибкам ручное управление памятью с Set<AnyCancellable>, что делало кодовую базу непригодной для сопровождения.

Второй подход предполагал использование @State из SwiftUI с моделями представления типа значений. Они рассматривали модель представления как неизменяемую величину и заменяли её при каждом изменении. Преимущество заключалось в естественной семантике значений и автоматических проверках равенства, предотвращающих избыточные обновления. Недостатком было потеря идентичности ссылок; каждое изменение создаёт новый экземпляр, что нарушает сохранение позиции ScrollView и делает невозможным сложную координацию объектов из-за потери идентичности.

Третий подход использовал макрос @Observable. Аннотируя класс с @Observable и произведя удаление всех атрибутов @Published, компилятор автоматически преобразовал свойства для использования ObservationRegistrar. Преимущество было двояким: синтаксис оставался чистым с простыми объявлениями var, и SwiftUI автоматически отслеживала, какие свойства использовались в теле каждого представления, обновляя только те конкретные подвиды. Недостатком было требование перейти на Swift 5.9 и обучить команду техникам отладки макросов.

Команда выбрала третий подход, так как он устранял 200 строк кода подписки Combine, решая тем самым проблему гранулярности. Они отметили 40% снижение использования ЦП при частых обновлениях цен. В результате получилась отзывчивая панель, где отдельные метки цен акций обновлялись независимо, не вызывая перерасчетов компоновки для статической секции новостей.

Что часто упускают кандидаты

Почему фреймворк Observation требует, чтобы наблюдаемым типом был класс (семантика ссылок), и какая ошибка компиляции возникает, если @Observable применяется к структуре?

Макрос @Observable расширяется, инжектируя ObservationRegistrar в качестве сохраняемого свойства и реализуя протокол Observable. Структуры являются типами значений с семантикой копирования при записи; каждое изменение концептуально создаёт новый экземпляр с отличительной идентичностью. ObservationRegistrar поддерживает внутреннее состояние — списки наблюдателей и области отслеживания — которые должны сохраняться при изменениях для поддержания графа наблюдений. Если применить к структуре, мутации неправильно скопируют состояние регистратора, разрывая связь между наблюдателями и экземпляром. Компилятор предотвращает это, генерируя ошибку, указывающую на то, что макрос не может добавить требуемое сохраняемое свойство к типу значения так, чтобы выполнить требования протокола Observable для стабильной идентичности или, более конкретно, что результирующий тип не может соответствовать Observable, поскольку ему не хватает необходимой относительной стабильности.

Как фреймворк Observation обрабатывает вложенные наблюдаемые объекты, и почему для отдельных свойств нет проекционного значения (как было с @Published)?

Когда класс @Observable содержит свойство, которое также является классом @Observable, фреймворк отслеживает доступ на уровне свойств, а не автоматически рекурсивно наблюдая за вложенными объектами. Доступ к outer.inner.name регистрирует зависимость от свойства inner внешнего объекта. Если экземпляр inner будет полностью заменён, наблюдатели будут уведомлены. Однако изменения в inner.name не уведомляют наблюдателей внешнего объекта, если внешний объект явно не отслеживает внутренний. В отличие от Combine, концепции проекционного значения для отдельных свойств в Observation не существует, потому что фреймворк использует прямое отслеживание свойств через регистратор, а не потоки публикаций. Синтаксис $ в SwiftUI для Observation используется вместо этого для всего экземпляра, когда он обёрнут с помощью @Bindable (например, @Bindable var viewModel: SettingsViewModel позволяет использовать $viewModel.isDarkModeEnabled), а не для отдельных объявлений свойств.

Какие конкретные гарантии потокобезопасности предоставляет фреймворк Observation, и как он взаимодействует с актами конкурентности Swift?

Сам ObservationRegistrar не является специально защищённым от потоков; он предполагает сериализованный доступ к наблюдаемому объекту. Когда класс @Observable изолирован в акте (например, @MainActor), все изменения и наблюдения автоматически происходят в контексте этого акта, предотвращая гонки данных. Фреймворк гарантирует, что обратные вызовы наблюдения уважает домен изоляции наблюдателя с помощью проверок Sendable. Критическая деталь реализации заключается в том, что механизм отслеживания использует хранилище TaskLocal для поддержания текущей области наблюдения во время выполнения тела представления. Это означает, что регистрация наблюдений неявно связана с контекстом текущей Task и не может утечь через неструктурированные границы конкурентности без явной передачи, что гарантирует, что наблюдения активны только во время конкретной асинхронной транзакции, где они были зарегистрированы.