Pull to refresh

Core Data в современном интерьере SwftUI. Взаимосвязь объектов. Динамический @FetchRequest. часть 2

Reading time18 min
Views2.3K
В первой части этой статьи мы рассмотрели основы работы с Core Data в SwiftUI на примере шаблонного приложения, предложенное Apple. Это было тривиальное приложение, в котором всего лишь один объект Core Data с одним единственным атрибутом, и тем не менее было показано, что давая объектам Core Data дополнительную функциональность с помощью „синтаксического сахара“ в расширении extension их классов class, автоматически генерируемых Xcode, можно добиться комфортной работы с Core Data в SwiftUI. Эти классы являются миниатюрными ViewModels для наших SwiftUI Views, так как они реализуют протоколы ObservableObject и Identifiable. И Apple научила их прекрасно «играть» на поле реактивности SwiftUI.

Во второй части статьи я хочу показать, что роль автоматически генерируемых Xcode классов class для объектов Core Data существенно возрастает при работе с реальными взаимосвязанными объектами — рейсами Flights, аэропортами Airports и авиакомпаниями AirLines, которые мы получаем в интернете на сайте компании FlightAware и размещаем в локальной базе данных Core Data.

Создаваемое в этой части статьи приложение с именем CoreDataSwiftUIFlights является сильно упрощенной модификацией реального приложения Enroute из стэнфордских курсов CS193P 2020, которое оперативно подкачивает данные с сервера FlightAware и требует от вас платной подписки на сервис FlightAware .

Однако, просто зарегистрировавшись на сайте компании FlightAware, вы можете бесплатно получить временной срез любой интересующей вас информации о рейсах Flights, аэропортах Airports и авиакомпаниях Airlines в JSON формате. Эти данные размещаются в Core Data с учетом взаимосвязей этих объектов, и вы можете не просто видеть всю информацию о рейсах, но и делать различные запросы к ней с помощью фильтров и сортировать ее нужным вам способом.



Код находится на Github.


МОДЕЛЬ ДАННЫХ в CORE DATA


Наше приложение CoreDataSwiftUIFlights загружает рейсы, связанные с международным аэропортом Сан-Франциско KSFO и международным аэропортом О'Хара в Чикаго KORD. Это коды аэропортов.

На UI нашего приложения мы видим достаточно большой список рейсов различных авиакомпаний с различными аэропортами отправления и назначения, различной дальностью и т.д. В частности в первой строке этого списка представлен рейс United Air Lines UAL412, который летит из Сан-Франциско в Мехико. Он недавно вылетел, пролетев всего 3% пути длиною 3032 км, и прибудет в Мехико приблизительно в 11:10.

Если вы хотите найти нужные вам рейсы, то в правом верхнем углу экрана имеется маленькая кнопка с именем “Filter”, которая позволит нам фильтровать результаты, например, по тому, откуда Origin и куда Destination летит самолет, какой авиакомпании Airline и находится самолет в воздухе в данный момент или нет Enroute only, потому что есть рейсы, которые запланированы на прибытие в определенное время, но, возможно, они ещё не взлетели или только что прилетели и уже находятся на земле.

Например, можно выбрать рейсы, которые следуют в международный аэропорт О'Хара в Чикаго и в данный момент находятся в воздухе…



… или рейсы, выполняемые авиакомпанией United Air Lines и находящиеся в данный момент в воздухе:



Полученный список рейсов Flights можно фильтровать по первым буквам названия города прибытия, вводимым в поисковой строке, если аэропорт прибытия Destination не задан жестко в критерии фильтрации:



Список рейсов Flights, полученный по какому угодно критерию, можно сортировать по расстоянию Distance между аэропортами отправления и прибытия и по времени прибытия ActualOn (реального или приблизительного):




Все эти манипуляции с данными Core Data возможны благодаря всего двум элементам SwiftUI: @FetchRequest и View модификатору onChange:



