Core Data + Swift for the smallest: the necessary minimum (part 2)

  • Tutorial
This is the second part of the Core Data trilogy, the first is available here: Core Data + Swift for the smallest: the minimum required (part 1) .

In the first part, we got acquainted with general information about Core Data, the main components (NSManagedObjectModel, NSPersistentStoreCoordinator, NSManagedObjectContext), Data Model Editor and created our data model.

In this part, we will work with objects, get acquainted with NSEntityDescription and NSManagedObject, class auto-generation, and write a helper class that significantly improves the usability of Core Data.


NSEntityDescription and NSManagedObject


Let's start with NSEntityDescription - as the name suggests, this is an object that contains a description of our essence. Everything that we fantasized with the entity in the Data Model Editor (attributes, relationships, deletion rules, etc.) is contained in this object. The only thing we will do with it is to receive it and pass it somewhere as a parameter, nothing more.

NSManagedObject is the managed entity itself, an entity instance. Continuing the analogy with the DBMS (started in the previous article), we can say that NSManagedObject is a record (row) in the database table.

To understand how to work with this, let's create a new Customer. Since we do not yet have a ready-made interface part (we will deal with this in the next article), let's program a bit directly in the application delegate module ( AppDelegate.swift). Do not worry, this is only a demonstration that is important for understanding, a little later we will transfer everything from here to another place. I will use to demonstrate the operation of the following function:

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

Creating a managed object (in this case, the Customer) is performed as follows:

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
    }

First, we get an entity description ( entityDescription ), passing to the appropriate constructor a string with the name of the entity we need and a link to the context. How it works: the context of the managed object, as we recall from the first part, is connected with the coordinator of the permanent storage, and the coordinator, in turn, is connected with the data object model, where the entity will be searched by the specified name. Please note that this function returns an optional value.

Then, based on the obtained description of the entity, we create the managed object itself ( managedObject ). The second parameter we pass the context in which this object should be created (in the general case, as you remember, there can be several contexts).

Ok, we created an object, how now to set the values ​​of its attributes? For this, encoding is of the Key-Value type , the essence of which is that there are two universal methods, one that sets the specified value by the specified name, and the second extracts the value by the specified name. It sounds a lot harder than it looks.

 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
    }

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

As you can see, everything is quite simple. Move on. Now we need to save this object in our database. Is it not enough that we created the object? No, any object "lives" in a specific specific context and only there . You can create, modify and even delete it there, but this will all happen within a specific context. Until you explicitly save all changes to the context, you do not change the actual data. You can draw an analogy with the file on the disk that you open for editing - until you click the "Save" button, no changes are recorded. In fact, it is very convenient and great to optimize the entire process of working with data.

Saving context changes is done elementarily:
	managedObjectContext.save()

We even have a ready-made function in the delegate module for a more “smart” save (we talked about it in passing in a previous article), recording occurs only if the data is really changed:

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

Thus, the entire code for creating and writing an object will look like this:

    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
    }

We created an object and recorded it in our database. How do we get it back now? This is not much more complicated. Let's take a look at the code.
      let fetchRequest = NSFetchRequest(entityName: "Customer")
        do {
            let results = try self.managedObjectContext.executeFetchRequest(fetchRequest)
        } catch {
            print(error)
        }

Here we create an NSFetchRequest request object , passing the name of the entity whose data we want to receive as a parameter to the constructor. Then we call the context method, passing this request as a parameter. This is the simplest option for retrieving records, in general NSFetchRequest is very flexible and provides extensive options for retrieving data under certain conditions. An example of filtering and sorting data with its help we will consider in the next part of the article.

Important note: the managedObjectContext.executeFetchRequest function always returns an array of objects, even if there is only one object, an array will be returned, if there are no objects at all, an empty array.

Based on the foregoing, we will have the following function text:
 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
    }

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

As soon as you receive the object, in the above listing it is the result variable inside the loop, then you can edit it arbitrarily (there are no differences from setting attributes for a new object), or delete it. Deletion is performed by calling the corresponding method of the context variable, to which the deleted object is passed as a parameter:
self.managedObjectContext.deleteObject(result)

After deletion, it is also necessary to force the persistence of the context, do not forget about it.

Small optional addition
If you want to “touch” Core Data closer at the table level, then this is easier than it might seem. If you use the Simulator, then the database file lies somewhere here:
/Users//Library/Developer/CoreSimulator/Devices//data/Containers/Data/Application//Documents/.sqlite

Do not rush to search for this file manually, guessing what kind of ID your application has. There is a wonderful utility that does it all for you - SimSim (I take this opportunity to say thanks to the authors).

After launch, it hangs in the menu bar and looks like this (bat icon):


Actually, the purpose is obvious: the utility shows the list of storages of applications installed on the simulator and allows you to go directly to them:


To view the SQLite file itself, you can use any free viewer, for example Datum Free


Auto Generate Core Data Classes


The Key-Value method is good in that it is simple, versatile and works out of the box. But there are two points that spoil the impression: firstly, there is more code than we would like, and secondly, passing the name of the props every time as a string, it is easy to make a mistake (there is no auto-completion here). And what should we do if we want a little more functionality from managed objects, for example, calculated fields or our own constructors? Core Data has a solution! We can easily create our own class (even more - Core Data will do it for us) by inheriting it from NSManagedObject and adding everything we need. As a result, we will be able to work with managed objects as a normal OOP object, creating it by calling our constructor and accessing its fields “through the point” using autocompletion (that is, all the power of OOP is in your hands).

Open the Data Model Editor and select any entity. Select in the menu (it is context-sensitive, so you need to select some entity) Editor \ Create NSManagedObject Subclass ...



A window for selecting a data model will open; Yes, in general, there may be several independent data models, but we have one, so the choice is obvious.


In the next window, we are prompted to select the entities for which we need to generate classes, let's select everything at once.


The next standard window should be familiar to you, the only thing that may alert you is the option “Use scalar properties for primitive data types”. What is the meaning of this option: if this option is not selected, then instead of primitive data types (Float, Double, Int, etc.), a kind of “wrapper” will be used that contains the value inside itself. This is more likely to be true for Objective-C , since there is no such thing as Optional . But we use Swift , so I see no reason not to choose this option (perhaps more experienced colleagues in the comments will correct me).



As a result, Core Data will create several files for us, let's see what these files are.


Each entity is represented by a pair of files, for example:
  • Customer.swift - this file is for you, you can add there any functionality you need, which we will do now.
  • Customer+CoreDataProperties.swift - this is a Core Data file, it’s better not to touch it and here’s why: this file contains a description of the attributes and relationships of your entity, that is, if you make changes to this part, then your entity and its representing class will not be consistent.

Also, if for some reason you decide to change the data model, then you can recreate these generated classes. In this case, the first file ( Customer.swift) will remain untouched, and the second ( Customer+CoreDataProperties.swift) will be completely replaced by the new one.

Well, we created classes for our entities, now we can access the fields of the class "through the point", let's give our example a more familiar look.
    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
    }

So much better. But creating an object looks a bit heavy. It would be possible to hide all this in the constructor, but for this we need a link to the managed context in which the object should be created. By the way, we are still writing code in the delegate module, since it is here that we have the Core Data Stack defined. Maybe you can come up with something better?

Core data manager


The most common practice when working with Core Data is to use the Singleton pattern based on the Core Data Stack. Let me remind you if someone does not know or forgot that Singleton guarantees the presence of only one instance of the class with a global access point. That is, a class always has one and only one object, regardless of who, when, and where it is accessed from. We are now implementing this approach, we will have Singleton for global access and management of the Core Data Stack.

Create a new empty file called CoreDataManager.swift







First, let's add the Core Data import directive and create Singleton itself.
import CoreData
import Foundation
class CoreDataManager {
    // Singleton
    static let instance = CoreDataManager()
    private init() {}
}

Now let's move all the functions and definitions related to Core Data from the application delegate module.
import CoreData
import Foundation
class CoreDataManager {
    // Singleton
    static let instance = CoreDataManager()
    private init() {}
    // 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()
            }
        }
    }
}


Now we have Singleton and we can access the Core Data Stack from anywhere in our application. For example, a call to a managed context would look like this:
CoreDataManager.instance.managedObjectContext 

Let's now transfer everything we need to create a managed object into its constructor.

//  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) 
    }
}

Let's go back to the application delegate module and make some changes. First, the creation of a managed object is simplified to one line (calling the new constructor of our class), and secondly, such a link to a managed context

self.managedObjectContext

need to be replaced with the following

CoreDataManager.instance.managedObjectContext


Now the code will look completely familiar, and work with managed objects will not differ much from ordinary OOP objects.
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
    }

Not bad, is it? All that remains for us to do is to create similar constructors for the rest of our entities. But first, let's make another improvement to reduce the amount of code - we will make a function that returns an entity description, c CoreDataManager.

Let's go back to the module CoreDataManager.swiftand add a function entityForName.
import CoreData
import Foundation
class CoreDataManager {
    // Singleton
    static let instance = CoreDataManager()
    private init() {}
    // Entity for Name
    func entityForName(entityName: String) -> NSEntityDescription {
        return NSEntityDescription.entityForName(entityName, inManagedObjectContext: self.managedObjectContext)!
    }

Now back to the module Customer.swiftand change the code as follows.
import Foundation
import CoreData
class Customer: NSManagedObject {
    convenience init() {
        self.init(entity: CoreDataManager.instance.entityForName("Customer"), insertIntoManagedObjectContext: CoreDataManager.instance.managedObjectContext)
    }
}

Now that’s exactly it, duplication of code is minimized. Let's create similar constructors for other entities. I will give only one for example, it is simple and should not cause any difficulties (absolutely everything is the same, except for the name of the entity).
//  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)
    }
}


Instead of a conclusion


Please note that the CoreDataManager we created is quite universal, in the sense that it can be used in any Core Data-based application. The only thing that connects it with our project is the name of the data model file. Nothing more. That is, by writing this module once, you can use it constantly in different projects.

In the next, final part , we will work a lot with Storyboardand UITableViewController, get to know NSFetchedResultsControllerand remember again NSFetchRequest.

This project is on github

Also popular now: