Search code examples
swiftcore-datansmanagedobjectnsmanagedobjectcontextnsset

Core Data relationship, NSSet & ManagedObjectContext


What I want to do

I am building a demo app to learn about using Core Data. But I have some issues:

  1. Is there an easier implementation for NSManagedObjectContext than my current one?

  2. I don't know how to complete the code with the NSSet

The app is just a table with a list of employees's names, dates of birth and 2 tasks for each. The tasks have multiple statuses(Active, Complete,etc.). There's an item button that leads to another view where you input the name and tasks and the date of birth is set to current date.

I have 2 entities (can it be done with just EmployeeEntity?):

  • EmployeeEntity with attributes name and dateOfBirth + a one-to-many tasks relationship with destination TaskEntity
  • TaskEntity with attribute name and the inverse relationship employee

1. The Managed Object Context implementation (is there a shorter/easier/better one?)

In AppDelegate(...didFinishLaunchingWithOptions...) I set the managedObjectContext for the 1st screen:

// Fetch Main Storyboard
    let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)

    // Instantiate Root Navigation Controller
    let rootNavigationController = mainStoryboard.instantiateViewControllerWithIdentifier("StoryboardIDRootNavigationController") as! UINavigationController

    // Configure View Controller
    let viewController = rootNavigationController.topViewController as? TableTVC

    if let viewController = viewController {
        viewController.managedObjectContext = self.managedObjectContext
    }

    // Configure Window
    window?.rootViewController = rootNavigationController

    return true

Then in prepareForSegue I pass it along (there's a managedObjectContext variable in the destination VC):

if segue.identifier == "idTableToNew" {
        if let navVC = segue.destinationViewController as? UINavigationController {
            if let newVC = navVC.topViewController as? NewVC {
                newVC.managedObjectContext = managedObjectContext
            }
        }
    }

Then the fetchedResultsController in the table VC:

lazy var frc: NSFetchedResultsController = {
    // Initialize Fetch Request
    let fetchRequest = NSFetchRequest(entityName: "EmployeeEntity")

    // Configure Predicate
    // Need to make changes because I only want today's date
    let predicate = NSPredicate(format: "%K == %@", "dateOfBirth", NSDate())
    fetchRequest.predicate = predicate

    // Initialize Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

    // Configure Fetched Results Controller
    fetchedResultsController.delegate = self

    return fetchedResultsController
}()

2. The code where I'm stuck at

The problem is with the data types. First of all, I don't know how to get the tasks' statuses. Second of all, I'm not sure if this might be the best way to do it.

Below is the TasksEntity.swift code(except import&generated comments):

enum TaskStatus: Int32 {
    case Complete, Incomplete, Active, Inactive
}

class TaskEntity: NSManagedObject {

    @NSManaged private var statusValue: Int32

    var status: TaskStatus {
        get {
            return TaskStatus(rawValue: self.statusValue)!
        }
        set {
            self.statusValue = newValue.rawValue
        }
    }
}

Saving the data (action for when I press an item button):

    let name = nameField.text
    let task1Text = task1Field.text
    let task2Text = task2Field.text

    // Create Entities
    let employee = NSEntityDescription.entityForName("EmployeeEntity", inManagedObjectContext: managedObjectContext)
    let task = NSEntityDescription.entityForName("TaskEntity", inManagedObjectContext: managedObjectContext)

    // Initialize Records
    let employeeRecord = EmployeeEntity(entity: employee!, insertIntoManagedObjectContext: managedObjectContext)
    let taskRecord = TaskEntity(entity: task!, insertIntoManagedObjectContext: managedObjectContext)
    let taskRecord2 = TaskEntity(entity: task!, insertIntoManagedObjectContext: managedObjectContext)

    // Populate Records
    employeeRecord.name = name
    employeeRecord.dateOfBirth = NSDate()
    taskRecord.taskName = task1Text
    taskRecord.status = TaskStatus.Inactive // Apparently no errors so it might work
    taskRecord2.taskName = task2Text
    taskRecord2.status = TaskStatus.Incomplete
    let taskArray = employeeRecord.mutableArrayValueForKey("tasks")
    taskArray.addObject(taskRecord)
    taskArray.addObject(taskRecord2)

    // Save MOC
    do {
        try managedObjectContext.save()
    } catch {
        let saveError = error as NSError
        print("\(saveError), \(saveError.userInfo)")
    }

Now in cellForRowAtIndexPath in the table VC (I'm stuck here):

let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! EmployeeTVCell

    // Fetch Record
    let employeeRecord = frc.objectAtIndexPath(indexPath) as! EmployeeEntity

    // Update Cell
    if let name = employeeRecord.name {
        cell.nameLabel.text = name
    }

    cell.dateLabel.text = String(employeeRecord.dateOfBirth)

    if let tasksArray = employeeRecord.tasks?.mutableArrayValueForKey("tasks") {
        if tasksArray.count != 0 {
            cell.task1Label.text = tasksArray[0] as? String
            cell.task1Label.text = tasksArray[1] as? String
            print(tasksArray[0].status)

            // I think that tasksArray[index] won't work but it's what I want to achieve
        }
    }

    return cell

Photos & thoughts

Employee Task

My guess is that I need to tell it explicitly to contain only TaskEntity type variables in the tasks NSSet or something like that. I saw others using Sets. I tried Set<TaskEntity> and I had no errors but I don't know how to adapt the code where I transform the sets into arrays.


Solution

  • The usual way to get the members of the Set object as array is the method allObjects, but apart from that you cannot cast the TaskEntity object to String.

    ...
    if let tasksArray = employeeRecord.tasks?.allObjects as? [TaskEntity] where !tasksArray.isEmpty {
        cell.task1Label.text = tasksArray[0].taskName
        cell.task1Label.text = tasksArray[1].taskName // or is it cell.task2Label ??
        print(tasksArray[0].status)
    }
    
    return cell
    

    Consider to rename the Core Data entities and their attributes. Actually the NSManagedObject subclasses represent always an entity so it's redundant to include Entity in the name. Or the attribute taskName in TaskEntity could be just name. While reading the code you can infer that name belongs to Task

    Regarding the status property you need to create an Int32 attribute statusValue in the Task entity to store the value in Core Data. The code to map the enumeration to Int32 and vice versa is fine.