Помимо рейсов Flights, нам предоставляется список аэропортов Airports, который мы также можем фильтровать по первым буквам названия города, в котором находится аэропорт:



… и получить о выбранном аэропорте более подробную информацию в виде расположения на карте и табло прилетов и вылетов:



Здесь также на помощь нам приходят @FetchRequest и View модификатор onChange:



UI приложения — это здорово! Но, когда вы работаете с базой данных Core Data, главное — это Модель данных с объектами и их атрибутами, а для этого мы должны знать, какая информация доступна нам на сайте компании FlightAware.

Работа с полетами Flights, аэропортами Airports и авиакомпаниями AirLines из FlightAware в базе данных Core Data

.

Вот как выглядит полученная с сайта FlightAware информация о рейсах Flights, аэропортах Airports и авиакомпаниях Airlines в JSON формате:



Я хочу разместить эти данные в Core Data и буду отображать их в виде списков аэропортов, авиакомпаний и рейсов, позволяя пользователю фильтровать эти списки по определенным критериям.

Как и в прошлый раз, мы начинаем создание нашего проекта с шаблона, отмечая галочкой опцию Use Core Data при создании нового проекта. Как и в прошлый раз, мы получаем файл Persistence.swift , который отвечает за доступ к Core Data и который мы модифицируем также, как в нашем прошлом шаблонном приложении, но с другой Моделью данных:



Я не буду подробно останавливаться на том, как создать такую Моделью данных:



Вы можете это посмотреть в русскоязычном конспекте стэнфордских Лекций CS193P 2020.
Мы видим, что между объектами существуют “взаимосвязи” типа „one to many“ (»один-ко многим") или „one to one“ (»один-к одному"). В частности, для объекта аэропорт Airport — это рейсы flightsFrom_, которые отправляются с этого аэропорта, и рейсы flightsTo_, которые прибывают в этот аэропорт, а для объекта авиакомпания Airline — это обслуживаемые ей рейсы flights_.

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

рейс Flight
actualIn Реальное время прибытия к гейту
actualOff Реальное время вылета
actualOn Реальное время прибытия на взлетную полосу
aircraftType_ Тип самолета
estimatedIn Приблизительное время прибытия к гейту
estimatedOff Приблизительное время вылета
estimatedOn_ Приблизительное время прибытия на взлетную полосу
filedAirspeed_ Скорость полета по ППП  knots (узлы)
filedAltitude Высота полета по ППП (100 футов) 100s foots (сотни футов)
ident_ идентификатор рейса
progressPercent Процент выполнения рейса, основанный на вылете/прибытии на взлетно-посадочной полосе. %
routeDistance Запланированное расстояние на основе указанного маршрута. Может отличаться от фактического расстояния. statute miles (мили)
scheduledIn Время прибытия к гейту по расписанию
scheduledOff_ Время вылета по расписанию
scheduledOn_ Время прибытия на взлетную полосу по расписанию
status_ Статус рейса
ВЗАИМОСВЯЗИ ----------------------рейса Flight  ---------------------
airline_ Авиакомпания Airline one-to-one
destination_ Аэропорт назначения Airport one-to-one
origin_ Аэропорт отправления Airport one-to-one

Минимальная информация об аэропортах, но есть информация о географическом положении аэропорта, что в дальнейшем позволит нам разместить их на карте.

аэропорт Airport
city_ Город
countryCode Код Страны
icao_ Код аэропорта
latitude географическая широта
location местоположение
longitude географическая долгота
name название аэропорта
state штат
timezone временной пояс
ВЗАИМОСВЯЗИ -------------------аэропорта Airport-------------------
flightsfrom_ вылеты one-to-many
flightsto_ прилеты one-to-many


авиакомпания Airline
code_ код авиакомпании
name_ название авиакомпании
shortname_ краткое название авиакомпании
ВЗАИМОСВЯЗИ -------------------аэропорта Airline-------------------
flights_ рейсы авиакомпании one-to-many

