Tuesday, January 22, 2019

Introduction to CoreData using Swift 3

Core Data framework reduces a hug amount of codes to store data on an iOS device. There are four types of datastore we can use with Core Data:
1. XML: readable and suitable for simple data
2. Binary:
3. SQLite: built-in and suitable for complex data like object graphs
4. In-memory:

To integrate Core Data into the app, we can either create traditional Core data stack or initialize NSPersistentContainer. Using Xcode 9, NSPersistentContainer is initialized by default in AppDelegate class when creating a SingleViewApplication then you can access the NSPersistentContainer object from a ViewController to persist or access the data to or from the store.

Sample App


1. Create a SingleViewApplication in Xcode (v9)


The persistentContainer property is added to the AppDelegate class as following:

    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentContainer(name: "CoreDataTest")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                 
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
 

Another file with the extension "xcdatamodeld" is created too. It's a configuration file for create and map your model classes to the tables in the store (the same concept as Hibernate's mapping files).

2. Configure the model file


Suppose we haven't created the model classes manually. Select the model file, CoreDataTest.xcdatamodeld in my case, and add a new entity named OperatorLog as following:


Under the Class settings in the Data Model Inspector:
- Set Module to Current Product Module - it's like a namespace for the model class. Without the namespace, I got the compile-time error "Class not found, using default NSManagedObject instead."
- Set Codegen to Manual/None - the default value is Class Definition, which means that Xcode creates and manages the model classes and extensions and hide them somewhere so that the developers can't modify them manually (any changes the developers want to make, make it in the model file then Xcode will update the classes and extensions). Manual/None tells Xcode to leave the work of creating and modifying the classes and extensions to the developers.

3. Generate the model class or entity


Select the model file then choose Editor menu and Create NSManagedObject Subclass... There will be two files created:

OperatorLog+CoreDataClass.swift

import Foundation
import CoreData

public class OperatorLog: NSManagedObject {

}
 

OperatorLog+CoreDataProperties.swift

import Foundation
import CoreData

extension OperatorLog {

    @nonobjc public class func fetchRequest() -> NSFetchRequest {
        return NSFetchRequest(entityName: "OperatorLog")
    }

    @NSManaged public var operatorTag: Double
    @NSManaged public var operand: Double

}
 

@NSManaged annotation is used to shut up the compiler because Swift does not allow an extension to contain a stored property (hard to manage and allocate memory for a type if they're in different module). Note that I moved the properties to OperatorLog class then removed the annotation, and the record was saved in the store but all the attributes had no values.

4. Implement the method to save data



import Foundation
import CoreData

class MyCalculatorStore {
    let context:NSManagedObjectContext?
    
    init(_ context:NSManagedObjectContext) {
        self.context = context
    }
    
    func save(_ operand:Double?, _ operatorTag:Double) throws -> OperatorLog {
        var savedEntity:OperatorLog!
        context?.performAndWait {
            savedEntity = NSEntityDescription.insertNewObject(forEntityName: "OperatorLog", into: context!) as! OperatorLog
            if operand != nil {
                savedEntity.operand = operand!
                savedEntity.operatorTag = operatorTag
            }
        }
        
        var error: Error?
        if (context?.hasChanges)! {
            do {
                try context?.save()
            } catch let saveError {
                error = saveError
            }
        }
        if let e = error {
            throw e
        }
        return savedEntity
    }
}
 

The NSManagedObject object requires two elements: an entity description (NSEntityDescription object) and a managed object context (a NSManagedObjectContext object). The entity description includes the name of the entity it represents and its attributes and relationships. The managed object context tracks changes to and relationships between objects (like a session object in Java).

The performAndWait(_:) method execute the block operations synchronously. In other words, the method does not return until the block is executed. If we want the method return immediately without waiting for the block is executed, we should use perform(_:) method instead.

The NSEntityDescription.insertNewObject(forEntityName:) method creates a new managed object in the context. Without explicitly calling the save() method of the NSManagedObjectContext, the object is not persisted. When the managed object is firstly created in the context, it is assigned a temporary ID. A new ID is assigned after it's been persisted to the store. The ID is assigned to objectID property (which is of type NSManagedObjectID). I printed the ID out and it looks like this "0xd000000000080000 <x-coredata://9848B21C-A886-4A18-BA35-44AB8E4CAAF1/OperationMO/p2"


5. Implement the method to retrieve data


Add the following method declaration to the MyCalculatorStore class above.

    func getOperatorLogs() throws -> [OperatorLog] {
        var result:[OperatorLog] = []
        
        var error: Error?
        context?.performAndWait {
            let req:NSFetchRequest = OperatorLog.fetchRequest()
            do {
                result = (try context?.fetch(req) as? [OperatorLog])!
            } catch let saveError {
                error = saveError
            }
        }
        if let e = error {
            throw e
        }
        
        return result
    }
 

The NSFetchRequest object contains search criteria to filter the result.

6. Calling the methods from within ViewController



import UIKit

class ViewController: UIViewController {
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    
    var datastore:MyCalculatorStore?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        datastore = MyCalculatorStore(context)

        do {
            try datastore?.save(21.342, 117)
            try datastore?.save(837.2, 118)
        } catch let e {
            print("\(e.localizedDescription)")
        }
        
        do {
            let operatorLogs:[OperatorLog] = try datastore!.getOperatorLogs()
            for entity in operatorLogs {
                print("> operand=\(entity.operand), tag=\(entity.operatorTag)")
            }
        } catch let e {
            print("\(e.localizedDescription)")
        }
    }
}
 

Run the project then there will be the output in the console as following:

> operand=21.342, tag=117.0
> operand=837.2, tag=118.0
 



No comments:

Post a Comment