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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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:

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

... и получили следующий список рейсов, кликнув на кнопке "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:

Схема Данных в SwiftData
Схема Данных в 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
        }
    }
}
Динамический с помощью init
Динамический @Queryс помощью init

Эта версия динамического@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:

#Preview для HomeView
#Preview для HomeView
#Preview для AirportDetail
#Preview для AirportDetail
#Preview для AirportMap
#Preview для AirportMap

Заполнение 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 Queue нет.

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

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

@Model Codable

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

Codable
Codable @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 

Источник: https://habr.com/ru/articles/740720/


Интересные статьи

Интересные статьи

О важности документации на проекте знают все, начиная от технических заданий на реализацию заканчивая пользовательской документацией. Про важность документации и необходимости документировать написано...
Сентиментный анализ (анализ тональности) – это область компьютерной лингвистики, занимающаяся изучением эмоций в текстовых документах, в основе которой лежит машинное обучение.В этой статье я покажу, ...
Здравствуйте, уважаемые читатели.Хочу написать здесь об одном из своих проектов -- языке Planning C (v2.0). Он является расширением C++, дополняющим базовый язык рядом новых конструкций. В настоящее в...
В предыдущих статьях из цикла “JavaScript библиотека Webix глазами новичка” вы узнали как создать приложение на основе компонентов Webix UI. В этой публикации я хочу подр...
Подробнее об этом хаке и особенностях его работы можно узнать из доклада на !!con 2020 «Playing Breakout… inside a PDF!!» Если вы его не смотрели, то попробуйте открыть файл breakout...