Фреймворк
Но на WWDC 2019 был впервые представлен
Но дело даже не в возрастном различии фреймворков, a в том, что
Apple проделала огромную работу, чтобы эти две мощные технологии прекрасно работали вместе, а это означает, что
Отчасти это удалось потому, что язык программирования
Уже сейчас
Для того, чтобы понять, за счет чего удалось так кардинально улучшить работу
Мы будем работать в
Код находится на Github.
— Управление Моделью Данных в виде графа связанных объектов
— Постоянное хранение
Типичную Модель данных вы видите на двух рисунках ниже (в табличной и графической формах).
На первом рисунке представлен Рейс
На втором рисунке вы видите графическое представление “взаимосвязей” между объектами Аэропорт
Например, вы видите, что у Рейса
“Взаимосвязи”
Но этого недостаточно.
В вашем коде управление графом объектов осуществляется через контекст
Что я имею в виду под
Если мы обратимся в упомянутому выше примеру с рейсами
Вы будете работать с редактором Модели Данных, встроенном в
Помимо управления графом связанных объектов, у нас есть еще слой “постоянного хранения”
За коммуникацию между этими двумя важными элементами в
Все вышеперечисленное — контекст
Со временем работа с
В
1.
2. добавлена новая “Обертка Свойства” (
3. контекст
4. появилась возможность создания “фантомной” (в памяти) базы данных
Так что Apple определенно много думала об интеграции
Сначала мы сосредоточимся на том, как инженеры Apple предлагают использовать
Я, как и вы, понимаю, что выбранный Apple дизайн приложения
Я видела, что многие люди пишут абстракции относительно
Для того, чтобы раскрыть потенциал
Используем меню
Когда вы это сделаете,
«Взаимосвязи» вы увидите во второй части этой статьи при работе с реальными объектами: аэропортами
Для того, чтобы с объектами, которые вы задали в редакторе Модели данных, можно было работать в коде,
Убедиться в этом мы можем, если пойдем в
Вы видите, что для отметки времени
К счастью, эти сгенерированные классы
Мы можем использовать расширения
Для работы в коде с
При инициализации
Структура
Экземпляр
Примечание.
Поэтому инициализатор
Безусловно, структура
Надо отметить, что база данных
Поэтому обязательно замените это на код, который создает фиктивные версии вашей собственной сущности, иначе проект не будет компилироваться, a лучше разделите общее
Теперь нужно сделать так, чтобы наши
В главном файле приложения CoreDataTestApp.swift мы передаем с помощью модификатора
Исключение составляют модальные
Итак, посмотрим на топовое
В топовом
Мы также получаем совершенно замечательную “Обертку Свойства” (Property Wrapper)
Так что использование массива
С помощью кнопок на навигационной панели …
… мы можем добавлять …
… и удалять отметки времени
… и везде нам нужен только контекст
Далее у нас есть предварительный просмотр
Поэтому на
Если мы запустим наше приложение на симуляторе, то оно будет работать в реальной базе данных
Мы можем кликнуть несколько раз на кнопке
Вот такое простое приложение с
Нужно сделать несколько замечаний относительно этого шаблонного приложения.
Для iOS 16 нам предлагается старая навигационная системы
Если мы заменим старый
… то получим заголовок на навигационной панели:
Теперь давайте обратим внимание ещё на одну вещь — на то, как используется единственный атрибут
В
.............
Это говорит о том, что атрибут
Когда “за кулисами”
… то в нашем случае появляется переменная
Вообще это самое противоречивое
Атрибут
С другой стороны,
Поэтому
На самом деле проблема не в
Типичная ошибка: разработчик работает над моделью
Но затем
Модель данных и сгенерированный
Можно ли убрать
Можно. Но вы только что нарушили правила инициализации переменных в
Мы поступим по-другому. Если атрибут
Как я могу делать переменную
Ведь у моего объекта
Поэтому я переименую переменную
И это то, что я люблю делать, когда объект в базе данных называется почти в точности так, как я хочу, но не совсем, но позже я буду использовать этот же прием и для других вещей.
В нашем случае это
Установку
Надеюсь вы поняли, что я здесь делаю. Я предоставляю
В результате теперь на нашем
Но есть еще одно место, где использовался атрибут
В дескрипторах сортировки мы должны использовать настоящие атрибуты
Лучше использовать при инициализации
… в котором можно указать и различные дескрипторы сортировки, и какой угодно сложный предикат. В нашем случае удобно разместить запрос в расширении
Правило такое: все действия, сортировки, предикаты, с реальными атрибутами объекта
A в
Итак, в нашем приложении мы можем создавать (Create), читать (Read) и удалять (Delete) из хранилища объекты
Давайте создадим
Вместе с передаваемой в
Мы будем редактировать
Здесь мы сохраняем наши изменения
…как это сделала Apple в
… и
Понятно, что мы должны где-то разместить этот повторяющийся код. Многие даже опытные разработчики размещают его почему-то в
Это упрощает наш код как в методе
… так и в методах
… и
Если вы не хотите обрабатывать ошибки при сохранении контекста
Для
Что еще примечательного в
Конечно, его предварительный просмотр
Теперь в
То же самое можно сделать с
В этом случае мы можем работать с
На самом деле не имеет смысл оставлять в
Наконец-то наша структура
Это немного улучшает читабельность
Вернемся в
Попутно мы использовали новый в iOS 15 метод
Давайте добавим в расширении
Это упростит наш код в
… вместе с компактной структурой
… и с расширением extension для
Добавим последний штрих в приложение CoreDataTest и заставим его принудительно запоминать данные в
В целом мне нравится то, что сделало Apple с установкой
Вполне возможно, что внедряя контекст
И нашлось немало желающих восстановить справедливость архитектуры
Эту идею пытались реализовать несколькими способами. Сразу скажу — все они плохие или очень плохие.
Сначала предлагалась обычная
С помощью функции
................... .
То есть реактивность от
Получается парадоксальная картина. До тех пор, пока мы не выполним какую-то CRUD операцию с нашими данными, наш список фруктов не обновится. Это выглядит странно в многозадачном режиме на iPad, когда на экране у нас будут присутствовать два приложения, работающие с одной и той же
Совсем иная картина с нашим шаблонным приложением CoreDataTest, основанным на предложенной Apple технологии с “Оберткой Свойства”
Конечно, можно было бы попробовать дальше работать над глобальной
Это идеально подходило в
Одно из сомнений состоит в том, что из-за
Поэтому, делая так, мы выбрасываете в “мусорное ведро ” большую часть оптимизации
Это может быть нормально для очень маленьких наборов данных, но для больших наборов данных это может быть проблемой, и, что более важно, такой код не поддерживает секции Sections, что также может быть проблемой.
Так что эта абстракция
Если вам абсолютно нужна
Вывод. Даже если вы не являетесь фанатом
Великолепно тема Core Data + SwiftUI изложена Полом Хегарти в стэнфордских курсах CS193P 2020 на Лекциях 11 и 12, если вам нужен русскоязычный конспект этих Лекций, то он также в открытом доступе. Там очень много чего рассказывается о
Достаточно хороший материал по тема Core Data + SwiftUI в 100 days SwiftUI у Пола Хадсона.
Неплохой курс SwiftUI & Core Data у Karin Pretter.
Абсолютно НЕ рекомендую курс Core Data in iOS на Udemy.
На примере шаблонного демонстрационного примера, предложенного Apple, я показала, что давая объектам
Код находится на Github.
Однако до сих пор мы рассматривали тривиальный шаблонный демонстрационный пример, в котором всего лишь одна сущность с одним атрибутом.
В следующем разделе я хочу показать вам такую же комфортную работу
Core Data
, разработанный Apple для постоянного хранения данных на своих платформах, эффективно работающий даже с очень большими объемами данных, используется очень давно, с версии iOS 3
и macOs 10.4
, так что прошло где-то порядка 10 лет с того момента, когда Apple впервые представила фреймворк Core Data
. Когда это произошло, языка программирования Swift
вообще не было в проекте, так что Core Data
была спроектирована с ориентацией на Objective-C
и, конечно, это отразилось на её API
.Но на WWDC 2019 был впервые представлен
SwiftUI
, который предложил нам новую парадигму конструирования UI
, он был предложен для iOS 13
и полностью опирался на Swift
, его корни — это Swift
, хотя он использует UIKit
“под капотом” и полностью зависит от UIKit
на iOS
, по крайней мере на данный момент, и от AppKit
на macOS
. Конечно, он это скрывает, как только может, он сконструирован и реализован с прицелом на Swift
. Более того, Swift
сам был существенно доработан с целью поддержки SwiftUI
и стал ещё более мощным и интересным.Но дело даже не в возрастном различии фреймворков, a в том, что
Core Data
принципиально связана с объектно-ориентированным программированием ( классы, наследование, делегирование и все такое), a суперсовременный SwiftUI
основан на декларативном функциональном программировании (структуры, протокольно-ориентированное программирование) и имеет реактивную природу, которая воплощается в использовании архитектуры MVVM
. Apple проделала огромную работу, чтобы эти две мощные технологии прекрасно работали вместе, а это означает, что
Core Data
интегрируется в SwiftUI
так, как будто он всегда был разработан исключительно для SwiftUI
.Отчасти это удалось потому, что язык программирования
Swift
поддерживает как объектно-ориентированное, так и функциональное программирование в равной степени, а, отчасти потому, что Apple удалось научить Core Data
превосходно „играть“ на поле „реактивности“ SwiftUI
. Уже сейчас
Core Data
довольно хорошо интегрирована в SwiftUI
, и со временем ситуация будет только улучшаться. Никогда не работалось с Core Data
так просто и комфортно, как в SwiftUI
, и я надеюсь, что сумею показать вам это. Для того, чтобы понять, за счет чего удалось так кардинально улучшить работу
Core Data
в SwiftUI
, давайте сначала кратко рассмотрим основы функционирования Core Data
, затем поработаем с простейшим шаблоном, который Apple предлагает для работы с Core Data
в SwiftUI
, a в заключении рассмотрим упрощенную модификацию реального приложения Enroute из стэнфордских курсов CS193P 2020, которое оперативно подкачивает данные о рейсах, аэропортах и авиакомпаниях с сервера FlightAware, записывает данные в Core Data
и позволяет оперативно выбрать любую нужную информацию по различным простым и сложным критериям. Подобное приложение необходимо для того, чтобы показать, как работать с взаимосвязями различных Core Data
объектов и как динамически конфигурировать запросы @FetchRequest
в SwiftUI
.Мы будем работать в
Xcode 14
, iOS 16
, SwiftUI 4.0
, так что будет показана новая Navigation
система в SwiftUI
и новая многопоточная Swift concurrency
.Код находится на Github.
Небольшое введение в Core Data. Что такое Core Data стек?
Core Data
— это “родное” Apple решение для постоянного хранения данных. У Core Data
есть две ответственности: — Управление Моделью Данных в виде графа связанных объектов
— Постоянное хранение
Типичную Модель данных вы видите на двух рисунках ниже (в табличной и графической формах).
На первом рисунке представлен Рейс
Flight
, у него есть ряд атрибутов: время прибытия actualOn
, время отправления actualOff
, есть идентификатор рейса ident
и даже некоторые “взаимосвязи” с другими объектами: с Аэропортом Airport
в качестве аэропорт назначения destination
и аэропорта отправления origin
, a также с авиакомпанией Airline
.На втором рисунке вы видите графическое представление “взаимосвязей” между объектами Аэропорт
Airport
, Рейсом Flight
и Авиакомпанией Airline
.Например, вы видите, что у Рейса
Flight
имеются две “взаимосвязи” с Аэропортом Airport
— аэропорт вылета origin
и аэропорт прилета destination
. Заметьте, что обе эти «взаимосвязи» действуют и в обратную сторону для объекта Airport
в виде рейсов flightsFrom
, вылетающих из этого аэропорта, и рейсов flightsTo
, прибывающих в этот аэропорт.“Взаимосвязи”
origin
и destination
для объекта Flight
являются просто объектами Airport
, а «взаимосвязи» flightsFrom
и flightsTo
для объекта Airport
представляют собой множества NSSet
рейсов Flight
. В Core Data
достаточно изменить лишь одну сторону «взаимосвязи», другая сторона будет формироваться автоматически.Но этого недостаточно.
Core Data
должна постоянно хранить эту связанную информацию на диске и уметь записывать и читать её в зависимости от запросов пользователя. И за это ответственна вторая часть Core Data
— взаимодействие с постоянным хранилищем.В вашем коде управление графом объектов осуществляется через контекст
Managed Object Context
. Внутри этого контекста “живет” множество объектов Managed Object
.Что я имею в виду под
Managed Object
объектами? Если мы обратимся в упомянутому выше примеру с рейсами
Flight
, аэропортами Airport
и авиакомпаниями AirLine
, то с точки зрения контекста Managed Object Context
это множество Managed Object
объектов. Вам не нужно создавать структуры struct
для Аэропорта, Рейса и Авиакомпании, Xcode
автоматически генерирует «за кулисами» для вас ManagedObject
классы class
для этих объектов, которые в дальнейшем будут принадлежать хотя бы одному контексту Managed Object Context
в вашем коде и ответственность за их изменение и постоянное хранение берет на себя Core Data
. Вполне допустимо иметь множество контекстов ManagedObjectContext
, но это редко используется и требует некоторых продвинутых навыков. Вы будете работать с редактором Модели Данных, встроенном в
Xcode
, а сама Модель данных хранится в файле с расширением .xcdatamodeld. Вам только необходимо убедиться, что ваш рабочий контекст Managed Object Context
знает о файле с Моделью данных. Помимо управления графом связанных объектов, у нас есть еще слой “постоянного хранения”
Persistence
. В слое “постоянного хранение” Persistence
присутствует хранилище, и Core Data
отвечаем за хранение наших данных в этом хранилище и за восстановления их в нужном формате. За коммуникацию между этими двумя важными элементами в
Core Data
отвечает координатор PersistenceStoreCoordinator
:Все вышеперечисленное — контекст
Managed Object Context
, “постоянное хранилище” Persistence и координатор PersistenceStoreCoordinator
— называется Core Data
стеком.Со временем работа с
Core Data
стеком постоянно улучшалась, но в SwiftUI
мы имеем возможность работать с Core Data
стеком, практически не замечая его.Какие изменения Core Data в SwiftUI?
В
SwiftUI
изменилось несколько вещей:1.
ManagedObject
объекты Core Data
теперь реализуют ObservableObject
и Identifiable
протоколы. Наши Core Data
объекты — рейс Flight
, аэропорт Airport
и авиакомпания AirLine
— теперь могут автоматически обновлять наши SwiftUI Views
как обычные @ObservedObject
или @StateObject
объекты. По существу, теперь эти Core Data
объекты превратились в миниатюрные ViewModel
, которые мы можем напрямую использовать в наших SwiftUI Views
. Кроме того, все свойства этих объектов являются @Published
свойствами, а это супер удобно, так как любое их изменение также приводит к автоматическому обновлению SwiftUI Views
. Объекты Core Data
также стали Identifiable
, что позволяет использовать их без каких-либо затруднений в таких SwiftUI
конструкциях, как ForEach
и List
.2. добавлена новая “Обертка Свойства” (
Property Wrapper
) @FetchRequest
, которая позволяет очень просто выбирать данные из Core Data
и очень просто отображать их на UI
, это маленький „шедевр“, изобретенный Apple для работы Core Data
с декларативным SwiftUI
. Это НЕ одноразовая выборка данных, @FetchRequest
постоянно постоянно обеспечивает наш UI актуальными данными. Таким образом, ваш UI
всегда будет синхронизирован с базой данных. Это полностью отвечает реактивной “природе” SwiftUI
. В iOS 15
ещё добавлена возможность динамического обновления @FetchRequest
, то есть динамического обновления предиката запроса nsPredicate
и дескрипторов сортировки sortDescriptor
. Там также появилась поддержка секций с помощью @SectionedFetchRequest
. 3. контекст
managedObjectContext
теперь включен в “среду” обитания вашего приложения @Environment
, так что передав его в топовый View
с помощью модификатора .environment(\.managedObjectContext,...)
, вы автоматически получаете его во всех дочерних Views
.4. появилась возможность создания “фантомной” (в памяти) базы данных
Core Data
для предварительных просмотров Preview
в SwiftUI
и для тестирования.Так что Apple определенно много думала об интеграции
Core Data
и SwiftUI
. Цель моей статьи — показать вам комфортную работу Core Data
в современном SwiftUI
приложении.Сначала мы сосредоточимся на том, как инженеры Apple предлагают использовать
SwiftUI + Core Data
, чтобы понять, что Apple считает хорошим или достаточно хорошим на данный момент для таких приложений. Мы создадим проект, чтобы лучше понять, как устанавливается контекст Managed Object Context
, на котором и будут разворачиваться все действия с вашими данными. Как Apple манипулирует записями, то есть как создает (Create), читает (Read) и удаляет (Delete) записи из Core Data
хранилища. Для создания полноценного CRUD приложения в шаблоне, предложенном Apple, не хватает только одной операции — обновления (Update). Мы ее добавим и поработаем с выборкой данных из Core Data
, представленной “оберткой свойства” @FetchRequest
. В ходе этого я немного усовершенствую код шаблона Apple в части использования расширения extention
классов class
, сгенерированных Xcode
для объектов Core Data
, с целью придания им дополнительной функциональность с помощью „синтаксического сахара“. И покажу, что архитектура MVVM
для Core Data + SwiftUI
проектов заключается вовсе не в создании какой-то глобальной ViewModel
для всего приложения в целом, a в настройке уже имеющихся мини-ViewModels
самих объектов Core Data
. Недооценка этого факта сильно ухудшает и запутывает структуру Core Data + SwiftUI
проектов и читаемость его кода. Я, как и вы, понимаю, что выбранный Apple дизайн приложения
Core Data + SwiftUI
может быть не идеальным с точки зрения архитектуры MVVM. Вам может показаться, что с высоты идеологии SwiftUI
удастся кардинально усовершенствовать подход Apple. Но если вы решились на это, то убедитесь, что четко знаете, что делаете.Я видела, что многие люди пишут абстракции относительно
Core Data
после одного дня попыток, и это лучший способ получить проблемы в будущем, потому что Core Data
— это довольно большой и заточенный на эффективность как по времени, так и по памяти фреймворк, не сказать, что уж очень сложный, но создавать абстракцию того, что вы не полностью понимаете, всегда сложно. Мы рассмотрим некоторые очевидные заблуждения такого рода, которыми кишит youtube.com и некоторые курсы на Udemy. Так что чем больше мы боремся с выбранным Apple способом интеграции Core Data
в SwiftUI
, тем более вероятно, что столкнемся с проблемами в будущем. Уберечь вас от подобного рода проблем тоже является моей задачей. Для того, чтобы раскрыть потенциал
Core Data
в SwiftUI
, одного шаблонного приложения с одной сущностью и одним атрибутом явно недостаточно. Поэтому во второй части статьи мы рассмотрим реальный пример с множеством взаимосвязанных сущностей, он опирается на то, что Пол Хегарти демонстрировал на стэнфордских курсах CS193P 2020, но сильно упрощен для того, чтобы сосредоточиться на главном — удобстве работы с Core Data
в SwiftUI
.Добавление Core Data в SwiftUI приложение
Используем меню
File
-> New
-> Project
и добавим новый проект, в котором отметим галочкой опцию Use Core Data
:Когда вы это сделаете,
Xcode
создаст в вашем проекте файл Модели данных с расширением .xcdatamodeld
, в нашем случае это файл CoreDataTest.xcdatamodeld
. В этом файле вы создаете свои объекты (или сущности, как их называют в Базах Данных) и их атрибуты. У Xcode
есть встроенный редактор для Модели данных. Он также позволяет вам графически создавать “взаимосвязи” между разными объектами (сущностями), хотя последнее мы не увидим в нашем шаблоне, так как у нас очень простая Модель данных, состоящая всего из одной сущности Item
(отметки времени), у которой всего один атрибут — собственно сама временная отметка timestamp
, имеющая ТИП Date
:«Взаимосвязи» вы увидите во второй части этой статьи при работе с реальными объектами: аэропортами
Airport
, рейсами Flight
и авиакомпаниями Airline
. Более подробную информацию о работе с Моделью данных в Core Data
можно получить в материалах Стэнфордского курса CS193P.Для того, чтобы с объектами, которые вы задали в редакторе Модели данных, можно было работать в коде,
Xcode
“за кулисами” автоматически генерирует NSManagedObject
классы class
с переменными vars
, соответствующими атрибутам этих объектов. Убедиться в этом мы можем, если пойдем в
ContentView
, в FetchedResults
command-кликнем на Item
и перейдем на определение этого объекта:Вы видите, что для отметки времени
Item
создан специальный класс class
Item
, который наследует от NSManagedObject
, при чем этот файл автоматически сгенерирован Xcode
, и вы не можете его редактировать в любом случае, потому что он read-only
:К счастью, эти сгенерированные классы
class
являются ObservableObject
и Identifiable
, представляя собой, по существу, миниатюрные ViewModels
, и они служат прекрасным “источником истины” (source of truth) для элементов нашего UI
в SwiftUI
.Мы можем использовать расширения
extension
этих классов class
так же, как мы делаем это с любыми другими структурами данных в Swift
, чтобы добавить туда наши собственные методы и вычисляемые переменные vars
.Получаем контекст Core Data в PersistenceСontroller
Для работы в коде с
Core Data
объектами нам нужен контекст Managed Object Context
. Для этого Apple создала в нашем приложении вспомогательную структуру struct
c именем PersistenceController
в файле Persistence.swift:При инициализации
init
структуры PersistenceController
используется другая вспомогательная структура — контейнер let
container: NSPersistentContainer
, который согласно документации упрощает создание Core Data
стека и позволяет легко извлечь из него контекст viewContext
для работы с Core Data
объектами. Именно через контейнер container
попадает в наш PersistenceController
Модель данных, в которой мы настраиваем сущности и их атрибуты и которая существует в виде файла с расширением .xcdatamodeld. Вот так все просто.Структура
PersistenceController
предоставляет в наше распоряжение две базы данных Core Data
, два static
свойства: let
shared
и var
preview
. Экземпляр
shared
— это полноценная Core Data
с хранением на диске, a preview
— это некоторая “фантомная” Core Data
, которая хранится в специальном /dev/null
файле, и годится исключительно для предварительного просмотра Preview
и тестирования.Примечание.
/dev/null
— специальный файл в системах класса UNIX
, представляющий собой так называемое «пустое устройство» или “черную дыру”. Запись в него происходит успешно, независимо от объёма «записанной» информации. Поэтому инициализатор
init
PersistenceController
принимает Bool
аргумент inMemory
для указания того, где находится хранилище Core Data
, “в памяти” или на диске.Безусловно, структура
struct
PersistentController
создана с одной единственной целью — облегчить получение контекста viewContext
для базы данных Core Data
: реальной shared
или “фантомной” preview
. Надо отметить, что база данных
preview
в шаблоне заполняется данными, то есть туда добавляются экземпляры сущности Item
, которой, вероятно, не будет в вашем проекте:Поэтому обязательно замените это на код, который создает фиктивные версии вашей собственной сущности, иначе проект не будет компилироваться, a лучше разделите общее
preview
на несколько специализированных preview
для отдельных View
, но об этом позже.Core Data в SwiftUI Views
Теперь нужно сделать так, чтобы наши
SwiftUI Views
узнали о полученном контексте базы данных Core Data
, и это осуществляется через “среду” обитания SwiftUI
приложения @Environment
и её переменную \.managedObjectContext
. В главном файле приложения CoreDataTestApp.swift мы передаем с помощью модификатора
.environment
на самый верхний уровень иерархии Views
наш полученный из контейнера container
контекст persistenceController.container.viewContext
“окружающей среде” @Environment(\.managedObjectContext)
. Когда вы передаете “среду обитания” @Environment
некоторому View
, то все Views
, которые находятся в его body
, получают ту же самую “среду” @Environment
. Так что вам не нужно передавать её дальше по иерархии Views
.Исключение составляют модальные
View
наподобие .sheet
или .popover
, когда вы покидаете ваше базовое body
и оказываетесь в новом базовомbody
. В этом случае вам необходимо отдельно передавать “среду” @Environment
с помощью модификатора .environment
. Итак, посмотрим на топовое
ContentView
в нашем шаблоне.В топовом
View
мы читаем контекст managedObjectContext
из “среды” обитания @Environment(\.managedObjectContext)
и сохраняем его как private
переменную var
с именем viewContext
.Мы также получаем совершенно замечательную “Обертку Свойства” (Property Wrapper)
@FetchRequest
, которая имеет несколько инициализаторов init
, и нам представлен один из них, наименее мною любимый, в который мы передаем только дескрипторы сортировки sortDescriptors
, чтобы указать, как выбранные данные должны быть отсортированы. Вы также можете передать @FetchRequest
полноценный запрос fetchRequest
с предикатом nsPredicate
и дескрипторами сортировки sortDescriptors
, в котором отразите все требования к выборке нужных данных Core Data
, которые будут находиться в вашей переменной var
items
.@FetchRequest
, по существу, является “живым” запросом, то есть переменная var
items
ВСЕГДА отражает актуальное состояние базы данных. И это действительно одна из лучших интеграций SwiftUI
и Core Data
, которая состоит в способности @FetchedResult
переменных vars
всегда поддерживать себя в актуальном состоянии.Так что использование массива
items
в ForEach
будет обновлять наш UI
каждый раз, когда будут добавляться новые или удаляться существующие отметки времени items
, и это будет происходить автоматически.С помощью кнопок на навигационной панели …
… мы можем добавлять …
… и удалять отметки времени
Item
…… и везде нам нужен только контекст
viewContext
, нам уже не нужно вспоминать о PersistentController
и о том, как мы получили контекст viewContext
.Далее у нас есть предварительный просмотр
Preview
для нашего ContentView
, и здесь используется уже не реальная база данных PersistentController.shared
, a “фантомная” PersistentController.preview
, уже наполненная некоторыми отметками времени:Поэтому на
Preview
мы видим те 12 временных отметок, которые мы разместили в PersistentController.preview
:Если мы запустим наше приложение на симуляторе, то оно будет работать в реальной базе данных
PersistentController.shared
, и мы не увидим ни одной временной отметки, так как их там никто не размещал:Мы можем кликнуть несколько раз на кнопке
“+”
, чтобы добавить в нашу базу данных несколько временных отметок, можем кликнуть на кнопке “Edit”
и удалить не нужную нам отметку времени. В результате мы получим две оставшиеся временные отметки и сможем. кликнув на стрелочке справа от каждой временной отметки, перейти на экран с детальным представлением временной отметки, что в нашем случае не актуально, так как у сущности Item
всего один атрибут:Вот такое простое приложение с
Core Data
нам предлагает Apple в качестве шаблона.Нужно сделать несколько замечаний относительно этого шаблонного приложения.
Для iOS 16 нам предлагается старая навигационная системы
SwiftUI
c NavigationView
, а вместо модификатора заголовка .navigationTitle ("Select an items")
, в коде шаблона присутствует Text("Select an items")
, что неправильно:Если мы заменим старый
NavigationView
на новый NavigationStack
, Text(“...”)
на модификатор .navigationTitle("Select an items")
, а для NavigationLink
используем новую конструкцию с navigationDestination
…… то получим заголовок на навигационной панели:
“Столкновение” Core Data Optional со Swift Optional
Теперь давайте обратим внимание ещё на одну вещь — на то, как используется единственный атрибут
timestamp
нашего Core Data
объекта Item
в нашем UI
.В
ContentView
атрибут timestamp
используется как “принудительно развернутое Optional
” с восклицательным !
знаком:.............
Это говорит о том, что атрибут
timestamp
является Optional
атрибутом, и так оно и есть. В Модели данных Core Data
есть нечто, что называется Optional
значением, но оно не имеет никакого отношения к Swift Optional
. Core Data
понимает это по-своему и так, что при записи в базу данных объекта Item
этот атрибут может вообще не иметь значения: Когда “за кулисами”
Xcode
генерирует для наших Core Data
объектов некоторый Swift
код в виде NSManagedObject
классов class
c @NSManaged
атрибутами …… то в нашем случае появляется переменная
@NSManaged var
timestamp: Date?
, которая являются Optional
, но это уже полноценный Swift Optional
:Вообще это самое противоречивое
Swift
предложение.Атрибут
@NSManaged
— это не “Обертка Свойства”, он появился задолго до SwiftUI
и означает, что Core Data
обращается с этим свойством так, как она считает нужным, достает его значение откуда-то на этапе runtime по требованию, она знать не знает, что такое Optional
в Swift
. Когда вы видите @NSManaged
в коде, это красный флаг, что правила Swift
здесь могут не применяться. Свойство @NSManaged
не существует, когда код компилируется. Вместо этого обещано, что Core Data
будет динамически добавлять это свойство при запуске приложения. В буквальном смысле Core Data
изменит определение класса в памяти, чтобы добавить свойства, соответствующие Модели данных, а вы обещаете, что свойства будет того же типа. Такого рода магия встроена в Objective-C
, a это, вероятно, означает, что Core Data
никогда не будет полностью Swiftified
без изменений кода.С другой стороны,
Swift
также знать не знает о Модели данных и о том, как там представлен атрибут timestamp
— Optional
или нет, он знает только, что это значение может быть равно nil
. Так что мы имеем разговор глухого со слепым.Поэтому
Xcode
всегда использует Swift Optional
значения в сгенерированном коде, так как это самый безопасный способ справиться с разницей Optional
в Swift
и Core Data
. Если свойство является Optional
для Swift
, то он может безопасно обрабатывать более слабые ограничения Core Data
. Здесь имеет место то, что называется “столкновением” Optionals
. Я не хочу вдаваться в подробные причины этого столкновения, об этом можно почитать в Столкновение Optionals и здесь. На самом деле проблема не в
Optional
атрибутах, a как раз в НЕ-Optional
атрибутах Core Data
объектов, когда вы точно знаете, что ваш атрибут НЕ является Optional
и должны как-то гарантировать, что при записи в Core Data
он всегда должен иметь значение.Типичная ошибка: разработчик работает над моделью
Core Data
. В редакторе моделей есть флажок «Optional», который может быть включен или выключен. Ах, думает разработчик, это поле никогда не должно быть nil
(неопределенным), поэтому я отключу установку «Optional».Но затем
Xcode
“за кулисами” генерирует для него некоторый код, и отмеченный вами как НЕ-Optional
атрибут выглядит в Swift
коде как Optional
, чтобы вы не поставили в Модели данных:@NSManaged var
timestamp: Date?
Модель данных и сгенерированный
Swift
код являются совершенно отдельными вещами, но они должны объявлять один и тот же тип данных. В противном случае ваше приложение вылетит с ошибкой, говорящей что-то вроде «Unacceptable type of value for attribute» («Недопустимый тип значения для атрибута»).Можно ли убрать
Swift Optional
?@NSManaged var
timestamp: Date
Можно. Но вы только что нарушили правила инициализации переменных в
Swift
! В частности, вы инициализировали объект, но не предоставили значения для всех его НЕ Optional
свойств. Вас никто не остановит, потому что @NSManaged
говорит, что правила Swift
на её территории не применяются. Таким образом, вы только что создали бомбу замедленного действия.Мы поступим по-другому. Если атрибут
Core Data
объекта в действительности НЕ является Optional
, то мы не будем трогать ни Модель данных, ни сгенерированный Xcode
код, пусть Core Data
работает в безопасном для себя режиме, a будем решать эту проблему с помощью “синтаксического сахара”, правда не того, который добавляется компилятором, а который мы добавим сами. Это было предложено Полом Хегарти в стэнфордском курсе CS193P 2020. Я собираюсь сделать переменную timestamp
вычисляемой переменной var
в расширении extension
класса class
Item
:Как я могу делать переменную
timestamp
вычисляемой переменной? Ведь у моего объекта
Item
в Модели данных уже есть эта переменная var
.Поэтому я переименую переменную
var
в Модели данных и добавляю в конец имени переменной символ “_” “подчеркивания”:И это то, что я люблю делать, когда объект в базе данных называется почти в точности так, как я хочу, но не совсем, но позже я буду использовать этот же прием и для других вещей.
В нашем случае это
timestamp_
с символом “_” “подчеркивания” в конце имени. Это свойство является Core Data
версией Optional
. И get { }
для моей вычисляемой переменной timestamp
будет возвращать timestamp_
, a если она не установлена, то текущую дату Date()
.Установку
set { }
вычисляемой переменной timestamp
также легко выполнить:Надеюсь вы поняли, что я здесь делаю. Я предоставляю
Core Data
те комфортные условия работы, к которым она привыкла, a сама формирую вычисляемую НЕ Optional
переменную var
timestamp
для работы c UI
.В результате теперь на нашем
UI
мы имеем дело с НЕ Optional
переменной code>var timestamp
и можем убрать восклицательный !
знак в Text (...)
в ContentView
:Выборка данных @FetchRequest
Но есть еще одно место, где использовался атрибут
Item.timestamp
— это в ContentView
в @FetchRequest
как дескриптор сортировки. И вот здесь нам придется поступить наоборот, исправить Item.timestamp
на Item.timestamp_
:В дескрипторах сортировки мы должны использовать настоящие атрибуты
Core Data
объектов, a не их вычисляемые аналоги. И получается, что Модель данных Core Data
в чистом виде присутствует в нашем SwiftUI View
.Лучше использовать при инициализации
@FetchRequest
более общее понятие — запрос NSFetchRequest
…… в котором можно указать и различные дескрипторы сортировки, и какой угодно сложный предикат. В нашем случае удобно разместить запрос в расширении
extension
класса Item
для Core Data
объекта:Правило такое: все действия, сортировки, предикаты, с реальными атрибутами объекта
Core Data
, которые имеют символ _ (подчеркивания), лучше держать в расширении extension
класса class
, сгенерированного Xcode
“за кулисами”:A в
SwiftUI Views
лучше использовать НЕ Optional
аналоги этих переменных без символа _ (подчеркивания).Операция обновления в UpdateView и сохранение изменений
Итак, в нашем приложении мы можем создавать (Create), читать (Read) и удалять (Delete) из хранилища объекты
Core Data
. Для создания полноценного CRUD приложения нам не хватает операции обновления (Update).Давайте создадим
UpdateView
, который позволит редактировать нашу временную отметку item
.Вместе с передаваемой в
UpdateView
временной отметкой item
, которая является объектом Core Data
, мы передаем контекст item.managedObjectContext
, который позволит нам сохранить изменения этой отметки в нашей базе данных. В то же время временная отметка item
является @ObservedObject
переменной, и, следовательно, любые изменения её свойств будут обновлять наш UI
, следовательно, мы можем рассматривать временную отметку item
как своего рода миниатюрную ViewModel
для нашего UpdateView
:Мы будем редактировать
$item.timestamp
в DatePicker
и при каждом её изменении будем сохранять измененную отметку времени item
в модификаторе onChange
:Здесь мы сохраняем наши изменения
Core Data
объекта item
также, ……как это сделала Apple в
ContentView
в функциях addItem
…… и
deleteItems
:Понятно, что мы должны где-то разместить этот повторяющийся код. Многие даже опытные разработчики размещают его почему-то в
PersistenceСontroller
, но мы ведь хотим уметь сохранять контекст, независимо от того, откуда он взялся, так что мы размещаем этот метод saveContext ()
в расширении extension
класса ManagedObjectContext
а в отдельном файле с именем CoreDataExtensions.swift:Это упрощает наш код как в методе
UpdateView ()
:… так и в методах
addItem
…… и
deleteItems
:Если вы не хотите обрабатывать ошибки при сохранении контекста
ManagedObjectContext
, то для UpdateView
можно воспользоваться одной строкой кода без всяких расширений:Для
addItem
и deleteItems
соответственно такой строкой кода:“Фантомные” Core Data данные в предварительном просмотре Preview
Что еще примечательного в
UpdateView
?Конечно, его предварительный просмотр
Preview
, в котором в качестве тестовых данных используется “фантомная” база данных, инициированная как PersistenceController(inMemory: true)
и заполненная единственной временной отметкой newItem
с текущим временем:Теперь в
Preview
мы можем полноценно тестировать наш UpdateView
: выбирать дату, время и запоминать в item
:То же самое можно сделать с
Preview
для ContentView
. Не будем брать уже готовые данные из static
свойства var
preview
в PersistenceController
, a опять воспользуемся новой “фантомной” базой данных и наполним её нужными нам данными:В этом случае мы можем работать с
Preview
напрямую:На самом деле не имеет смысл оставлять в
PersistenceController
“фантомную” базу данных static var
preview
, так как в каждом отдельном случае мы будем создавать её заново исключительно под нужды того или иного View
:Наконец-то наша структура
PersistenceController
приобретает компактный вид без единой лишней строчки кода. В PersistenceController
мы будем оперировать только тремя понятиями: реальной базой данных shared
, контекстом viewContext
для неё и возможностью инициализировать ”фантомную” ( в памяти) базу данных с Моделью данных “CoreDataTest“.Это немного улучшает читабельность
CoreDataTestApp
:“Намерения” Intents в расширениях extension объектов Core Data и другое
Вернемся в
ContentView
и заменим Text (...)
в .navigationDestination
на UpdateView
:Попутно мы использовали новый в iOS 15 метод
formatted
для форматирования даты Date
и это позволило нам избавиться от itemFormatter
, предоставленного в шаблоне Apple:Давайте добавим в расширении
extension
класса class
Item
, который является миниатюрной ViewModel
для нашего UI
, “Намерение” (Intent), связанное с с удалением ряда объектов Item
:Это упростит наш код в
ContentView
, и в результате мы получили полноценное CRUD приложение:… вместе с компактной структурой
PersistenceController
…… и с расширением extension для
Core Data
объектов в качестве миниатюрной ViewModel
:Добавим последний штрих в приложение CoreDataTest и заставим его принудительно запоминать данные в
Core Data
в случае выхода из приложения или в случае перехода его, например, в фоновый режим:В целом мне нравится то, что сделало Apple с установкой
Core Data
в приложении SwiftUI
, за исключением того, что следует создавать “фантомную” базу данных для предварительного просмотра Preview
каждого отдельного SwiftUI View
и использовать классы class
Core Data
объектов как ViewModel
, в которых помимо прочего можно «спрятать» с помощью “синтаксического сахара” все особенности работы Core Data
в Swift
коде.Заблуждения
Вполне возможно, что внедряя контекст
ManagedObjectContext
в наш UI, Apple нарушает архитектуру MVVM
, провозглашенную для SwiftUI
, позволяя SwiftUI Views
напрямую получать информацию о Модели, позволяя им знать, откуда берутся данные. И нашлось немало желающих восстановить справедливость архитектуры
MVVM
и создать некую глобальную ViewModel
, чтобы только она имела бы дело с Core Data
, поставляя уже готовые данные в виде нужных массивов в Views
. В youtube.com можно найти много видео с броским заголовком CoreData + SwiftUI + MVVM. Но если вам будут рассказывать о глобальной ViewModel
, способной отделить Core Data
от Views
, то — не верьте, и я покажу, вам почему.Эту идею пытались реализовать несколькими способами. Сразу скажу — все они плохие или очень плохие.
Сначала предлагалась обычная
ViewModel
c @Published
свойством, которая представляет собой массив объектов Core Data
, в нашем случае [FruitEntity]
:С помощью функции
func
getAllFruits()
мы выбираем все фрукты [FruitEntity]
из Core Data
и размещаем в @Published
var
fruits
, a та в свою очередь уведомляет об этом наш ContentView
и он обновляет список фруктов vm.fruits
на нашем UI
:................... .
То есть реактивность от
ViewModel
к View
присутствует, a вот в обратном направлении — нет. Такая ViewModel
не отрабатывает автоматически наши “Намерения” (Intents). Если мы добавляем фрукты или удаляем их из Core Data
, то наш @Published
массив var
fruits
не меняется автоматически, нам приходится вручную заново выбирать из Core Data
фрукты с помощью функции func
getAllFruits()
и вручную обновлять:Получается парадоксальная картина. До тех пор, пока мы не выполним какую-то CRUD операцию с нашими данными, наш список фруктов не обновится. Это выглядит странно в многозадачном режиме на iPad, когда на экране у нас будут присутствовать два приложения, работающие с одной и той же
Core Data
, но которые не будут чувствовать изменений Core Data
, производимые в каждом из этих приложений (они не видят изменений друг друга до тех пор, пока не выполнят хоть какую-нибудь операцию):Совсем иная картина с нашим шаблонным приложением CoreDataTest, основанным на предложенной Apple технологии с “Оберткой Свойства”
@FetchRequest
. В этом случае оба приложения отображают Core Data
данные на экране абсолютно синхронно благодаря тому, что @FetchRequest
автоматически отслеживает все изменения происходящие в Core Data
, даже если они произведены другим приложением:Конечно, можно было бы попробовать дальше работать над глобальной
ViewModel
, которая является ObservableObject
, и сделать её “Оберткой Свойства” (Property Wrapper) вокруг NSFetchedResultsController
, объекта, который мы использовали в прошлом для выборки данных из Core Data
и для отслеживания изменений в Core Data
, и заставить её выполнять повторную выборку и обновлять UI
:Это идеально подходило в
UIKit
для динамического отображения данных в таблицах TableView
и коллекциях CollectionView
, и NSFetchedResultsController
уведомляет с помощью Notifications
об изменении Core Data
через своего делегата NSFetchedResultsControllerDelegate
. Эта пара — NSFetchedResultsController
и NSFetchedResultsControllerDelegate
— в основном, делали бы то, что @FetchRequest
уже делает для вас, так что я вообще не уверена, что это лучшая идея, которую вы могли бы предложить, и это выглядело бы так:Одно из сомнений состоит в том, что из-за
@Published
массива var
items
я читаю все объекты из моего NSFetchedResultsController
за один раз, a это означает, что Core Data
загружает все мои объекты “в память”, хотя в нормальном состоянии в UIKit
она этого не делала бы, пытаясь отложить фактическую загрузку объекта в память как можно дольше.Поэтому, делая так, мы выбрасываете в “мусорное ведро ” большую часть оптимизации
NSFetchedResultsController
.Это может быть нормально для очень маленьких наборов данных, но для больших наборов данных это может быть проблемой, и, что более важно, такой код не поддерживает секции Sections, что также может быть проблемой.
Так что эта абстракция
Core Data
тоже не из лучших.Если вам абсолютно нужна
Core Data
, то лучший способ абстракция Core Data
, который я видела до сих пор, предложен в посте Дэйва Делонге (Dave Delong) “Core Data and SwiftUI”, он работает в Apple и знает массу разных вещей о Core Data
, у него есть несколько очень крутых статей на тему работы Core Data
в SwiftUI
, так что это действительно очень круто.Вывод. Даже если вы не являетесь фанатом
@FetchRequest
, я думаю, что @FetchRequest
может прекрасно на вас поработать, поэтому следует использовать его для выборки данных из Core Data
и просто позволить ему делать то, что он умеет делать лучше нас, a не придумывать заменяющие ему абстракции, если вы не хотите иметь приключений и трений в вашем приложении. Используйте то, как задумано Apple, и просто внедрите контекст ManagedObjectContext
в ваше View
, это сделает вашу жизнь проще. Великолепно тема Core Data + SwiftUI изложена Полом Хегарти в стэнфордских курсах CS193P 2020 на Лекциях 11 и 12, если вам нужен русскоязычный конспект этих Лекций, то он также в открытом доступе. Там очень много чего рассказывается о
Core Data
в SwiftUI
и показываются различные динамические варианты выборки данных, в том числе и на карте Map
. Достаточно хороший материал по тема Core Data + SwiftUI в 100 days SwiftUI у Пола Хадсона.
Неплохой курс SwiftUI & Core Data у Karin Pretter.
Абсолютно НЕ рекомендую курс Core Data in iOS на Udemy.
На примере шаблонного демонстрационного примера, предложенного Apple, я показала, что давая объектам
Core Data
дополнительную функциональность с помощью „синтаксического сахара“ в расширении extension
их классов class
, сгенерированных Xcode
, можно добиться комфортной работы с Core Data
в SwiftUI
. Код находится на Github.
Однако до сих пор мы рассматривали тривиальный шаблонный демонстрационный пример, в котором всего лишь одна сущность с одним атрибутом.
В следующем разделе я хочу показать вам такую же комфортную работу
Core Data
и SwiftUI
с реальными объектами — полетами Flights
, аэропортами Airport
и авиакомпаниями AirLine
, которые мы получим на бесплатном сервисе FlightAware и разместим в Core Data
. Это сильно упрощенная модификация реального приложения Enroute из стэнфордских курсов CS193P 2020, которое оперативно подкачивает данные с сервера FlightAware . Такое приложение необходимо для того, чтобы показать работу с взаимосвязями объектов типа one-to-many и динамической настройкой @FetchRequest
в SwiftUI
.