WWDC 2023. Новый фреймворк SwiftData для управления данными. Эксперименты

SwiftData дебютировал на WWDC 2023 в качестве замены фреймворка Core Data и обеспечивает постоянное хранение данных на Apple устройствах и беспрепятственную синхронизацию с облаком iCloud. Весь API SwiftData построен вокруг современного Swift.

Примечание. SwiftData является частью iOS 17, и на момент написания этой статьи мы имеем версии Xcode 15.0 и iOS 17.0 .

В SwiftData, в отличие от своего предшественника, базы данных Core Data, очень просто создать Схему (или Модель Данных) для постоянного хранения информации в вашем приложении. Для этого прямо в коде создаются обычные Swift классы class со свойствами, имеющими обычные базовые Swift ТИПы, ТИПы или другие Swift классы Схемы. Вы также можете использовать как Optional, так и НЕ-Optional ТИПы.

Чтобы превратить эти обычные Swift классы в постоянно хранимые объекты, Apple дала нам «волшебную палочку» в виде макросов, самым главным из которых является макрос @Model.

Если вы пометите макросом @Model обычные Swift классы, то получите не только постоянно хранимые объекты, но и сделаете их Observable, Hashable и Identifiable, и вам не нужно предпринимать никаких дополнительных усилий при использовали их в SwiftUI, ибо новый в iOS 17 протокол Observable обеспечит вам «живое» отображение на UI всех изменений ваших хранимых объектов, а Identifiable и Hashable позволят беспрепятственное использовать их в списках ForEach.

В SwiftData, в отличие от Core Data, нет никаких внешних файлов для Модели Данных и никакой «закулисной» генерации старых Objective-C классов, которые еще нужно адаптировать для использования в Swift. В SwiftData всё исключительно просто.

Кроме того, в SwiftData существенно, по сравнению с Core Data, упрощена выборка данных и отображение её результатов на UI. Для этого предназначена «обертка свойства» @Query, для которой вы можете указать предикат Predicate (то есть условия выборки данных) и сортировку результата SoreDescriptor. Новый мощный предикат Predicate выгодно отличается от старого предиката NSPredicate Core Data тем, что теперь вы можете задавать условия выборки данных, используя операции самого языка программирования Swift, а не какую-то замысловатую форматированную строку.

SwiftData дополнен такими современными возможностями как Swift многопоточность и макросы. В результате в Swift 5.9 мы получили, по определению самого Apple, “бесшовное” взаимодействие с постоянным хранилищем данных в нашем приложении. SwiftData совершенно естественным образом интегрируется в SwiftUI и прекрасно работает с CloudKit и Widgets.

Если вы начнете работать со SwiftData, то вообще не почувствуете даже «духа» Core Data, всё очень Swifty. Apple настаивает на том, что SwiftData — это совершенно отдельный от Core Data фреймворк, нам точно неизвестно, является ли SwiftData «оболочкой» Core Data, но даже если это так, то она настолько элегантно, интуитивно и мастерски реализована, что у вас будет ощущение работы исключительно в «родной» cреде языка программирования Swift.

В этом посте я покажу вам, как:

  • определить Схему данных в SwiftData, 
  • выполнить CRUD операции (Create — Создать, Read — прочитать, Update — модифицировать, Delete — удалить),;
  • сформировать различные запросы Query к данным с помощью предиката Predicate
  • использовать «живой» запрос @Query в SwiftUI и как его динамически настраивать,
  • эффективно «закачать» JSON данные в SwiftData хранилище без блокировки пользовательского интерфейса (UI).

Определение Схемы Данных в SwiftData

В качестве демонстрационного приложения я буду использовать упрощенный вариант приложения Enroute из стэнфордских курсов CS193P 2020, которое отображает в некоторый фиксированный момент все рейсы, обслуживаемые двумя международными аэропортами: аэропортом Чикаго «Chicago O’Hare Intl» и аэропортом Сан-Франциско «San Francisco Int’l». Данные об этих рейсах получены мною с сайта FlightAware в JSON формате. Мы «закачаем» эти данные в постоянное хранилище в нашем приложении и сможем не просто видеть всю информацию о рейсах, аэропортах и авиакомпаниях …

Просмотр рейсов, аэропортов и авиакомпаний

… но и делать различные запросы с помощью фильтров.

