Core Data + Swift для самых маленьких: необходимый минимум (часть 2)

в 16:49, , рубрики: core data, ios development, swift, разработка под iOS

Это вторая часть трилогии о Core Data, первая доступна здесь: Core Data + Swift для самых маленьких: необходимый минимум (часть 1).

В первой части мы познакомились с общими сведениями о Core Data, основными компонентами (NSManagedObjectModel, NSPersistentStoreCoordinator, NSManagedObjectContext), Редактором модели данных и создали нашу модель данных.

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

NSEntityDescription и NSManagedObject

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

NSManagedObject — это сам управляемый объект, экземпляр сущности. Продолжая аналогию с СУБД (начатую в прошлой статье), можно сказать, что NSManagedObject — запись (строка) в таблице базы данных.

Чтобы понять, как с этим работать, давайте создадим нового Заказчика. Так как у нас еще нет готовой интерфейсной части (мы займемся этим в следующей статье), то давайте немного попрограммируем прямо в модуле делегата приложения (AppDelegate.swift). Не беспокойтесь, это только демонстрация, которая важна для понимания, чуть позже мы все перенесем отсюда в другое место. Я воспользуюсь для демонстрации работы следующей функцией:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
	// Здесь я буду размещать примеры
	// …
	return true
}

Создание управляемого объекта (в данном случае Заказчика) выполняется следующим образом:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        // Описание сущности
        let entityDescription = NSEntityDescription.entityForName("Customer", inManagedObjectContext: self.managedObjectContext)

        // Создание нового объекта
        let managedObject = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
        
        return true
    }

Сначала мы получаем описание сущности (entityDescription), передав в соответствующий конструктор строку с именем нужной нам сущности и ссылку на контекст. Как это работает: контекст управляемого объекта, как мы помним из первой части, связан с координатором постоянного хранилища, а координатор, в свою очередь, связан с объектной моделью данных, где и будет произведен поиск сущности по указанному имени. Обратите внимание, что данная функция возвращает опциональное значение.

Затем, на основании полученного описания сущности, мы создаем сам управляемый объект (managedObject). Вторым параметром мы передаем контекст, в котором этот объект должен быть создан (в общем случае, как вы помните, может быть несколько контекстов).

Хорошо, мы создали объект, как теперь установить значения его атрибутов? Для это используется кодирование по типу Key-Value, суть которого в том, что есть два универсальных метода, один который устанавливает указанное значение по указанному имени, а второй извлекает значение по указанному имени. Звучит гораздо сложнее, чем выглядит.

 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        // Описание сущности
        let entityDescription = NSEntityDescription.entityForName("Customer", inManagedObjectContext: self.managedObjectContext)

        // Создание нового объекта
        let managedObject = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
        
        // Установка значения атрибута
        managedObject.setValue("ООО «Ромашка»", forKey: "name")
        
        // Извлечение значения атрибута
        let name = managedObject.valueForKey("name")
        print("name = (name)")
        
        return true
    }

Вывод консоли:

	name = Optional(ООО «Ромашка»)

Как видите, все довольно просто. Идем дальше. Теперь надо сохранить этот объект в нашей базе данных. Разве то, что мы создали объект — недостаточно? Нет, любой объект «живет» в конкретном определенном контексте и только там. Вы можете его там создавать, модифицировать и даже удалять, но это все будет происходить внутри определенного контекста. До тех пор, пока вы явно не сохраните все изменения контекста, вы не измените реальных данных. Можно провести аналогию с файлом на диске, который вы открываете для редактирования — пока вы не нажали кнопку «Сохранить» никакие изменения не записаны. На самом деле это очень удобно и здорово оптимизирует весь процесс работы с данными.

Сохранение изменений контекста выполняется элементарно:

	managedObjectContext.save()

У нас даже есть в модуле делегата готовая функция для более «умного» сохранения (мы говорили о ней вскользь в прошлой статье), запись происходит только в том случае, если данные действительно изменены:

  func saveContext () {
        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                let nserror = error as NSError
                NSLog("Unresolved error (nserror), (nserror.userInfo)")
                abort()
            }
        }
    }

Таким образом, весь код создания и записи объекта будет выглядеть следующим образом:

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        // Описание сущности
        let entityDescription = NSEntityDescription.entityForName("Customer", inManagedObjectContext: self.managedObjectContext)

        // Создание нового объекта
        let managedObject = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
        
        // Установка значения атрибута
        managedObject.setValue("ООО «Ромашка»", forKey: "name")
        
        // Извлечение значения атрибута
        let name = managedObject.valueForKey("name")
        print("name = (name)")
        
        // Запись объекта
        self.saveContext()
        
        return true
    }

Мы создали объект и записали его в нашу базу данных. Как нам теперь его получить обратно? Это не намного сложнее. Давайте взглянем на код.

      let fetchRequest = NSFetchRequest(entityName: "Customer")
        do {
            let results = try self.managedObjectContext.executeFetchRequest(fetchRequest)
        } catch {
            print(error)
        }

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

Важное замечание: функция managedObjectContext.executeFetchRequest всегда возвращает массив объектов, даже если объект всего один — возвращен будет массив, если объектов нет вообще — пустой массив.

С учетом вышесказанного, у нас будет следующий текст функции:

 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        // Описание сущности
        let entityDescription = NSEntityDescription.entityForName("Customer", inManagedObjectContext: self.managedObjectContext)

        // Создание нового объекта
        let managedObject = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
        
        // Установка значения атрибута
        managedObject.setValue("ООО «Василек»", forKey: "name")
        
        // Извлечение значения атрибута
        let name = managedObject.valueForKey("name")
        print("name = (name)")
        
        // Запись объекта
        self.saveContext()
        
        // Извление записей
        let fetchRequest = NSFetchRequest(entityName: "Customer")
        do {
            let results = try self.managedObjectContext.executeFetchRequest(fetchRequest)
            for result in results as! [NSManagedObject] {
                print("name - (result.valueForKey("name")!)")
            }
        } catch {
            print(error)
        }

        return true
    }

Вывод консоли:

name = Optional(ООО «Василек»)
name - ООО «Василек»
name - ООО «Ромашка»

Как только вы получили объект, в вышеприведенном листинге это переменная result внутри цикла, то вы можете его произвольным образом редактировать (здесь нет никаких отличий от установки атрибутов для нового объекта), либо удалить. Удаление осуществляется вызовом соответствующего метода переменной контекста, которому в качестве параметра и передается удаляемый объект:

self.managedObjectContext.deleteObject(result)

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

Небольшое факультативное дополнение

Если вы хотите «пощупать» Core Data поближе, на уровне таблиц, то это проще чем может показаться. Если вы используете Симулятор, то файл базы данных лежит где-то здесь:

/Users/<USER>/Library/Developer/CoreSimulator/Devices/<DEVICE_ID>/data/Containers/Data/Application/<APPLICATION_ID>/Documents/<FileName>.sqlite

Не торопитесь искать этот файл вручную, гадая какой-же ID у вашего приложения. Есть замечательная утилита, делающая это все за вас — SimSim (Пользуясь случаем, хочу сказать спасибо авторам).

После запуска она висит в строке меню и выглядит вот так (значок летучей мышки):
Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 1

Собственно, назначение очевидно: утилита показывает список хранилищ установленных на симуляторе приложений и позволяет сразу к ним перейти:
Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 2

Для просмотра самого файла SQLite можно воспользоваться любым бесплатным просмоторщиком, например Datum Free
Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 3

Автогенерация классов Core Data

Метод Key-Value, хорош тем, что он прост, универсален и работает «из коробки». Но есть два момента, которые портят впечатление: во-первых, кода больше, чем хотелось бы, а во-вторых, передавая имя реквизита каждый раз в виде строки, легко ошибиться (автодополнения здесь нет). И как нам быть, если захочется немного больше функциональности от управляемых объектов, например, вычисляемые поля или свои конструкторы? У Core Data есть решение! Мы легко можем создать свой класс (даже больше — Core Data сделает это за нас), унаследовав его от NSManagedObject и дополнив всем необходимым. В результате, мы сможем работать с управляемым объектов как с обычным объектом ООП, создавая его путем вызова своего конструктора и обращаясь к его полям «через точку» используя автодополнение (то есть вся мощь ООП в ваших руках).

Откройте Редактор модели данных и выделите любую сущность. Выберете в меню (оно контекстно-зависимое, поэтому надо выделить какую-нибудь сущность) Editor Create NSManagedObject Subclass…

Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 4

Откроется окно выбора модели данных; да, в общем случае, может быть несколько независимых моделей данных, но у нас она одна, поэтому выбор очевиден.
Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 5

В следующем окне нам предлагают выбрать сущности, для которых необходимо сгенерировать классы, давайте выберем сразу все.
Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 6

Следующее стандартное окно вам должно быть знакомо, единственное что здесь может насторожить, это опция «Use scalar properties for primitive data types». В чем смысл этой опции: если эту опцию не выбирать, то вместо примитивных типов данных (Float, Double, Int и прочее) будет использоваться своеобразная «обертка», содержащая значение внутри себя. Это скорее актуально для Objective-C, так как там нет такого понятия как Optional. Но мы используем Swift, так что я не вижу причин не выбирать эту опцию (возможно, более опытные коллеги в комментариях меня поправят).

Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 7

В итоге, Core Data создаст для нас несколько файлов, давайте посмотрим, что это за файлы.
Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 8

Каждая сущность представлена парой файлов, например:

  • Customer.swift — этот файл для вас, вы можете добавить туда любую необходимую вам функциональность, что мы сейчас и сделаем.
  • Customer+CoreDataProperties.swift — это файл Core Data, его лучше не трогать и вот почему: в этом файле содержится описание атрибутов и взаимосвязей вашей сущности, то есть, если вы внесете изменения в эту часть, то у вас сущность и ее представляющий класс не будут согласованы.

Также, если вы по каким-то причинам решите изменить модель данных, то вы можете пересоздать эти сгенерированные классы. В этом случае, первый файл (Customer.swift) останется не тронутым, а второй (Customer+CoreDataProperties.swift) будет полностью замещен новым.

Хорошо, мы создали классы для наших сущностей, теперь мы можем обращаться к полям класса «через точку», давайте придадим нашему примеру более привычный вид.

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        // Описание сущности
        let entityDescription = NSEntityDescription.entityForName("Customer", inManagedObjectContext: self.managedObjectContext)

        // Создание нового объекта
        let managedObject = Customer(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
        
        // Установка значения атрибута
        managedObject.name = "ООО «Василек»"
        
        // Извлечение значения атрибута
        let name = managedObject.name
        print("name = (name)")
        
        // Запись объекта
        self.saveContext()
        
        // Извление записей
        let fetchRequest = NSFetchRequest(entityName: "Customer")
        do {
            let results = try self.managedObjectContext.executeFetchRequest(fetchRequest)
            for result in results as! [Customer] {
                print("name - (result.name!)")
            }
        } catch {
            print(error)
        }
        
        return true
    }

Так гораздо лучше. Но создание объекта выглядит несколько тяжеловесно. Можно было бы спрятать все это в конструктор, но для этого нам нужна ссылка на управляемый контекст в котором должен быть создан объект. Кстати, мы до сих пор пишем код в модуле делегата, так как именно здесь у нас определен Core Data Stack. Может можно придумать что-нибудь получше?

Core Data Manager

Наиболее распространенной практикой при работе с Core Data является использование паттерна Singleton на базе Core Data Stack. Напомню, если кто не знает или забыл, что Singleton гарантирует наличие только одного экземпляра класса с глобальной точкой доступа. То есть, у класса всегда существует один и только один объект, независимо от того, кто, когда и откуда к нему обращается. Этот подход мы сейчас и реализуем, у нас будет Singleton для глобального доступа и управления Core Data Stack.

Создайте новый пустой файл с именем CoreDataManager.swift

Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 9

Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 10

Core Data + Swift для самых маленьких: необходимый минимум (часть 2) - 11

Для начала давайте добавим директиву импорта Core Data и создадим сам Singleton.

import CoreData
import Foundation

class CoreDataManager {
    
    // Singleton
    static let instance = CoreDataManager()
   
}

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

