CoreData модель из кода. Или «Как обойтись без .XCDataModel» (Часть 1)

  • Tutorial

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


Краткая предыстория


Я уже около полугода разрабатываю собственное приложение под iOS, и до недавнего времени процесс разработки шёл гладко и без особых проблем в области хранения данных.


Но недавно coreData-модель разрослась до неприличных с моей точки зрения размеров, и, так как я незрячий, и работать с большим объёмом полувизуальных данных мне физически тяжело, мной было принято решение перенести всё, что касается coreData в код.


Материала по "невизуальной" работе с coreData крайне мало, так что половина моих выводов и наработок основывается на безумных экспериментах в Playgroundах и не менее безумных попытках нагуглить недостающую информацию.


В общем, поехали!


Начнём с классов


Если при стандартной работе с coreData мы описываем все сущности и взаимосвязи между ними в отдельном файле с моделью, то в нашем случае для начала нам понадобятся классы, описывающие те самые сущности (так будет проще осознавать, с чем работаем, да и XCode будет помогать, подсказывая имена уже созданных классов).


Кстати, ниже будет использоваться странная приписка в конце каждого класса, "MO" — Managed Object. Это чтобы не перепутать с другими, возможно похожими, классами, но не относящимися к coreData.


Будем описывать следующую ситуацию:


  • Есть компания (описывается классом CompanyMO);
  • Есть сотрудники (описываются классом WorkerMO);
  • Каждый сотрудник может работать только в одной компании (или не работать вовсе);
  • Каждая компания может иметь любое количество сотрудников (в том числе и не иметь сотрудников вовсе).

@objc(CompanyMO)
public class CompanyMO: NSManagedObject {
@NSManaged public var companyName: String
@NSManaged public var workers: NSSet
}

@objc(WorkerMO)
public class WorkerMO: NSManagedObject {
@NSManaged public var firstname: String
@NSManaged public var lastname: String
@NSManaged public var company: CompanyMO?
}

Теперь разберёмся с незнакомыми обозначениями.


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


objc(CompanyMO) перед объявлением класса CompanyMO говорит о том, что при использовании класса CompanyMO в среде Objective-C его имя будет CompanyMO. То есть objc(NameOfClass) указывает имя объявленного следом класса для среды Objective-C.


Следующая непонятность — @NSManaged перед каждым свойством класса.


@NSManaged обозначает, что инициализацией, значением и всем прочим для этой переменной будет управлять нечто уже в процессе выполнения кода. Я не силён в терминологии этой области, но знаю точно (с сайта Apple, кстати), что @NSManaged используется только в случае с coreData.


Подготовимся к созданию модели


Описание модели данных из кода — довольно некрасивая вещь, особенно с точки зрения количества написанного кода.


Поэтому перед тем, как описывать саму модель, мы добавим в код пару extensionов для более комфортной работы в будущем.


extension NSEntityDescription {
convenience init(from classType: AnyClass) {
self.init()
self.name = NSStringFromClass(classType)
self.managedObjectClassName = NSStringFromClass(classType)
}

func addProperty(_ property: NSPropertyDescription) {
self.properties.append(property)
}
}

extension NSAttributeDescription {
convenience init(name: String, ofType: NSAttributeType, isOptional: Bool = false) {
self.init()
self.name = name
self.attributeType = ofType
self.isOptional = isOptional
}
}

А теперь разберёмся в выше написанном коде.


Класс NSEntityDescription — это класс coreData, описывающий сущность (компанию, сотрудника). У этого класса есть имя (то имя, с которым сущности этого класса будут храниться в базе) и имя класса (managedObjectClassName), этот атрибут должен содержать имя субкласса, описывающего модель (то, что мы писали выше — CompanyMO, WorkerMO и т д).


Так как нам не особо хочется, чтобы создание каждой сущности в модели выглядело вот так:


let company = NSEntityDescription()
company.name = "CompanyMO"
company.managedObjectClassName = "CompanyMO"

Мы сделали инициализатор, которому можем передать класс, описанный выше и на выходе получить готовый NSEntityDescription. Например, вот так:


let company = NSEntityDescription(from: CompanyMO.self)
let worker = NSEntityDescription(from: WorkerMO.self)

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


Перейдём к описанию самой модели


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


var model: NSManagedObjectModel {
let _model = NSManagedObjectModel()

let companyEntity = NSEntityDescription(from: CompanyMO.self)
companyEntity.addProperty(NSAttributeDescription(name: "companyName", ofType: .stringAttributeType))
// Здесь должно быть описание свойства CompanyMO.workers, но о взаимосвязях мы поговорим во второй части статьи
let workerEntity = NSEntityDescription(from: WorkerMO.self)
workerEntity.addProperty(NSAttributeDescription(name: "firstname", ofType: .stringAttributeType))
workerEntity.addPropertyNSAttributeDescription((name: "lastname", ofType: .stringAttributeType))

_model.entities = [companyEntity, workerEntity]
return _model
}

