Перейти к содержимому

Core data что это

  • автор:

Core Data + Swift для самых маленьких: необходимый минимум (часть 1)

Про Core Data и Swift написано не так много, как хотелось бы, особенно это касается русскоязычного сегмента Интернета. При этом большинство статей и примеров используют довольно примитивные модели данных, чтобы показать только саму суть Core Data, не вдаваясь в подробности. Данной статьей я хотел бы восполнить этот пробел, показав немного больше о Core Data на практическом примере. Изначально, я планировал уместить весь материал в одну статью, но в процессе написания стало ясно, что для одной публикации объем явно великоват, а так как из песни слов не выкинешь, то я все-таки разобью данный материал на три части.

Вместо Введения

Core Data — это мощный и гибкий фреймворк для хранения и управления графом вашей модели, который заслуженно занимает свое место в арсенале любого iOS-разработчика. Наверняка вы, как минимум, слышали об этом фреймворке, и не один раз, и если по каким-то причинам вы его еще не используете, — то самое время начать это делать.

Так как голая теория, как правило, довольно скучна и плохо усваивается, рассматривать работу c Core Data мы будем на практическом примере, создавая приложение. Такие распространенные примеры работы с Core Data, как «Список дел» и им подобные, на мой взгляд, не слишком подходят, так как используют всего одну сущность и не используют взаимосвязи, что является существенным упрощением работы с данным фреймворком. В данной статье мы разработаем приложение, где будет использоваться несколько сущностей и взаимосвязей между ними.

Предполагается, что читатель знаком с основами разработки под iOS: знает Storyboard и понимает MVC, умеет использовать базовые элементы управления. Я сам переключился на iOS недавно, поэтому, возможно, в статье есть ошибки, неточности или игнорирование best practices, просьба за это сильно не пинать, лучше аргументированно ткнуть носом, чем поможете мне и другим начинающим iOS-разработчикам. Я буду использовать Xcode 7.3.1 и iOS 9.3.2, но все должно работать и в других версиях.

Общие сведения о Core Data

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

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

  • managed object model (управляемая объектная модель) — фактически это ваша модель (в парадигме MVC), которая содержит все сущности, их атрибуты и взаимосвязи;
  • managed object contexts (контекст управляемого объекта) — используется для управления коллекциями объектов модели (в общем случае, может быть несколько контекстов);
  • persistent store coordinator (координатор постоянного хранилища) — посредник между хранилищем данных и контекстом, в которых эти данные используются, отвечает за хранение данных и их кэширование

Давайте, продолжим рассмотрение Core Data на примере.
Создайте новый проект на основе шаблона Single View Application и на странице выбора опций нового проекта поставьте флажок «Use Core Data».

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

По умолчанию, Xcode добавляет код для работы с Core Data в класс делегата приложения ( AppDelegate.swift ). Давайте рассмотрим его более детально, он начинается с комментария:

 // MARK: - Core Data stack 

Здесь четыре переменные, все они инициализируются с помощью замыкания. Однако, первая из них, applicationDocumentsDirectory — просто вспомогательный метод, который возвращает директорию для хранения данных. По умолчанию, это Document Directory , можно изменить, но маловероятно, что вам это действительно надо. Реализация проста и не должна вызывать затруднений для понимания.

 lazy var applicationDocumentsDirectory: NSURL = < let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) return urls[urls.count-1] >() 

Следующее определение — managedObjectModel — более интересно, так как имеет самое непосредственное отношение к Core Data:

 lazy var managedObjectModel: NSManagedObjectModel = < let modelURL = NSBundle.mainBundle().URLForResource("core_data_habrahabr_swift", withExtension: "momd")! return NSManagedObjectModel(contentsOfURL: modelURL)! >() 

Логика программного кода незамысловата — получаем из сборки приложения некий файл с расширением momd и создаем на основании его объектную модель данных. Осталось выяснить, что это за файл такой. Посмотрите на файлы в Навигаторе проекта (Project navigator), там вы найдете файл с расширением xdatamodel — это наша модель данных Core Data (как с ней работать мы рассмотрим чуть позже), которая при компиляции проекта включается в файл-сборку приложения с расширением momd .

Идем дальше, — persistentStoreCoordinator — наиболее объемное определение, но, несмотря на несколько устрашающий вид, не стоит его пугаться — большую часть кода занимает обработка исключений:

 lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = < let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite") var failureReason = "There was an error creating or loading the application's saved data." do < try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil) >catch < var dict = [String: AnyObject]() dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" dict[NSLocalizedFailureReasonErrorKey] = failureReason dict[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() >return coordinator >() 

Здесь на основе объектной управляемой модели создается координатор постоянного хранилища. Затем мы определяем, где именно должны храниться данные. И в заключении подключаем собственно само хранилище ( coordinator.addPersistentStoreWithType ), передав соответствующему методу в качестве параметров тип хранилища и его расположение. По умолчанию используется SQLite. В двух других параметрах могут передаваться дополнительные параметры и опции, но на данном этапе нам это не надо, поэтому просто передадим nil .

Последнее определение — managedObjectContext — уверен, проблем с ним не будет:

 lazy var managedObjectContext: NSManagedObjectContext = < let coordinator = self.persistentStoreCoordinator var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = coordinator return managedObjectContext >() 

Здесь мы создаем новый контекст управляемого объекта и присваиваем ему ссылку на наш координатор постоянного хранилища, с помощью которого он и будет читать и писать необходимые нам данные. Деталь, заслуживающая внимания — аргумент конструктора NSManagedObjectContext . В общем случае, может быть несколько рабочих контекстов выполняемых в разных потоках (например, один для интерактивной работы, другой — для фоновой подгрузки данных). Передавая в качестве аргумента MainQueueConcurrencyType , мы указываем, что данный контекст должен быть создан в основном потоке.

Также у нас здесь есть одна вспомогательный функция для удобства сохранения контекста. Смысл ее очевиден — запись данных происходит только в том случаем, если они действительно были изменены.

 func saveContext () < if managedObjectContext.hasChanges < do < try managedObjectContext.save() >catch < let nserror = error as NSError NSLog("Unresolved error \(nserror), \(nserror.userInfo)") abort() >> > 

