В первой части этой статьи мы рассмотрели основы работы с
Во второй части статьи я хочу показать, что роль автоматически генерируемых
Создаваемое в этой части статьи приложение с именем
Однако, просто зарегистрировавшись на сайте компании FlightAware, вы можете бесплатно получить временной срез любой интересующей вас информации о рейсах
Код находится на Github.
Наше приложение
На UI нашего приложения мы видим достаточно большой список рейсов различных авиакомпаний с различными аэропортами отправления и назначения, различной дальностью и т.д. В частности в первой строке этого списка представлен рейс
Если вы хотите найти нужные вам рейсы, то в правом верхнем углу экрана имеется маленькая кнопка с именем “Filter”, которая позволит нам фильтровать результаты, например, по тому, откуда
Например, можно выбрать рейсы, которые следуют в международный аэропорт О'Хара в Чикаго и в данный момент находятся в воздухе…
… или рейсы, выполняемые авиакомпанией
Полученный список рейсов
Список рейсов
Все эти манипуляции с данными
Помимо рейсов
… и получить о выбранном аэропорте более подробную информацию в виде расположения на карте и табло прилетов и вылетов:
Здесь также на помощь нам приходят
UI приложения — это здорово! Но, когда вы работаете с базой данных
Вот как выглядит полученная с сайта FlightAware информация о рейсах
Я хочу разместить эти данные в
Как и в прошлый раз, мы начинаем создание нашего проекта с шаблона, отмечая галочкой опцию Use Core Data при создании нового проекта. Как и в прошлый раз, мы получаем файл Persistence.swift , который отвечает за доступ к
Я не буду подробно останавливаться на том, как создать такую Моделью данных:
Вы можете это посмотреть в русскоязычном конспекте стэнфордских Лекций CS193P 2020.
Мы видим, что между объектами существуют “взаимосвязи” типа „one to many“ (»один-ко многим") или „one to one“ (»один-к одному"). В частности, для объекта аэропорт
Центральным объектом нашей Модели данных является рейс
рейс Flight
Минимальная информация об аэропортах, но есть информация о географическом положении аэропорта, что в дальнейшем позволит нам разместить их на карте.
аэропорт Airport
авиакомпания Airline
Понятно, что “за кулисами”
Мы будем использовать расширения
Итак, при разработке
Первое, что мы должны сделать, это создать заведомо НЕ-
В расширении
.......................... .
Если для аэропорта
Мы могли бы попытаться как-то обработать эту ошибку получше, чем просто заканчивать аварийно приложение, а мы знаем, что восклицательный
Далее класс
Но есть еще один кусок в нашей объектно-ориентированной головоломке, это “взаимосвязи” между объектами типа «one-to-many“ (»один-ко многим») или «many-to-many» («многие-ко многим»).
Для объекта
Эти взаимосвязи,
Вы получаете их либо, имея рейс
Либо добавляете соответствующий рейс
Если вы добавите рейс
Это очень круто. Все это автоматически настраивается для вас.
Понятно, что как
...................... .
Давайте также избавимся от
У меня есть объект — авиакомпания
.................... .
Я хочу, чтобы переменные
Давайте сделаем то же самое с объектом
Я хочу, чтобы переменные
.............. .
… a также время взлета
Давайте в нашей Модели объектов посмотрим на
Это всё, что мы должны были сделать. Теперь наш
То же самое с аэропортами прибытия
Нужно быть осторожным, когда вы проходите через эту технологию избавления от
........................ .
Потому что запрос
То же самое с дескрипторами сортировки
.................... .
Если у нас нет символа “подчеркивания” “_” в Модели объектов, его не должно быть и в предикате
Итак, мы наделили наши классы class
Но сначала нужно считать данные из файла с
Информация о рейсах в сервисе FlightAware выдается для определенного аэропорта по группам: прибывшие
Вне зависимости от принадлежности к определенной группе, информация о рейсах выдается одинаковая, она скомпонована в структуре
Мы читаем
С помощью этого API мы считываем данные об аэропортах
Считываем данные об авиакомпаниях
Считываем данные о рейсах
В отличие от FlightAware информации об аэропортах и авиакомпаниях, сосредоточенных в отдельных файлах, данные о рейсах, прибывших
Загрузка FlightAware информации в
Теперь опять вернемся к классам
Вот расширение
........................ .
Вот логика работы этого кода. Если нам удалось получить код Аэропорта
Если в
Пока мы установили не все переменные
Если я вернусь в Модель данных и взгляну на объект
Мы установим их со стороны рейса
У класса
........................ .
В этой функции мы также, как и в случае с аэропортами
Пара — тройка интересных вещей происходит здесь, а именно, когда я устанавливаю некоторые переменные
… которые со стороны аэропорта
То же самое с “взаимосвязью” c авиакомпанией
Тем самым мы формируем “взаимосвязь”
У класса
В этой функции мы также, как и в случае с аэропортами
Итак, данные закачены в
В нашем проекте будет файл Persistence.swift с точно такой же структурой
Там нет переменной
У нас будет очень простой файл приложения CoreDataFlightsApp.swift:
Топовое
Давайте последовательно рассмотрим отдельные
Наш
… и поисковой строки
… которая создается с помощью модификатора
… и обрабатывается модификатором
Это дает возможность фильтровать аэропорты по значению первых букв города
Для функционирования
A изменение поисковой строки
Сам предикат поиска
В результате мы можем задавать в поисковой строке начало названия города, в котором находится аэропорт (например, “San” или “С”), и получать отфильтрованный список таких аэропортов
В отфильтрованном или в НЕ-отфильтрованном списке можно выбрать любой аэропорт, например, аэропорт San Francisco Int’l, и посмотреть более детальную информацию о нем:
Давайте посмотрим, как устроен код для просмотра детальной информации об аэропорте
Мы передаем в
Инициируем регион
Предварительно мы сделали так, что объект
В
Для этих списков нам даже не нужно делать никакой выборки данных из
Но это Objective-C множества
В списках
Для предварительного просмотра
… и пару рейсов:
Для каждого рейса в списках Прилетов и Вылетов выводится краткая информация о рейсе в
Мы задаем рейс
Если вы внимательно посмотрите на код, то не увидите нигде следов того, что вы общаетесь с
Точно также как и в случае с
Наш
… который получается с помощью
… и поисковой строки
Она задействована в модификаторе
… и обрабатывается модификатором
На навигационной панели имеются две кнопки:
В результате мы можем задавать в поисковой строке начало названия города, в котором находится аэропорт назначения (например, “San Fr” или “Сhi”), и получить отфильтрованный список рейсов
Мы можем задать более сложный критерий фильтрации, используя при этом структуру
Критерий фильтрации включает в себя аэропорт назначения
Безусловно, есть UI для задания параметров выборки
В качестве аэропорта назначения Destination мы выбрали аэропорт Chicago O'Hare, a в качестве авиакомпании Airline — United, кроме того, нас интересуют рейсы, находящиеся в воздухе, то есть переключатель Enroute Only установлен в
В результат применения этого фильтра (кнопка
Если мы переключим Enroute Only в состояние
И опять слева вы видите UI для задания параметров выборки
Для задания аэропорта назначения Destination используем
Нужный нам аэропорт мы выбираем с помощью
В результате получаем список всех рейсов, направляющихся a аэропорт San Francisco, либо недавно там приземлившихся там:
Для выбора аэропорта назначения Destination мы можем воспользоваться картой: либо “родной”
Мы можем начать выбирать пункт назначения Destination прямо на карте, a когда увидим нужный нам аэропорт, то просто щелкнем на его индикаторе:
Мы выбрали аэропорт Los Angeles Int и получили все рейсы, направляющиеся в этот аэропорт:
Каждый отдельный рейс в списке рейсов представлен значительным количеством информации:
Нужно отметить, что исходные данные, которые мы получаем от FlightAware о рейсах самолетов, имеют единицы измерения отличные от привычных нам СИ единиц измерения. Например, дальность полета представлена в милях (miles), высота в футах (foots), скорость в узлах (knots). Мы, естественно, хотим отображать на нашем UI значения полетных параметров в СИ единицах измерения: дальность и высоту — в километрах km, скорость — в km/h. Для этого нам не нужно писать дополнительный код, так как в
За работу с физическими значениями и их единицами измерения в iOS отвечают объекты
Это позволит нашей маленькой ViewModel
… сформировать на нашем UI строки с расстоянием от аэропорта отправления до аэропорта назначения distance = 887 km и скоростью полета по приборам (ППП) speed = 800 km/h, a также с длительностью рейса 1 hours 18 min и средней скоростью полета aveSpeed = 682 km/h в нужных нам единицах измерения. Причем единицы измерения добавляются к нашим строкам со значениями автоматически. Но этого мало. В зависимости от языка, на который настроен ваш iPhone:
… единицы измерения указываются на соответствующем языке. Например, если язык вашего iPhone — русский, то и единицы измерения также будут на русском языке:
Соответственно, если язык вашего iPhone — английский…
… то и единицы измерения также будут на английском языке:
Показана комфортная работа
В нашем приложении мы просто демонстрируем работу с взаимосвязями объектов типа one-to-many, а также динамическую настройку фантастической «обертки»
Мои эксперименты с полётной информацией показали, что
Код находится на Github.
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
переменная var
to
для сегментов Прилеты и Вылеты. Инициируем регион
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<Flight>
в расширении 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 в качестве авиакомпании Airline — United, кроме того, нас интересуют рейсы, находящиеся в воздухе, то есть переключатель 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.