Например, выбирать определенные рейсы Flights по аэропорту назначения — destination, аэропорту вылета — origin, авиакомпании — airline и нахождению в данный момент в воздухе — Enroute Only:

Параметры фильтрации рейсов

В качестве примера мы выбрали с помощью Picker в качестве аэропорта назначения destination международный аэропорт в Чикаго — «Chicago O’Hare Intl», а также рейсы, которые в данный момент находятся в воздухе Enroute Only:

Выбор аэропорта назначения с помощью Picker

… и получили следующий список рейсов, кликнув на кнопке «Done»:

Результат выборки

Кроме того, в списке аэропортов мы можем искать аэропорты по первым буквам имени, например, «San » и, выбрав из сформировавшегося списка аэропортов, например, «San Francisco Int’l» (международный аэропорт Сан-Франциско), посмотреть более подробную информацию о нём: местоположение, рейсы, вылетающие и прилетающие в этот аэропорт:

Поиск аэропорта по имени

Мы можем сортировать рейсы нужным нам способом. На рисунке ниже представлена сортировка по дальности полета distance в порядке убывания и в порядке возрастания:

Сортировка Рейсов по дальности полета в убывающем и возрастающем порядке

У меня есть точно такое же приложение Enroute, написанное с применением Core Data, так что при желании очень легко сравнить его с вновь создаваемым приложением SwiftData Airport в Github.

Итак, центральным объектом нашей Схемы является рейс Flight, который выполняется авиакомпанией airline: Airline между аэропортом отправления origin: Airport и аэропортом назначения destination: Airport. Модель данных в нашем приложения мы представим обычными Swift классами: Flight, Airport и Airline:

Модель РЕЙСА представлена обычным Swift классом Flight

У рейса Flight имеется уникальный идентификатор рейса ident, время взлета (по расписанию scheduledOff, приблизительное estimatedOff и действительное actualOff), время приземления (по расписанию scheduledOn, приблизительное estimatedOn и действительное actualOn), аэропорт отправления origin: Airport, аэропорт прибытия destination: Airport, авиакомпания airline: Airline, выполняющая этот рейс, тип самолета aircraftTypе, а также расстояние routeDistance между пунктами отравления и назначения, скорость filedAirspeed, высота filedAltitude и процент пройденного пути progressPercent.

Модель АЭРОПОРТА представлена обычным Swift классом Airport

Аэропорт Airport имеет уникальный код icao, имя name, город city, штат state, код страны countryCode, географические координаты latitude и longitude его местоположения, а также временной пояс timezone. Кроме того, нас интересует информация о рейсах, вылетающих из этого аэропорта, flightsFrom, и рейсах, прибывающих в этот аэропорт, flightsTo.

Модель АВИАКОМПАНИИ представлена обычным Swift классом Airline

Авиакомпания Airline имеет уникальный код code, имя name и краткое имя shortName. Нас также интересует информация о всех рейсах flights, выполняемых в данный момент этой авиакомпанией.

В SwiftData достаточно перед Swift классами разместить макрос @Model, и мы получим «постоянно хранимую» Модель Данных, то есть в приложении будут постоянно сохраняться «рейсы» Flight, «авиакомпании» Airline и «аэропорты» Airport , каждый со своими атрибутами и взаимосвязями:

SwiftData @Model класс Flight — для постоянного хранения рейсов
SwiftData @Model класс Airport — для постоянного хранения аэропортов
SwiftData @Model класс Airline — для постоянного хранения авиакомпаний

Благодаря макросу @Model SwiftData по умолчанию автоматически преобразует все хранимые свойства Swift класса в постоянно хранимые свойства.

Если свойство имеет Value ТИП, SwiftData автоматически адаптирует его как атрибут. Такие свойства могут включать в себя:

  • базовые Value ТИПы (String, Int, Float и т.д.)
  • более сложные Value ТИПы :
    • структуры struct
    • перечисления enum
    • Codable ТИПы
    • коллекции Value ТИПов

Если свойство имеет ссылочный Reference ТИП (то есть класс class), то SwiftData адаптирует его как взаимосвязь. Вы можете создавать взаимосвязи:

  • с другими @Model ТИПами
  • с коллекциями @Model ТИПов

Макросы @Attribute, @Relationship и @Transient