Понятно, что “за кулисами” Xcode сгенерирует для Core Data объектов классы class Airport, Airline и Flight, если у нас указана опция “Class Definition”:



Мы будем использовать расширения extension этих классов class для того, чтобы продемонстрировать вам еще одну функцию мини-ViewModels, которыми являются эти классы class объектов Core Data, и которая состоит в преобразованиях данных Модели к удобному для SwiftUI Views виду. А именно мы будем преобразовывать полётную информацию в метрическую систему координат СИ. За работу с физическими значениями и их единицами измерения в iOS отвечают объекты Measurement — это отдельная тема, которой мы чуть-чуть коснемся ниже и о которой можно почитать здесь.

Итак, при разработке SwiftUI приложений c Core Data мы фокусируемся на расширениях extension классов class Core Data: Flight, Airport и AirLine. Мы должны «запрятать» туда все, с чем SwiftUI Views будет не комфортно работать.

Расширение extension класса Airport.


Первое, что мы должны сделать, это создать заведомо НЕ-Optional атрибуты объекта Airport, такие как код icao и город расположения аэропорта city. Как было сказано в первой части статьи, мы оставляем Core Data комфортную для нее работу с Optional атрибутами, а сами формируем нужные нам НЕ-Optional атрибуты как вычисляемые переменные var с get{} и set {}.

В расширении extension класса Airport мы создадим аналоги задействованных в Моделе Данных Core Data атрибутовicao_ (код аэропорта) и city_ (город расположения) с символом “подчеркивания” “_” в конце имени. Это их НЕ-Optional версии, которые мы оформляем в виде вычисляемых переменные var icao и var city:


.......................... .


Если для аэропорта Airport атрибут или переменная var icao_ будет равна nil, то это будет действительно ошибка, её не должно быть. Когда я создаю аэропорт Airport, то первое, что я делаю буквально на следующей строке кода, это устанавливаю свойство icao, так что оно никогда не равно nil. Если это происходит, то явно произошла какая-то ошибка в данных.

Мы могли бы попытаться как-то обработать эту ошибку получше, чем просто заканчивать аварийно приложение, а мы знаем, что восклицательный ! знак в случае ошибки просто “обрушивает” ваше приложение. Если вы находитесь в процессе разработки приложения, то обрушение приложения поможет обнаружить такие случаи повреждения базы данных.

Далее класс class Airport является Identifiable, и мы в качестве переменной var id предлагаем использовать код аэропорта icao. Я также сделала объект Airport Comparable, это позволяет сортировать аэропорты:



Но есть еще один кусок в нашей объектно-ориентированной головоломке, это “взаимосвязи” между объектами типа «one-to-many“ (»один-ко многим») или «many-to-many» («многие-ко многим»).

Для объекта Airport это рейсы flightsFrom, которые отправляются, и flightsTo, которые прибывают в этот аэропорт:



Эти взаимосвязи, flightsFrom и flightsTo, достаточно установить с одной из сторон.

Вы получаете их либо, имея рейс Flight с аэропортами назначения destination и отправления origin, и это автоматически формирует множества NSSet для flightsFrom и flightsTo для аэропортов Airport.



Либо добавляете соответствующий рейс Flight непосредственно к множествам flightsFrom и flightsTo.



Если вы добавите рейс Flight к flightsTo на стороне аэропорта Airport, это автоматически заставит этот рейс Flight указать на этот аэропорт Airport в качестве пункта назначения destination.

Это очень круто. Все это автоматически настраивается для вас.

Понятно, что как Swift, так и SwiftUI удобнее взаимодействовать со своими множествами Set<Flight>, a не осуществлять каждый раз приведение ТИПа NSSet as? Set<Flight >, поэтому мы используем “синтаксический сахар” для получения переменных var flightsTo и var flightsFrom в виде Set<Flight >, a в Модели данных “взаимосвязи” опять обозначим теми же именами с символом “подчеркиванием” “_” ( в автоматически генерируемых классах class они будут NSSet):