Разберём выше написанный код.


У объекта NSManagedObjectModel есть свойство entities, являющееся массивом объектов NSEntityDescription.


NSManagedObjectModel.entities: [NSEntityDescription]

Класс NSEntityDescription описывает сущность в базе данных. Если вы создаёте описание сущности, вы не создаёте объект этой сущности, не заполняете базу какими-либо данными. Вы просто сообщаете модели, что такая сущность есть, и с ней надо уметь работать.


У каждого NSEntityDescription объекта, в нашем случае companyEntity и workerEntity есть массив свойств companyEntity.properties и workerEntity.properties, который хранит в себе объекты NSPropertyDescription.


Класс NSPropertyDescription является родительским для NSAttributeDescription (описывает атрибуты или, проще говоря, свойства) и NSRelationshipDescription (описывает взаимосвязи между сущностями).


Про NSRelationshipDescription мы поговорим во второй части данной статьи.


Итак, мы делаем следующее:


  1. Создаём описания сущностей CompanyMO и WorkerMO;
  2. Добавляем в массив свойств каждой сущности необходимые объекты, описывающие свойства. Для companyEntity это companyName (про workers во второй части), а для workerEntity это firstname и lastname.
  3. Заполняем массив entities у создаваемой модели только что созданными и "наполненными" сущностями.

Процесс несложный и довольно понятный.


Остался последний штрих.


Создаём контейнер


Недавно на смену NSPersistentStore и NSPersistentStoreCoordinator пришёл более удобный и понятный NSPersistentContainer (собственно, только с ним я и умею работать. Не застал те ужасные времена).


В стандартной работе с coreData через графический интерфейс при создании контейнера мы передаём ему лишь имя модели.


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


Но у persistentContainer есть и другой инициализатор.


lazy var persistentContainer: NSPersistentContainer {
let _container = NSPersistentContainer(name: "CyrmaxModel", managedObjectModel: model)
_container.loadPersistentStores {
(description, error) in
// Если нужно, делаем что-то
}
return _container
}

Как видите, мы передаём инициализатору контейнера не только имя будущей (или существующей) базы данных, но также передаём ранее созданную модель данных.


После создания persistentContainer мы можем работать с coreData абсолютно так же, как работали до переноса модели в код.


Плюсы описания модели в коде


  1. При запуске приложения не может возникнуть ошибка чтения файла модели, так как файла изначально просто нет;
  2. Приложение запускается чуть быстрее (опять-таки из-за того, что не нужно считывать файл);
  3. Мы автоматически избавляемся от ненужных опционалов в субклассах (это можно сделать и вручную, сгенерировав субклассы и удалив лишние "?", но всё же).

Сокращаем количество кода


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


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


Давайте упакуем этот код внутрь класса сущности, чтобы каждый класс описывал все свои свойства внутри себя.


Рассмотрим на примере класса WorkerMO, так как у него больше свойств.


@objc(WorkerMO)
public class WorkerMO: NSManagedObject {
@NSManaged public var firstname: String
@NSManaged public var lastname: String

static private var _entityDescription: NSEntityDescription?
static func entityDescription() -> NSEntityDescription {
guard self._entityDescription == nil else {
return _entityDescription!
}
let des = NSEntityDescription(from: self)
des.addProperty(NSAttributeDescription(name: "firstname", ofType: .stringAttributeType))
des.addProperty(NSAttributeDescription(name: "lastname", ofType: .stringAttributeType))
self._entityDescription = des
return self._entityDescription!
}
}

То же самое делаем с CompanyMO (но с этим вы и сами справитесь).


А теперь разбор полётов.


Зачем мы добавили статичное свойство _entityDescription и возвращаем именно его значение, а не создаём каждый раз новое описание сущности?


Об этом мы поговорим во второй части статьи, так как в первую очередь подобное поведение связано именно с NSRelationshipDescription и его особенностями.


Но в общих словах, нам необходимо, чтобы CompanyMO.entityDescription() возвращала всегда один и тот же экземпляр NSEntityDescription, а не создавала новый.


Заключение


Создание coreData-модели из кода — сложный процесс со своими нюансами и подводными камнями. В большинстве случаев программисту это не нужно, проще воспользоваться функционалом графического редактора модели. Но модель в коде ускоряет сборку (не так сильно, как избавление от StoryBoard, но всё же ощутимо), уменьшает время на запуск приложения у пользователя и избавляет нас от необходимости обрабатывать ошибки при чтении файла модели с диска.


Ну а я пошёл на подобное усложнение исключительно из-за того, что я незрячий, и мне вслепую очень трудно работать как со StoryBoard, так и с XCDataModel.


Очень рекомендую изучить


  1. NSManagedObjectModel;
  2. NSEntityDescription;
  3. NSPropertyDescription и NSAttributeDescription;
  4. Необязательно, но можно ещё почитать про NSPersistentContainer и его init(name:managedObjectModel:) инициализатор.

Комментарии 0

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

Самое читаемое