@Model автоматически адаптирует все хранимые свойства вашего Swift класса либо в атрибуты, либо во взаимосвязи, и в большинстве случаев можно вообще больше ничего не делать. Но вы можете влиять на то, как SwiftData будет выполнять эту адаптацию, используя «волшебные палочки» (то есть макросы) и аннотировать отдельные свойства @Model классов с помощью макросов:

  • @Attribute
  • @Relationship
  • @Transient

С помощью макроса @Attribute вы можете, например, добавить ограничение уникальности для идентификатора рейса ident :

Использование @Attribute для указания уникальности свойства

Если вы попытаетесь вставить рейс Flight с тем же самым значением идентификатора ident, то существующий рейс будет обновлен и заменит значения свойств на новые свойства вставляемого объекта. Это может помочь вам поддерживать актуальность и согласованность данных вашего приложения, если информация периодически скачивается с сервера.

С помощью макроса @Relationship можно управлять взаимосвязями c @Model объектами и явно указать взаимосвязи типа «один-ко-многим» или «многие-ко-многим». Например, для аэропорта Airport с помощью макроса @Relationship нам нужно явно указать «инверсивные» взаимосвязи для flightsFrom и flightsTo, то есть свойства в рейсе Flight, соответствующие аэропорту отправления origin и аэропорту назначения destination:

@Relationship с inverse

Мы можем указать для аэропорта уникальность свойства icao :

Уникальное свойство в @Model классе — @Attrubute (.unique) и @Relationship (deleteRule: …)

Точно также в объекте Airline с помощью макроса @Relationship мы указываем «инверсивную» взаимосвязь для flights и обеспечиваем уникальность атрибута code:

Уникальное свойство в @Model классе — @Attrubute (.unique) и @Relationship (deleteRule: …)

Вот как выглядит Схема Данных для нашего приложения в SwiftData:

Схема данных в SwiftData

С помощью макроса @Transient можно исключить из постоянного хранилища определенные свойства вашего класса, они могут участвовать в формировании пользовательского интерфейса (UI) или во вспомогательных вычислениях, но сохраняться не будут.

Настройка Схемы Данных с помощью макроса @Attribute 

У макроса @Attribute есть следующие опции:

  • unique: гарантирует уникальное значение свойства 
  • transient: позволяет контексту игнорировать это свойство при сохранении модели-владельца
  • transformable: преобразует значение свойства между формой «в памяти» и сохраняемой формой
  • externalStorage: сохраняет значение свойства как двоичные данные отдельно от постоянного хранилища 
  • encrypt: хранит значение свойства в зашифрованном виде
  • preserveValueOnDeletion: сохраняет значение свойства в истории «постоянного хранения», когда контекст удаляет модель-владелеца
  • spotlight: индексирует значение свойства, чтобы оно отображалось в результатах поиска Spotlight

Хранение данных отдельно от хранилища

Объемные данные не должны храниться непосредственно в вашем хранилище, потому что это может замедлить его работу. Вы можете указать SwiftData хранить свойство во внешнем хранилище с помощью опции externalStorage макроса @Attribute для этого свойства. Например, если вы хотели бы сохранить изображение, полученное извне:

Значения свойства imageData сохраняется как двоичные данные отдельно от постоянного хранилища 

Контейнер ModelContainer и контекст ModelContext

Когда Схема Данных определена, пришло время создавать объекты, модифицировать их, удалять и выбирать из хранилища. Для управления операциями с @Model ТИПами в SwiftData используются два важных класса: контейнер ModelContainer и контекст ModelContext

Контейнер ModelContainer обеспечивает “бэкенд” постоянного хранения для @Model ТИПов. Вы можете создать контейнер ModelContainer, просто указав список @Model ТИПов, которые вы хотите сохранять. 

контейнер ModelContainer со списком @Model ТИПов.

Если вы хотите дополнительно настроить свой контейнер container, вы можете использовать конфигурации configurations для изменения своего URL-адреса, и CloudKit, а также опций миграции. 

Как только ваш контейнер container установлен, вы готовы создавать новые данные, изменять существующие и сохранять изменения, а также осуществлять выборку данных с помощью контекста ModelContext

Вы также можете использовать SwiftUI View и Scene модификаторы, чтобы установить контейнер container

Устанавливаем ModelContainer в SwiftUI как View модификатор

… и использовать его в среде @Environment вашего View для получения контекста context:

Получение контекста context в SwiftUI.

Контекст ModelContext отслеживает все изменения в ваших моделях @Model и предоставляет множество действий для работы с ними. Они являются вашим интерфейс для отслеживания обновлений, сохранения изменений и даже отмены этих изменений.