...................... .


Расширение extension классов Airline и Flight.


Давайте также избавимся от Optional и для других объектов в нашей базе данных.
У меня есть объект — авиакомпания Airline, и я сделала для этого объекта те же самые вещи, что и для объекта Airport:


.................... .


Я хочу, чтобы переменные vars code, name, shortname были НЕ Optional и, конечно, переменная flights была бы Swift множеством Set. Так что code, name, shortname и flights, то есть все переменные в объекте Airline, мы должны переименовать, добавив в конце имени символом “подчеркивания” “_”:



Давайте сделаем то же самое с объектом Flight.

Я хочу, чтобы переменные var идентификатор ident, аэропорт назначения destination, аэропорт отправления origin, тип самолета aircraftType были НЕ Optional:


.............. .


… a также время взлета sheduledOff и посадки sheduledOnпо расписанию, приблизительное время прибытия estimatedOn, скорость filedAirspeed, статус state, авиакомпания airline:



Давайте в нашей Модели объектов посмотрим на Flight и добавим символ “подчеркивания” “_” в конце имён всех этих переменных vars ident, destination, origin, aircraftType, sheduledOff, sheduledOn, estimatedOn, filedAirspeed, state, airline. Сами же вычисляемые переменные (без символа “подчеркивания”) не будут у нас равняться nil.



Это всё, что мы должны были сделать. Теперь наш SwiftUI код будет выглядеть намного лучше. Нам не нужно постоянно проверять, что ident не равен nil, потому что идентификатор ident рейса Flight никогда не может быть равен nil. Если у рейса Flight нет ident, то, фактически, этот рейс не существует.

То же самое с аэропортами прибытия destination и отправления origin, рейсы Flight обязаны также иметь по крайней мере время взлета sheduledOff и посадки sheduledOnпо расписанию, а также примерное время прибытия estimatedOn.

Нужно быть осторожным, когда вы проходите через эту технологию избавления от Optional атрибутов с помощью вычисляемых переменных. Потому что если вы делаете выборку с помощью fetch, как мы делали выборку аэропортов Airport, то в предикате predicate должна быть версия с символом “подчеркивания” “_” — icao_:


........................ .


Потому что запрос request и его предикат predicate выполняются по полям объекта в базе данных, а не по переменным vars, которые находятся в нашем коде. Это реальная выборка в базе данных и там должны быть имена полей в базе данных.

То же самое с дескрипторами сортировки NSSortDescriptor. У нас используется name_, и если мы посмотрим на объект Airline в Модели объектов, то увидим, что там присутствует именно name_ c символом “подчеркивания” “_”:


.................... .


Если у нас нет символа “подчеркивания” “_” в Модели объектов, его не должно быть и в предикате predicate, и в сортировке NSSortDescriptors.

Итак, мы наделили наши классы class Airport, Airline и Flight многими функциональными возможностями, но пока у нас нет самого главного — записи данных FlightAware в Core Data.

Запись FlightAware данных в Core Data.


Но сначала нужно считать данные из файла с JSON данными. Для этого создадим Модель данных для информации, получаемой из сервиса FlightAware. Это те же объекты: аэропорт AirportInfo, авиакомпания AirlineInfo и рейсы FlightsInfo, но только настроенные на чтение JSON данных:




Информация о рейсах в сервисе FlightAware выдается для определенного аэропорта по группам: прибывшие arrivals, вылетевшие departures, прибывающие по расписанию scheduledArrivals, вылетающие по расписанию scheduledDepartures:



Вне зависимости от принадлежности к определенной группе, информация о рейсах выдается одинаковая, она скомпонована в структуре struct Arrival:



Мы читаем JSON файлы с помощью очень простого API на основе Combine и специально настроенного декодера jsonDecoder, который считывает даты и время в ISO8601 формате, a также обеспечивает преобразование имен из формата Snake case. Код размещен в файле FromJSONAPI.swift





