Hello CoreData!

Сьогодні ми розглянемо фреймворк під жахливою назвою CoreData від Apple. Я його дуже не люблю. Це рішення Apple для роботи з SQLite (реляційною базою даних). CoreData може зберігати об'єкти Swift в SQLite, а також виконувати зворотну операцію.

Вступ

Я особисто віддаю перевагу Realm, оскільки вам не потрібно робити багато рухів, щоб змусити його працювати: просто встановіть і починайте створювати все, що хочете, плюс на офіційному сайті є чудова документація для всіх функцій. Але коли ви переглядаєте вакансії інших компаній (я особисто ніколи цього не роблю. Інші компанії... вони існують? Справді?!:)). Часто ви бачите, що CoreData є обов'язковою, і якщо у вас немає досвіду з цим фреймворком, то ви легко можете «зникнути в повітрі» на початку співбесіди, і ніхто не дізнається про всі ті красиві кнопки, які ви можете створити :).

Отже, чому мені не подобається цей фреймворк? Тому що він важкий для розуміння для початківців, і легко заплутатися і зробити помилки, а чим більше коду ви написали, тим більше шансів, що десь ви могли помилитися.

Починаючи з iOS 10, Apple зробила багато для полегшення роботи з CoreData. Наприклад, вони представили об'єкт NSPersistentContainer, який бере на себе завдання створення NSManagedObjectModel, NSPersistentStoreCoordinator та NSManagedObjectContext. Тепер, щоб почати, вам просто потрібно ініціалізувати об'єкт persistentContainer = NSPersistentContainer(name: "MyModel"). Спочатку ця стаття була задумана як огляд сторонніх рішень для CoreData, але оскільки Apple змінила цей фреймворк, багато з тих рішень були або в процесі переробки, або, ще гірше, — написані на Objective-C. Отже, у нас буде три приклади. У першому ми пройдемо через NSFetchedResultsController та NSPersistentContainer; у другому ми почнемо писати зручний клас, а в останньому прикладі ми покращимо клас, додавши генерики. .

Приклад 1

Проект доступний для вивчення і називається MagicalRecord_project (спочатку я хотів використовувати цей фреймворк). Давайте переглянемо файл Model.xcdatamodeld:

Перегляд Model.xcdatamodeld

Перегляд Model.xcdatamodeld

Для атрибута avatar (типу Binary Data) була активована опція Allows External Storage.

Перейдемо до контролера:

fileprivate let persistentContainer: NSPersistentContainer = NSPersistentContainer(name:"Model")
 
    fileprivate lazy var fetchedResultsController: NSFetchedResultsController<person> ={
        let fetchRequest = NSFetchRequest<person>()
        fetchRequest.entity = Person.entity()
        fetchRequest.sortDescriptors =[NSSortDescriptor(key:"name", ascending:true)]
        let frc = NSFetchedResultsController(fetchRequest: fetchRequest,
                                             managedObjectContext: self.persistentContainer.viewContext,
                                             sectionNameKeyPath:"name",
                                             cacheName:nil)
        frc.delegate = self
        return frc
    }()</person></person>

Тут ми створюємо об'єкти persistentContainer та fetchedResultsController, які відповідатимуть за заповнення даними. Також ми створимо метод, який допоможе нам створювати об'єкти:

func createPersonObject(name: String, about: String, image: UIImage){
        let person =NSEntityDescription.insertNewObject(forEntityName:"Person", into: persistentContainer.viewContext) as! Person
        person.name = name
        person.about = about
        person.avatar = UIImageJPEGRepresentation(image, 1.0) as NSData?
    }

Подивіться, як це виглядає у viewDidLoad:

override func viewDidLoad(){
        super.viewDidLoad()
 
        tableView.delegate = self
        tableView.dataSource = self
 
        //створити дані
        createPersonObject(name:"Ім'я A", about:"деяка інформація...", image: UIImage(named:"01")!)
        createPersonObject(name:"Ім'я B", about:"деяка інформація...", image: UIImage(named:"02")!)
 
        perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0)
 
        //отримати дані в контролеріdo{
            try fetchedResultsController.performFetch()} catch {
            print(error)}}

Цей рядок perform(#selector(insertAfterDelay), with: nil, afterDelay: 2.0) додає третій об'єкт до контексту, а NSFetchedResultsController оновлює tableView (про це ми поговоримо пізніше).

fetchedResultsController.performFetch() передає дані з CoreData до контролера.

Тепер запустіть цей додаток.

Емулятор

Емулятор після додавання нового об'єкта

На початку ми бачимо два доданих об'єкти, а через дві секунди — три. Давайте створимо extension для ViewController:

Оскільки у нас є fetchedResultsController замість масиву dataSource, ми використовуємо його для отримання всіх необхідних даних. Змінну fileprivate lazy var fetchedResultsController ми оголосили як делегат NSFetchedResultsController. Тепер ми створимо необхідні методи:

extension ViewController: NSFetchedResultsControllerDelegate {
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<nsfetchrequestresult>){
        tableView.beginUpdates()}
 
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<nsfetchrequestresult>){
        tableView.endUpdates()}
 
    func controller(_ controller: NSFetchedResultsController<nsfetchrequestresult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){
 
        switch type {case .insert: tableView.insertRows(at:[newIndexPath!], with: .automatic)case .delete: tableView.deleteRows(at:[indexPath!], with: .automatic)case .update: tableView.reloadRows(at:[indexPath!], with: .automatic)case .move: tableView.moveRow(at: indexPath!, to: newIndexPath!)}}
 
    func controller(_ controller: NSFetchedResultsController<nsfetchrequestresult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType){
 
        switch type {case .insert: tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic)case .delete: tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic)case .move, .update: tableView.reloadSections(IndexSet(integer: sectionIndex), with: .automatic)}}}
</nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult>

Тепер, коли щось змінюється (додається, оновлюється або видаляється якийсь елемент), NSFetchedResultsController знає про це і може внести необхідні корективи в UITableView. Подібний механізм реалізовано в Realm:

Це не зовсім відповідає нашому прикладу, але, як ви можете побачити, механізми оновлення UITableView подібні.

Приклад 2

У цьому прикладі ми створимо StoreManager. Відкрийте проект StoreManager_project. StoreManager був створений за допомогою патерну Singleton (один екземпляр на все життя програми):

fileprivate override init(){}static let sharedInstance = StoreManager()
    override func copy()-&gt; Any {
        fatalError("Вам не дозволено використовувати метод copy в singleton!")}
 
fileprivate lazy var fetchRequest: NSFetchRequest<car> ={
        let request = NSFetchRequest<car>()
        request.entity = Car.entity()
        request.sortDescriptors =[NSSortDescriptor(key:"name", ascending:true)]return request
    }()
 
    fileprivate lazy var persistentContainer: NSPersistentContainer ={
        let container = NSPersistentContainer(name:"Model")
        container.loadPersistentStores(completionHandler:{(storeDescription, error)inif let error = error as NSError? {
                fatalError("Невирішена помилка \(error), \(error.userInfo)")}})return container
    }()
</car></car>

NSPersistentContainer має дві властивості, а саме, властивість і метод: viewContext та newBackgroundContext. Перша пов'язана з main queue, друга — з privateQueueConcurrencyType. Коли ми щось записуємо в newBackgroundContext, він надсилає сповіщення для об'єкта viewContext, щоб об'єднати вміст. Тому, здається, нам більше не потрібно підписуватися на це сповіщення. З документації до viewContext:

Цей контекст налаштований на те, щоб бути генераційним і автоматично споживати сповіщення про збереження з інших контекстів.

Завдяки newBackgroundContext:

…встановлено на автоматичне споживання трансляцій NSManagedObjectContextDidSave.

Я додав кілька методів для модифікації в третьому прикладі:

По-перше, ми очищаємо сховище, а потім додаємо моделі автомобілів. Потім є метод для додавання третього автомобіля через дві секунди (ніколи не використовуйте performSelector: afterDelay: — це погана практика). А також два методи showMainStorage(), які показують, як змінилося сховище. Наприклад, ви можете розкоментувати наступний фрагмент коду:

NotificationCenter.default.addObserver(self, selector:#selector(storageHasChanged(note:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
 
deinit {
        NotificationCenter.default.removeObserver(self)}
 
//MARK: Дії
    func storageHasChanged(note:NSNotification){
        let inserted = note.userInfo?["inserted"]
        let deleted = note.userInfo?["deleted"]
        let updated = note.userInfo?["updated"]
 
        if let insertedSet = inserted as? Set<car>, insertedSet.count &gt; 0{
            print("ДОДАНО")
            print(insertedSet.map { $0.name })}if let deletedSet = deleted as? Set<car>, deletedSet.count &gt; 0{
            print("ВИДАЛЕНО")
            print(deletedSet.map { $0.name })}if let updatedSet = updated as? Set<car>, updatedSet.count &gt; 0{
            print("ОНОВЛЕНО")
            print(updatedSet.map { $0.name })}}
</car></car></car>

Ви можете експериментувати з повідомленням про зміни в базі даних. Мій результат:

[Optional("Zapor")]
INSERTED
[Optional("Fiat")]
INSERTED
[Optional("F1")]
DELETED
[Optional("Zapor"), Optional("F1"), Optional("Fiat"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("Alfa Romeo"), Optional("Alfa Romeo"), Optional("BMW"), Optional("Fiat"), Optional("BMW"), Optional("F1"), Optional("F1"), Optional("Alfa Romeo"), Optional("F1"), Optional("Fiat"), Optional("Alfa Romeo"), Optional("Zapor"), Optional("Zapor"), Optional("Zapor")]
CARS: Optional(["BMW", "F1", "Fiat", "Zapor"])
INSERTED
[Optional("Alfa Romeo")]
CARS: Optional(["Alfa Romeo", "BMW", "F1", "Fiat", “Zapor"])

Приклад 3

У цьому прикладі я хотів продемонструвати, як працювати з JSON. Для невеликого уроку я запросив JSON у вигляді рядка, а потім розпарсив його:

let json ="{\"cars\":[{\"name\":\"ZaZ\"},{\"name\":\"Lada\"},{\"name\":\"Lexus\"}]}" 
        let dict = try! JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.allowFragments) as![String:Any]

А в файлі StoreManager:

//щоб переконатися, що у нас є лише одна копія
    lazy var backgroundContext:NSManagedObjectContext={return self.persistentContainer.newBackgroundContext()}()
 
    fileprivate lazy var persistentContainer: NSPersistentContainer ={
        let container = NSPersistentContainer(name:"Model")
        container.loadPersistentStores(completionHandler:{(storeDescription, error)inif let error = error as NSError? {
                fatalError("Невирішена помилка \(error), \(error.userInfo)")}})return container
    }()
 
    func saveContext(type: ContextType){
        let context = type == .main ? persistentContainer.viewContext : backgroundContext
        if context.hasChanges {do{
                try context.save()} catch {
                let nserror = error as NSError
                fatalError("Невирішена помилка \(nserror), \(nserror.userInfo)")}}}
 
    func getMainContextObjects<t: NSManagedObject>(entity: T.Type)-> [NSManagedObject]? {return persistentContainer.viewContext.fetchAll(entity: entity, fetchConfiguration:nil)}
 
    func clearStorage<t: NSManagedObject>(type: ContextType, entity: T.Type){
        let context = type == .main ? persistentContainer.viewContext : backgroundContext
        let objects = context.fetchAll(entity: entity, fetchConfiguration:nil)if let objects = objects {for item in objects {
                context.delete(item)}}}
 
    func showStorage<t: NSManagedObject>(type: ContextType, entity: T.Type){
        let context = type == .main ? persistentContainer.viewContext : backgroundContext
        let objects = context.fetchAll(entity: entity, fetchConfiguration:nil)
        print("OBJECTS: \(objects)")
        print("COUNT: \(objects?.count)")}
</t:></t:></t:>

Тут я зберігаю backgroundContext як змінну, щоб завжди отримувати лише один backgroundContext, який потім можу зберегти за допомогою методу saveContext(type:). Інші методи зрозумілі. Оскільки у нас є enum...

enum ContextType {case main
    case background
}

...ми завжди знаємо контекст, з яким працюємо. Повернемося до ViewController. Зверніть увагу, як я створюю об'єкти:

for item in dict["cars"] as![Any]{
            let car = StoreManager.sharedInstance.backgroundContext.insert(entity: Car.self)
            car.name =(item as? [String:Any])?["name"] as! String?
        }

Але в методі insert(entity:) немає контексту! Справа в тому, що я написав розширення для роботи з узагальненими даними.

//MARK: Розширення CoreData
extension NSManagedObject{
    class var entityName : String {
        let components = NSStringFromClass(self)return components
    }
 
    class func fetchRequestObj()-&gt; NSFetchRequest<nsfetchrequestresult> {
        let request = NSFetchRequest<nsfetchrequestresult>(entityName: self.entityName)return request
    }
 
    class func fetchRequestWithKey(key: String, ascending: Bool =true)-&gt; NSFetchRequest<nsfetchrequestresult> {
        let request = fetchRequestObj()
        request.sortDescriptors =[NSSortDescriptor(key: key, ascending: ascending)]return request
    }}
 
extension NSManagedObjectContext{
    func insert<t: nsmanagedobject>(entity: T.Type)-&gt; T {
        let entityName = entity.entityName
        returnNSEntityDescription.insertNewObject(forEntityName: entityName, into: self) as! T
    }
 
    func fetchAll<t: nsmanagedobject>(entity: T.Type, fetchConfiguration:((NSFetchRequest<nsmanagedobject>)-&gt; Void)?)-&gt; [NSManagedObject]? {
        let dataFetchRequest = NSFetchRequest<nsmanagedobject>(entityName: entity.entityName)
 
        fetchConfiguration?(dataFetchRequest)
        var result =[NSManagedObject]()do{
            result = try self.fetch(dataFetchRequest)} catch {
            print("Не вдалося отримати дані, критична помилка: \(error)")}return result
    }}
</nsmanagedobject></nsmanagedobject></t:></t:></nsfetchrequestresult></nsfetchrequestresult></nsfetchrequestresult>

Таким чином, ви можете додати сутність у контекст, використовуючи її тип і self (Car.self). Також було додано метод fetchAll для роботи з узагальненнями. Ми використовуємо його в методах StoreManager. Тепер наш менеджер готовий до роботи. Сподіваюсь, що протестую і покращу його, якщо буде потрібно.