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 не відмежовується тільки цими компонентами (деякі інші ми розглянемо нижче), але ці три становлять основу фреймворку і дуже важливо зрозуміти їх призначення та принцип роботи.

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



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

За замовчуванням, Xcode додає код для роботи з Core Data у клас делегата додатки (
<b>AppDelegate.swift</b>
). Давайте розглянемо його більш детально, він починається з коментаря:

// MARK: - Core Data stack

Тут чотири змінні, всі вони ініціалізуються з допомогою замикання. Проте, перша з них,
<b>applicationDocumentsDirectory</b>
— просто допоміжний метод, який повертає директорію для зберігання даних. За замовчуванням, це
<b>Document Directory</b>
, можна змінити, але малоймовірно, що вам це дійсно треба. Реалізація проста і не повинна викликати труднощів для розуміння.

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

Наступне визначення —
<b>managedObjectModel</b>
— цікаво, так як має саме безпосереднє відношення до Core Data:

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

Логіка програмного коду нехитра — отримуємо з складання програми якийсь файл з розширенням
<b>momd</b>
і створюємо на підставі його об'єктну модель даних. Залишилося з'ясувати, що це за файл такий. Подивіться на файли Навігаторі проекту (Project navigator), там ви знайдете файл з розширенням
<b>xdatamodel</b>
— це наша модель даних Core Data (як з нею працювати ми розглянемо трохи пізніше), яка при компіляції проекту включається в файл-складання програми з розширенням
<b>momd</b>
.



Йдемо далі, —
<b>persistentStoreCoordinator</b>
— найбільш об'ємне визначення, але, незважаючи на кілька страхітливий вигляд, не варто лякатися — більшу частину коду займає обробка винятків:

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
}()

Тут на основі об'єктної керованої моделі створюється координатор постійного сховища. Потім ми визначаємо, де саме повинні зберігатися дані. І в ув'язненні підключаємо власне сховище (
<b>coordinator.addPersistentStoreWithType</b>
), передавши відповідного методу в якості параметрів тип сховища і його розташування. За замовчуванням використовується SQLite. У двох інших параметрах можуть передаватися параметри та опції, але на даному етапі нам це не треба, тому просто передамо
<b>nil</b>
.

Останнє визначення —
<b>managedObjectContext</b>
— впевнений, проблем з ним не буде:

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

Тут ми створюємо новий контекст керованого об'єкта і присвоюємо йому посилання на наш координатор постійного сховища, з допомогою якого він і буде читати і писати необхідні нам дані. Деталь, що заслуговує уваги — аргумент конструктора
<b>NSManagedObjectContext</b>
. У загальному випадку, може бути кілька робочих контекстів виконуються в різних потоках (наприклад, один для інтерактивної роботи, інший — для фонової підвантаження даних). Передаючи в якості аргументу
<b>MainQueueConcurrencyType</b>
, ми зазначаємо, що даний контекст повинен бути створений в основному потоці.

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

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» внизу) і назвіть її
<b>«Customer»</b>
. Ця сутність буде уособлювати Замовника (одного). У сутності можуть бути атрибути, взаємозв'язку і одержувані властивості (fetch-властивості). Дещо спростивши, можна сказати, що різниця між атрибутами і взаємозв'язками в типі можливих значень: атрибути підтримують тільки прості типи даних (рядок, число, дата тощо), взаємозв'язку — це посилання на іншу сутність (більш докладно про взаємозв'язки ми поговоримо через кілька хвилин). Fetch-властивості — це аналог обчислюваних властивостей, тобто значення обчислюється динамічно (і кешується) на підставі зумовленого запиту.

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

  • модель даних — схеми бази даних
  • сутність — таблиця бази даних
  • атрибути і взаємозв'язку — поля таблиці