Вне иерархии Views вы можете попросить контейнер container предоставить вам разделяемый (shared) MainActor контекст context:

MainActor context

… или вы можете просто инициализировать новые контексты context для данного контейнера container:

Новый контекст для контейнера.

CRUD в SwiftData

Если у вас есть контекст ModelContext, то вы готовы к операциям CRUD (создание — Create, чтение — Read, модификация — Updata, удаление — Delete) над данными.

Cоздание новых @Model объектов , как и экземпляров любых других Swift классов class, производится с помощью инициализаторов init, затем вы вставляете insert вновь созданный объект в контекст ModelContext. Но класс class в Swift, в отличие от структуры struct, не имеет инициализаторов по умолчанию. Вам нужно предоставить свои собственные инициализаторы. К счастью, Xcode может помочь вам в этом и автоматически сгенерирует для вас инициализацию. Просто начните вводить init и используйте автодополнение Xcode.

Но тонким вопросом при создании нового объекта @Model является определение взаимосвязей, и здесь нужно принимать во внимание два фактора: 

  • взаимосвязь с другим объектом @Model создается только в том случае, когда @Model объект уже находится в SwiftData хранилище,
  • достаточно создание взаимосвязи с одной из сторон — другая сторона формируется автоматически, например, создавая взаимосвязь destination: Airport в рейсе Flight, нет необходимости добавлять этот рейс в массив рейсов flightsTo для аэропорта destination, это будет сделано автоматически.

Поэтому, если мы создаем новый рейс Flight, то указать взаимосвязи: аэропорт отправления origin, аэропорт прибытия destination и авиакомпанию airline, выполняющую этот рейс, мы сможем только после того, как рейс Flight уже будет находиться в нашем хранилище, так что нам понадобится только один инициализатор рейса Flightinit (ident:String) с единственным уникальным атрибутом ident:

Инициализатор класса Flight

С помощью этого инициализатора мы сначала создадим новый рейс Flight с заданным идентификатором ident, а затем вставим его в контекст context:

Создание рейса Flight с заданным идентификатором ident и вставка в контекст context

Сохранение нового объекта flight не понадобится, потому что по умолчанию работает режим автосохранения контекста context.

После получения уже записанного в SwiftData хранилище рейса flight с заданным идентификатором ident, мы модифицируем все его атрибуты, включая взаимосвязи origin, destination и airline с помощью static функции func update:

Модификация рейса flight по информации с FlightAware.

Для придания универсального характера функции update, которая будет работать бы как с новыми, так и с уже существующими рейсами flight, мы использовали static функцию func withIdent класса Flight, которая ищет рейс flight с заданным идентификатором ident, и если находит его, то возвращает уже существующий рейс Flight, а если нет, то создает новый с помощью insert:

Получение рейса flight с заданным идентификатором ident из SwiftData хранилища

Надо сказать, что это еще и программный способ обеспечения уникальности свойства ident, и в принципе можно не указывать это с помощью макроса @Attribute(.unique) var ident: String, это может очень пригодится при синхронизации SwiftData информации с iCloud, который не поддерживает ограничение @Attribute(.unique) var ident: String.

Аналогичным образом мы поступим с Airport и с Airline (код на  Github проект SwiftDataEnroute).

Итак, мы научились читать (READ) объекты из хранилища SwiftData :

Выборка или чтение рейса с заданным идентификатором ident.

Создавать (CREATE) новые объекты:

Создание рейса с заданным идентификатором ident.

Примечание. Если происходит вставка insert SwiftData объекта, у которого есть уникальный атрибут @Attribute (.unique), и этот объект уже находится в хранилище, то новый объект не создается, а атрибуты существующего объекта просто обновляются. 

Модифицировать (UPDATE) существующий объект:

Модификация существующего объекта.

Удалить (DELETE) постоянно хранимый SwiftData объект так же просто. Достаточно попросить ModelContext “пометить” его для удаления :

Пометка на удаление объекта из контекста context.

Примечание. Все SwiftData объекты имеют свойство context, которое дает вам контекст, к которому они принадлежат. Вот как вы можете использовать его для оптимизации функции удаления delete:

Удаление объекта с использованием своего собственного контекста.

Когда сработает механизм автосохранения, произойдет фактическое удаление объекта. Однако, если вы хотите немедленно выполнить удаление объекта, a также зафиксировать другие ожидающие изменения, вы можете попросить ModelContext сохранить их с помощью явной операции save:

Сохранение контекста с помощью явной операции save.

Выборка данных. Новые Swift ТИПы: Query, Predicate и FetchDescriptor. 

Для выборки данных SwiftData привлекает такие «чисто» Swift ТИПы, как предикат Predicate и дескриптор выборки FetchDescriptor, а также значительно улучшенный уже существующий в Swift дескриптора сортировки SortDescriptor.

Новый в iOS 17 предикат Predicate работает с “родными” ТИПами Swift. Это современная замена старого ТИПа NSPredicate с полной проверкой ТИПов. Реализация ваших предикатов также сильно упрощается благодаря такой поддержке Xcode, как автозаполнение.

Вот несколько примеров построения предикатов для нашего приложения. 

Во-первых, я могу указать все рейсы, вылетающие из международного аэропорта San Francisco

Предикат для рейсов, вылетающих из Сан-Франциско.

Я могу сузить наш запрос до рейсов, выполняемых авиакомпанией United:

Предикат для рейсов, вылетающих из Сан-Франциско и выполняемых авиакомпанией United.

Можно запросить рейсы, находящие в данный момент в воздухе:

Предикат для рейсов, находящихся в данный момент в воздухе.

После того, как мы решили, какие рейсы нам нужны, мы можем использовать новый ТИП FetchDescriptor для формирования запроса к нашему постоянному хранилищу и дать указание контексту ModelContext выбрать эти рейсы:

Новый ТИП FetchDescriptor.

Помимо предикатов и сортировки, вы можете ограничить количество результатов FetchDescriptor, исключить не сохраненные изменения из результатов и многое другое.

Чтобы узнать больше о контейнерах и контекстах SwiftData, а также об их возможностях, ознакомьтесь с сессией «Dive Deeper into SwiftData» («Углубленное изучение SwiftData»). 

SwiftData и SwiftUI

SwiftData был создан с расчетом на SwiftUI, и их совместное использование невероятно просто. SwiftUI — это самый простой способ начать использовать SwiftData. Будь то настройка контейнера SwiftData, выборка данных или управление обновлениями вашего View, компания Apple создала прекрасный API, напрямую интегрирующий эти фреймворки. 

Новые SwiftUI Scene и View модификатор .modelContaner — это самый простой способ начать создание приложения SwiftData:

Модификатор .modelContainer.

SwiftData использует в SwiftUI «обертку свойства» @Query для выборки данных их хранилища. В представленном ниже коде мы выбираем все рейсы flights и показываем их в списке:

Выбираем все рейсы flights из хранилища и показываем их в списке.

@Query напоминает нам @FetchRequest в Core Data. Они имеют много общего. @Query также является «живым» запросом к хранилищу SwiftData, то есть все изменения в нем автоматически отражаются на UI. @Query также поддерживает фильтрацию данных с помощью аргумента filter, сортировку с помощью аргумента sort, порядок сортировки с помощью аргумента order и анимацию animation. Вот @Query, который поддерживает сортировку массива рейсов flights по длине маршрута routeDistance и упорядочивание по возрастанию:

Приведенный ниже @Query выбирает из хранилища только рейсы flights, вылетающие из аэропорта Сан-Франциско «San Francisco Int’l» и выполняемые авиакомпанией United Airline и сортирует их по % пройденного пути progressPercent:

Однако в отличие от @FetchRequest в Core Data, @Query в SwiftData на данный момент (Xcode 15.0) не умеет динамически настраивать фильтр filter и сортировку sort в зависимости от изменения @State переменных.

Например, если с помощью @State переменной originICAO задать код icao аэропорта отправления…

… то в модификаторе .onChange (originICAO) {…} не удастся динамически скорректировать параметр filter для @Query:

Тем не менее, вы можете создавать динамические предикаты с помощью инициализатора init (originICAO: String) вашего View, когда изменяемый параметр originICAO передается из предыдущего View.

Например, у нас есть HomeView с закладками для рейсов, аэропортов и авиакомпаний и @State переменная var originICAO: String?, которая задает код icao для аэропорта отправления:

Использование @State переменной в инициализаторе «дочернего» View.

Мы передаем переменную originICAO в инициализатор init (originICAO: Binding<String?>) нашего FlightsView, отображающего рейсы flights, у которых аэропорт отправления origin определяется этой переменной:

Динамический запрос с инициализатором View с Query, зависящем от @State параметра.

Сама переменная originICAO может выбираться в FilterICAOView с помощью Picker из списка аэропортов отправления airportsFROM:

Выбираем код icao для аэропорта отправления.

Для выбора icao аэропорта отправления в FlightsView мы используем кнопку Button(«Filter») и sheet:

View для выбора аэропорта отправления origin.
Работа с фильтром рейсов.

Эта версия динамического @Query представлена в Github в проекте SwiftDataEnroute.

Можно, конечно, в FlightsView организовать точно такой же динамический выбор рейсов вообще без настройки инициализатора, а просто выбрать с помощью @Query все рейсы flights: [Flight], а затем использовать функцию filter для массива flights и получить уже отфильтрованный массив filteredFlights, который и использовать в UI:

Динамический запрос к SwiftData хранилищу с использованием функции filter для Array.

Эта версия представлена в Github в проекте SwiftDataEnroute1.

Более полная версия фильтрации рейсов по аэропортам отправления origin и назначения destination по авиакомпании airline и нахождению в данный момент в воздухе Entouter Only представлена в Github в проекте SwiftData Airport.

Предварительные просмотры #Preview в Xcode для SwiftUI

Предварительные просмотры #Preview в Xcode играют жизненно важную роль в разработке приложений на SwiftUI, предлагая быструю визуальную проверку логики создания UI.

Один из способов использования предварительных просмотров #Preview в SwiftData — это создание пользовательского ModelContainer. Этот способ был показан в видео WWDC 23 под названием «Build an app with SwiftData» («Создание приложения с помощью SwiftData»). Основная идея состоит в том, чтобы создать контейнер ModelContainer исключительно для #Preview в SwiftData. Контейнер может находиться в памяти (inMemory) и содержать необязательно реальные данные. Возможная реализация показана ниже:

Preview ModalContainer, Airport и Airline.

А вот как выглядят фрагменты данные для #Preview:

Аэропорты для @Preview.
Авиакомпании для #Preview.
Рейс для #Preview.

Теперь для любого View у нас есть предварительный просмотр #Preview:

#Preview для HomeView.
#Preview для AirportDetailView.
#Preview для AirportMap.

Заполнение SwiftData хранилища JSON данными

Если при запуске или в процессе работы приложения вам необходимо заполнять SwiftData хранилище  JSON данными, то можно использовать современные возможности Swift в реализации многопоточности (Swift Concurrency).

MainActor

Для простоты мы будем считывать JSON данные непосредственно из файла, размещать их в промежуточные Codable Модели: AirportInfoAirlineInfoFlightsInfo (источник JSON данных — сайт FlightAware) , а затем использовать MainActor для записи в хранилище SwiftData без явного сохранения, поскольку действует режим автосохранения для контекста context. Вот пример считывания информации об аэропортах:

Использование MainActor для записи данных хранилище SwiftData.

Аналогичные функции используются для авиакомпаний и рейсов и всё собирается в async функции asyncLoadMainActor ()

async загрузка JSON данных в хранилище SwiftData.

… которая используется при запуске приложения в модификаторе .task, если в хранилище нет данных, и при нажатии кнопки Button («Load»):

Загрузка данных в SwiftData хранилище несмотря большое количество рейсов (349), аэропортов (203) и авиакомпаний (84) происходит настолько быстро, что вы даже не заметите блокировки Main Queue, и все же она есть. 

Эта версия загрузки данных в SwiftData хранилище на MainActor представлена в проектах SwiftDataEnrouteSwiftDataEnroute1 и SwiftData Airport на Github.

Background actor

Мы можем пойти еще дальше и загружать данные в SwiftData на фоновой очереди (background queue). Для этого мы создаем actor LoadModelActor: ModelActor, инициализируем его с использованием ModelContainer, создаем новый контекст context для фоновых операций и используем его для создания DefaultModelExecutor:

Запись JSON данных в хранилище SwiftData на background.

Как видите, для actor нам пришлось выполнить сохранение контекста context.saveContext(), чтобы данные отражались мгновенно на UI (может быть это особенности работы данной версии SwiftData).

Добавляем async функцию func asyncLoad () async в наш FlightsView:

… и вызываем её, также как и предыдущую, в модификаторе .task, если в хранилище нет данных, и при нажатии кнопки Button («Load»):

Вызываем asyncLoad в FlightsView.

При старте приложения SwiftData Airport1, в котором реализована загрузка JSON данных на background, мы попадаем на пустую закладку Flights, идет загрузка данных, и мы практически мгновенно можем перейти на закладки Airports и Airlines, обнаружить там загруженную информацию, а затем вернуться на закладку Flights и обнаружить там уже 349 загруженных рейсов. Так что никакой блокировки Main Queue нет.

Нет блокировки Main Queue

Эта версия загрузки данных в SwiftData хранилище на background представлена в проекте SwiftData Airport1 на Github.

@Model Codable

Мы можем пойти еще дальше и сделать Codable SwiftData @Model, например Airport, чтобы загружать JSON данные непосредственно в @Model:

Codable @Model

Однако, мы получили сообщение об ошибке «ТИП Airport не соответствует протоколам Decodable и Encodable«, хотя все свойства класса Airport являются Codable, и если бы не было макроса @Model, мы бы не получили сообщения об ошибках, так как компилятор автоматически реализует эти протоколы для классов и структур, в которых все свойства Codable. К сожалению, в бета версии SwiftData @Model объекты не получили Codable поддержки. Правда механизм Codable очень хорошо отработан, и в качестве «обходного пути» мы можем сами его реализовать для @Model классов Airport и Airline, добавив перечисление enum CodingKeys: String, CodingKey, инициализатор required init (from decoder:Decoder) и функцию func encode (to encoder), если это необходимо:

@Model final class Airport: Codable

Несколько сложнее это сделать для класса Flight, так как в JSON данных есть только уникальные icao коды для аэропортов отправления origin и назначения destination, поэтому нам придется записать эти коды в дополнительные свойства icaoOrigin: String и icaoDestination: String

Дополнительные свойства для Codable
Ручная реализация Codable Flight.

… а потом с помощью контекста context выбирать нужные аэропорты:

Выбирает нужные аэропорты и авиакомпанию.

Эта версия загрузки данных в SwiftData хранилище на background с использованием протокола Codable представлена в проекте SwiftData Airport2 на Github.

Заключение

SwiftData, построенный вокруг современного Swift, эффективно заменяет Core Data, повышая производительность ваших приложений и упрощая хранение данных.

В статье рассматривается, как SwiftData организует описание Схемы данные с помощью макросов @Model, @Attribute, @Relationship непосредственно в коде в виде обычных Swift классов со свойствами, имеющими обычные базовые Swift ТИПы, как использует контейнер ModelContainer и контекст ModelContext для выполнения CRUD (создание, чтение, обновление, удаление) операций как на Main Queue, так и на Background Queue.

Показано использование предикатов Predicate с запросами Query в SwiftData, обеспечивающими мощный механизм фильтрации и сортировки данных в ваших Swift приложениях. SwiftData пока находится в бета тестировании и, к сожалению, предикаты Predicate имеют некоторые ограничения.

SwiftData был создан с расчетом на SwiftUI, и их совместное использование невероятно просто. В довольно простом демонстрационном примере на SwiftUI раскрываются нюансы динамической настройки «живого» запроса @Query и формирования пользовательского контейнера ModelContainer для #Previews в Xcode.

Показано, как сделать @Model классы Codable (хотя на данный момент это не поддерживается компилятором автоматически) и загружать JSON данные непосредственно в SwiftData хранилище без промежуточных структур данных.

И все-таки в будущих версиях SwiftData хотелось бы иметь:

  • Возможность динамической настройки @Query как @FetchRequest в Core Data
  • Возможность использования внешних функций и более сложных логических выражений в #Predicate
  • Автоматическую поддержку Codable протокола для @Model классов

Имея многолетний опыт работы с Core Data могу сказать, что со SwiftData работать фантастически легко и комфортно, и уже сейчас, на этапе бета тестирования, видно, каким большим потенциалом SwiftData будет обладать в ближайшие годы.

Фреймворку SwiftData посвящено несколько сессий на WWDC 2023: 

Meet SwiftData — WWDC23 — Video 

Model your schema with SwiftData — WWDC23 -Video 

Build an app with SwiftData — WWDC23 — Video

Migrate to SwiftData — WWDC23 — Videos 

Dive deeper into SwiftData

Советую также почитать статьи Karin Prater (очень подробные с хорошими демонстрационными примерами):

Background actor:

SwiftData Background Tasks

Codable