import CoreData
import Foundation

class CoreDataManager {
    
    // Singleton
    static let instance = CoreDataManager()
    
    // MARK: - Core Data stack
    
    lazy var applicationDocumentsDirectory: NSURL = {
        let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
        return urls[urls.count-1]
    }()
    
    lazy var managedObjectModel: NSManagedObjectModel = {
        let modelURL = NSBundle.mainBundle().URLForResource("core_data_habrahabr_swift", withExtension: "momd")!
        return NSManagedObjectModel(contentsOfURL: modelURL)!
    }()
    
    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
    }()
    
    lazy var managedObjectContext: NSManagedObjectContext = {
        let coordinator = self.persistentStoreCoordinator
        var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = coordinator
        return managedObjectContext
    }()
    
    // MARK: - Core Data Saving support
    func saveContext () {
        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                let nserror = error as NSError
                NSLog("Unresolved error (nserror), (nserror.userInfo)")
                abort()
            }
        }
    }
}

Теперь у нас есть Singleton и мы можем обращаться к Core Data Stack из любой точки нашего приложения. Например, обращение к управляемому контексту будет выглядеть так:

CoreDataManager.instance.managedObjectContext 

Давайте теперь перенесем все необходимое для создания управляемого объекта в его конструктор.

//  Customer.swift
//  core-data-habrahabr-swift
import Foundation
import CoreData

class Customer: NSManagedObject {
    convenience init() {
        // Описание сущности
        let entity = NSEntityDescription.entityForName("Customer", inManagedObjectContext: CoreDataManager.instance.managedObjectContext)
   
        // Создание нового объекта
        self.init(entity: entity!, insertIntoManagedObjectContext: CoreDataManager.instance.managedObjectContext) 
    }
}

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

self.managedObjectContext

надо заменить следующей

CoreDataManager.instance.managedObjectContext

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

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Создание нового объекта
        let managedObject = Customer()
        
        // Установка значения атрибута
       managedObject.name = "ООО «Колокольчик»"
         
        // Извлечение значения атрибута
        let name = managedObject.name
        print("name = (name)")
        
        // Запись объекта
        CoreDataManager.instance.saveContext()
        
        // Извление записей
        let fetchRequest = NSFetchRequest(entityName: "Customer")
        do {
            let results = try CoreDataManager.instance.managedObjectContext.executeFetchRequest(fetchRequest)
            for result in results as! [Customer] {
                print("name - (result.name!)")
            }
        } catch {
            print(error)
        }
        return true
    }

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

Давайте вернемся в модуль CoreDataManager.swift и добавим функцию entityForName.

import CoreData
import Foundation

class CoreDataManager {
    // Singleton
    static let instance = CoreDataManager()
    
    // Entity for Name
    func entityForName(entityName: String) -> NSEntityDescription {
        return NSEntityDescription.entityForName(entityName, inManagedObjectContext: self.managedObjectContext)!
    }

Теперь вернемся в модуль Customer.swift и изменим код следующий образом.

import Foundation
import CoreData

class Customer: NSManagedObject {
    convenience init() {
        self.init(entity: CoreDataManager.instance.entityForName("Customer"), insertIntoManagedObjectContext: CoreDataManager.instance.managedObjectContext)
    }
}

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

//  Order.swift
//  core-data-habrahabr-swift

import Foundation
import CoreData

class Order: NSManagedObject {
    convenience init() {
        self.init(entity: CoreDataManager.instance.entityForName("Order"), insertIntoManagedObjectContext: CoreDataManager.instance.managedObjectContext)
    }
}

Вместо заключения

Обратите внимание, что CoreDataManager, который мы создали, довольно универсален, в том смысле, что его можно использовать в любом приложении, основанном на Core Data. Единственное, что его связывает именно с нашим проектом, — это имя файла модели данных. Больше ничего. То есть, написав этот модуль один раз, вы можете им пользоваться постоянно в разных проектах.

В следующей, заключительной части, мы будем много работать со Storyboard и UITableViewController, познакомимся с NSFetchedResultsController и еще раз вспомним NSFetchRequest.

Этот проект на GitHub

Автор: angryscorp

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js