Здесь важно отметить: вся работа с данными (создание, модификация, удаление) всегда происходит в рамках какого-либо контекста. Фактическая запись в хранилище будет выполнена только при явном вызове функции сохранения контекста.

Создание модели данных

Для создания Модели данных используется встроенный редактор. Так как мы поставили флажок «Use Core Data» при создании нового проекта, то у нас уже есть пустая модель данных, автоматически создания Xcode. Давайте ее откроем и создадим модель данных для нашего приложения.

Мы будем создавать приложение для учета заказов от контрагентов на выполнение определенных услуг. Это приложение не будет очень сложным, но в нем будет несколько различных сущностей, тесно связанных между собой. Это позволит показать различные аспекты и приемы работы с Core Data. Итак, у нас будет два справочника: «Заказчики» и «Услуги», и один документ «Заказ», в котором может быть несколько услуг.

Лирическое отступление

Термины «Справочник» и «Документ» я взял из терминологии «1С: Предприятие», потому что именно эту систему мне очень сильно напоминает Core Data. Схожая логика построения сущностей (справочников/документов), аналогичные параметры атрибутов, инкапсулирование операций чтения/записи данных, кэширование и много другое. Я бы сказал, что «1С: Предприятие» — это следующий уровень абстракции работы с данными по отношению к Core Data.

Ладно, давайте напишем свое «1С: Предприятие» с блэкджеком и с нормальным дизайном!

Создание справочников

Давайте начнем с Заказчиков. В редакторе модели данные добавьте новую сущность (кнопка с подписью «Add Entity» внизу) и назовите ее «Customer» . Эта сущность будет олицетворять Заказчика (одного). У сущности могут быть атрибуты, взаимосвязи и получаемые свойства (fetch-свойства). Немного упростив, можно сказать, что разница между атрибутами и взаимосвязями в типе возможных значений: атрибуты поддерживают только простые типы данных (строка, число, дата и пр.), взаимосвязи — это ссылка на другую сущность (более подробно про взаимосвязи мы поговорим через несколько минут). Fetch-свойства — это аналог вычисляемых свойств, то есть значение вычисляется динамически (и кэшируется) на основании предопределенного запроса.

Можно провести следующую аналогию с СУБД:

  • модель данных — схемы базы данных
  • сущность — таблица базы данных
  • атрибуты и взаимосвязи — поля таблицы

Следующая важная часть — Инспектор модели данных, вы видите его справа от редактора модели данных. С его помощью можно задавать различные атрибуты и параметры для сущностей, атрибутов сущностей (уж простите за тавтологию), взаимосвязей и других объектов. Например, сущность можно отметить как абстрактную, либо задать для нее родительскую сущность (принципы такие же, как и в целом в ООП).

Для атрибута сущности список доступных параметров меняется в зависимости от типа атрибута. Например, для числовых значений можно задать нижнюю и/или верхнюю границу, для даты можно задать допустимый диапазон. Также для большинства типов значений можно задать значение по умолчанию.

Важным свойством атрибута является Optional (опциональный). Смысл у него точно такой же, как в программном коде Swift: если атрибут помечен как Optional, то его значение может отсутствовать, и наоборот, — если такой пометки нет — запись сущности будет невозможна. По умолчанию, все атрибуты отмечаются как опциональные. В нашем случае, атрибут name не должен быть опциональным (надо снять флажок Optional), так как Заказчик без имени лишен какого-либо практического смысла.

На этом создание сущности Customer можно считать завершенным. Давайте создадим и настроим следующую сущность — Услуги. Создайте новую сущность — Services и добавьте два атрибута: name (наименование услуги) и info (дополнительная информация). Тип данных в обоих случаях — String , атрибут name — не должен быть опциональным. В общем, все то же самое, что с предыдущей сущностью, никаких проблем здесь возникнуть не должно.

Создание документа «Заказ»

Переходим к документу «Заказ» — здесь все немного сложнее. Так как в одном документе у нас может быть несколько различных услуг, а для каждой услуги будет своя сумма, то документ у нас будет представлен двумя сущностями:

  • «шапка» документа, где будет содержаться дата документа, заказчик и ссылка на табличную часть
  • строка табличной части документа, где будет содержать Услуга и ее стоимость, а также ссылка на «шапку» документа.

Начнем с «шапки» документа — создадим новую сущность «Order» и добавим три атрибута (здесь все уже знакомо по созданию предыдущих сущностей):

  • date — дата документа, тип Date , не опциональный
  • paid — признак оплаты, тип Boolean , не опциональный, значение по умолчанию — NO
  • made — признак выполнения заказа, тип Boolean , не опциональный, значение по умолчанию — NO

Обратите внимание, что взаимосвязи по умолчанию тоже являются Optional. Кроме того, в Инспекторе атрибутов присутствуют следующие очень важные свойства, которые мы сейчас подробно рассмотрим:

  • Type (тип связи)
  • Delete Rule (правило удаление)
  • Inverse (обратная связь)
Type (тип связи)

Если вы работали с какими-либо базами данных, то это понятие вам наверняка знакомо. Здесь нам предлагается на выбор два варианта: To One и To Many. To One — означает, что наш Заказ связан к одним конкретным Заказчиком, To Many — с несколькими заказчиками. В нашем случае надо оставить значение по умолчанию — To One.

Delete Rule (правило удаления)

Очень важное свойство, здесь надо выбрать одно из возможных поведений сущности в том момент, когда данная связь по каким-то причинам удаляется. Возможны следующие варианты:

  • No Action (Не выполнять никаких действий) — Core Data не будет выполнять какие-либо действия, в том числе уведомлять о таком удалении; сущность «будет думать», что удаления не было. В этом случае, вы должны самостоятельное реализовать необходимое поведение приложения. Маловероятно, что вы захотите это использовать.
  • Nullify (Аннулирование) — при удалении связи, ее значение будет установлено в nil . Наиболее распространенный вариант, используется по умолчанию.
  • Cascade (Каскадное удаление) — при удалении связи, автоматически будут удалены все заказчики, ссылающиеся на нее (явно не наш случай)
  • Deny (Отказ) — правило, противоположное предыдущему, его суть в том, что нельзя удалить объект, пока на него есть хотя бы одна ссылка. Такой подход, например, применяется в отношении всех объектов в «1С: Предприятие».
Inverse (обратная связь)

Мы добавили связь «Заказа» с «Заказчиком», но «Заказчик» ничего не знает о «Заказах», в которых он участвует. Об этом же нас предупреждает и Warning.

Для того чтобы это исправить, надо создать реверсивную связь у сущности «Заказчик» и указать ее в качестве обратной. Надо заметить, что официальная документация по Core Data настоятельно рекомендует делать всегда реверсивные связи — так мы и поступим. Строго говоря, вы можете этого не делать (все-таки это Warning, а не Error), но вы должны четко понимать, почему и зачем вы так поступаете.

Давайте это исправим, создайте для сущности Customer новую взаимосвязь с именем orders , выберете Destination = Order и в качестве обратной связи укажите, созданную нами ранее связь customer . Еще один момент — так как у одного Заказчика может быть, в общем случае, много документов — изменим тип связи на To Many .

Если вы вернетесь в сущность «Order» , то увидите, что обратная связь уже установлена автоматически в значение orders .

Давайте теперь сделаем табличную часть нашего документа. Добавьте новую сущность с именем «RowOfOrder» . У нас будет один атрибут — «sum» («Сумма за услугу») с типом Float (это вы уже умеете делать, не буду расписывать подробно) и две взаимосвязи («Услуга» и «Заказ»). Давайте начнем с Заказа — добавьте новую взаимосвязь с именем order и назначением ( Destination ) равным Order . Так как строка документа может принадлежать только одному документу, то тип связи ( Type ) должен быть To One . Ну а если мы решим удалить документ, то логично, что его строки тоже должны быть удалены, потому Delete Rule у нас будет Cascade .

Теперь возвращаемся в сущность Order, чтобы создать обратную связь. Добавьте новую связь с именем rowsOfOrder (Destination = RowOfOrder, Inverse = order) . Не забудьте изменить тип связи на To Many (так как в одном документе может быть несколько строк).

Осталось в сущность RowOfOrder добавить только связь с сущностью Услуга. С учетом всего вышесказанного этого не должно быть сложным, все по тому же сценарию. Добавляем для сущности «RowOfOrder» новую взаимосвязь с именем service (Destination = Service) , остальное оставляем по умолчанию. Затем для сущности Service добавляем новую взаимосвязь «rowsOfOrders» (Destination = rowOfOrder, Inverse = service) и устанавливаем тип связи равным To Many .

Важное замечание! После создания модели данных ее нельзя менять — при первом запуске приложения Core Data в соответствии с моделью данных создает хранилище, а при последующих — проверяет структуру хранилища на соответствие. Если по каким-либо причинам структура хранилища не соответствует модели данных, то происходит критическая ошибка времени выполнения (то есть приложение у вас будет неработоспособно). Как же быть в случае, если модель данных требуется изменить — для этого необходимо использовать механизм миграции Core Data, это отдельная тема повышенной сложности, и мы не будем ее рассматривать в рамках данной статьи. Есть и другой вариант — можно просто удалить приложение с устройства (или эмулятора), а при старте приложения Core Data просто создаст новое хранилище с новой структурой. Очевидно, что данный способ уместен только на этапе разработки приложения.

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

Вы видите созданные нами сущности с атрибутами и все их взаимосвязи в виде графической структуры. Линия с обычной стрелкой на конце означает связь To One, с двойной стрелкой — To Many. Графический вид хорошо помогает сориентироваться в объемных моделях.

На этом первая часть закончена, в следующей статье будет много кода, мы будем создавать сами объекты, связывать их между собой, познакомимся с NSEntityDescription и NSManagedObject , а также напишем вспомогательный класс, существенно повышающий удобство работы с Core Data.

Core Data в деталях

Недавно я начала работать над большим проектом с использованием Core Data. Обычное дело, что люди на проектах меняются, опыт теряется, а нюансы забываются. Углубить всех в изучение конкретного фреймворка невозможно — у всех полно своих рабочих проблем. Поэтому я подготовила небольшую презентацию, из тех пунктов, которые считаю важными или недостаточно освещенными в туториалах. Делюсь со всеми и надеюсь, что это поможет писать эффективный код и не делать ошибок. Предполагается, что вы уже немного в теме.

Начну с банального.

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

Объекты Core Data

image

Для создания хранилища в приложении используются классы NSPersistentStoreCoordinator или NSPersistentContainer. NSPersistentStoreCoordinator создает хранилище указанного типа на основе модели, можно указать размещение и дополнительные опции. NSPersistentContainer можно использовать с IOS10, дает возможность создания с минимальным количеством кода.

Работает это следующим образом: если по указанному пути существует база данных, то координатор проверяет ее версию и, при необходимости, делает миграцию. Если база не существует, то она создается на основании модели NSManagedObjectModel. Чтобы все это работало правильно, перед внесением изменений в модель создавайте новую версию в Xcode через меню Editor -> Add Model Version. Если вывести путь, то можно найти и открыть базу в эмуляторе.

Пример с NSPersistentStoreCoordinator

var persistentCoordinator: NSPersistentStoreCoordinator = < let modelURL = Bundle.main.url(forResource: "Test", withExtension: "momd") let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL!) let persistentCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!) let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let storeURL = URL(fileURLWithPath: documentsPath.appending("/Test.sqlite")) print("storeUrl = \(storeURL)") do < try persistentCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: [NSSQLitePragmasOption: ["journal_mode":"MEMORY"]]) return persistentCoordinator >catch < abort() >> () 

Пример с NSPersistentContainer

var persistentContainer: NSPersistentContainer = < let container = NSPersistentContainer(name: "CoreDataTest") container.loadPersistentStores(completionHandler: < (storeDescription, error) in print("storeDescription = \(storeDescription)") if let error = error as NSError? < fatalError("Unresolved error \(error), \(error.userInfo)") >>) return container > () 

Core Data использует 4 типа хранилища:

— SQLite
— Binary
— In-Memory
— XML (только для Mac OS)

