In the previous installments of this series, we covered the fundamentals of the Core Data framework. It's time we put our knowledge to use by building a simple application powered by Core Data.
In this tutorial, we'll also meet another star player of the Core Data framework, the NSFetchedResultsController
class. The application that we're about to create manages a list of to-do items. With the application, we can add, update, and delete to-do items. You'll quickly learn that the NSFetchedResultsController
class makes this very easy to do.
Prerequisites
What I cover in this series on Core Data is applicable to iOS 7+ and OS X 10.10+, but the focus will be on iOS. In this series, I will work with Xcode 7.1 and Swift 2.1. If you prefer Objective-C, then I recommend reading my earlier series on the Core Data framework.
1. Project Setup
Open Xcode, select New > Project... from the File menu, and choose the Single View Application template from the iOS > Application category.
Name the project Done, set Language to Swift, and set Devices to iPhone. Because I'd like to show you how to create a Core Data application from scratch, make sure that the checkbox labeled Use Core Data is unchecked. Tell Xcode where you want to save the project and hit Create to create the project.
2. Core Data Setup
Open AppDelegate.swift and declare three lazy stored properties managedObjectModel
of type NSManagedObjectModel
, managedObjectContext
of type NSManagedObjectContext
, and persistentStoreCoordinator
of type NSPersistentStoreCoordinator
. If you're confused by this step, then I recommend you revisit the first article of this series, which covers the Core Data stack in detail.
// MARK: - // MARK: Core Data Stack lazy var managedObjectModel: NSManagedObjectModel = { }() lazy var managedObjectContext: NSManagedObjectContext = { }() lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { }()
Note that I've also added an import statement for the Core Data framework at the top of AppDelegate.swift.
import UIKit import CoreData
Remember that we use the lazy
keyword to lazily set up the Core Data stack. This means that we instantiate the managed object context, the managed object model, and the persistent store coordinator the moment they are needed by the application. The following implementation should look very familiar.
lazy var managedObjectModel: NSManagedObjectModel = { let modelURL = NSBundle.mainBundle().URLForResource("Done", withExtension: "momd")! return NSManagedObjectModel(contentsOfURL: modelURL)! }()
lazy var managedObjectContext: NSManagedObjectContext = { let persistentStoreCoordinator = self.persistentStoreCoordinator // Initialize Managed Object Context var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) // Configure Managed Object Context managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator return managedObjectContext }()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { // Initialize Persistent Store Coordinator let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) // URL Documents Directory let URLs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) let applicationDocumentsDirectory = URLs[(URLs.count - 1)] // URL Persistent Store let URLPersistentStore = applicationDocumentsDirectory.URLByAppendingPathComponent("Done.sqlite") do { // Add Persistent Store to Persistent Store Coordinator try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: nil) } catch { // Populate Error var userInfo = [String: AnyObject]() userInfo[NSLocalizedDescriptionKey] = "There was an error creating or loading the application's saved data." userInfo[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data." userInfo[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "com.tutsplus.Done", code: 1001, userInfo: userInfo) NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() } return persistentStoreCoordinator }()
There are three things you should be aware of. First, the data model, which we'll create next, will be named Done.momd. Second, we'll name the backing store Done and it will be a SQLite database. Third, if the backing store is incompatible with the data model, we invoke abort
, killing the application. As I mentioned earlier in this series, while this is fine during development, you should never call abort
in production. We'll revisit migrations and incompatibility issues later in this series.
While our application won't crash if you try to run it, the Core Data stack won't get properly set up. The reason is simple, we haven't created a data model yet. Let's take care of that in the next step.
3. Creating the Data Model
Select New > File... from the File menu and choose Data Model from the iOS > Core Data category.
Name the file Done, double-check that it's added to the Done target, and tell Xcode where it needs to be saved.
The data model is going to be very simple. Create a new entity and name it Item. Add three attributes to the entity, name of type String, createdAt of type Date, and done of type Boolean.
Mark the attributes as required, not optional, and set the default value of the done attribute to NO
.
The Core Data stack is set up and the data model is configured correctly. It's time to become familiar with a new class of the Core Data framework, the NSFetchedResultsController
class.
4. Managing Data
This article isn't just about the NSFetchedResultsController
class, it's about what the NSFetchedResultsController
class does behind the scenes. Let me clarify what I mean by this.
If we were to build our application without the NSFetchedResultsController
class, we would need to find a way to keep the application's user interface synchronized with the data managed by Core Data. Fortunately, Core Data has an elegant solution to this problem.
Whenever a record is inserted, updated, or deleted in a managed object context, the managed object context posts a notification through notification center. A managed object context posts three types of notifications:
NSManagedObjectContextObjectsDidChangeNotification
: This notification is posted every time a record in the managed object context is inserted, updated, or deleted.NSManagedObjectContextWillSaveNotification
: This notification is posted by the managed object context before pending changes are committed to the backing store.NSManagedObjectContextDidSaveNotification
: This notification is posted by the managed object context immediately after pending changes have been committed to the backing store.
The contents of these notifications are identical, that is, the object
property of the notification is the NSManagedObjectContext
instance that posted the notification and the notification's userInfo
dictionary contains the records that were inserted, updated, and deleted.
The gist is that it requires a fair amount of boilerplate code to keep the results of a fetch request up to date. In other words, if we were to create our application without using the NSFetchedResultsController
class, we would have to implement a mechanism that monitored the managed object context for changes and update the user interface accordingly. Let's see how the NSFetchedResultsController
can help us with this task.
5. Setting Up the User Interface
Working with the NSFetchedResultsController
class is pretty easy. An instance of the NSFetchedResultsController
class takes a fetch request and has a delegate object. The NSFetchedResultsController
object makes sure that it keeps the results of the fetch request up to date by monitoring the managed object context the fetch request was executed by.
If the NSFetchedResultsController
object is notified of any changes by the NSManagedObjectContext
object of the fetch request, it notifies its delegate. You may be wondering how this is different from a view controller directly monitoring the NSManagedObjectContext
object. The beauty of the NSFetchedResultsController
class is that it processes the notifications it receives from the NSManagedObjectContext
object and tells the delegate only what it needs to know to update the user interface in response to these changes. The methods of the NSFetchedResultsControllerDelegate
protocol should clarify this.
optional public func controllerWillChangeContent(controller: NSFetchedResultsController) optional public func controllerDidChangeContent(controller: NSFetchedResultsController) optional public func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) optional public func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) optional public func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String?
The signatures of the above delegate methods reveal the true purpose of the NSFetchedResultsController
class. On iOS, the NSFetchedResultsController
class was designed to manage the data displayed by a UITableView
or a UICollectionView
. It tells its delegate exactly which records changed, how to update the user interface, and when to do this.
Don't worry if you're still unsure about the purpose or advantages of the NSFetchedResultsController
class. It'll make more sense once we've implemented the NSFetchedResultsControllerDelegate
protocol. Let's revisit our application and put the NSFetchedResultsController
class to use.
Step 1: Populating the Storyboard
Open the project's main storyboard, Main.storyboard, select the View Controller Scene, and embed it in a navigation controller by selecting Embed In > Navigation Controller from the Editor menu.
Drag a UITableView
object in the View Controller Scene, create an outlet in the ViewController
class, and connect it in the storyboard. Don't forget to make the ViewController
class conform to the UITableViewDataSource
and UITableViewDelegate
protocols.
import UIKit class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() } }
Select the table view, open the Connections Inspector, and connect the table view's dataSource
and delegate
outlets to the View Controller object. With the table view selected, open the Attributes Inspector and set the number of Prototype Cells to 1.
Before we continue, we need to create a UITableViewCell
subclass for the prototype cell. Create a new Objective-C subclass, ToDoCell
, and set its superclass to UITableViewCell
. Create two outlets, nameLabel
of type UILabel
and doneButton
of type UIButton
.
Head back to the storyboard, select the prototype cell in the table view, and set the class to ToDoCell
in the Identity Inspector. Add a UILabel
and a UIButton
object to the cell's content view and connect the outlets in the Connections Inspector. With the prototype cell selected, open the Attributes Inspector and set the identifier of the prototype cell to ToDoCell
. This identifier will serve as the cell's reuse identifier. The prototype cell's layout should look something like the screenshot below.
Create a new ViewController
subclass and name it AddToDoViewController
. Open AddToDoViewController.swift, declare an outlet textField
of type UITextField
and conform the view controller to the UITextFieldDelegate
protocol.
import UIKit class AddToDoViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var textField: UITextField! override func viewDidLoad() { super.viewDidLoad() } }
Before we add the view controller to the storyboard, add the following two actions to the view controller's implementation file.
// MARK: - // MARK: Actions @IBAction func cancel(sender: AnyObject) { } @IBAction func save(sender: AnyObject) { }
Open the storyboard one more time and add a bar button item with an identifier of Add to the right of the navigation bar of the ViewController
. Add a new view controller to the storyboard and set its class to AddToDoViewController
in the Identity Inspector. With the view controller selected, choose Embed In > Navigation Controller from the Editor menu.
The new view controller should now have a navigation bar. Add two bar button items to the navigation bar, one on the left with an identity of Cancel and one on the right with an identity of Save. Connect the cancel(_:)
action to the left bar button item and the save(_:)
action to the right bar button item.
Add a UITextField
object to the view controller's view and position it 20 points below the navigation bar. The text field should remain at 20 points below the navigation bar. Note that the layout constraint at the top references the top layout guide.
Connect the text field with the corresponding outlet in the view controller and set the view controller as the text field's delegate. Finally, control drag from the bar button item of the ViewController
to the navigation controller of which the AddToDoViewController
is the root view controller. Set the segue type to Present Modally and set the segue's identifier to SegueAddToDoViewController
in the Attributes Inspector. That was a lot to take in. The interesting part is yet to come though.
Step 2: Implementing the Table View
Before we can take our application for a spin, we need to implement the UITableViewDataSource
protocol in the ViewController
class. However, this is where the NSFetchedResultsController
class comes into play. To make sure that everything is working return 0
from the tableView(_:numberOfRowsInSection:)
method. This will result in an empty table view, but it will allow us to run the application without running into a crash.
// MARK: - // MARK: Table View Data Source Methods func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 0 }
To satisfy the compiler, we also need to implement tableView(_:cellForRowAtIndexPath:)
. At the top of AddToDoViewController.swift, add a constant for the cell reuse identifier.
import UIKit class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let ReuseIdentifierToDoCell = "ToDoCell" ... }
Implementing tableView(_:cellForRowAtIndexPath:)
is pretty simple since we're not doing anything special with the table view cell yet.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierToDoCell, forIndexPath: indexPath) as! ToDoCell return cell }
Step 3: Save and Cancel
Open the AddToDoViewController
class and implement the cancel(_:)
and save(_:)
methods as shown below. We'll update their implementations later in this tutorial.
// MARK: - // MARK: Actions @IBAction func cancel(sender: AnyObject) { dismissViewControllerAnimated(true, completion: nil) } @IBAction func save(sender: AnyObject) { dismissViewControllerAnimated(true, completion: nil) }
Build and run the application in the simulator to see if everything is wired up correctly. You should be able to tap the add button in the top right to bring up the AddToDoViewController
and dismiss it by tapping either the cancel or the save button.
6. Adding the NSFetchedResultsController
Class
The NSFetchedResultsController
class is part of the Core Data framework and it's meant to manage the results of a fetch request. The class was designed to work seamlessly with UITableView
and UICollectionView
on iOS and NSTableView
on OS X. However, it can be used for other purposes as well.
Step 1: Laying the Groundwork
Before we can start working with the NSFetchedResultsController
class, however, the ViewController
class needs access to an NSManagedObjectContext
instance, the NSManagedObjectContext
instance we created earlier in the application delegate. Start by declaring a property managedObjectContext
of type NSManagedObjectContext!
in the header file of the ViewController
class. Note that we're also adding an import statement for the Core Data framework at the top.
import UIKit import CoreData class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let ReuseIdentifierToDoCell = "ToDoCell" @IBOutlet weak var tableView: UITableView! var managedObjectContext: NSManagedObjectContext! ... }
Open Main.storyboard, select the storyboard's initial view controller, an UINavigationController
instance, and set its Storyboard ID to StoryboardIDRootNavigationController
in the Identity Inspector.
In the application delegate's application(_:didFinishLaunchingWithOptions:)
method, we get a reference to the ViewController
instance, the root view controller of the navigation controller, and set its managedObjectContext
property. The updated application(_:didFinishLaunchingWithOptions:)
method looks as follows:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // 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? ViewController if let viewController = viewController { viewController.managedObjectContext = self.managedObjectContext } // Configure Window window?.rootViewController = rootNavigationController return true }
To make sure that everything is working, add the following print statement to the viewDidLoad()
method of the ViewController
class.
override func viewDidLoad() { super.viewDidLoad() print(managedObjectContext) }
Step 2: Initializing the NSFetchedResultsController
Instance
Open the implementation file of the ViewController
class and declare a lazy stored property of type NSFetchedResultsController
. Name the property fetchedResultsController
. An NSFetchedResultsController
instance also has a delegate property that needs to conform to the NSFetchedResultsControllerDelegate
protocol. Because the ViewController
instance will serve as the delegate of the NSFetchedResultsController
instance, we need to conform the ViewController
class to the NSFetchedResultsControllerDelegate
protocol as shown below.
import UIKit import CoreData class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate { let ReuseIdentifierToDoCell = "ToDoCell" @IBOutlet weak var tableView: UITableView! var managedObjectContext: NSManagedObjectContext! var fetchedResultsController: NSFetchedResultsController = { }() ... }
It's time to initialize the NSFetchedResultsController
instance. The heart of a fetched results controller is the NSFetchRequest
object, because it determines which records the fetched results controller will manage. In the view controller's viewDidLoad()
method, we initialize the fetch request by passing "Item"
to the initWithEntityName(_:)
method. This should be familiar by now and so is the next line in which we add sort descriptors to the fetch request to sort the results based on the value of the createdAt attribute of each record.
lazy var fetchedResultsController: NSFetchedResultsController = { // Initialize Fetch Request let fetchRequest = NSFetchRequest(entityName: "Item") // Add Sort Descriptors let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: true) fetchRequest.sortDescriptors = [sortDescriptor] // 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 }()
The initialization of the fetched results controller is pretty straightforward. The init(fetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:)
method takes four arguments:
- the fetch request
- the managed object context the fetched results controller will be monitoring
- a section key path if you want the results to be split up into sections
- a cache name if you want to enable caching
We pass nil
for the last two parameters for now. The first argument is obvious, but why do we need to pass a NSManagedObjectContext
object as well? Not only is the passed in managed object context used to execute the fetch request, it is also the managed object context that the fetched results controller will be monitoring for changes. This will become clearer in a few minutes when we start implementing the delegate methods of the NSFetchedResultsControllerDelegate
protocol.
Finally, we need to tell the fetched results controller to execute the fetch request we passed it. We do this by invoking performFetch()
in viewDidLoad()
. Note that this is a throwing method, which means we need to wrap it in a do-catch
statement. The performFetch()
method is similar to the executeFetchRequest(_:)
method of the NSManagedObjectContext
class.
override func viewDidLoad() { super.viewDidLoad() do { try self.fetchedResultsController.performFetch() } catch { let fetchError = error as NSError print("\(fetchError), \(fetchError.userInfo)") } }
Step 3: Implementing the Delegate Protocol
With the fetched results controller set up and ready to use, we need to implement the NSFetchedResultsControllerDelegate
protocol. As we saw earlier, the protocol defines five methods, three of which are of interest to us in this tutorial:
controllerWillChangeContent(controller: NSFetchedResultsController)
controllerDidChangeContent(controller: NSFetchedResultsController)
controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
The first two methods tell us when the data the fetched results controller is managing will and did change. This is important to batch update the user interface. For example, it is possible that multiple changes occur at the same time. Instead of updating the user interface for every change, we batch update the user interface once all changes have been made.
In our example, this boils down to the following implementations of controllerWillChangeContent(controller: NSFetchedResultsController)
and controllerDidChangeContent(controller: NSFetchedResultsController)
.
// MARK: - // MARK: Fetched Results Controller Delegate Methods func controllerWillChangeContent(controller: NSFetchedResultsController) { tableView.beginUpdates() } func controllerDidChangeContent(controller: NSFetchedResultsController) { tableView.endUpdates() }
The implementation of controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
is a bit trickier. This delegate method takes no less than five arguments:
- the
NSFetchedResultsController
instance - the
NSManagedObject
instance that changed - the current index path of the record in the fetched results controller
- the type of change, that is, insert, update, or delete
- the new index path of the record in the fetched results controller, after the change
Note that the index paths have nothing to do with our table view. An NSIndexPath
is nothing more than an object that contains one or more indexes to represent a path in a hierarchical structure hence the class's name.
Internally the fetched results controller manages a hierarchical data structure and it notifies its delegate when that data structure changes. It's up to us to visualize those changes in, for example, a table view.
The implementation of controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
looks daunting, but let me walk you through it.
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { switch (type) { case .Insert: if let indexPath = newIndexPath { tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) } break; case .Delete: if let indexPath = indexPath { tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) } break; case .Update: if let indexPath = indexPath { let cell = tableView.cellForRowAtIndexPath(indexPath) as! ToDoCell configureCell(cell, atIndexPath: indexPath) } break; case .Move: if let indexPath = indexPath { tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) } if let newIndexPath = newIndexPath { tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade) } break; } }
The change type is of type NSFetchedResultsChangeType
. This enumeration has four member values:
Insert
Delete
Move
Update
The names are pretty self-explanatory. If the type is Insert
, we tell the table view to insert a row at newIndexPath
. Similarly, if the type is Delete
, we remove the row at indexPath
from the table view.
If a record is updated, we update the corresponding row in the table view by invoking configureCell(_:atIndexPath:)
, a helper method that accepts a ToDoCell
object and an NSIndexPath
object. We'll implement this method in a moment.
If the change type is equal to Move
, we remove the row at indexPath
and insert a row at newIndexPath
to reflect the record's updated position in the fetched results controller's internal data structure.
Step 4: Implementing the UITableViewDataSource
Protocol
That wasn't too difficult. Was it? Implementing the UITableViewDataSource
protocol is much easier, but there are a few things you should be aware of. Let's start with numberOfSectionsInTableView(_:)
and tableView(_:numberOfRowsInSection:
.
Even though the table view in our sample application will only have one section, let's ask the fetched results controller for the number of sections. We do this by calling sections
on it, which returns an array of objects that conform to the NSFetchedResultsSectionInfo
protocol.
Objects conforming to the NSFetchedResultsSectionInfo
protocol need to implement a few methods and properties, including numberOfObjects
. This gives us what we need to implement the first two methods of the UITableViewDataSource
protocol.
func numberOfSectionsInTableView(tableView: UITableView) -> Int { if let sections = fetchedResultsController.sections { return sections.count } return 0 }
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let sections = fetchedResultsController.sections { let sectionInfo = sections[section] return sectionInfo.numberOfObjects } return 0 }
Next up are tableView(_:cellForRowAtIndexPath:)
and configureCell(_:atIndexPath:)
. The implementation of tableView(_:cellForRowAtIndexPath:)
is short, because we move most of the cell's configuration to configureCell(_:atIndexPath:)
. We ask the table view for a reusable cell with reuse identifier ReuseIdentifierToDoCell
and pass the cell and the index path to configureCell(_:atIndexPath:)
.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifierToDoCell, forIndexPath: indexPath) as! ToDoCell // Configure Table View Cell configureCell(cell, atIndexPath: indexPath) return cell }
The magic happens in configureCell(_:atIndexPath:)
. We ask the fetched results controller for the item at indexPath
. The fetched results controller returns an NSManagedObject
instance to us. We update the nameLabel
and the state of the doneButton
by asking the record for its name and done attributes.
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) { // Fetch Record let record = fetchedResultsController.objectAtIndexPath(indexPath) // Update Cell if let name = record.valueForKey("name") as? String { cell.nameLabel.text = name } if let done = record.valueForKey("done") as? Bool { cell.doneButton.selected = done } }
We'll revisit the UITableViewDataSource
protocol later in this tutorial when we delete items from the list. We first need to populate the table view with some data.
7. Adding Records
Let's finish this tutorial by adding the ability to create to-do items. Open the AddToDoViewController
class, add an import statement for the Core Data framework, and declare a property managedObjectContext
of type NSManagedObjectContext!
.
import UIKit import CoreData class AddToDoViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var textField: UITextField! var managedObjectContext: NSManagedObjectContext! ... }
Head back to the ViewController
class and implement the prepareForSegue(_:sender:)
method. In this method, we set the managedObjectContext
property of the AddToDoViewController
instance. If you've worked with storyboards before, then the implementation of prepareForSegue(_:sender:)
should be straightforward.
// MARK: - // MARK: Prepare for Segue override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "SegueAddToDoViewController" { if let navigationController = segue.destinationViewController as? UINavigationController { if let viewController = navigationController.topViewController as? AddToDoViewController { viewController.managedObjectContext = managedObjectContext } } } }
If the user enters text in the text field of the AddToDoViewController
and taps the Save button, we create a new record, populate it with data, and save it. This logic goes into the save(_:)
method we created earlier.
@IBAction func save(sender: AnyObject) { let name = textField.text if let isEmpty = name?.isEmpty where isEmpty == false { // Create Entity let entity = NSEntityDescription.entityForName("Item", inManagedObjectContext: self.managedObjectContext) // Initialize Record let record = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: self.managedObjectContext) // Populate Record record.setValue(name, forKey: "name") record.setValue(NSDate(), forKey: "createdAt") do { // Save Record try record.managedObjectContext?.save() // Dismiss View Controller dismissViewControllerAnimated(true, completion: nil) } catch { let saveError = error as NSError print("\(saveError), \(saveError.userInfo)") // Show Alert View showAlertWithTitle("Warning", message: "Your to-do could not be saved.", cancelButtonTitle: "OK") } } else { // Show Alert View showAlertWithTitle("Warning", message: "Your to-do needs a name.", cancelButtonTitle: "OK") } }
The save(_:)
method looks pretty impressive, but there's nothing in there that we haven't covered yet. We create a new managed object, using an NSEntityDescription
instance and an NSManagedObjectContext
instance. We then populate the managed object with a name and date.
If saving the managed object context is successful, we dismiss the view controller, otherwise we show an alert by invoking showAlertWithTitle(_:message:cancelButtonTitle:)
, a helper method. If the user taps the save button without entering any text, we also show an alert. In showAlertWithTitle(_:message:cancelButtonTitle:)
, we create, configure, and present a UIAlertController
.
// MARK: - // MARK: Helper Methods private func showAlertWithTitle(title: String, message: String, cancelButtonTitle: String) { // Initialize Alert Controller let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) // Configure Alert Controller alertController.addAction(UIAlertAction(title: cancelButtonTitle, style: .Default, handler: nil)) // Present Alert Controller presentViewController(alertController, animated: true, completion: nil) }
Run the application and add a few items. I'm sure you agree that the NSFetchedResultsController
class makes the process of adding items incredibly easy. It takes care of monitoring the managed object context for changes and we update the user interface, the table view of the ViewController
class, based on what the NSFetchedResultsController
instance tells us through the NSFetchedResultsControllerDelegate
protocol.
Conclusion
In the next article, we'll finish the application by adding the ability to delete and update to-do items. It's important that you understand the concepts we discussed in this article. The way Core Data broadcasts the changes of a managed object context's state is essential so make sure you understand this before moving on.