Если вам по какой-то причине не нравится процесс создания 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 мы поговорим во второй части данной статьи.
Итак, мы делаем следующее:
- Создаём описания сущностей CompanyMO и WorkerMO;
- Добавляем в массив свойств каждой сущности необходимые объекты, описывающие свойства. Для companyEntity это companyName (про workers во второй части), а для workerEntity это firstname и lastname.
- Заполняем массив 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 абсолютно так же, как работали до переноса модели в код.
Плюсы описания модели в коде
- При запуске приложения не может возникнуть ошибка чтения файла модели, так как файла изначально просто нет;
- Приложение запускается чуть быстрее (опять-таки из-за того, что не нужно считывать файл);
- Мы автоматически избавляемся от ненужных опционалов в субклассах (это можно сделать и вручную, сгенерировав субклассы и удалив лишние "?", но всё же).
Сокращаем количество кода
С добавленными нами инициализаторами всё выглядит довольно симпатично, но представим ситуацию, когда у нашей сущности не два свойства, а, допустим, десять или больше.
Тогда описание модели превращается в длинную простыню похожего кода, который неудобно читать.
Давайте упакуем этот код внутрь класса сущности, чтобы каждый класс описывал все свои свойства внутри себя.
Рассмотрим на примере класса 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.
Очень рекомендую изучить
- NSManagedObjectModel;
- NSEntityDescription;
- NSPropertyDescription и NSAttributeDescription;
- Необязательно, но можно ещё почитать про NSPersistentContainer и его init(name:managedObjectModel:) инициализатор.