Если, например, по соображениям безопасности Вы не хотите хранить данные в файловом виде, но при этом хотите использовать кеширование в течении сессии и данные в виде объектов, вполне подойдет хранилище типа «In-Memory». Собственно, не запрещается иметь в одном приложении несколько хранилищ разного типа.

Несколько слов хочется сказать об объекте NSManagedObjectContext. Вообще, Apple дает весьма туманную формулировку для NSManagedObjectContext — среда для работы с объектами Core Data. Все это от желания отмежеваться от ассоциаций с реляционными базами, и представить Core Data, как простое в использовании средство, не требующее понимания ключей, транзакций и прочей базданской атрибутики. Но на языке реляционных баз NSManagedObjectContext можно, в некотором смысле, назвать менеджером транзакций. Вы, наверное, заметили, что он имеет методы save и rollback, хотя скорее всего вы пользуетесь только первым.

Недопонимание этого простого факта, приводит к использованию одноконтекстной модели, даже в тех ситуациях, где ее недостаточно. К примеру, вы редактируете большой документ, и при этом вам потребовалось загрузить пару справочников. В какой момент вызывать save? Если бы мы работали с реляционной базой, то тут бы не возникло вопросов, поскольку каждая операция выполнялась бы в своей транзакции. В Core Data тоже есть вполне удобный способ для решения этой проблемы — это ответвление дочернего контекста. Но к сожалению, это почему-то используется редко. Вот тут есть неплохая статья на эту тему.

Наследование

По непонятной мне причине, существует очень большое количество мануалов и примеров, где никак не используется наследование для Entity/NSManagedObject(таблиц). Между тем, это очень удобный инструмент. Если вы не используете наследование, то присваивать значения атрибутам (полям) можно только через KVС механизм, который, не выполняет проверку имен и типов атрибутов, а это легко может привести к ошибкам времени выполнения.

Переопределение классов для NSManagedObject делается в дизайнере Core Data:

image

Наследование и кодогенерация

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

image

image

Если вы хотите посмотреть автогенерируемый код, но при этом, не хотите добавлять файлы в проект, можно воспользоваться другим способом: установить у Entity опцию «Codegen». В этом случае код нужно поискать в ../DerivedData/…

image

Используйте кодогенерацию для создания классов, опечатки в названии переменных могут привести к ошибкам времени выполнения.

Вот примерно такой код будет создан:

@objc public class Company: NSManagedObject

В swift @NSManaged имеет тот же смысл, что и dynamic в Objective C.
Core Data сама заботится о получении данных (имеет внутренние аксессоры) для атрибутов своих классов. Если у вас есть транзитные поля, то нужно добавлять функции для их расчета.

Классы, наследуемые, от NSManagedObject (таблицы), не имели до IOS10 «обычного» конструктора, в отличии от остальных классов. Чтобы создать объект типа Company, нужно было написать достаточно неповоротливую конструкцию с использованием NSEntityDescription. Сейчас появился более удобный метод инициализации через контекст (NSManagedObjectContext). Код ниже. Обратите внимание на преимущество наследования при присвоении атрибутов перед механизм KVC:

// 1 - создание записи через NSEntityDescription, присвоение значений через KVO let company1 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) company1.setValue("077456789111", forKey: "inn") company1.setValue("Натура кура", forKey: "name") // 2 - создание записи через NSEntityDescription, присвоение значений через свойства let company2 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) as! Company company2.inn = "077456789222" company2.name = "Крошка макарошка" // 3 - создание записи через инициализатор (IOS10+), присвоение значений через свойства let company3 = Company(context: moc) company3.inn = "077456789222" company3.name = "Крошка макарошка" 

Пространство имен для NSManagedObject

Еще одна вещь которая стоит отдельного упоминания — это пространство имен.

image

У вас не возникнет затруднений, если вы работаете на ObjectiveC или Swift. Обычно, это поле заполняется правильно по умолчанию. А вот в смешанных проектах, возможно, для вас станет сюрпризом, что для классов в swift и ObjectiveC нужно проставить разные опции. В Swift «Модуль» должен быть заполнен. Если это поле не будет заполнено, то к имени класса добавится префикс с названием проекта, что вызовет ошибку выполнения. В Objetive C «Модуль» оставляйте пустым, иначе NSManagedObject не будет найден при обращении к нему через имя класса.

Связи между объектами

В принципе тема связей неплохо освещена, но я хочу сделать акцент на способах добавления дочерних сущностей в родительскую. Поэтому сначала быстренько напомню механизм создания связей. Рассмотрим традиционный пример, компания — сотрудники, связь один ко многим:

image

  • Создаем связь на каждой стороне (таблице)
  • После этого становится доступным поле Inverse, его нужно заполнить в каждой таблице.

image

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

Также важно правильно указывать правило удаления. Правило удаления – это действие, которое будет выполнятся с данным объектом при удалении родительского объекта.

  • Cascade — удаление всех дочерних объектов, при удалении родительского.
  • Deny — запрет удаления родителя, если есть дочерний объект
  • Nullify — обнуление ссылки на родителя
  • No action — действие не указано, выдаст предупреждении при компиляции

Способы добавления дочерних сущностей в родительскую

1) Первый способ — это добавление через NSSet. Для примера добавим 2 сотрудника в компанию:

let set = NSMutableSet(); if let employee1 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee < employee1.firstName = "Дима" employee1.secondName = "Васильев" set.add(employee1) >if let emploee2 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee < employee2.firstName = "Наташа" employee2.secondName = "Ростова" set.add(employee2) >company.employee = set 

Этот способ удобен при первичной инициализации объекта или при заполнении базы. Тут есть небольшой нюанс. Если в компании уже были сотрудники, а вы присвоили новый set, то у бывших сотрудников обнулится ссылка на компанию, но они не будут удалены. Как вариант можно получить список сотрудников и работать уже с этим set’ом.

let set = company.mutableSetValue(forKey: "employee") 

2) Добавление дочерних объектов через идентификатор родителя

if let employee = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee

Второй способ удобен при добавлении или редактировании дочернего объекта в
отдельной форме.

3) Добавление дочерних объектов через автогенерируемые методы

extension Company

