- PVSM.RU - https://www.pvsm.ru -
Про 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 не отграничивается только этими компонентами (некоторые другие мы рассмотрим ниже), но эти три составляют основу фреймворка и очень важно понять их назначение и принцип работы.
Давайте, продолжим рассмотрение 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С: Предприятие» с блэкджеком и с нормальным дизайном!
Давайте начнем с Заказчиков. В редакторе модели данные добавьте новую сущность (кнопка с подписью «Add Entity» внизу) и назовите ее «Customer»
. Эта сущность будет олицетворять Заказчика (одного). У сущности могут быть атрибуты, взаимосвязи и получаемые свойства (fetch-свойства). Немного упростив, можно сказать, что разница между атрибутами и взаимосвязями в типе возможных значений: атрибуты поддерживают только простые типы данных (строка, число, дата и пр.), взаимосвязи — это ссылка на другую сущность (более подробно про взаимосвязи мы поговорим через несколько минут). Fetch-свойства — это аналог вычисляемых свойств, то есть значение вычисляется динамически (и кэшируется) на основании предопределенного запроса.
Можно провести следующую аналогию с СУБД:
У нашей сущности «Customer»
будет два атрибута: «Имя» (name
) и «Доп. информация» (info
). Давайте их добавим и установим им тип значения String
. Обратите внимание, что в редакторе модели данных существуют определенные требования к именованию объектов — имя сущности должно обязательно начинаться с большой буквы, а имя атрибута и взаимосвязи — с маленькой.
Следующая важная часть — Инспектор модели данных, вы видите его слева от редактора модели данных. С его помощью можно задавать различные атрибуты и параметры для сущностей, атрибутов сущностей (уж простите за тавтологию), взаимосвязей и других объектов. Например, сущность можно отметить как абстрактную, либо задать для нее родительскую сущность (принципы такие же, как и в целом в ООП).
Для атрибута сущности список доступных параметров меняется в зависимости от типа атрибута. Например, для числовых значений можно задать нижнюю и/или верхнюю границу, для даты можно задать допустимый диапазон. Также для большинства типов значений можно задать значение по умолчанию.
Важным свойством атрибута является Optional (опциональный). Смысл у него точно такой же, как в программном коде Swift: если атрибут помечен как Optional, то его значение может отсутствовать, и наоборот, — если такой пометки нет — запись сущности будет невозможна. По умолчанию, все атрибуты отмечаются как опциональные. В нашем случае, атрибут name
не должен быть опциональным (надо снять флажок Optional), так как Заказчик без имени лишен какого-либо практического смысла.
На этом создание сущности Customer
можно считать завершенным. Давайте создадим и настроим следующую сущность — Услуги. Создайте новую сущность — Services
и добавьте два атрибута: name
(наименование услуги) и info
(дополнительная информация). Тип данных в обоих случаях — String
, атрибут name
— не должен быть опциональным. В общем, все то же самое, что с предыдущей сущностью, никаких проблем здесь возникнуть не должно.
Переходим к документу «Заказ» — здесь все немного сложнее. Так как в одном документе у нас может быть несколько различных услуг, а для каждой услуги будет своя сумма, то документ у нас будет представлен двумя сущностями:
Не волнуйтесь, если из последнего абзаца вы ничего не поняли. Сейчас мы вместе все это проделаем в редакторе модели данных и, в конце, посмотрим на графическое представление нашей модели — после этого все должно встать на свои места.
Начнем с «шапки» документа — создадим новую сущность «Order»
и добавим три атрибута (здесь все уже знакомо по созданию предыдущих сущностей):
date
— дата документа, тип Date
, не опциональныйpaid
— признак оплаты, тип Boolean
, не опциональный, значение по умолчанию — NO
made
— признак выполнения заказа, тип Boolean
, не опциональный, значение по умолчанию — NO
Теперь переходим к Взаимосвязям. Добавим новую связь с именем «customer»
и установим ее назначение (Destination
) в значение Customer
. С некоторой натяжкой, но, продолжая аналогию, можно сказать, что мы добавили новую колонку с типом «Customer»
в таблицу Order
.
Обратите внимание, что взаимосвязи по умолчанию тоже являются Optional. Кроме того, в Инспекторе атрибутов присутствуют следующие очень важные свойства, которые мы сейчас подробно рассмотрим:
Если вы работали с какими-либо базами данных, то это понятие вам наверняка знакомо. Здесь нам предлагается на выбор два варианта: To One и To Many. To One — означает, что наш Заказ связан к одним конкретным Заказчиком, To Many — с несколькими заказчиками. В нашем случае надо оставить значение по умолчанию — To One.
Очень важное свойство, здесь надо выбрать одно из возможных поведений сущности в том момент, когда данная связь по каким-то причинам удаляется. Возможны следующие варианты:
nil
. Наиболее распространенный вариант, используется по умолчанию.Собственно, какое поведение выбрать, определяется сугубо логикой программы. Сейчас мы не будем заморачиваться с этим и оставим значение по умолчанию — Nullify, оно нам вполне подходит.
Мы добавили связь «Заказа» с «Заказчиком», но «Заказчик» ничего не знает о «Заказах», в которых он участвует. Об этом же нас предупреждает и 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.
Автор: angryscorp
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/135849
Ссылки в тексте:
[1] Этот проект на GitHub: https://github.com/angryscorp/core-data-habrahabr-swift/archive/master.zip
[2] Источник: https://habrahabr.ru/post/303512/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.