SwiftData дебютировал на WWDC 2023 в качестве замены фреймворка Core Data. и обеспечивает постоянное хранение данных на Apple устройствах, беспрепятственную синхронизацию с облаком iCloud, Весь API SwiftData построен вокруг современного 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 в Github.
Итак, центральным объектом нашей Схемы является рейс 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
, каждый со своими атрибутами и взаимосвязями:
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 автоматически адаптирует его как атрибут. Такие свойства могут включать в себя:
базовые 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
:
@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
У макроса @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 бета 7) не умеет динамически настраивать фильтр 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 Queue
, и все же она есть.
Эта версия загрузки данных в 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 Queue
нет.
Эта версия загрузки данных в SwiftData хранилище на background
представлена в проекте SwiftData Airport1 на
Github.
@Model Codable
Мы можем пойти еще дальше и сделать Codable
SwiftData @Model
, например 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, эффективно заменяет Core Data, повышая производительность ваших приложений и упрощая хранение данных.
В статье рассматривается, как SwiftData организует описание Схемы данные с помощью макросов @Model
, @Attribute
, @Relationship
непосредственно в коде в виде обычных Swift классов со свойствами, имеющими обычные базовые Swift ТИПы, как использует контейнер ModelContainer
и контекст ModelContext
для выполнения CRUD (создание, чтение, обновление, удаление) операций как на Main Queue
, так и на Background Queue
.
Показано использование предикатов Predicate
с запросами Query
в Swift Data, обеспечивающими мощный механизм фильтрации и сортировки данных в ваших 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
Советую также почитать статьи Karin Prater (очень подробные с хорошими демонстрационными примерами):
debug45
Зачем было выделять каждое второе слово кавычками для кода? Читать невозможно из-за этого, глаза разбегаются.
WildGreyPlus Автор
По-моему как раз наоборот: сразу видны все Swift ключевые слова и легче соориентироваться.
WildGreyPlus Автор
Это обычная практика. Посмотрите, например, эту статью SwiftUI views versus modifiers.