У нашій сутності
<b>«Customer»</b>
буде два атрибути: «Ім'я» (
name
)
та «Доп. інформація» (
info
)
. Давайте додамо їх і встановимо їм тип значення
<b>String</b>
. Зверніть увагу, що у редакторі моделі даних існують певні вимоги до імен об'єктів — ім'я сутності має обов'язково починатися з великої літери, а ім'я атрибута і взаємозв'язку — з маленької.



Наступна важлива частина — Інспектор моделі даних, ви бачите його праворуч від редактора моделі даних. З його допомогою можна задавати різні атрибути і параметри для сутностей, атрибутів сутностей (пробачте за тавтологію), взаємозв'язків та інших об'єктів. Наприклад, сутність можна відзначити як абстрактну, або задати для неї батьківську сутність (принципи такі ж, як і в цілому в ООП).

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

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

На цьому створення сутності
<b>Customer</b>
можна вважати завершеним. Давайте створимо і налаштуємо наступну сутність — Послуги. Створіть нову сутність —
<b>Services</b>
і додайте два атрибути:
<b>name</b>
(найменування послуги) та
<b>info</b>
(додаткова інформація). Тип даних в обох випадках
<b>String</b>
, атрибут
<b>name</b>
— не повинен бути опціональним. Загалом, все те ж саме, що з попередньої сутністю, жодних проблем тут виникнути не повинно.



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

  • шапка документа, де буде міститися дата документа, замовник і посилання на табличну частину
  • рядок табличної частини документа, де буде містити Послуга та її вартість, а також посилання на «шапку» документа.
Не хвилюйтеся, якщо з останнього абзацу ви нічого не зрозуміли. Зараз ми разом це зробимо в редакторі моделі даних і, наприкінці, розглянемо графічне представлення нашої моделі — після цього все повинно стати на свої місця.

Почнемо з «шапки» документа — створимо нову сутність
<b>«Order»</b>
і додамо три атрибута (тут все вже знайоме по створенню попередніх сутностей):

  • <b>date</b>
    — дата документа, тип
    <b>Date</b>
    , не опціональний
  • <b>paid</b>
    — ознака оплати, тип
    <b>Boolean</b>
    , не опціональний, значення за замовчуванням —
    <b>NO</b>
  • <b>made</b>
    — ознака виконання замовлення, тип
    <b>Boolean</b>
    , не опціональний, значення за замовчуванням —
    <b>NO</b>
Тепер переходимо до Взаємозв'язкам. Додамо нову зв'язок з іменем
«customer»
і встановимо її призначення (
Destination
) значення
Customer
. З деякою натяжкою, але, продовжуючи аналогію, можна сказати, що ми додали нову колонку з типом
«Customer»
в таблицю
Order
.



Зверніть увагу, що взаємозв'язки за замовчуванням теж є Optional. Крім того, у Інспектора атрибутів присутні такі дуже важливі властивості, які ми зараз детально розглянемо:

  • Type (тип зв'язку)
  • Delete Rule (правило видалення)
  • Inverse (зворотній зв'язок)
Type (тип зв'язку)
Якщо ви працювали з будь-якими базами даних, то це поняття вам напевно знайоме. Тут нам пропонується на вибір два варіанти: To One та To Many. To One — означає, що наше Замовлення пов'язаний одним конкретним Замовником, To Many — з кількома замовниками. У нашому випадку треба залишити значення за замовчуванням — To One.

Delete Rule (правило видалення)
Дуже важлива властивість, тут треба вибрати одне з можливих поводжень сутності в тому момент, коли дана зв'язок з якихось причин видаляється. Можливі наступні варіанти:

  • No Action (Не виконувати жодних дій) — Core Data не буде виконувати які-небудь дії, в тому числі повідомляти про такому віддаленні; сутність «буде думати», що вилучення не було. У цьому випадку, ви повинні самостійне реалізувати необхідну поведінку програми. Малоймовірно, що ви захочете використовувати.
  • Nullify (Анулювання) — при видаленні зв'язку, її значення буде встановлено в
    <b>nil</b>
    . Найбільш поширений варіант, використовується за замовчуванням.
  • Cascade (Каскадне видалення) — при видаленні зв'язку, автоматично будуть видалені всі замовники, які посилаються на неї (явно не наш випадок)
  • Deny (Відмова) — правило, протилежне попередньому, його суть в тому, що не можна видалити об'єкт, поки на нього є хоча б одне посилання. Такий підхід, наприклад, застосовується у відношенні всіх об'єктів в «1С: Підприємство».
Власне, яке поведінка вибрати, визначається суто логікою програми. Зараз ми не будемо морочитися з цим і залишимо значення за замовчуванням — Nullify, воно нам цілком підходить.

Inverse (зворотній зв'язок)
Ми додали зв'язок «Замовлення» з «Замовником», а «Замовник» нічого не знає про «Замовлення», в яких він бере участь. Про це ж нас попереджає і Warning.



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

Давайте це виправимо, створіть для сутності
<b>Customer</b>
новий взаємозв'язок з ім'ям
<b>"</b>
, виберіть
<b>Destination = Order</b>
і в якості зворотного зв'язку вкажіть, створену нами раніше зв'язок
<b>customer</b>
. Ще один момент — оскільки у одного Замовника може бути, у загальному випадку, багато документів — змінимо тип зв'язку на
<b>To Many</b>
.



Якщо ви повернетеся в сутність
<b>«Order»</b>
, то побачите, що зворотний зв'язок вже встановлена автоматично значення
<b>"</b>
.

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



Тепер повертаємося в сутність Order, щоб створити зворотний зв'язок. Додайте нову зв'язок з ім'ям
<b>rowsOfOrder (Destination = RowOfOrder, Inverse = order)</b>
. Не забудьте змінити тип зв'язку на
<b>To Many</b>
(так як в одному документі може бути кілька рядків).



Залишилося в сутність RowOfOrder додати тільки зв'язок з сутністю Послуга. З урахуванням всього вищесказаного цього не повинно бути складним, все за тим же сценарієм. Додаємо для сутності
<b>«RowOfOrder»</b>
новий взаємозв'язок з ім'ям
<b>service (Destination = Service)</b>
, інше залишаємо за замовчуванням. Потім для сутності
<b>Service</b>
додаємо нову взаємозв'язок
<b>«rowsOfOrders» (Destination = rowOfOrder, Inverse = service)</b>
і встановлюємо тип зв'язку рівним
<b>To Many</b>
.



Важливе зауваження! Після створення моделі даних її не можна змінювати — при першому запуску програми Core Data у відповідності з моделлю даних створює сховище, а при наступних — перевіряє структуру сховища на відповідність. Якщо з якихось причин структура сховища не відповідає моделі даних, то відбувається критична помилка часу виконання (тобто додаток у вас буде непрацездатним). Як же бути у випадку, якщо модель даних потрібно змінити для цього необхідно використовувати механізм міграції Core Data, це окрема тема підвищеної складності, і ми не будемо її розглядати в рамках даної статті. Є й інший варіант — можна просто видалити додаток з пристрою (або емулятора), а при старті програми Core Data просто створить нове сховище з новою структурою. Очевидно, що даний спосіб можливий тільки на етапі розробки програми.

На закінчення даної статті давайте поглянемо на її графічне подання, для цього переведітьEditor Style редактора моделі даних (знаходиться внизу) в положення Graph.



Ви бачите створені нами з атрибутами сутності і всі їх взаємозв'язку у вигляді графічної структури. Лінія з звичайної стрілкою на кінці означає зв'язок To One, з подвійною стрілкою — To Many. Графічний вигляд добре допомагає зорієнтуватися в об'ємних моделях.

На цьому перша частина закінчена, в наступній статті буде багато коду, ми будемо створювати самі об'єкти, пов'язувати їх між собою, познайомимося з
<b>NSEntityDescription</b>
та
<b>NSManagedObject</b>
, а також напишемо допоміжний клас, що істотно підвищує зручність роботи з Core Data.

Цей проект на GitHub
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.