Про 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 в класс делегата приложения (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-свойства — это аналог вычисляемых свойств, то есть значение вычисляется динамически (и кэшируется) на основании предопределенного запроса.

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

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

У нашей сущности «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. Кроме того, в Инспекторе атрибутов присутствуют следующие очень важные свойства, которые мы сейчас подробно рассмотрим:

  • 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С: Предприятие».

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

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.

Этот проект на GitHub
Поделиться с друзьями
-->

Комментарии (9)


  1. 4FreeD
    17.06.2016 11:40

    Поправка: «Следующая важная часть — Инспектор модели данных, вы видите его *справа от редактора модели данных. „

    Спасибо, довольно хорошо подан материал для новичков.


    1. angryscorp
      17.06.2016 17:19

      Да, конечно, справа. Спасибо, исправил.


  1. InstaRobot
    17.06.2016 21:07
    -1

    Спасибо за перевод. Зачет! Если честно, прочел «по диагонвли», но не из-за материала, перевод сделан в нормальном формате, просто решил прочесть оригинал. Не могли бы Вы сослаться на первоисточник? Так, как я понимаю, есть продолжение (еще не переведенное), я бы с удовольствием все посмотрел на одном проходе.

    Пробовал угадать откуда, но не получилось, поиск по картинке в Гугл также не дал результатов, Вы используете свои). Это не Рэй случаем? Я не видел у него подобной статьи, хоть и имею платную подписку на его ресурсе. Там случаем ничего нет о многопоточности CoreData в контексте Swift?


    1. angryscorp
      17.06.2016 23:20
      +2

      Это не перевод, материал авторский )
      Собственно, я поэтому и решил сделать такой сквозной пример, так как ничего подобного не нашел. Большинство статей или ограничивается примером типа «To-Do List», либо рассматривает какой-нибудь один аспект работы с Core Data.
      Хотя часть статьи с описанием Core Data Stack, я думаю, будет, так или иначе, похожа у многих авторов.


      1. InstaRobot
        17.06.2016 23:51

        Сори, добавил в закладки! Когда продолжение? У Рэя также толковые материалы, но у него я больше по кастомным контролам просвещаюсь. В любом случае, очень полезный материал. Про синхронизацию потоков напишете? А то я больше с Реалм всегда развоекался и CloudKit, хочется пощупать тезнологию поплотнее


        1. angryscorp
          18.06.2016 16:46

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


  1. slutsker
    18.06.2016 10:20

    Спасибо за статью, жду продолжения.
    Лично я не смог полюбить Core Data — слишком громоздким он мне показался.

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


    1. angryscorp
      18.06.2016 17:33
      -1

      Core Data может казаться громоздким на первый взгляд (просто потому, что чтобы с ним начать работать надо усвоить сразу несколько новых понятий), но при этом он очень логичен и довольно универсален. Думаю, Вы измените свое мнение по итогам остальной части статьи.


  1. kraps
    18.06.2016 10:29

    Неплохой материал есть здесь: iOS Разработка на языке Swift — Национальный Исследовательский Университет «Высшая Школа Экономики» (https://itun.es/ru/e6Vg4)
    Пятый урок посвящён именно CoreData