6

It's time to use Core Data, but the open documentation and guides out there spend a lot of time talking about general setup and nitty gritty "behind the scenes" details. Those things are important, but I would love a quick, clean source showing how to actually use information stored in a Core Data model.

The Scenario

In my simple example I have a single entity type:

Job
 - salary [Double]
 - dateCreated [Date]

This is a Swift iOS app powered by story boards, with the default generated AppDelegate.swift which handles the generation of my Managed Object Context.

The Question

How do I use Job instances in my application?

Bonus points if you can also provide insight around these items:

  • As someone who is used to the MVC design pattern, how do I avoid including dirty data access inside of my controllers without bucking iOS development best practices?
  • How can I access entities from Core Data while following DRY?
  • How do I pass managed objects between methods and controllers while maintaining their type?

The Core Data documentation provides some snippets for fetching records. This question is essentially asking where that logic belongs in an iOS application, and how to actually interact with the fetched records after fetching them.

An Example

This question isn't meant to be a broad, sweeping question so I will ground it in an example attempt to use Core Data. In my example I have a single UIViewController which has a label. I want this label to show the salary from a job.

import UIKit
import CoreData

class JobViewController: UIViewController {
    
    @IBOutlet var salaryLabel: UILabel!
    let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
    
    func updateLabel() {
        var job = getCurrentJob()
        salaryLabel.text = job.salary // ERRORS
    }
    
    func getCurrentJob()->(???) {
        var error: NSError?
        if let fetchedResults = managedObjectContext!.executeFetchRequest(NSFetchRequest(entityName:"Job"), error: &error) {
            return fetchedResults[0]
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

This example will not compile for two reasons:

  1. I didn't specify a return type for getCurrentJob, since I wasn't sure what type to return
  2. The line with "ERRORS", which tries to access the salary attribute, will error because there is no way of knowing that salary is an actual attribute of job.

How do I pass around and use the job object?

Community
  • 1
  • 1
slifty
  • 13,062
  • 13
  • 71
  • 109
  • It's a really big issue... You can download nice and simple library, that have a good core data manager [ObjectiveRecord](https://github.com/supermarin/ObjectiveRecord) and also let you map some data from JSON Dictionary to your core data. It is open source project so you can see manager implementation as example – aquarium_moose Mar 07 '15 at 17:14
  • 1
    @aquarium_moose IMHO no need for special library: Core Data can cover this quite well by itself – Drux Mar 07 '15 at 19:02
  • What type of `UIViewController`s? – Drux Mar 07 '15 at 19:07
  • @Drux the UIViewControllers for this are linked to a storyboard, aside from that I'm not sure what you mean :-/ – slifty Mar 07 '15 at 19:44
  • I'm asking because to the extent they are `UITableViewController`s, `NSFetchedResultsController` should be part of your answer – Drux Mar 07 '15 at 19:52
  • @Drux I tried to make it a little more concrete with a code example of what I'm trying. – slifty Mar 07 '15 at 19:55
  • 1
    It certainly sound more down-do-earth now :) So you need a Core Data model (New / File / Core Data / Data Model) with one entity (Job) and two attributes. You can then generate Swift code for Job.swift from it (Editor / Create NSManagedObject Subclass ...) that will allow you to do job.salary. These are usual steps for Objective-C and work for for Swift as well. However, with Swift you can alternatively use the @NSManagedObject annotation. I have less experience with that but Google is your friend ... – Drux Mar 07 '15 at 20:11
  • Thanks @Drux! I'm going to do a bit more digging with your advice in mind and probably wind up posting an answer to the question for future folks trying to learn all this (I think my Google keywords must be all wrong since this probably isn't actually as opaque as the initial results I found made it out to be). Note: feel free to throw in the answer yourself if you want the karma! – slifty Mar 07 '15 at 20:13
  • I've also dug a bit more: looks like I was wrong re `@NSManagedObject` annotation. Instead the generated Swift code employs `@NSManaged` annotation. So: above steps for Objective-C apply to Swift as well. Also, generating the source code is optional and you could use KVC instead (see [here](http://stackoverflow.com/questions/28898966/prefer-property-accessor-or-kvc-style-for-accessing-core-data-properties)). Good luck. – Drux Mar 07 '15 at 20:21
  • @Drux, I didn't said he needs this lib, i said there is a good example of using core data without some waste staffs... – aquarium_moose Mar 07 '15 at 20:41

3 Answers3

5

The key missing piece from the above example are NSManagedObject subclasses, and in Swift, the @NSManaged Swift annotation. NSManagedObject is a generic class which, in its most simple form, can be extended to simply provide access to attributes of a Core Data entity, but in reality this is where traditional model logic should live.

Creating NSManagedObject Subclasses

You can automatically generate these objects by viewing the Core Data model, and using the menu command: Editor->Create NSManagedObject Subclass.

This will generate Job.swift (or whatever your entity name is)

import Foundation
import CoreData

class Job: NSManagedObject {

    @NSManaged var dateCreated: NSDate
    @NSManaged var salary: NSNumber

}

Using NSManagedObject Subclasses

Your new class is now available for use, and you can typecast the fetched result accordingly! For completion, here's the updated version of the previously broken example

import UIKit
import CoreData

class JobViewController: UIViewController {

    @IBOutlet var salaryLabel: UILabel!
    let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext

    func updateLabel() {
        var job:Job = getCurrentJob()
        salaryLabel.text = job.salary // ERRORS
    }

    func getCurrentJob()->Job {
        var error: NSError?
        if let fetchedResults = managedObjectContext!.executeFetchRequest(NSFetchRequest(entityName:"Job"), error: &error) {
            return fetchedResults[0]
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}
slifty
  • 13,062
  • 13
  • 71
  • 109
2

For instance, create a Core Data model like this:

enter image description here

Now generate Swift source code from it (with Editor | Create NSManagedObject Subclass). This will allow you to compile the following version of JobViewController (which currently lacks error handling and more):

import UIKit
import CoreData

class JobViewController: UIViewController {

    @IBOutlet var salaryLabel: UILabel!
    let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
    var jobs: [Job] = []

    override func viewDidLoad() {
        super.viewDidLoad();

        jobs = managedObjectContext.executeFetchRequest(NSFetchRequest(entityName:"Job"), error: nil) as [Job];
    }

    func updateLabel() {
        salaryLabel.text = "\(jobs[0].salary) $"
    }
}
Drux
  • 11,992
  • 13
  • 66
  • 116
  • Out of curiosity, would you normally put the managedObjectContext fetch code inside of a controller? My instinct would be to create static methods of some type in the Job.swift, but maybe there is a more standard iOS approach for that? (a menu item I don't know about for instance!) – slifty Mar 07 '15 at 21:04
  • @slifty I usually keep it in the controller (depends on the lifetime of entities relative to view controllers; or in a `NSFetchedResultsController` -- see above), also usually off the main thread because fetching may take some time (depending on what store is behind a context). – Drux Mar 07 '15 at 21:18
1

Although I know that some hardcore OOP advocates will frown at this solution, I suggest using singleton classes to manage Core Data across an application.

I'd advise setting up a global CoreDataManager that can be accessed through a shared instance. Now you have global access to your retrieval, update, deletion methods, etc, and your global variable is kept private.

private var sharedCoreDataManager: CoreDataManager!

class CoreDataManager {

    let managedContext: NSManagedObjectContext

    class var shared: CoreDataManager {
        return sharedCoreDataManager
    }

    class func initialize(context: NSManagedObjectContext) {
        sharedCoreDataManager = CoreDataManager(context: context)
    }

    private init(context: NSManagedObjectContext) {
        managedContext = context
    }

    func delete(entity: String, index: Int) -> Bool {
        var data = fetch(entity)
        if data != nil {
            managedContext.deleteObject(data![index])
            data!.removeAtIndex(index)
            managedContext.save(nil)
            return true
        }

        return false
    }


    func fetch(entity: String) -> [NSManagedObject]? {
        var request = NSFetchRequest(entityName: entity)
        var error: NSError?
        if let entities = managedContext.executeFetchRequest(request, error: &error) as? [NSManagedObject] {
            if entities.count > 0 {
                return entities
            }
        }
        return nil
    }

    func save(entity: String, _ attributes: [String: AnyObject]) -> NSManagedObject? {

        var entity = NSEntityDescription.entityForName(entity, inManagedObjectContext: managedContext)
        let object = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: self.managedContext)

        for (key, attr) in attributes {
            object.setValue(attr, forKey: key)
        }

        var error: NSError?

        if !managedContext.save(&error) {
            return nil
        }

        return object
    }
}

This can be initialized inside of your AppDelegate's didFinishingLaunchingWithOptions function

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    CoreDataManager.initialize(self.managedObjectContext!)
    return true
}

You can set up your NSManagedObject by clicking on YourProject.xcdatamodeld in the navigator. In your case, you'll add a Job entity with attributes salary(double) and date(date). On the top menu access Editor > CreateNSManagedObjectSubclass to automatically generate your Job subclass. While you're still in the xcdatmodel editor, open up the rightmost pane--you should see text fields for 'Name' and 'Class'. Be sure to change your class to 'ProjectName.Name`--in your case 'ProjectName.Job'--or you won't be able to instantiate your new NSManagedObject class.

Your NSManagedObject class should be automatically generated for you and available for inspection in the project navigator. It will look like this:

import Foundation
import CoreData

@objc class Job: NSManagedObject {
    @NSManaged var salary: NSNumber
    @NSManaged var date: NSDate
}

In order to restrict access to your managed objects, you should create mediator classes with get- and set-style variable. Although Swift doesn't have a 'protected' access level, you can keep your NSManagedObjects private and allow access through the object Mediators by grouping them into one class file:

class ManagedObjectMediator<T: NSManagedObject> {
    private var managedObject: T!

    init?(_ type: String, attributes: [String: AnyObject]) {
        if let newManagedObject = CoreDataManager.shared.save(type, attributes) {
            managedObject = newManagedObject as T
        } else {
            return nil
        }
    }
}

class JobMediator<T: Job>: ManagedObjectMediator<Job> {

    var date: NSDate {
        return managedObject.date
    }

    var salary: NSNumber {
        return managedObject.salary
    }

    init?(attributes: [String:AnyObject]) {
        super.init("Job", attributes: attributes)
    }
}
kellanburket
  • 12,250
  • 3
  • 46
  • 73
  • Did you ever scale this to a case with parent and child contexts? – Drux Mar 07 '15 at 21:21
  • I haven't--I suppose that's an issue I'll have to tackle when I do. Are there any reasons for concern you can think of? – kellanburket Mar 07 '15 at 21:23
  • I have also used this pattern in the past but since adopting parent/child contexts more frequently no longer do. (Was just curious if you perhaps combine both practices.) – Drux Mar 07 '15 at 21:25
  • What you described in your first example is basically an Entity / Repository, think data mapper pattern. Good job – Jimbo Feb 25 '18 at 19:26