Для полноты картины полезно знать про этот метод, но мне он как-то не пригодился, а лишний код я удаляю, чтобы не загромождать проект.

Запросы с условием по дочернему элементу

В Core Data вы не можете составить произвольный запрос между любыми данными, как мы это можем делать в SQL. Но между зависимыми объектами это следить несложно используя стандартный предикат. Ниже пример запроса, выбирающего все компании в который есть сотрудник с указанным именем:

public static func getCompanyWithEmployee(name: String) -> [Company] < let request = NSFetchRequest(entityName: self.className()) request.predicate = NSPredicate(format: "ANY employee.firstName = %@", name) do < if let result = try moc.fetch(request) as? [Company] < return result >> catch < >return [Company]() > 

Вызов метода в коде будет выглядеть так:

// выбрать комании, где работает Миша let companies = Company.getCompanyWithEmployee(name: "Миша") 

Не используйте в запросах транзитные поля, их значения не определены в момент выполнения запроса. Ошибки не произойдет, но результат будет неправильный.

Настройка атрибутов (полей)

Вы, наверное заметили, что атрибуты Entity имеют несколько опций.
C опциональностью все понятно из названия.

Oпция «использовать скалярный тип» появилась в swif. В Objective C не используются скалярные типы для атрибутов, так как они не могут принимать значение nil. Попытка присвоить скалярное значение атрибуту через KVC вызовет ошибку выполнения. Отсюда становится понятным, почему типы атрибутов в Core Data не имеют строгого соответствия с типами языка. В swift, и в смешанных проектах атрибуты скалярного типа можно использовать.

Транзитные атрибуты – это расчетные поля, которые не сохраняются в базе. Их можно использовать для шифрования. Эти атрибуты получают значения через переопределенный аксессор, либо через присваивание примитивов по мере надобности (например, в переопределенных willSave и awakeFromFetch).

Аксессоры атрибутов:

Если вам не нужно использовать расчетные поля, например, делать шифрование или что-то другое, то можно вообще не задумываться о том, чем являются аксессуары атрибутов. Между тем операции получения и присвоения значений атрибутам не являются «атомарными». Чтобы понять, что я имею ввиду смотрите код ниже:

// чтение let name = company.name // чтение company.willAccessValue(forKey: "name") let name = company.primitiveValue(forKey: "name") company.didAccessValue(forKey: "name") // присвоение company.name = "Азбука укуса" // присвоение company.willChangeValue(forKey: "name") company.setPrimitiveValue("Азбука укуса", forKey: "name") company.didChangeValue(forKey: "name") 

Используйте примитивы в event’ах NSManagedObject вместо обычного присваивания, чтобы избежать зацикливания. Пример:

override func willSave() < let nameP = encrypt(field: primitiveValue(forKey: "name"), password: password) setPrimitiveValue(nameP, forKey: "nameC") super.willSave() >override func awakeFromFetch()

Если вдруг когда-то вам придется прикрутить в проект функцию awakeFromFetch, то вы удивитесь, что работает она весьма странно, а по факту вызывается она совсем не тогда, когда вы выполните запрос. Связано это с тем, что Core Data имеет весьма интеллектуальный механизм кеширования, и если выборка уже находится в памяти (например, по причине того, что вы только что заполнили эту таблицу), то метод вызываться не будет. Тем не менее, мои эксперименты показали, что в плане вычисляемых значений, можно смело положиться на использование awakeFromFetch, об этом же говорит документация Apple. Если же для тестирования и разработки вам нужно принудительно вызвать awakeFromFetch, добавьте перед запросом managedObjectContext.refreshAllObjects().

Спасибо всем, кто дочитал до конца.

  • Разработка под iOS
  • Swift

Core Data

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

Чтобы разобраться в принципах работы с Core Data, я хочу написать небольшое туду приложение. Звучит банально, но в этом приложении список дел можно будет сохранять как изображение и делать его заставкой на экране.

Новый проект

Начну с создания нового проекта. Желательно, отметить галочку как указано на картинке. В этом случае в файлах AppDelegate.swift и SceneDelegate.swift` сгенерируется дополнительный код и добавится специальный файл Memo.xcdatamodeld. Memo – это название моего проекта.

В файле SceneDelegate.swift в функции scene появилась новая строчка

// Get the managed object context from the shared persistent container. let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext 

Эта функция возвращает контекст управляемого объекта(managed object context). Контекст управляемого объекта похож на блокнот, который хранится в оперативной памяти. Это место, где объекты создаются, выбираются, обновляются, удаляются и сохраняются обратно в постоянное хранилище на устройстве.

Ниже еше одна интересная строчка кода:

// Add `@Environment(\.managedObjectContext)` in the views that will need the context. let contentView = ContentView().environment(\.managedObjectContext, context) 

Тут создается ContentView , который потом передается в конструктор контроллера. Но что это за environment ?

Перед тем, как запустится, наше корневое представление ContentView – в окружении установится новый параметр managedObjectContext с контекстом, который мы только что создали. Окружение – это место где хранятся различные системны параметры, например локаль календаря, цветовая схема и т.д. Все эти параметры хранятся по разным ключам. И теперь наш контекст тоже хранится там по ключу managedObjectContext .

Теперь каждое представление может использовать “блокнот” для работы с объектами.

Определимся со структурой данных которыми буду манипулировать в приложении. Для этого в файл Memo.xcdatamodeld добавлю новую сущность Todo . Для этого надо кликнуть на большой жирный плюс Add Entity

Теперь к новой сущности добавляем нужные аттрибуты. Для простого туду достаточно добавить text , done и поля id с типом UUID(это поле будет использоваться как уникальный идентификатор объекта).

Генерировать код в ручную не обязательно. Можно оставить значение Class Definition в поле Codegen и успокоится. Но я выберу Manual/None – это позволит сгенерировать файлы в папке проекта и посмотреть что там будет.

Теперь генерирую файлы.

Получается пара новый файлов в проекте: Memo+CoreDataClass.swift и Memo+CoreDataProperties.swift . В Memo+CoreDataClass.swift описан сам объект. А в Memo+CoreDataClass.swift описываются его свойства.

Замечу, что еще один плюс генерации классов в ручную – возможность вносить разные дополнения, которые недоступны при работе только через интерфейс для .xcdatamodeld . Например, можно сделать нужные поля как enum . Или добавить реализацию нужного интерфейса.

Интерфейс

Нужно убедиться, что блок предпросмотра имеет доступ к параметру окружения managedObjectContext как и все приложение.

struct ContentView_Previews: PreviewProvider < static var previews: some View < let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext return ContentView().environment(\.managedObjectContext, context) >> 

Теперь к интерфейсу самого приложения. Делать его буду стильно, модно и молодежно с помощью SwidtUI. Нужен заголовок, список с задачами, текстовое поле и кнопка для добавления новых задач. В ContentView добавил такой код:

import SwiftUI import CoreData struct ContentView: View < @State var todo = "" var body: some View < NavigationView < VStack < // форма для создания нового таска HStack < TextField("Task",text: $todo).padding(.all, 20) Button(action: < >)< Text("Add Task") >.padding(.all, 20) > // список всех задач List < HStack < Text("Hello, World!") Spacer() в Button(action: < >) < Text("Complete") .foregroundColor(.blue) >> HStack < Text("Hello, World!") Spacer() Button(action: < >) < Text("Complete") .foregroundColor(.blue) >> > >.navigationBarTitle(Text("Tasks") .font(.largeTitle)) > > > struct ContentView_Previews: PreviewProvider < static var previews: some View < ContentView() >> 

Теперь подружу интерфейс с Core Data.

Сохраняем данные

Пока у меня ничего не сохранятся и не выводится. Чтобы сохранить новую задачу, нужно иметь доступ к контексту управляемого объекта. И тут становится понятным, зачем мы добавляли контекст в окружение. К нему можно получить доступ, используя свойство @Environment внутри нашего ContentView .

@Environment(\.managedObjectContext) var managedObjectContext @State var todo = "" 

И теперь можем написать код, который создает новый объект Task . Этот код нужно поместить в экшене кнопки «Add Task»

Button(action: < guard self.todo != "" else let todo = Todo(context: self.managedObjectContext) todo.text = self.todo todo.done = false todo.id = UUID() >)< Text("Add Task") >.padding(.all, 20) 

И теперь с помощью managedObjectContext сохраняем данные на устройстве.

Читаем данные

Чтобы прочитать сохраненные данные из хранилища, воспользуюсь свойством @FetchRequest , которое добавлю в ContentView . С помощью этого свойства очень легко манипулировать сохраненными данными.

@FetchRequest(entity: Todo.entity(), sortDescriptors: []) var todos: FetchedResults

Это, конечно, круто что можно одной строчкой объявить переменную и сразу указать что она заполнится данными их хранилища. Но как пом не, это не очень очевидно и читать такие объявления в чужом коде – самая настоящая боль. И это еще не используется параметр predicate в свойстве @FetchRequest который может сделать все объявление нечитаемым.

Теперь отредактирую отображение списка задач, добавлю цикл по todos с помощью ForEach

ForEach(todos) < todo in HStack < Text(todo.text!) Spacer() Button(action: < >) < Text("Complete") .foregroundColor(.blue) >> > 

Этот код у меня не захотел сразу работать.

Чтобы избавиться от этих ошибок – нужно добавить интерфейс Identifiable для класса Todo в файле Todo+CoreDataProperties.swift

extension Todo: Identifiable < // . >

У нас уже есть аттрибут id , поэтому после добавления интерфейса все заработает как нужно.

Запускаем эмулятор и добавляем новые задачи.

Редактирование и удаление

Отлично! Можно добавлять новые задачи. Осталось добавить возможность помечать их как выполненными и удалять. Для этого изменю блок с кнопкой – добавлю условие и сделаю две кнопки.

HStack < Text(todo.text!) Spacer() if todo.done < Button(action: < >) < Text("Remove") .foregroundColor(.blue) >> else < Button(action: < >) < Text("Complete") .foregroundColor(.blue) >> > 

Теперь в несколько строчек реализуем выполнение задачи

Button(action: < todo.done = true do < try self.managedObjectContext.save() >catch < print(error.localizedDescription) >>)
Button(action: < self.managedObjectContext.delete(todo) >)

Заключение

В приложениях на SwiftUI стало еще проще использовать Core Data. Всю основную магию за вас делает свойство @FetchRequest . Но если вы новичек в Swift и iOS, то вам стоит подробнее изучить как работает выборка объектов из хранилища и что такое предикаты.

  • Core Data в деталях
  • Core Data: Часть 1
  • Core Data: Часть 2. Lightweight Миграции
  • Introduction to using Core Data with SwiftUI
  • How to make a task list using SwiftUI and Core Data

5 сентября 2017 г. Hello CoreData!

Hello CoreData!

Сегодня будем рассматривать ненавистный мне framework от Apple под страшным названием CoreData. Это решения от Apple для работы с SQLite (реляционная база данных). CoreData может сохранять объекты Swift в SQLite, а также выполнять обратную операцию.

Введение

Лично мне больше нравится Realm, так как там мало что нужно делать, чтобы framework заработал — просто устанавливаете и начинаете создавать то, что хотите, плюс на официальном сайте есть прекрасная документация по всем фичам. Но когда вы просматриваете вакансии других компаний (я лично — никогда! А есть другие компании? 😉 ) часто попадаете на требования CoreData, и если вы не имеете опыта работы с этим framework, то вас легко могут «завалить&raquo уже в начале интервью, так и не узнав, какие красивые кнопочки вы умеете делать 🙂

Почему я не любил это framework? Потому что он достаточно тяжелый для понимания новичками, и в нем легко запутаться и допустить ошибки, а чем больше кода у вас написано, тем больше вероятность, что где-то вы ошиблись.

Начиная с ios 10, Apple многое сделали для упрощения работы с CoreData. Например, представили объект NSPersistentContainer , который берет на себя работу по созданию NSManagedObjectModel , NSPersistentStoreCoordinator и NSManagedObjectContext . Теперь для начала работы достаточно инициализировать объект persistentContainer = NSPersistentContainer(name: «MyModel») . В самом начале статья задумывалась как обзор сторонних решений для работы с CoreData, но так как Apple переделали framework, многие из этих решений находились или в процессе переделки или, того хуже, написаны на Objective-C. Так что мы рассмотрим три примера. В первом пройдемся по NSFetchedResultsController и NSPersistentContainer ; во втором, начнем писать свой класс для упрощения работы, а в последнем примере улучшим класс путем добавления generics .

Пример 1

Проект доступен для изучения и называется MagicalRecord_project (изначально хотел использовать именно этот framework). Рассмотрим файл Model.xcdatamodeld :

Просмотр Model.xcdatamodeld

Просмотр Model.xcdatamodeld

Для атрибута avatar (тип которого Binary Data ) выставлено в опциях Allows External Storage .

Переходим в контроллер:

fileprivate let persistentContainer: NSPersistentContainer = NSPersistentContainer(name: "Model") fileprivate lazy var fetchedResultsController: NSFetchedResultsController = { let fetchRequest = NSFetchRequest() fetchRequest.entity = Person.entity() fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: "name", cacheName: nil) frc.delegate = self return frc }()

Тут мы создаем объекты persistentContainer и fetchedResultsController , который будет отвечать за наполнение данными. Также создадим метод, который будет помогать создавать объекты:

func createPersonObject(name: String, about: String, image: UIImage) { let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: persistentContainer.viewContext) as! Person person.name = name person.about = about person.avatar = UIImageJPEGRepresentation(image, 1.0) as NSData? }

Смотрим, как это все выглядит в viewDidLoad :

override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self //create data createPersonObject(name: "Name A", about: "some inform. ", image: UIImage(named: "01")!) createPersonObject(name: "Name B", about: "some inform. ", image: UIImage(named: "02")!) perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0) //fetch data in controller do { try fetchedResultsController.performFetch() } catch { print(error) } }

Вот эта строка perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0) добавляет третий объект к контексту и NSFetchedResultsController обновляет tableView (об этом чуть позже).

fetchedResultsController.performFetch() передает данные с CoreData в контроллер.

Окно эмулятора

Окно эмулятора после добавления нового объекта

В начале видим два добавленных объекта, спустя две секунды — три. Создаем extension для ViewController :

extension ViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return false } func numberOfSections(in tableView: UITableView) -> Int { return fetchedResultsController.sections?.count ?? 0 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fetchedResultsController.sections?[section].numberOfObjects ?? 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell guard let sections = fetchedResultsController.sections else { fatalError("Sections missing") } let section = sections[indexPath.section] guard let itemsInSection = section.objects as? [Person] else { fatalError("Items missing") } let person = itemsInSection[indexPath.row] cell.aboutLbl.text = person.about cell.nameLbl.text = person.name cell.avatarImView.image = UIImage(data: person.avatar! as Data) return cell } }

Так как вместо массива dataSource мы имеем fetchedResultsController , то используем его для получения всех необходимых данных. В переменной fileprivate lazy var fetchedResultsController мы объявили себя делегатом NSFetchedResultsController . Реализуем необходимые методы:

extension ViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { tableView.beginUpdates() } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { tableView.endUpdates() } func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: tableView.insertRows(at: [newIndexPath!], with: .automatic) case .delete: tableView.deleteRows(at: [indexPath!], with: .automatic) case .update: tableView.reloadRows(at: [indexPath!], with: .automatic) case .move: tableView.moveRow(at: indexPath!, to: newIndexPath!) } } func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { switch type { case .insert: tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic) case .delete: tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic) case .move, .update: tableView.reloadSections(IndexSet(integer: sectionIndex), with: .automatic) } } }

Теперь, когда что-то меняется (добавляем, обновляем или удаляем какой-то элемент), NSFetchedResultsController знает об этом и может сделать необходимые коррекции в UITableView . Подобный механизм реализован и в Realm:

let token = realm.addNotificationBlock { notification, realm in viewController.updateUI() } token = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in guard let tableView = self?.tableView else { return } switch changes { case .initial: // Results are now populated and can be accessed without blocking the UI tableView.reloadData() break case .update(_, let deletions, let insertions, let modifications): // Query results have changed, so apply them to the UITableView tableView.beginUpdates() tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic) tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.endUpdates() break case .error(let error): // An error occurred while opening the Realm file on the background worker thread fatalError("\(error)") break } } }

К нашему примеру это не относится, но как видите, механизмы обновления UITableView похожи.

Пример 2

В этом примере будем создавать StoreManager . Откройте проект StoreManager_project . StoreManager создан с использованием патерна Singleton (одна копия на протяжении жизни программы):

fileprivate override init(){} static let sharedInstance = StoreManager() override func copy() -> Any { fatalError("You are not allowed to use copy method on singleton!") } fileprivate lazy var fetchRequest: NSFetchRequest = { let request = NSFetchRequest() request.entity = Car.entity() request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] return request }() fileprivate lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "Model") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }()

NSPersistentContainer имеет два свойства, точнее свойство и метод: viewContext и newBackgroundContext . Первый ассоциирован с main queue , второй — privateQueueConcurrencyType . Когда мы пишем что-то в newBackgroundContext , он отсылает нотификацию для объекта viewContext на мердж содержимого. Поэтому выходит, что нам уже не нужно самим подписываться на эту нотификацию. Из документации по viewContext :

This context is configured to be generational and to automatically consume save notifications from other contexts.

…is set to consume NSManagedObjectContextDidSave broadcasts automatically.

Я добавил несколько методов, которые мы будем модифицировать в третьем примере:

func saveMainContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } func clearMainStorage() { var cars: [Car]? do { cars = try persistentContainer.viewContext.fetch(fetchRequest) } catch { print("Storage reading error!") } if let cars = cars { for item in cars { persistentContainer.viewContext.delete(item) } } } func addACar(name: String, hasRoof: Bool, numberOfWheels: Int) { //add data to background context let context = persistentContainer.newBackgroundContext() let car = NSEntityDescription.insertNewObject(forEntityName: "Car", into: context) as! Car car.name = name car.numberOfWheels = Int16(numberOfWheels) car.hasRoof = hasRoof do { try context.save() } catch { print(error) } } func showMainStorage() { var cars: [Car]? do { cars = try persistentContainer.viewContext.fetch(fetchRequest) } catch { print("Storage reading error!") } print("CARS: \(cars?.map < $0.name! >)") } viewDidLoad из ViewController: StoreManager.sharedInstance.clearMainStorage() StoreManager.sharedInstance.addACar(name: "BMW", hasRoof: true, numberOfWheels: 4) StoreManager.sharedInstance.addACar(name: "Zapor", hasRoof: false, numberOfWheels: 3) StoreManager.sharedInstance.addACar(name: "Fiat", hasRoof: true, numberOfWheels: 5) StoreManager.sharedInstance.addACar(name: "F1", hasRoof: false, numberOfWheels: 4) perform(#selector(insertAnotherCar), with: nil, afterDelay: 2.0) StoreManager.sharedInstance.showMainStorage()

Сначала чистим хранилище, потом добавляем модели авто. Дальше идет метод, который добавит третье авто, после двух секунд (никогда не используйте performSelector: afterDelay: — это плохая практика). И два метода showMainStorage() , которые показывают, как изменилась база данных. Для примера можете раскоментировать следующие куски кода:

NotificationCenter.default.addObserver(self, selector: #selector(storageHasChanged(note:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil) deinit { NotificationCenter.default.removeObserver(self) } //MARK: Actions func storageHasChanged(note: NSNotification) { let inserted = note.userInfo?["inserted"] let deleted = note.userInfo?["deleted"] let updated = note.userInfo?["updated"] if let insertedSet = inserted as? Set, insertedSet.count > 0 { print("INSERTED") print(insertedSet.map { $0.name }) } if let deletedSet = deleted as? Set, deletedSet.count > 0 { print("DELETED") print(deletedSet.map { $0.name }) } if let updatedSet = updated as? Set, updatedSet.count > 0 { print("UPDATED") print(updatedSet.map { $0.name }) } }

Можете поэкспериментировать с нотификацией на изменение базы данных. Мой результат:

[Optional("Zapor")] INSERTED [Optional("Fiat")] INSERTED [Optional("F1")] DELETED [Optional("Zapor"), Optional("F1"), Optional("Fiat"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("Alfa Romeo"), Optional("Alfa Romeo"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("F1"), Optional("F1"), Optional("Alfa Romeo"), Optional("F1"), Optional("Fiat"), Optional("Alfa Romeo"), Optional("Zapor"), Optional("Zapor"), Optional("Zapor")] CARS: Optional(["BMW", "F1", "Fiat", "Zapor"]) INSERTED [Optional("Alfa Romeo")] CARS: Optional(["Alfa Romeo", "BMW", "F1", "Fiat", “Zapor"])

Пример 3

В этом примере я хотел продемонстрировать работу с JSON. Конечно, в реальном проекте я бы использовал что-то для мапинга, возможно SwiftyJSON для простоты. Но для небольшого урока, как в этот, я задал JSON строкой и потом его распарсил:

et json = "\"cars\":[\"name\":\"ZaZ\">,\"name\":\"Lada\">,\"name\":\"Lexus\">]>" let dict = try! JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.allowFragments) as! [String:Any]

Смотрим на файл StoreManager :

//to make sure we have only one copy lazy var backgroundContext: NSManagedObjectContext = { return self.persistentContainer.newBackgroundContext() }() fileprivate lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "Model") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() func saveContext(type: ContextType) { let context = type == .main ? persistentContainer.viewContext : backgroundContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } func getMainContextObjects: NSManagedObject>(entity: T.Type) -> [NSManagedObject]? { return persistentContainer.viewContext.fetchAll(entity: entity, fetchConfiguration: nil) } func clearStorage: NSManagedObject>(type: ContextType, entity: T.Type) { let context = type == .main ? persistentContainer.viewContext : backgroundContext let objects = context.fetchAll(entity: entity, fetchConfiguration: nil) if let objects = objects { for item in objects { context.delete(item) } } } func showStorage: NSManagedObject>(type: ContextType, entity: T.Type) { let context = type == .main ? persistentContainer.viewContext : backgroundContext let objects = context.fetchAll(entity: entity, fetchConfiguration: nil) print("OBJECTS: \(objects)") print("COUNT: \(objects?.count)") }

Здесь я храню backgroundContext как переменную, чтобы всегда был только один backgroundContext , который в дальнейшем я могу сохранять методом saveContext(type:) . Остальные методы должны быть понятны. Так как у нас есть enum .

enum ContextType { case main case background }

. мы всегда знаем, с каким контекстом работаем. Вернемся в ViewController . Обратите внимание, как я создаю объекты:

for item in dict["cars"] as! [Any] { let car = StoreManager.sharedInstance.backgroundContext.insert(entity: Car.self) car.name = (item as? [String:Any])?["name"] as! String? }

Но в стандартном контексте метода insert(entity:) нет! Дело в том, что я написал extensions, которые работают с generics -данными:

//MARK: CoreData extensions extension NSManagedObject { class var entityName : String { let components = NSStringFromClass(self) return components } class func fetchRequestObj() -> NSFetchRequest { let request = NSFetchRequest(entityName: self.entityName) return request } class func fetchRequestWithKey(key: String, ascending: Bool = true) -> NSFetchRequest { let request = fetchRequestObj() request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)] return request } } extension NSManagedObjectContext { func insert: NSManagedObject>(entity: T.Type) -> T { let entityName = entity.entityName return NSEntityDescription.insertNewObject(forEntityName: entityName, into: self) as! T } func fetchAll: NSManagedObject>(entity: T.Type, fetchConfiguration: ((NSFetchRequest) -> Void)?) -> [NSManagedObject]? { let dataFetchRequest = NSFetchRequest(entityName: entity.entityName) fetchConfiguration?(dataFetchRequest) var result = [NSManagedObject]() do { result = try self.fetch(dataFetchRequest) } catch { print("Failed to fetch feed data, critical error: \(error)") } return result } }

Поэтому можно добавлять сущность в контекст, используя его тип и слово self (Car.self) . Также добавлен метод fetchAll , работающий с generics . Его используем в методах StoreManager . Теперь наш менеджер готов к работе. Надеюсь испытать и улучшить его при первой необходимости

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *