SwiftData
дебютировал на WWDC 2023 в качестве замены фреймворка Core Data
. SwiftData
обеспечивает постоянное хранение данных на Apple устройствах, беспрепятственную синхронизацию с облаком iCloud
и весь его API построен вокруг современного Swift
.
Бета версия iOS 17
SwiftData
является частью iOS 17,
и на момент написания этой статьи Xcode 15
все еще находится на стадии бета-тестирования. Это означает, что содержимое, обсуждаемое в этой статье, может быть изменено. Я буду обновлять статью по мере необходимости.
В SwiftData
, в отличие от своего предшественника, базы данных Core Data
, очень просто создать Схему (или Модель Данных) для постоянного хранения информации в вашем приложении. Для этого прямо в коде создаются обычные Swift
классы class
со свойствами, имеющими обычные базовые Swift
ТИПы , Codable
ТИПы или другие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, которое отображает в некоторый фиксированный момент все рейсы Flight
, обслуживаемые двумя международными аэропортами: аэропортом Чикаго "Chicago O'Hare Intl" и аэропортом Сан-Франциско "San Francisco Int'l". Данные об этих рейсах получены мною с сайта FlightAware в JSON
формате. Мы "закачаем" эти данные в постоянное хранилище в нашем приложении и сможем не просто видеть всю информацию о рейсах, аэропортах и авиакомпаниях ...
... но и делать различные запросы с помощью фильтров.
Например, выбирать определенные рейсы Flights
по аэропорту назначения - destination
, аэропорту вылета - origin
, авиакомпании - airline
и нахождении в данный момент в воздухе - Enroute Only
:
В качестве примера мы выбрали с помощью Picker
в качестве аэропорта назначения destination
международный аэропорт в Чикаго - "Chicago O'Hare Intl", а также рейсы, которые в данный момент находятся в воздухе Enroute Only
:
... и получили следующий список рейсов, кликнув на кнопке "Done":
Кроме того, в списке аэропортов мы можем искать аэропорты по первым буквам имени, например, "San " и, выбрав из сформировавшегося списка аэропортов, например, "San Francisco Int'l" (международный аэропорт Сан-Франциско), посмотреть более подробную информацию о нём: местоположение, рейсы, вылетающие и прилетающие в этот аэропорт:
Мы можем сортировать рейсы нужным нам способом. На рисунке ниже представлена сортировка по дальности полета distance
в порядке убывания и в порядке возрастания:
У меня есть точно такое же приложение Enroute, написанное с применением Core Data, так что при желании очень легко сравнить его с вновь создаваемым приложением SwiftData Airport.
Итак, центральным объектом нашей Схемы является рейс Flight
, который выполняется авиакомпанией airline:
Airline
между аэропортом отправления origin: Airport
и аэропортом назначения destination: Airport
. Модель данных такого приложения мы представим обычнымиSwift
классами: Flight
,Airport
и Airline
:
class Flight {
var ident: String
var actualOff: Date?
var scheduledOff: Date
var estimatedOff: Date
var scheduledOn: Date
var estimatedOn: Date
var actualOn: Date?
var progressPercent: Int
var status: String
var routeDistance: Int
var filedAirspeed:Int
var filedAltitude: Int
var origin: Airport
var destination: Airport
var airline: Airline
var aircraftType: String
}
У рейса Flight
имеется уникальный идентификатор рейса ident
, время взлета (по расписанию scheduledOff
, приблизительное estimatedOff
и действительное actualOff
), время приземления (по расписанию scheduledOn
, приблизительное estimatedOn
и действительное actualOn
), аэропорт отправления origin: Airport
, аэропорт прибытияdestination: Airport
, авиакомпания airline: Airline
, выполняющая этот рейс, тип самолета aircraftTypе
. а также расстояние routeDistance
между пунктами отравления и назначения, скорость filedAirspeed
, высота filedAltitude
и процент пройденного пути progressPercent
.
class Airport {
var icao: String
var name: String
var city: String
var countryCode: String
var latitude: Double
var longitude: Double
var timezone: String
var flightsTo: [Flight] = []
var flightsFrom: [Flight] = []
}
Аэропорт Airport
имеет код icao
, имя name
, город city
, штат state
, код страны countryCode
, географические координаты latitude
и longitude
его местоположения , а также временной пояс timezone
. Кроме того, нас интересует информация о рейсах, вылетающих из этого аэропорта flightsFrom
, и о рейсах, прибывающих в этот аэропорт, flightsTo
.
class Airline {
var code: String
var name: String
var shortName: String
var flights: [Flight] = []
}
Авиакомпания Airline
имеет код code
, имя name
, краткое имя shortName
. И нас также интересует информация о всех рейсах flights
, выполняемых в данный момент этой авиакомпанией.
В SwiftData
достаточно перед Swift
классами разместить макрос @Model, и мы получим "постоянно хранимую" Модель Данных, то есть в приложении будут постоянно сохраняться "рейсы" Flight
, "авиакомпании" Airline
и "аэропорты"Airport
, каждый со своими атрибутами (Attribute
) и взаимосвязями (RelationShip
) :
import SwiftUI
import SwiftData
@Model
final class Flight {
var ident: String
var actualOff: Date?
var scheduledOff: Date
var estimatedOff: Date
var scheduledOn: Date
var estimatedOn: Date
var actualOn: Date?
var progressPercent: Int
var status: String
var routeDistance: Int
var filedAirspeed:Int
var filedAltitude: Int
var origin: Airport
var destination: Airport
var airline: Airline
var aircraftType: String
}
@Model
final class Airport {
var icao: String
var name: String
var city: String
var state: String
var countryCode: String
var latitude: Double
var longitude: Double
var timezone: String
var flightsFrom: [Flight]
var flightsTo: [Flight]
}
@Model
class Airline {
var code: String
var name: String
var shortName: String
var flights: [Flight] = []
}
Благодаря @Model SwiftData
по умолчанию автоматически преобразует все хранимые свойства Swift
класса в постоянно хранимые свойства.
Если свойство имеет Value ТИП,SwiftData
автоматически адаптирует его как атрибут Attribute
. Такие свойства могут включать в себя:
базовые Value ТИПы (
String
,Int
иFloat
и т.д.)
более сложные Value ТИПы :
- структуры
struct
- перечисления
enum
-
Codable
ТИПы- коллекции Value ТИПов
Если свойство
имеет сылочный Reference ТИП (то есть класс class
), то SwiftData
адаптирует его как взаимосвязьRelationShip
. Вы можете создавать взаимосвязи RelationShip
:
с другими @ModelТИПами
с коллекциями @ModelТИПов
Макросы @Attribute, @Relationship и @Transient
@Model автоматически адаптирует все хранимые свойства вашего Swift
класса либо в атрибуты, либо во взаимосвязи, и в большинстве случаев можно вообще больше ничего не делать, но все же вы можете влиять на то, как SwiftData
будет выполнять эту адаптацию, используя "волшебные палочки" (то есть макросы) и аннотировать отдельные свойства @Model классов с помощью:
@Attribute
@Relationship
@Transient
С помощью макроса @Attributeвы можете, например, добавить ограничение уникальности для идентификатора рейса ident
:
@Model
final class Flight {
@Attribute (.unique) var ident: String
var actualOff: Date?
var scheduledOff: Date
var estimatedOff: Date
var scheduledOn: Date
var estimatedOn: Date
var actualOn: Date?
var progressPercent: Int
var status: String
var routeDistance: Int
var filedAirspeed:Int
var filedAltitude: Int
var origin: Airport
var destination: Airport
var airline: Airline
var aircraftType: String
}
Если вы попытаетесь вставить рейс Flight
с тем же самым значением идентификатор ident, то существующий рейс будет обновлен и заменит значения свойств на новые свойства вставляемого объекта. Это может помочь вам поддерживать актуальность и согласованность данных вашего приложения, если информация периодически скачивается с сервера.
С помощью макроса @Relationshipможно управлять взаимосвязями c @Model объектами и явно указать взаимосвязи типа "один-ко-многим" или "многие-ко-многим" . Например, для аэропорта Airport
с помощью
макроса @Relationshipнам нужно явно указать "инверсивные" взаимосвязи для flightsFrom
и flightsTo
, то есть свойства в рейсе Flight
соответствующие аэропорту отправления origin
и аэропорту назначения destination
:
@Model
final class Airport {
var icao: String
var name: String
var city: String
var state: String
var countryCode: String
var latitude: Double
var longitude: Double
var timezone: String
@Relationship (inverse: \Flight.origin) var flightsFrom: [Flight]
@Relationship (inverse: \Flight.destination) var flightsTo: [Flight]
}
Мы можем указать для аэропорта уникальность свойства icao
, но тогда нам придется указать для взаимосвязей flightsFrom
и flightsTo
способ удаления взаимосвязанных рейсов deleteRule:.cascade
:
@Model final class Airport {
@Attribute (.unique) var icao: String
var name: String
var city: String
var state: String
var countryCode: String
var latitude: Double
var longitude: Double
var timezone: String
@Relationship (deleteRule:.cascade, inverse: \Flight.origin) var flightsFrom: [Flight]
@Relationship (deleteRule:.cascade, inverse: \Flight.destination) var flightsTo: [Flight]
}
Точно также в объекте Airline с помощью
макроса @Relationshipмы
указываем "инверсивную" взаимосвязь для flights
и обеспечиваем уникальность атрибута code
:
@Model final class Airline {
@Attribute (.unique) var code: String
var name: String
var shortname: String
@Relationship (deleteRule:.cascade, inverse: \Flight.airline) var flights: [Flight]
}
Вот как выглядит Схема Данных для нашего приложения в SwiftData
:
у С помощью макроса@Transient можно исключить из постоянного хранилища определенные свойства вашего класса, они могут участвовать в формировании пользовательского интерфейса UI
или во вспомогательных вычислениях, но сохраняться не будут.
Настройка Схемы Данных с помощью макроса
У макроса @Attribute есть следующие опции:
unique: гарантирует уникальное значение свойства
transient: позволяет контексту игнорировать это свойство при сохранении модели-владельца
transformable: преобразует значение свойства между формой "в памяти" и сохраняемой формой
externalStorage: сохраняет значение свойства как двоичные данные отдельно от постоянного хранилища
encrypt: хранит значение свойства в зашифрованном виде
preserveValueOnDeletion: сохраняет значение свойства в истории "постоянного хранения", когда контекст удаляет модель-владелеца
spotlight: индексирует значение свойства, чтобы оно отображалось в результатах поиска Spotlight
Хранение данных отдельно от хранилища
Объемные данные не должны храниться непосредственно в вашем хранилище, потому что это может замедлить его работу. Вы можете указать SwiftData
хранить свойство во внешнем хранилище с помощью опции externalStorage
макроса @Attribute для этого свойства. Например, если вы хотели бы сохранить изображение, полученное извне:
@Attribute(.externalStorage)
var imageData: Data?
Контейнер ModelContainer и контекст ModelContext
Когда Схема Данных определена, пришло время создавать объекты, модифицировать их, удалять и выбирать из хранилища. Для управления операциями с @ModelТИПами в SwiftData
используются два важных класса: контейнер ModelContainer
и контекст ModelContext
.
Контейнер ModelContainer
обеспечивает “бэкенд” постоянного хранения для @ModelТИПов. Вы можете создать контейнер ModelContainer
, просто указав список @ModelТИПов, которые вы хотите сохранять.
// Инициализация только с помощью @Model ТИПов
let container = try ModelContainer([Airport.self, Airline.self, Flight.self])
Если вы хотите дополнительно настроить свой контейнер container
, вы можете использовать конфигурации configurations
для изменения своего URL
-адреса, иCloudKit
, а также опций миграции.
// Инициализация только с помощью @Model ТИПов
let container = try ModelContainer([Airport.self, Airline.self, Flight.self])
// Инициализация с помощью конфигураций configurations
let container = try ModelContainer(
for: [Airport.self, Airline.self, Flight.self],
ModelConfiguration(url: URL(string: "path")!)
)
let container = try ModelContainer(
for: [Airport.self, Airline.self, Flight.self],
ModelConfiguration(inMemory: true)
)
// Синхронизация с iCloud
let container = try ModelContainer(
for: [Airport.self, Airline.self, Flight.self],
cloudKitContainerIdentifier: "bestkora.com.flights"
)
Как только ваш контейнер container
установлен, вы готовы создавать новые данные, изменять существующие и сохранять изменения, а также осуществлять выборку данных с помощью контекста ModelContext
.
Вы также можете использовать SwiftUI View
и Scene
модификаторы, чтобы установить
контейнер container
...
import SwiftUI
@main
struct SwidtData_AirportApp: App {
var body: some Scene {
WindowGroup {
HomeView()
.constant(true))
}
.modelContainer(for: [Airport.self, Airline.self, Flight.self])
}
}
... и использовать его в среде @Environment вашего View
для получения контекста context
:
import SwiftUI
import SwiftData
struct FlightsView: View {
@Environment(\.modelContext) private var context
@Query( sort: \.routeDistance, order: .forward) var flights: [Flight]
var body: some View {
NavigationView {
List {...}
.listStyle(PlainListStyle())
.navigationTitle("Flights (\(flights.count))")
.toolbar{
ToolbarItem(placement: .topBarLeading) {load}
ToolbarItem(placement: .topBarTrailing) {...}
ToolbarItem(placement: .topBarTrailing) {...}
}
.sheet(isPresented: $showFilter) {...}
}
.task {
if flights.count == 0 { LoadFlights (context: context).load() }
}
.refreshable {LoadFlights (context: context).load()} // KIAN
}
var load: some View {
Button("Load") {
LoadFlights (context: context).load()
}
}
}
Контекст ModelContext
отслеживает все изменения в ваших моделях @Model и предоставляет множество действий для работы с ними. Они являются вашим интерфейс для отслеживания обновлений, сохранения изменений и даже отмены этих изменений.
Вне иерархии Views
вы можете попросить контейнер container
предоставить вам разделяемый (shared) MainActor
контекст context
:
import SwiftData
let context = container.mainContext
… или вы можете просто инициализировать новые контексты context
для данного контейнера container
:
import SwiftData
let context = ModelContext(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: Airport
, аэропорт прибытияdestination: Airport
и
авиакомпанияairline: Airline
, выполняющую этот рейс, мы сможем только после того, как рейс Flight
уже будет находится в нашем хранилище, так что нам понадобиться только один инициализатор Flight init(ident:String)
с единственным уникальным атрибутом ident:
@Model final class Flight {
@Attribute (.unique) var ident: String
//......
init(ident: String) {
self.ident = ident
}
}
С помощью этого инициализатор мы сначала создадим новый рейс Flight с заданным идентификатором ident, а затем вставим его в контекст context:
let flight = Flight(ident: ident)
context.insert(flight)
Сохранение нового объекта flight
не понадобится, потому что по умолчанию работает режим автосохранения контекста context
.
После получения уже записанного в SwiftData
хранилища рейса flight
с заданным индентификатором ident
, мы модифицируем все его атрибуты, включая взаимосвязи origin, destination и airline с помощью static
функции func
update :
extension Flight {
//--------------- Update ------------------
static func update (from faflight: Arrival, in context: ModelContext) {
if faflight.ident != "",
faflight.airlineCode != "" && faflight.airlineCode.count >= 1,
faflight.origin.code != "",
faflight.destination.code != "" {
// ищем рейс flight в SwiftData по ident
let flight = self.withIdent(faflight.ident, in: context)
flight.origin = Airport.withICAO(faflight.origin.code, context: context)
flight.destination = Airport.withICAO(faflight.destination.code, context: context)
flight.actualOff = faflight.actualOff
flight.scheduledOff = faflight.scheduledOff!
flight.estimatedOff = faflight.estimatedOff ?? faflight.scheduledOff!
flight.scheduledOn = faflight.scheduledOn!
flight.estimatedOn = faflight.estimatedOn ?? faflight.scheduledOn!
flight.actualOn = faflight.actualOn
flight.aircraftType = faflight.aircraftType ?? "Unknown"
flight.progressPercent = faflight.progressPercent
flight.status = faflight.status
flight.routeDistance = faflight.routeDistance
flight.filedAirspeed = faflight.filedAirspeed ?? 0
flight.filedAltitude = faflight.filedAltitude ?? 0
flight.airline = Airline.withCode(faflight.airlineCode, context: context)
}
}
}
Для придания универсального характера функции update
, которая работала бы как с новыми, так и с уже существующими объектами flight
, мы использовали static
функцию func
withIdent класса Flight
, которая ищет рейс flight
с заданным ident
, и если находит его, то возвращает, а если не находит, то создает новый с помощью insert
:
static func withIdent(_ ident: String, in context: ModelContext) -> Flight {
// ищем рейс flight в SwiftData по ident
let flightPredicate = #Predicate<Flight> {
$0.ident == ident
}
let descriptor = FetchDescriptor<Flight>(predicate: flightPredicate)
let results = (try? context.fetch(descriptor)) ?? []
// если находим, то возвращаем его
if let flight = results.first {
return flight
} else {
// если нет, то создаем новый
let flight = Flight(ident: ident)
context.insert(flight)
return flight
}
}
Надо сказать, что это еще и программный способ обеспечения уникальности свойства ident
, и в принципе нам не требуется указывать это с помощью макроса @Attribute(.unique) var ident: String
, что может очень пригодится при синхронизации SwiftData
информации с iCloud
, который не поддерживает ограничение @Attribute (.unique)
.
Аналогичным образом мы поступим с Airport
и с Airline
( код в Github проект SwiftDataEnroute).
Итак, мы научились читать (READ
) объекты из хранилища SwiftData
:
let flightPredicate = #Predicate<Flight> {
$0.ident == ident
}
let descriptor = FetchDescriptor<Flight>(predicate: flightPredicate)
let results = (try? context.fetch(descriptor)) ?? []
Создавать (CREATE
) новые объекты:
let flight = Flight(ident: ident)
context.insert(flight)
Примечание. Если происходит вставка insert
SwiftData
объекта, у которого есть уникальный атрибут@Attribute (.unique)
, и этот объект уже находится в хранилище, то новый объект не создается, а атрибуты существующего объекта просто обновляются.
Модифицировать (UPDATE
) существующие:
flight.actualOff = faflight.actualOff
flight.scheduledOff = faflight.scheduledOff!
Удалить (DELETE
) постоянно хранимый SwiftData
объект так же просто. Достаточно попросить ModelContext
“пометить” его для удаления :
context.delete(flight)
Примечание. Все SwiftData
объекты имеют свойство context
, которое дает вам контекст, к которому они принадлежат. Вот как вы можете использовать его для оптимизации функции удаления delete
:
private func delete(flight: Flight) {
if let context = flight.context {
context.delete(flight)
}
}
Когда сработает механизм автосохранения, произойдет фактическое удаление объекта. Однако, если вы хотите немедленно выполнить удаление объекта, a также зафиксировать другие ожидающие изменения, вы можете попросить ModelContext
сохранить их с помощью save
.
do {
// Try to save
try context.save()
} catch {
// We couldn't save :(
// Failures include issues such as an invalid unique constraint
print(error.localizedDescription)
}
Выборка данных. Новые Swift ТИПы: Query, Predicate и FetchDescriptor.
Для выборки данных SwiftData
привлекает такие "чисто" Swift
ТИПы, как предикат Predicate
и дескриптор выборки FetchDescriptor
, а также значительно улучшенный уже существующий в Swift
дескриптора сортировки SortDescriptor
.
Новый в iOS 17
предикат Predicate
работает с “родными” ТИПами Swift
. Это современная замена старого ТИПа NSPredicate
с полной проверкой ТИПов. Реализация ваших предикатов также сильно упрощается благодаря такой поддержке Xcode
, как автозаполнение.
Вот несколько примеров построения предикатов для нашего приложения.
Во-первых, я могу указать все рейсы, вылетающие из международного аэропорта San Francisco
:
// Примеры Predicate
let sanFranciscoFlightsPredicate =
#Predicate<Flight> { $0.origin.icao == "KSFO"}
Я могу сузить наш запрос до рейсов, выполняемых авиакомпанией United
:
let sanFranciscoUnitedFlightsPredicate =
#Predicate<Flight> { $0.origin.icao == "KSFO" && $0.airline.code == "UAL"}
Можно запросить рейсы, находящие в данный момент в воздухе:
let flightsInAirPredicate =
#Predicate<Flight> { $0.actualOn == nil && $0.actualOff != nil}
После того, как мы решили, какие рейсы нам нужны, мы можем использовать новый ТИП FetchDescriptor
для формирования запроса к нашему постоянному хранилищу и дать указание контексту ModelContext выбрать эти рейсы.
let descriptor = FetchDescriptor<Flight> ( predicate:flightsInAirPredicate )
let flightsInAir = try context.fetch(descriptor)
Работая вместе с FetchDescriptor
, Swift
ТИП SortDescriptor
получил некоторые обновления для поддержки “родных” ТИПов Swift
и keypaths
.
let descriptor = FetchDescriptor<Flight> ( sort: \.routeDistance,
predicate:flightsInAirPredicate )
let flightsInAir = try context.fetch(descriptor)
Помимо предикатов и сортировки, вы можете ограничить количество результатов в FetchDescriptor
, исключить не сохраненные изменения из результатов и многое другое.
Чтобы узнать больше о контейнерах и контекстах SwiftData
, а также об их возможностях, ознакомьтесь с сессией "Dive Deeper into SwiftData" («Углубленное изучение SwiftData»).
SwiftData и SwiftUI
SwiftData
был создан с расчетом на SwiftUI
, и их совместное использование невероятно просто. SwiftUI
— это самый простой способ начать использовать SwiftData
. Будь то настройка контейнера SwiftData
, выборка данных или управление обновлениями вашего View
, компания Apple создала прекрасный API, напрямую интегрирующий эти фреймворки.
Новые SwiftUI Scene
и View
модификатор .modelContaner
— это самый простой способ начать создание приложения SwiftData
.
import SwiftUI
import SwiftData
@main
struct SwidtData_AirportApp: App {
var body: some Scene {
WindowGroup {
HomeView()
}
.modelContainer(for: [Airport.self, Airline.self, Flight.self])
}
}
SwiftData
использует "обертку свойства" @Query
для выбора данных их хранилища. В представленном ниже коде мы выбираем все рейсы flights
и показываем их в списке:
import SwiftUI
import SwiftData
struct FlightsView: View {
@Query private var flights: [Flight]
var body: some View {
NavigationView {
List {
ForEach ( flights ) { flight in
FlightView(flight: flight) }
}
.listStyle(PlainListStyle())
.navigationTitle("Flights (\(flights.count))")
}
}
}
@Queryнапоминает нам @FetchRequest в Core Data
. Они имеют много общего. @Queryтакже является "живым" запросом к хранилищу SwiftData
, то есть все изменения в нем автоматически отражаются на UI. @Query также поддерживает фильтрацию данных с помощью аргумента filter
, сортировку с помощью sort
, порядок сортировки с помощьюorder
и анимацию animation
. Вот @Query, который поддерживает сортировку массива рейсов flights
по длине маршрута routeDistance
и упорядочивание по возрастанию:
@Query( sort: \.routeDistance, order: .forward) var flights: [Flight]
Приведенный ниже @Query выбирает из хранилища только рейсы flights
, вылетающие из аэропорта Сан-Франциско "San Francisco Int'l" и выполняемые авиакомпанией United Airline и сортирует их по % пройденного пути:
@Query (
filter:
#Predicate<Flight> { flight in flight.origin.icao == "KSFO" && flight.airline.code == "UAL"},
sort: \Flight.progressPercent) var flights: [Flight]
Однако в отличие от @FetchRequest в Core Data
@Queryв SwiftData
на данный момент (Xcode 15 бета 6) не умеет динамически настраивать фильтр filter
и сортировку sort
в зависимости от изменения@Stateпеременных. Например, если с помощью@Stateпеременной originICAO
задать код icao
аэропорта отправления...
struct FlightsView: View {
@State private var originICAO = "KSFO"
... то в модификаторе .onChange(
originICAO){...}
не удастся динамически скорректировать параметр filter
для @Query:
#Predicate<Flight> { flight in flight.origin.icao.contains(originICAO) }
Тем не менее, вы можете создавать динамические предикаты с помощью инициализатора init (originICAO: String)
вашего View
, когда изменяемый параметр originICAO
передается из предыдущего View
.
Например, у нас есть HomeView
с закладками для рейсов, аэропортов и авиакомпаний и @Stateпеременная varoriginICAO: String?
, которая задает код icao
для аэропорта отправления:
import SwiftUI
import SwiftData
struct HomeView: View {
@State private var originICAO : String?
var body: some View {
TabView {
FlightsView(originICAO: $originICAO)
.tabItem{
Label("Flights", systemImage: "airplane")
Text("Flights")
}
AirportsView()
.tabItem{
Label("Airports", systemImage: "globe")
Text("Airports")
}
AirlinesView()
.tabItem{
Label("Airlines", systemImage: "airplane.circle")
Text("Airlines")
}
}
}
}
Мы передаем переменную originICAO
в инициализатор init(originICAO: Binding<String?>, isPresented: Binding, context: ModelContext) нашего FlightsView
, отображающего рейсы flights
, у которых аэропорт отправления origin
, определяется этой переменной:
import SwiftUI
import SwiftData
struct FlightsView: View {
@Environment(\.modelContext) private var context
@Query( sort: \Flight.routeDistance, order: .forward) var flights: [Flight]
@State private var showFilter = false
@Binding var originICAO: String?
init(originICAO: Binding<String?>) {
self._originICAO = originICAO
guard let icao = originICAO.wrappedValue,icao.count > 0 else {return}
self._flights = Query(filter: #Predicate<Flight> { flight in flight.origin.icao.contains(icao)}, sort: \.routeDistance, order: .forward)
}
. . . . . . . . . . . . . .
Сама переменная originICAO
может выбираться в FilterICAOView
с помощью Picker
из списка аэропортов отправления airportsFROM
:
import SwiftUI
import SwiftData
struct FilterICAOView: View {
@Query(filter: #Predicate<Airport> { $0.flightsFrom.count > 0 },
sort: \Airport.name, order: .forward) var airportsFROM: [Airport]
@Binding var originICAO: String?
@Binding var isPresented: Bool
@State private var airportFrom: Airport?
init(originICAO: Binding<String?>, isPresented: Binding<Bool>, context: ModelContext) {
_originICAO = originICAO
_isPresented = isPresented
guard let icao = originICAO.wrappedValue,icao.count > 0 else {return}
_airportFrom = State(wrappedValue:Airport.withICAO(icao, context: context))
}
var body: some View {
NavigationStack {
Form {
Picker("Origin", selection: $airportFrom) {
Text("Any").tag(Airport?.none)
ForEach(airportsFROM) { (airport: Airport?) in
Text("\(airport?.name/*friendlyName*/ ?? "Any")").tag(airport)
}
}
.pickerStyle(.inline)
}
.toolbar{
ToolbarItem(placement: .topBarLeading) {cancel}
ToolbarItem(placement: .topBarTrailing) {done}
}
.navigationTitle("Filter Flights")
}
}
var cancel: some View {
Button("Cancel") {
isPresented = false
}
}
var done: some View {
Button("Done") {
originICAO = airportFrom?.icao
isPresented = false
}
}
}
Для выбора аэропорта отправления мы используем кнопку Button("Filter")
и sheet
:
struct FlightsView: View {
. . . . . . . . .
var body: some View {
NavigationView {
List {...}
.listStyle(PlainListStyle())
.navigationTitle("Flights (\(flights.count) \(originICAO == nil ? "" : originICAO!))")
.toolbar{
ToolbarItem(placement: .topBarLeading) {load}
ToolbarItem(placement: .topBarTrailing) {filter}
}
.sheet(isPresented: $showFilter) {
FilterICAOView(originICAO: $originICAO, isPresented: self.$showFilter, context: context)
.presentationDetents([.large])
}
}
.task {...}
}
var load: some View {...}
var filter: some View {
Button("Filter") {
self.showFilter = true
}
}
}
Эта версия динамического@Queryпредставлена в проекте SwiftDataEnroute
в Github.
Можно, конечно, вFlightsView
организовать точно такой же динамический выбор рейсов вообще без настройки инициализатора, а просто выбрать с помощью @Query все рейсы flights: [Flight]
, а затем использовать функцию filter
для массива flights
и получить уже отфильтрованный массив filteredFlights
, который и использовать в UI:
struct FlightsView: View {
@Environment(\.modelContext) private var context
@Query( sort: \Flight.routeDistance, order: .forward) var flights: [Flight]
@State private var showFilter = false
@State var originICAO: String?
var filteredFlights: [Flight] {
guard originICAO != nil, originICAO!.count > 0 else {return flights}
return flights.filter {$0.origin.icao.contains(originICAO!)}
}
var body: some View {
NavigationView {
List {
ForEach (filteredFlights) { flight in FlightView(flight: flight) }
}
.listStyle(PlainListStyle())
.navigationTitle("Flights (\(filteredFlights.count) \(originICAO == nil ? "" : originICAO!))")
.toolbar{
ToolbarItem(placement: .topBarLeading) {load}
ToolbarItem(placement: .topBarTrailing) {filter}
}
.sheet(isPresented: $showFilter) {
FilterICAOView(originICAO: $originICAO, isPresented: self.$showFilter, context: context)
.presentationDetents([.large])
}
}
.task {...}
}
var load: some View {...}
var filter: some View {...}
}
Эта версия представлена в проекте SwiftDataEnroute1
в Github.
Более полная версия фильтрации рейсов по аэропортам отправления origiin
и назначения destination
по авиакомпании airline
и нахождению в данный момент в воздухе Entouter Only
представлена в проекте SwiftData Airport
в Github.
Предварительные просмотры #Preview в Xcode для SwiftUI
Предварительные просмотры #Preview
в Xcode
играют жизненно важную роль в разработке приложений на SwiftUI, предлагая быструю визуальную проверку логики создания UI.
Один из способов использования предварительных просмотров #Preview
в SwiftData
— это создание пользовательского ModelContainer
. Этот способ был показан в видео WWDC 23 под названием "Build an app with SwiftData" ("Создание приложения с помощью SwiftData"). Основная идея состоит в том, чтобы создать контейнер ModelContainer
исключительно для #Preview
в SwiftData
. Контейнер может находиться в памяти (inMemory
) и содержать необязательно реальные данные. Возможная реализация показана ниже:
import SwiftUI
import SwiftData
@MainActor
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer (
for: [Airport.self, Airline.self, Flight.self],
ModelConfiguration(inMemory: true)
)
// Добавляем данные
SampleData.airportsInsert(context: container.mainContext)
SampleData.airlinesInsert(context: container.mainContext)
SampleData.flightsInsert(context: container.mainContext)
return container
} catch {
fatalError("Failed to create preview container")
}
}()
// airport
let previewAirport: Airport = {
MainActor.assumeIsolated {
return Airport.withICAO("KSFO", context: previewContainer.mainContext)
}
} ()
// airline
let previewAirline: Airline = {
MainActor.assumeIsolated {
return Airline.withCode ("UAL", context: previewContainer.mainContext)
}
} ()
А вот как выглядят данные для #Preview:
struct SampleData {
// ----- 1 ----
static let airports: [Airport] = {
let airportData1: Airport = {
var airport = Airport(icao: "KSFO")
airport.latitude = 37.6188056
airport.longitude = -122.3754167
airport.name = "San Francisco Int'l"
airport.city = "San Francisco"
airport.state = "CA"
airport.countryCode = "US"
airport.timezone = "America/Los_Angeles"
return airport
} ()
// ----- 2 ----
let airportData2: Airport = {
var airport = Airport(icao: "KJFK")
airport.latitude = 40.6399278
airport.longitude = -73.7786925
airport.name = "John F Kennedy Intl"
airport.city = "New York"
airport.state = "NY"
airport.countryCode = "US"
airport.timezone = "America/New_York"
return airport
} ()
// ----- 3 ----
let airportData3: Airport = {
var airport = Airport(icao: "KPDX")
airport.latitude = 45.5887089
airport.longitude = -122.5968694
airport.name = "Portland Intl"
airport.city = "Portland"
airport.state = "OR"
airport.countryCode = "US"
airport.timezone = "America/Los_Angeles"
return airport
} ()
// ----- 4 ----
let airportData4: Airport = {
var airport = Airport(icao: "KSEA")
airport.latitude = 47.4498889
airport.longitude = -122.3117778
airport.name = "Seattle-Tacoma Intl"
airport.city = "Seattle"
airport.state = "WA"
airport.countryCode = "US"
airport.timezone = "America/Los_Angeles"
return airport
} ()
// ----- 5 ----
let airportData5: Airport = {
var airport = Airport(icao: "KACV")
airport.latitude = 40.9778333
airport.longitude = -124.1084722
airport.name = "California Redwood Coast-Humboldt County"
airport.city = "Arcata/Eureka"
airport.state = "CA"
airport.countryCode = "US"
airport.timezone = "America/Los_Angeles"
return airport
} ()
return [airportData1,airportData2, airportData3, airportData4, airportData5]
}()
static let airlines: [Airline] = {
// ----- 1 ----
let airlineData1: Airline = {
var airline = Airline(code: "UAL")
airline.name = "United Air Lines Inc."
airline.shortname = "United"
return airline
} ()
// ----- 2 ----
let airlineData2: Airline = {
var airline = Airline(code: "SKW")
airline.name = "SkyWest Airlines"
airline.shortname = "SkyWest"
return airline
} ()
return [airlineData1, airlineData2]
}()
static func flightsInsert(context: ModelContext) {
// ----- 1 ----
let flight = Flight(ident: "UAL1780")
context.insert(flight)
flight.origin = Airport.withICAO("KPDX", context: context)
flight.destination = Airport.withICAO("KSFO", context: context)
flight.actualOff = ISO8601DateFormatter().date(from:"2022-01-26T16:22:56Z")
flight.scheduledOff = ISO8601DateFormatter().date(from:"2022-01-26T16:10:00Z")!
flight.estimatedOff = ISO8601DateFormatter().date(from:"2022-01-26T16:22:56Z")!
flight.scheduledOn = ISO8601DateFormatter().date(from:"2022-01-26T17:13:00Z")!
flight.estimatedOn = ISO8601DateFormatter().date(from:"2022-01-26T17:41:00Z")!
// flight.actualOn = faflight.actualOn
flight.aircraftType = "A319"
flight.progressPercent = 100
flight.status = "Приземл. / Вырулив."
flight.routeDistance = 551
flight.filedAirspeed = 432
flight.filedAltitude = 350
flight.airline = Airline.withCode("UAL", context: context)
// ----- 2 ----
let flight1 = Flight(ident: "UAL1541")
context.insert(flight1)
flight1.origin = Airport.withICAO("KSFO", context: context)
flight1.destination = Airport.withICAO("KSEA", context: context)
flight1.scheduledOff = ISO8601DateFormatter().date(from:"2022-01-26T17:41:00Z")!
flight1.estimatedOff = ISO8601DateFormatter().date(from:"2022-01-26T17:41:00Z")!
flight1.scheduledOn = ISO8601DateFormatter().date(from:"2022-01-26T18:59:00Z")!
flight1.estimatedOn = ISO8601DateFormatter().date(from:"2022-01-26T19:18:00Z")!
flight1.aircraftType = "A319"
flight1.progressPercent = 0
flight1.status = "Вырулив. / Посадка закончена"
flight1.routeDistance = 680
flight1.filedAirspeed = 446
flight1.filedAltitude = 380
flight1.airline = Airline.withCode("UAL", context: context)
// ----- 3 ----
let flight3 = Flight(ident: "SKW5892")
context.insert(flight3)
flight3.origin = Airport.withICAO("KSFO", context: context)
flight3.destination = Airport.withICAO("KACV", context: context)
flight3.actualOff = ISO8601DateFormatter().date(from:"22022-08-25T06:07:15Z")
flight3.scheduledOff = ISO8601DateFormatter().date(from:"2022-08-25T05:46:00Z")!
flight3.estimatedOff = ISO8601DateFormatter().date(from:"22022-08-25T06:07:15Z")!
flight3.scheduledOn = ISO8601DateFormatter().date(from:"2022-08-25T06:29:00Z")!
flight3.estimatedOn = ISO8601DateFormatter().date(from:"2022-08-25T06:48:00Z")!
flight3.aircraftType = "E75L"
flight3.progressPercent = 61
flight3.status = "В пути / По расписанию"
flight3.routeDistance = 269
flight3.filedAirspeed = 413
flight3.filedAltitude = 260
flight3.airline = Airline.withCode("SKW", context: context)
}
static func airportsInsert(context: ModelContext) {
airports.forEach{context.insert($0)}
}
static func airlinesInsert(context: ModelContext) {
airlines.forEach{context.insert($0)}
}
}
Теперь для любого View
у нас есть предварительный просмотр #Preview
:
Заполнение SwiftData хранилища JSON данными
Если при запуске или в процессе работы приложения вам необходимо заполнять SwiftData
хранилище JSON
данными, то можно использовать современные возможности Swift
в реализации многопоточности (Swift
Concurrency)
.
MainActor
Для простоты мы будем считывать JSON данные непосредственно из файла, размещать их в промежуточную Codable
Модель Model FA: AirportInfo
, AirlineInfo
, FlightsInfo
(сокращение FA соответствует источнику JSON данных - сайту FlightAware) , а затем использовать MainActor
для записи в хранилище SwiftData
без явного сохранения, поскольку действует режим автосохранения для контекста context
. Вот пример считывания информации об аэропортах :
func getAirportsAsync(_ nameJSON: String) async {
var airports: [AirportInfo]? = []
do {
airports = try await FromJSONAPI.shared.fetchAsyncThrows (nameJSON)
if let airportsFA = airports {
await MainActor.run {
for airport in airportsFA {
Airport.update(from: airport, context: context)
}
}
}
} catch {
print (" In file \(nameJSON) \(error)")
}
}
Аналогичный функции используются для авиакомпаний и рейсов и всё собирается в async
функции asyncLoadMainActor () ...
struct LoadFlights {
var context : ModelContext
var flightsFromFA = FlightsInfo (
arrivals: [], departures: [],
scheduledArrivals: [], scheduledDepartures: [])
init(context: ModelContext){
self.context = context
}
//----------------------------------------------ASYNC AIRLINE
func airLinesAsync (_ nameJSON: String) async { ... }
//----------------------------------------------ASYNC AIRPORT
func airportsAsync(_ nameJSON: String) async { ... }
//----------------------------------------------ASYNC FLIGHT
func flightsAsync(_ nameJSON: String) async { ... }
//--------------------------------------------------------- Main Actor
func asyncLoadMainActor () async {
await airLinesAsync(FilesJSON.airlinesFile) //-----Airlines
await airportsAsync(FilesJSON.airportsFile) //-----Airport
await flightsAsync(FilesJSON.flightsFileKSFO4) //-----Flights SFO4
await flightsAsync(FilesJSON.flightsFileKORD1) //-----Flights ORD1
}
//--------------------------------------------------------------------
}
... которая используется при запуске приложения в модификаторе .task
, если в хранилище нет данных и при нажатии кнопки Button ("Load")
:
import SwiftUI
import SwiftData
struct FlightsView: View {
@Environment(\.modelContext) private var context
@Query var flights: [Flight]
@Binding var flightFilter: FlightFilter
@Binding var sorting: FlightSorting
@State private var showFilter = false
init (flightFilter: Binding<FlightFilter>,
flightSorting: Binding<FlightSorting>) { ... }
var body: some View {
NavigationView {
List {
ForEach (flights) { flight in FlightView(flight: flight) }
}
.listStyle(PlainListStyle())
.navigationTitle("Flights (\(flights.count))")
.toolbar{ ... }
.sheet(isPresented: $showFilter) { ... }
}
.task {
if flights.count == 0 {
await LoadFlights (context: context).asyncLoadMainActor ()
}
}
}
var load: some View {
Button("Load") {
Task {
await LoadFlights (context: context).asyncLoadMainActor ()
}
}
}
var filter: some View { ... }
}
Загрузка данных в SwiftData
хранилище несмотря большое количество рейсов ( 349), аэропортов (203) и авиакомпаний (84) происходит настолько быстро, что вы даже не заметите блокировки Main Query
, и все же она есть.
Эта версия загрузки данных в SwiftData хранилище на MainActor
представлена в проектах SwiftDataEnroute, SwiftDataEnroute1 и SwiftData Airport
на
Github.
Background actor
Мы можем пойти еще дальше и загружать данные в SwiftData
на фоновой очереди (background queue
). Для этого мы создаем actor LoadModelActor: ModelActor
, инициализируем его с использованием ModelContainer
, создаем новый контекст context
для фоновых операций и используем его для создания DefaultModelExecutor
:
import Foundation
import SwiftData
actor LoadModelActor: ModelActor {
let executor: any ModelExecutor
lazy var flightTaskKSFO = Task {await flightsAsync (FilesJSON.flightsFileKSFO4)}
lazy var flightTaskKORD = Task {await flightsAsync (FilesJSON.flightsFileKORD1)}
lazy var airportTask = Task {await airportsAsync (FilesJSON.airportsFile) }
lazy var airlineTask = Task {await airLinesAsync (FilesJSON.airlinesFile) }
init(container: ModelContainer) {
let context = ModelContext(container)
executor = DefaultModelExecutor(context: context)
}
func flightsAsync(_ nameJSON: String) async { ... }
func airportsAsync(_ nameJSON: String) async {
var airports: [AirportInfo]? = []
do {
airports = try await FromJSONAPI.shared.fetchAsyncThrows (nameJSON)
if let airportsFA = airports {
for airport in airportsFA {
Airport.update(from: airport, context: context)
}
context.saveContext()
}
} catch {
print (" In file \(nameJSON) \(error)")
}
}
func airLinesAsync(_ nameJSON: String) async { ... }
}
Как видите, для actor
нам пришлось выполнить сохранение контекста context.saveContext()
, чтобы данные отражались мгновенно на UI (может быть это особенности работы бета версии SwiftData
).
Добавляем async
функцию func asyncLoad () async
в наш FlightsView
:
private func asyncLoad () async { // actor
let actor = LoadModelActor(container: context.container)
await actor.airportTask.finish()
await actor.airlineTask.finish()
await actor.flightTaskKSFO.finish()
await actor.flightTaskKORD.finish()
}
... и вызываем её в модификаторе .task
, если в хранилище нет данных и при нажатии кнопки Button ("Load")
:
import SwiftUI
import SwiftData
struct FlightsView: View {
@Environment(\.modelContext) private var context
@Query var flights: [Flight]
@Binding var flightFilter: FlightFilter
@Binding var sorting: FlightSorting
@State private var showFilter = false
init (flightFilter: Binding<FlightFilter>,
flightSorting: Binding<FlightSorting>) { ... }
var body: some View {
NavigationView { ... }
.task {
if flights.count == 0 {
await asyncLoad () // background actor
//await LoadFlights (context: context).asyncLoadMainActor ()
}
}
}
var load: some View {
Button("Load") {
Task {
await asyncLoad () // background actor
//await LoadFlights (context: context).asyncLoadMainActor ()
}
}
}
var filter: some View { ... }
}
При старте приложения SwiftData Airport
мы попадаем на пустую закладку Flights
, идет загрузка данных и мы легко и практически мгновенно можем перейти на закладки Airports
и Airlines
обнаружить там загруженную информацию, а затем вернуться на закладку Flights
и обнаружить там 349 загруженных рейсов. Так что никакой блокировки Main Queu
e нет.
Эта версия загрузки данных в SwiftData
хранилище на background
представлена в проекте SwiftData Airport1
на
Github.
@Model Codable
Мы можем пойти еще дальше и сделать SwiftData
@ModelCodable
, например Airport
, чтобы загружать JSON данные непосредственно в @Model:
Однако, мы получили сообщение об ошибке "ТИП Airport
не соответствует протоколам Decodable
и Encodable
", хотя все свойства класса Airport
являются Codable
, и если бы не было макроса @Model, мы бв не получили сообщения об ошибках, так как компилятор автоматически реализует эти протоколы для классов и структур, в которых все свойства Codable
. К сожалению, в бета версии SwiftData
@Modelобъекты не получили Codable
поддержки. Правда механизм Codable
очень хорошо отработан, и в качестве "обходного пути" мы можем сами его реализовать для @Modelклассов Airport
и Airline:
@Model final class Airport: Codable
@Model final class Airport: Codable {
@Attribute (.unique) var icao: String
var name: String
var city: String
var state: String
var countryCode: String
var latitude: Double
var longitude: Double
var timezone: String
@Relationship (deleteRule: .cascade, inverse: \Flight.origin) var flightsFrom: [Flight]
@Relationship (deleteRule: .cascade, inverse: \Flight.destination) var flightsTo: [Flight]
init(icao: String) {
self.icao = icao
}
enum CodingKeys: String, CodingKey {
case airportCode // icao
case name
case city
case state
case countryCode
case latitude
case longitude
case timezone
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.icao = try container.decode(String.self, forKey: .airportCode)
self.name = try container.decode(String.self, forKey: .name)
self.city = try container.decode(String.self, forKey: .city)
self.state = try container.decode(String.self, forKey: .state)
self.countryCode = try container.decode(String.self, forKey: .countryCode)
self.latitude = try container.decode(Double.self, forKey: .latitude)
self.longitude = try container.decode(Double.self, forKey: .longitude)
self.timezone = try container.decode(String.self, forKey: .timezone)
}
func encode(to encoder: Encoder) throws {
// TODO: Handle encoding if you need to here
}
}
Несколько сложнее это сделать для класса Flight
, так как в JSON
данных есть только уникальные icao
коды для аэропортов отправления origin:Airport
и назначения destination: Airport
, поэтому нам придется записать эти коды в дополнительные свойства icaoOrigin: String
и icaoDestination: String
...
@Model final class Flight: Codable
// MARK: - @Model Flight
@Model final class Flight: Codable {
@Attribute (.unique) var ident: String
var actualOff: Date?
var scheduledOff: Date
var estimatedOff: Date
var scheduledOn: Date
var estimatedOn: Date
var actualOn: Date?
var aircraftType: String
var progressPercent: Int
var status: String
var routeDistance: Int
var filedAirspeed:Int
var filedAltitude: Int
var origin: Airport
var destination: Airport
var airline: Airline
//------- for from JSON ----
var icaoOrigin: String
var icaoDestination: String
var codeAirLine: String
//--------------------------
init(ident: String) {
self.ident = ident
}
// ----- implementation of Codable ---
enum CodingKeys: String, CodingKey {
case ident
case actualOff
case scheduledOff
case estimatedOff
case actualOn
case scheduledOn
case estimatedOn
case aircraftType
case progressPercent
case status
case routeDistance
case filedAirspeed
case filedAltitude
case origin
case destination
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.ident = try container.decode(String.self, forKey: .ident)
self.actualOff = try container.decode(Date?.self, forKey: .actualOff)
self.scheduledOff = try container.decode(Date.self, forKey: .scheduledOff)
self.estimatedOff = try! container.decode(Date?.self, forKey: .estimatedOff) ?? self.scheduledOff
self.actualOn = try container.decode(Date?.self, forKey: .actualOn)
self.scheduledOn = try container.decode(Date.self, forKey: .scheduledOn)
self.estimatedOn = try! container.decode(Date?.self, forKey: .estimatedOn) ?? self.scheduledOn
self.aircraftType = try container.decode(String?.self, forKey: .aircraftType) ?? "Unknown"
self.progressPercent = try container.decode(Int.self, forKey: .progressPercent)
self.status = try container.decode(String.self, forKey: .status)
self.routeDistance = try container.decode(Int.self, forKey: .routeDistance)
self.filedAirspeed = try container.decode(Int?.self, forKey: .filedAirspeed) ?? 0
self.filedAltitude = try container.decode(Int?.self, forKey: .filedAltitude) ?? 0
let dictionaryOrigin: [String: String?] = try container.decode(Dictionary<String, String?>.self, forKey: .origin)
self.icaoOrigin = dictionaryOrigin ["code"]!!
let dictionaryDestination: [String: String?] = try container.decode(Dictionary<String, String?>.self, forKey: .destination)
self.icaoDestination = dictionaryDestination["code"]!!
self.codeAirLine = String(ident.prefix(while: { !$0.isNumber }))
}
func encode(to encoder: Encoder) throws {
// TODO: Handle encoding if you need to here
}
//---------------------------------------------
}
а потом с помощью контекста context
выбирать нужные аэропорты:
func flightsAsyncCodable (_ nameJSON: String) async {
var flights: FlightsCodable?
do {
flights = try await FromJSONAPI.shared.fetchAsyncThrows (nameJSON)
if let flightsFromFA = flights {
let flightsFA = (flightsFromFA.arrivals)
+ (flightsFromFA.departures )
+ (flightsFromFA.scheduledArrivals )
+ (flightsFromFA.scheduledDepartures)
for flightFA in flightsFA {
flightFA.origin = Airport.withICAO(flightFA.icaoOrigin, context: context)
flightFA.destination = Airport.withICAO(flightFA.icaoDestination, context: context)
flightFA.airline = Airline.withCode(flightFA.codeAirLine, context: context)
context.insert(flightFA)
// Flight.update(from: flightFA, in: context)
}
context.saveContext()
}
} catch {
print (" In file \(nameJSON) \(error)")
}
}
Эта версия загрузки данных в SwiftData
хранилище на background
с использованием протоколаCodable
представлена в проекте SwiftData Airport2
на
Github.
Заключение
SwiftData
, построенный вокруг современного Swift
, эффективно заменяет CoreData
, повышая производительность ваших приложений и упрощая хранение данных.
В статье рассматривается, как SwiftData
организует описание Схемы данные с помощью макросов@Model, @Attribute, @Relationshipнепосредственно в коде в виде обычных Swift
классов class
со свойствами, имеющими обычные базовые 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 (очень подробные с хорошими демонстрационными примерами):
Modeling Data in SwiftData
Introduction to Data Persistence in SwiftUI with SwiftData
Data Handling in SwiftData: Create, Read, Update, Delete
How to fetch and filter data in SwiftData with Predicates
SwiftData Stack: Understanding Schema, Container & Context
How to convert a CoreData project to SwiftData
Background actor
SwiftData Background Tasks
Codable
Making your SwiftData models Codable
Easily Preload SwiftData Using JSON On Your App’s First Launch