С помощью этого API мы считываем данные об аэропортах [AirportInfo] из файла с именем AIRPORTS.json и размещаем с помощью функции update в объектах Core Data с именем Airport:



Считываем данные об авиакомпаниях [AirlineInfo] из файла AIRLINES.json и размещаем с помощью своей функции update в объектах Core Data с именем Airline:



Считываем данные о рейсах FlightInfo из файла SFO.json, который соответствует вполне определенному аэропорту, в данном случае SFO (San Francisco Int ), и размещаем с помощью своей функции update в объектах Core Data с именем Flight.

В отличие от FlightAware информации об аэропортах и авиакомпаниях, сосредоточенных в отдельных файлах, данные о рейсах, прибывших arrivals, покинувших departures, прибывающих по расписанию scheduledArrivals и убывающих по расписанию scheduledDepartures, сосредоточены в файлах соответствующих определенным аэропортам:



Загрузка FlightAware информации в Core Data осуществляется с помощью класса class LoadFlights и его метода load() из файлов с именами AIRPORT.json, AIRLINE.json и SFO.json:



Теперь опять вернемся к классам class объектов Core Data, сгенерированным для нас Xcode, и разместим там static функции func update для записи FlightAware информации в соответствующие объекты Core Data.

Вот расширение extension класса class Airport с функцией update, записывающей AirportInfo в Core Data:


........................ .


Вот логика работы этого кода. Если нам удалось получить код Аэропорта icao из FlightAware информации info, то будем искать аэропорт airport с этим кодом icao с помощью функции func withICAO, находящейся в том же расширении extension класса class Airport. Обратите внимание, что при выборке данных из Core Data в предикате мы всегда используем имя icao_ с “подчеркиванием” “_”, если это НЕ-Optional атрибут:



Если в Core Data уже есть аэропорт airport с этим кодом icao, то возвращаем его в функцию update, если нет, то создаем новый аэропорт airport с заданным в FlightAware кодом аэропорта icao и также возвращаем его в функцию update для обновления остальных атрибутов.

Пока мы установили не все переменные var, которые есть в объекте Airport.

Если я вернусь в Модель данных и взгляну на объект Airport, то увижу все переменные, которые мы установили, кроме двух переменные var flightsFrom и flightsTo, которые представляют собой взаимосвязи с объектом Flight:



Мы установим их со стороны рейса Flight, когда будем заполнять информацию о рейсе Flight и определим для него соответствующие аэропорты Airport назначения destination и прибытия origin.

У класса class рейс Flight есть своя static функцию func update для записи информации FlightInfo FlightAware в Core Data:


........................ .


В этой функции мы также, как и в случае с аэропортами Airport, ищем рейс flight с нужным идентификатором ident_ (мы всегда используем имя с “подчеркиванием” “_” ). Находим в Core Data этот рейс или создаем новый рейс Flight (context:context). В любом случае обновляем его в соответствии с указанной информацией из FlightAware. Ну, а далее следует код, который мы уже видели прежде.

Пара — тройка интересных вещей происходит здесь, а именно, когда я устанавливаю некоторые переменные var “взаимосвязей”. Здесь я устанавливаю аэропорт отправления origin и аэропорт назначения destination с теми кодами icao, какие указаны для этого рейса, путем поиска уже существующего в Core Data или создания нового аэропорта Airport. Тем самым мы формируем “взаимосвязи” Flight - Airport в виде origin_ и destination_:



… которые со стороны аэропорта Airport - Flight превращаются в недостающие “взаимосвязи” flightsTo_ и flightsFrom_ для объекта аэропорт Airport. Все это делается автоматически.

То же самое с “взаимосвязью” c авиакомпанией Airline- Flight. В FlightAware информации о рейсе faflight указывается код авиакомпании airlineCode. Используя его и функцию func Airline.withCode мы находим нужную авиакомпанию в Core Data или создаем новую авиакомпанию airline_ для нашего рейса Flight:



Тем самым мы формируем “взаимосвязь” Flight - Airline в виде airline_, которая со стороны авиакомпании Airline превращается в недостающую “взаимосвязь” flights_.

У класса class авиакомпании Airline есть своя static функцию func update для записи FlightAware информации AirlineInfo в Core Data:



В этой функции мы также, как и в случае с аэропортами Airport, ищем авиакомпанию с нужным кодом code_ (мы всегда используем имя с “подчеркиванием” “_” ). Находим в Core Data эту авиакомпанию или, если она отсутствует. то создаем новую авиакомпанию Airline (context:context), a затем обновляем её в соответствии с информацией из FlightAware.

ИНТЕРФЕЙС в SwiftUI.


Итак, данные закачены в Core Data. Пришло время проектировать UI в SwiftUI.

В нашем проекте будет файл Persistence.swift с точно такой же структурой struct PersistenceController, как и в нашем предыдущем шаблонном приложении:



Там нет переменной var preview, в которой формируются данные для предварительного просмотра Preview и которая присутствует в шаблоне, мы будем формировать их локально для каждого отдельного View.

У нас будет очень простой файл приложения CoreDataFlightsApp.swift:



Топовое HomeView предоставляет возможность работы с Core Data данными: со списком рейсов FlightsView, аэропортов AirportsView и авиалиний AirlinesView:



АЭРОПОРТЫ AirportsView


Давайте последовательно рассмотрим отдельные View и начнем с AirportsView:



Наш View состоит из списка аэропортов airports, который получается с помощью @FetchRequest:


… и поисковой строки query:


… которая создается с помощью модификатора .searchable:


… и обрабатывается модификатором .onChange(of: query):


Это дает возможность фильтровать аэропорты по значению первых букв города city_, в котором находится аэропорт.

Для функционирования @FetchRequest требуется контекст managedObjectContext, и мы получаем его с помощью @Environment (\.managedObjectContext) в виде переменной var viewContext:


A изменение поисковой строки query приводит к настройке предиката nsPredicate, лежащего в основе @FetchRequest:


Сам предикат поиска searchPredicate находится в классе class Airport:


В результате мы можем задавать в поисковой строке начало названия города, в котором находится аэропорт (например, “San” или “С”), и получать отфильтрованный список таких аэропортов Airport:



В отфильтрованном или в НЕ-отфильтрованном списке можно выбрать любой аэропорт, например, аэропорт San Francisco Int’l, и посмотреть более детальную информацию о нем:
  • Полное название аэропорта,
  • Расположение на карте,
  • Прилеты и Вылеты (это наши flightsTo и flightsFrom)



Давайте посмотрим, как устроен код для просмотра детальной информации об аэропорте AirportDetailView.

Мы передаем в AirportDetailView выбранный в предыдущем списке аэропорт var airport: Airport, благодаря чему нам даже не нужен контекст managedObjectContext. Кроме того, нам нужна @State переменная varto для сегментов Прилеты и Вылеты.

Инициируем регион mapRegion для отображения аэропорта airport на карте с помощью Map:



Предварительно мы сделали так, что объект Airport базы данных Core Data также реализует протокол MKAnnotation, что позволит нам без труда отображать его на карте Map:



В body мы размещаем VStack с картой Map, c Picker для выбора сегментов Прилеты и Вылеты и список Прилетов или список Вылетов в зависимости от того, какой сегмент выбирает пользователь:



Для этих списков нам даже не нужно делать никакой выборки данных из Core Data. Благодаря связям one-to-many между объектами Flight и Airport, эти списки формируются автоматически в виде множеств flightsFrom_ и flightsTo_:



Но это Objective-C множества NSSet, которые мы с помощью вычисляемых переменных flightsFrom и flightsTo превратили в Swift множества Set&ltFlight&gt в расширении extension класса class Airport:



В списках List Прилетов и Вылетов мы используем массив Прилетов Array(airport.flightsTo) и массив Вылетов Array(airport.flightsTo), которые отсортированы соответственно по времени прилета и вылета:



Для предварительного просмотра AirportDetailView_Preview мы сформируем базу данных Core Data “в памяти” (in memory) и добавим туда тройку аэропортов:



… и пару рейсов:



Для каждого рейса в списках Прилетов и Вылетов выводится краткая информация о рейсе в FlightViewShort:



Мы задаем рейс flight и аэропорт airport, и в зависимости от того, является ли этот аэропорт airport пунктом отправления origin или пунктом назначения destination, мы создаем нужный UI.

Если вы внимательно посмотрите на код, то не увидите нигде следов того, что вы общаетесь с Core Data, это просто объекты: рейс var flight : Flight и аэропорт var airport: Airport, и мы соответствующим образом форматируем информацию о них, размещая на нашем UI время прилета / вылета по расписанию и реальное время прилета / вылета, куда прилетает или откуда улетает самолет.

Точно также как и в случае с AirportDetailView, мы формируем для предварительного просмотра Preview базу данных Core Data “в памяти” (in memory) и добавляем туда пару аэропортов и рейс:



РЕЙСЫ FlightsView




Наш View состоит из списка рейсов flights:



… который получается с помощью @FetchRequest:


… и поисковой строки query:


Она задействована в модификаторе .searchable:


… и обрабатывается модификатором .onChange(of: query):


На навигационной панели имеются две кнопки: Load и Filter:




В результате мы можем задавать в поисковой строке начало названия города, в котором находится аэропорт назначения (например, “San Fr” или “Сhi”), и получить отфильтрованный список рейсов flights по названию города аэропорта назначения. В нашем случае это San Francisco и Chicago:



Мы можем задать более сложный критерий фильтрации, используя при этом структуру struct FlightSearch:



Критерий фильтрации включает в себя аэропорт назначения destination, аэропорт отправления origin, авиакомпанию airline и нахождение в воздухе inAir По значениям этих переменных мы формируем критерий выборки в виде предиката predicate:



Безусловно, есть UI для задания параметров выборки destination, origin, airline иinAir. Это FilterFlights (слева), a справа вы видите результат выборки рейсов по заданному критерию:



В качестве аэропорта назначения Destination мы выбрали аэропорт Chicago O'Hare, a в качестве авиакомпании AirlineUnited, кроме того, нас интересуют рейсы, находящиеся в воздухе, то есть переключатель Enroute Only установлен в True.

В результат применения этого фильтра (кнопка Done), мы получили 6 рейсов с характеристиками, удовлетворяющими этому критерию.

Если мы переключим Enroute Only в состояние False, то есть нас будут интересовать не только рейсы, находящиеся в воздухе, но и те, которые уже прилетели или собираются улетать, то мы увидим, что число рейсов увеличится до 8 рейсов, туда войдут два рейса, которые уже прилетели в аэропорт Chicago O'Hare и находятся на земле.

И опять слева вы видите UI для задания параметров выборки destination, origin, airline иinAir, a справа — результат выборки рейсов по заданному критерию:



Для задания аэропорта назначения Destination используем Picker:



Нужный нам аэропорт мы выбираем с помощью Picker из списка аэропортов, который включает в себя Any (то есть любой аэропорт). В нашем конкретном случае мы выбираем аэропорт San Francisco:



В результате получаем список всех рейсов, направляющихся a аэропорт San Francisco, либо недавно там приземлившихся там:



Для выбора аэропорта назначения Destination мы можем воспользоваться картой: либо “родной” SwiftUI Map, либо интегрированной MapKit в SwiftUI:



Мы можем начать выбирать пункт назначения Destination прямо на карте, a когда увидим нужный нам аэропорт, то просто щелкнем на его индикаторе:



Мы выбрали аэропорт Los Angeles Int и получили все рейсы, направляющиеся в этот аэропорт:



Единицы измерения полетной информации.


Каждый отдельный рейс в списке рейсов представлен значительным количеством информации:


  • кодом рейса UAL1780,
  • городом вылета Portland,
  • городом прилета San Francisco,
  • полным названием авиакомпании United Air Lines Inc.,
  • типом самолета A319,
  • расстоянием от аэропорта отправления до аэропорта назначения distance = 887 km,
  • скоростью полета по приборам (ППП) speed = 800 km/h,
  • временем взлета Jan 26, 2022, 7:22 PM,
  • временем приземления Jan 26, 2022, 8:41 PM,
  • длительностью рейса 1 hours 18 min,
  • средней скоростью полета aveSpeed = 682 km/h,
  • статусом рейса Приземл. / Вырулив.,
  • процентом пройденного расстояния 100 %

Нужно отметить, что исходные данные, которые мы получаем от FlightAware о рейсах самолетов, имеют единицы измерения отличные от привычных нам СИ единиц измерения. Например, дальность полета представлена в милях (miles), высота в футах (foots), скорость в узлах (knots). Мы, естественно, хотим отображать на нашем UI значения полетных параметров в СИ единицах измерения: дальность и высоту — в километрах km, скорость — в km/h. Для этого нам не нужно писать дополнительный код, так как в Swift уже есть встроенная система единиц измерения длины, скорости и продолжительности соответственно UnitLength, UnitSpeed и UnitDuration, и мы можем легко преобразовывать значения в одних единицах измерения в значения в других единицах измерения, подписывая эти значения соответствующими единицами измерения в определенном формате, которые могут также локализоваться в зависимости от страны.

За работу с физическими значениями и их единицами измерения в iOS отвечают объекты Measurement — это отдельная тема, о которой можно почитать здесь. Для нашего случая нам понадобятся некоторые дополнительные функции и расширения extension, которые мы разместили в файле FoundationExtensions.swift:




Это позволит нашей маленькой ViewModel Flight преобразовать информацию, поступающую из Model, к виду, необходимому для отображения вView



… сформировать на нашем UI строки с расстоянием от аэропорта отправления до аэропорта назначения distance = 887 km и скоростью полета по приборам (ППП) speed = 800 km/h, a также с длительностью рейса 1 hours 18 min и средней скоростью полета aveSpeed = 682 km/h в нужных нам единицах измерения. Причем единицы измерения добавляются к нашим строкам со значениями автоматически. Но этого мало. В зависимости от языка, на который настроен ваш iPhone:



… единицы измерения указываются на соответствующем языке. Например, если язык вашего iPhone — русский, то и единицы измерения также будут на русском языке:



Соответственно, если язык вашего iPhone — английский…


… то и единицы измерения также будут на английском языке:


ЗАКЛЮЧЕНИЕ


Показана комфортная работа Core Data и SwiftUI с реальными взаимосвязанными объектами — полетами Flight, аэропортами Airport и авиакомпаниями AirLine, которые мы получаем на бесплатном сервисе FlightAware и размещаем в Core Data. Это сильно упрощенная модификация реального приложения Enroute из стэнфордских курсов CS193P 2020, которое а отличие от нашего простого приложения оперативно подкачивает данные с сервера FlightAware.

В нашем приложении мы просто демонстрируем работу с взаимосвязями объектов типа one-to-many, а также динамическую настройку фантастической «обертки» @FetchRequest в SwiftUI. Код приложения удается сильно упростить путем использования расширения extension классов class объектов Core Data, сгенерированных Xcode, которые сами по себе уже являются реактивными ViewModel для этих объектов, и в которые удобно спрятать все «шероховатости» взаимодействия Objective-Cориентированного API Core Data с современным SwiftUI.

Мои эксперименты с полётной информацией показали, что Core Data очень эффективно справляется с огромным количеством информации, так что в любом случае «овчинка стоит выделки».

Код находится на Github.
Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments0

Articles