This post compatible with Xcode 6.3, Updated on February 16, 2015
This is part two of a tutorial series covering the usage of Core Data in Swift to write iOS apps with persistence. If you haven’t read part one yet, read that first.
If you really want to get your feet wet, my Swift book which is now available for pre-order with early access.
Creating more records
First off, if we’re going to do anything more interesting than just showing a single record pop up in an alert, we’ll need to generate some more records to play with. Let’s open up our LogItem.swift file and add a helper function for adding new records.
Basically, we want to be able to simply call a method on the LogItem class, pass in some parameters, and get a new LogItem back.
For example, when we’re done the actual function call might look like this:
let newItem = LogItem.createInManagedObjectContext(managedObjectContext, "Item Title", "Item text")
The LogItem class is not tied to any specific NSManagedObjectContext, so we want to make sure we aren’t storing the reference to the managed object context anywhere in the model, it needs to be passed in when we want to create an object like this.
Okay so let’s implement the method in LogItem.swift:
class LogItem: NSManagedObject { @NSManaged var itemText: String @NSManaged var title: String class func createInManagedObjectContext(moc: NSManagedObjectContext, title: String, text: String) -> LogItem { let newItem = NSEntityDescription.insertNewObjectForEntityForName("LogItem", inManagedObjectContext: moc) as! LogItem newItem.title = title newItem.itemText = text return newItem } }
The first line is the function definition. It’s a class function called createInManagedObjectContext, which takes an object of type NSManagedObjectContext as the argument moc, a String called title, and a String called text. The function returns a LogItem object that’s been inserted in to the specified managed object context.
Then it executes nearly identical code as before to create a new LogItem object, except now it’s using the arguments passed in to the function to set up the new LogItem object.
We can replace our original code in ViewController.swift now to just use the new method. Let’s add a bunch of new items…
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. // Use optional binding to confirm the managedObjectContext if let moc = self.managedObjectContext { // Create some dummy data to work with var items = [ ("Best Animal", "Dog"), ("Best Language","Swift"), ("Worst Animal","Cthulu"), ("Worst Language","LOLCODE") ] // Loop through, creating items for (itemTitle, itemText) in items { // Create an individual item LogItem.createInManagedObjectContext(moc, title: itemTitle, text: itemText) } } }
To keep the code simple, we’re using some shortcuts in order to seed our data. What you’re seeing when I’m setting items is an array (using square brackets []), then each element is a tuple of two String values, [(String, String)].
Next, I’m decomposing them back in to two variables, itemTitle and itemText for each of the tuples in the array.
Finally, I call the createInManagedObjectContext method, which we created earlier, passing in the new itemTitle and itemText.
If you already know how to set up a UITableView programmatically and want to skip ahead to the Core Data stuff, click here.
Now that we have a couple of records, let’s remove presentItemInfo and instead opt for a table view here. We’ll add this all right under viewDidLoad and programmatically create the UITableView. In my iTunes tutorial we do this using storyboards. If you are more interested in working with storyboards I recommend taking a pit stop there to read about how to get that set up.
We’ll set up the tableView by adding a logTableView to the ViewController class, and set it all up in viewDidLoad()
// Create the table view as soon as this class loads var logTableView = UITableView(frame: CGRectZero, style: .Plain) override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. // Use optional binding to confirm the managedObjectContext if let moc = self.managedObjectContext { // Create some dummy data to work with var items = [ ("Best Animal", "Dog"), ("Best Language","Swift"), ("Worst Animal","Cthulu"), ("Worst Language","LOLCODE") ] // Loop through, creating items for (itemTitle, itemText) in items { // Create an individual item LogItem.createInManagedObjectContext(moc, title: itemTitle, text: itemText) } // Now that the view loaded, we have a frame for the view, which will be (0,0,screen width, screen height) // This is a good size for the table view as well, so let's use that // The only adjust we'll make is to move it down by 20 pixels, and reduce the size by 20 pixels // in order to account for the status bar // Store the full frame in a temporary variable var viewFrame = self.view.frame // Adjust it down by 20 points viewFrame.origin.y += 20 // Reduce the total height by 20 points viewFrame.size.height -= 20 // Set the logTableview's frame to equal our temporary variable with the full size of the view // adjusted to account for the status bar height logTableView.frame = viewFrame // Add the table view to this view controller's view self.view.addSubview(logTableView) // Here, we tell the table view that we intend to use a cell we're going to call "LogCell" // This will be associated with the standard UITableViewCell class for now logTableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "LogCell") // This tells the table view that it should get it's data from this class, ViewController logTableView.dataSource = self } }
Since we set the dataSource to be our ViewController class, we also need to adhere to the UITableViewDataSource protocol, so add that to the ViewController’s class definition:
class ViewController: UIViewController, UITableViewDataSource {
…and add the actual dataSource methods…
// MARK: UITableViewDataSource func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 5 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! UITableViewCell cell.textLabel?.text = "\(indexPath.row)" return cell }
Still with me? Hope so… if not here’s the full ViewController.swift code up until this point. Note that we also removed the viewDidAppear function since we were only using that for testing out some things earlier.
import UIKit import CoreData class ViewController: UIViewController, UITableViewDataSource { // Retreive the managedObjectContext from AppDelegate let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext // Create the table view as soon as this class loads var logTableView = UITableView(frame: CGRectZero, style: .Plain) override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. // Use optional binding to confirm the managedObjectContext if let moc = self.managedObjectContext { // Create some dummy data to work with var items = [ ("Best Animal", "Dog"), ("Best Language","Swift"), ("Worst Animal","Cthulu"), ("Worst Language","LOLCODE") ] // Loop through, creating items for (itemTitle, itemText) in items { // Create an individual item LogItem.createInManagedObjectContext(moc, title: itemTitle, text: itemText) } // Now that the view loaded, we have a frame for the view, which will be (0,0,screen width, screen height) // This is a good size for the table view as well, so let's use that // The only adjust we'll make is to move it down by 20 pixels, and reduce the size by 20 pixels // in order to account for the status bar // Store the full frame in a temporary variable var viewFrame = self.view.frame // Adjust it down by 20 points viewFrame.origin.y += 20 // Reduce the total height by 20 points viewFrame.size.height -= 20 // Set the logTableview's frame to equal our temporary variable with the full size of the view // adjusted to account for the status bar height logTableView.frame = viewFrame // Add the table view to this view controller's view self.view.addSubview(logTableView) // Here, we tell the table view that we intend to use a cell we're going to call "LogCell" // This will be associated with the standard UITableViewCell class for now logTableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "LogCell") // This tells the table view that it should get it's data from this class, ViewController logTableView.dataSource = self } } // MARK: UITableViewDataSource func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 5 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! UITableViewCell cell.textLabel?.text = "\(indexPath.row)" return cell } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
This will give us a numbered list of rows if we run the app. This just confirms the table view is set up correctly.
If you ever get an error that says something like, The model used to open the store is incompatible with the one used to create the store
This means that the model Core Data thinks it’s working with doesn’t quite match what’s been stored in the database. Resolving this with a live app means migrating between versions of your model. But for the purpose of learning and doing the initial development, usually the quickest way to just keep moving is to delete the app from the simulator; wiping out the data, and therefore resolving the conflict.
Now that we have this all set up and ready to go, we can start working on getting our log data showing up in the table view.
Please note that at this point in the tutorial, I’m intentionally avoiding the NSFetchedResultsController class. I’m doing this because I believe it will make more sense from the Core Data perspective the first time around if you see it done the old fashioned way. After this tutorial is over, I encourage you to look over the class and see how you might use it to implement some of these things instead. I think it is important to first learn how to implement a core data backed table view without using the helper class. The helper class is not applicable in all situations, and learning it first would be doing yourself a disservice. You’ll find it doesn’t work for all use cases (not even close), and in it’s magic it hides some of what’s going on.
First off, let’s fetch all the results from Core Data in viewDidLoad(), and store them in an array of LogItem’s, logItems.
First we’ll add the variable to the ViewController class:
// Create an empty array of LogItem's var logItems = [LogItem]()
Next, we’ll populate it from viewDidLoad(), inside of a function called fetchLog().
override func viewDidLoad() { ... fetchLog() } func fetchLog() { let fetchRequest = NSFetchRequest(entityName: "LogItem") if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] { logItems = fetchResults } }
Now we can modify the tableView dataSource methods to refer to this array instead of hard-coding values.
// MARK: UITableViewDataSource func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // How many rows are there in this section? // There's only 1 section, and it has a number of rows // equal to the number of logItems, so return the count return logItems.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! UITableViewCell // Get the LogItem for this index let logItem = logItems[indexPath.row] // Set the title of the cell to be the title of the logItem cell.textLabel?.text = logItem.title return cell }
This should set us up to see the items listed in the table view, but we want to show the text of an item when it’s clicked. So we need to set the table view to use this view controller as it’s delegate, so we can receive the callback method, didSelectRowAtIndexPath.
Similar to before, add the UITableViewDelegate protocol to the class…
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
And set the delegate to be self, you can do this right under where you set the dataSource in viewDidLoad…
// This tells the table view that it should get it's data from this class, ViewController logTableView.dataSource = self logTableView.delegate = self
Finally we can implement the method, knowing the Table View will call it when a cell is clicked…
// MARK: UITableViewDelegate func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { let logItem = logItems[indexPath.row] println(logItem.itemText) }
This will set us up so that when the button is clicked, a message will show in the console (in the Xcode window) showing the itemText for the clicked item.
The purpose of this tutorial is not really to explain how to set up a table view manually, but it’s sort of necessary in order to get a good look at the data. For this reason, I’m providing the source code up until this point in the tutorial here. Feel free to simply check this out and work from this code base, from here we’ll be talking more about the Core Data side of things.
https://github.com/jquave/Core-Data-In-Swift-Tutorial/tree/Part1.5
You may see something kind of like this now, except with different data:
Your ordering may be different every time you run your app, this is because it contains no sorting when it performs the fetch request. In some data stores you might get this data back in the same order as it was inserted, but with Core Data it ends up being fairly random. Let’s change this by adding a sort to the fetch request.
func fetchLog() { let fetchRequest = NSFetchRequest(entityName: "LogItem") // Create a sort descriptor object that sorts on the "title" // property of the Core Data object let sortDescriptor = NSSortDescriptor(key: "title", ascending: true) // Set the list of sort descriptors in the fetch request, // so it includes the sort descriptor fetchRequest.sortDescriptors = [sortDescriptor] if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] { logItems = fetchResults } }
Now the fetch request has it’s sortDescriptors property set. Note that this is an array, which is why we need the brackets around the single sortDescriptor we created using the title key. Running the app you should now see the sorted (alphabetically) list of items, much better. Note your data is expected to be different.
Let’s also look at filtering out certain elements. First, let’s just try and only get the item named “Best Language”. We’ll create an NSPredicate that uses a string to represent the requirement that any object must fulfill in order to pass through the query.
func fetchLog() { let fetchRequest = NSFetchRequest(entityName: "LogItem") // Create a sort descriptor object that sorts on the "title" // property of the Core Data object let sortDescriptor = NSSortDescriptor(key: "title", ascending: true) // Set the list of sort descriptors in the fetch request, // so it includes the sort descriptor fetchRequest.sortDescriptors = [sortDescriptor] // Create a new predicate that filters out any object that // doesn't have a title of "Best Language" exactly. let predicate = NSPredicate(format: "title == %@", "Best Language") // Set the predicate on the fetch request fetchRequest.predicate = predicate if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] { logItems = fetchResults } }
If you haven’t seen the format syntax yet, or haven’t seen it in a while, it’s simple enough to say that any time you see format as a named parameter, it comes from the Objective-C methods “predicateWithFormat”, or “stringWithFormat”, and so on. This replaces any instances of the %@ symbol with an object’s description (the value of a string, or an otherwise useful representation of other types of objects). For primitive types such as an Int, you’ll want to instead opt for %i, %f for Float, and so on.
So when you see
(format: “title == %@”, “Best Language”)
What the compiler sees is something like this:
(“title == ‘Best Language'”)
So we’re just specifying we want title to equal that exact string.
Running the app now we should see just one item.
We could also do a string comparison using the contains keyword, if we look for the substring “Worst” we’ll only get the items that contain that string…
// Search for only items using the substring "Worst" let predicate = NSPredicate(format: "title contains %@", "Worst") // Set the predicate on the fetch request fetchRequest.predicate = predicate
What if we wanted to combine the two though? We want both items containing the string “Worst” and any one with a title “Best Language”?
First, let’s create the two separate predicates:
// Create a new predicate that filters out any object that // doesn't have a title of "Best Language" exactly. let firstPredicate = NSPredicate(format: "title == %@", "Best Language") // Search for only items using the substring "Worst" let thPredicate = NSPredicate(format: "title contains %@", "Worst")
Then combine them using the NSCompoundPredicate constructor:
// Combine the two predicates above in to one compound predicate let predicate = NSCompoundPredicate(type: NSCompoundPredicateType.OrPredicateType, subpredicates: [firstPredicate, thPredicate]) // Set the predicate on the fetch request fetchRequest.predicate = predicate
Since we want both cases of the “Best Language” and any item containing “Worst”, we use a compound predicate type of NSCompoundPredicateType.OrPredicateType
All this is just a confusing way of saying, “give me any items where the firstPredicate or the thPredicate is true.”
What we did here is quite powerful in practice. We can use string comparison as predicates, and filter/order large lists of data through Core Data. The Core Data API will then connect to the backing store (SQLite) and produce an efficient query to quickly retrieve the needed information. This is a very common pattern in iOS development, and understanding it is very important. So, if you got stuck on this tutorial or have any questions, don’t hesitate to ask for help on the forums.
This concludes Part 2 for now, in the next section we’ll move in to a more applicable scenario by adding a way for user’s to create log items, edit, save, and delete them.
The complete source code to this section »
If you’re reading this scratching your head, or just want a deeper dive in to Core Data, be sure to look at my Swift book, which is now available for early access. And don’t forget to subscribe to get updates on this tutorial series via e-mail.
awesome! thank you so much.
you always can lead me learn swift & cocoa step by step.
i input the codes follow your every tutorial. all of them can be ran successfully except chapter “Async image loading and caching”. my problem is when the project launch without any image loading. if i click one of rows, this row can show image, but others not. i tried to copy your github codes (*.swift) into my project folder, the problem still exist. but i git clone all of your codes, it is okay.
i have no idea for this, please do me a favor.
thanks in advance.
Thanks, good tutorial.
I cannot compile NSCompoundPredicate with Xcode 6.1 though.
// Combine the two predicates above in to one compound predicate
let predicate = NSCompoundPredicate(type: .OrPredicateType, subpredicates: [firstPredicate, thPredicate])
produces: “Could not find member OrPredicateType”
let predicate = NSCompoundPredicate(type: NSCompoundPredicateType.OrPredicateType, subpredicates: [firstPredicate, thPredicate])
produces: “Cannot invoke ‘init’ with an argument list of type ‘(type: NSCompoundPredicateType, subpredicates: $T3)”
Thanks in advance
let predicate = NSCompoundPredicate(type: .OrPredicateType, subpredicates: [firstPredicate!, thPredicate!])
made it working
Thanks! These APIs are constantly changing and audited for Optional conformance (which means the ! changes often)
I’ve updated my tutorial now 🙂
It’s fixed now for 6.1, sorry about that!
Great Tutorial, thanks! I think I’m finally beginning to understand Core Data. There is something I don’t understand, though. I tried an experiment to test the persistence of this exercise. Specifically, I added a User Default condition so that the values assigned to the “title” and “text” elements would only execute on the initial launch but not on subsequent launches. Here is the code:
if userDefaults.valueForKey(“runThrough”) == nil {
LogItem.createInManagedObjectContext(self.managedObjectContext!, title: “1st Item”, text: “This is my first log item”)
LogItem.createInManagedObjectContext(self.managedObjectContext!, title: “2nd Item”, text: “This is my second log item”)
LogItem.createInManagedObjectContext(self.managedObjectContext!, title: “3rd Item”, text: “This is my third log item”)
LogItem.createInManagedObjectContext(self.managedObjectContext!, title: “4th Item”, text: “This is my fourth log item”)
LogItem.createInManagedObjectContext(self.managedObjectContext!, title: “5th Item”, text: “This is my fifth log item”)
LogItem.createInManagedObjectContext(self.managedObjectContext!, title: “6th Item”, text: “This is my sixth log item”)
userDefaults.setValue(“x”, forKey: “runThrough”)
userDefaults.synchronize()
}
When I run this code, the items are populated in the table view on the first launch, but not on any launches after that – unless of course I delete the app and relaunch again. It was my understanding that the “LogItem.createIn…. lines were supposed to persist, and wouldn’t need to execute every time. But when I introduce a condition that only lets this code execute once, the data does not seem to persist. What am I not understanding?
Thanks!
That’s because there is no save() happening! Check out part 3 to see the persistence in action 🙂
Hahahaha… there’s a Part 3? (facepalm)
Maybe it’s because of my XCode version (6.1) but for the predicate method I had to add exclamation points like this:
let predicate = NSCompoundPredicate(type: .OrPredicateType, subpredicates: [firstPredicate!, thPredicate!])
Thanks for the tuto!
So when and where is episode 3?
Now
http://jamesonquave.com/blog/core-data-in-swift-tutorial-part-3/
🙂
Cool!
I had some problems with compiling the part 2 of your amazing tutorial.
It was caused because of the following:
cell.textLabel.text = “\(indexPath.row)”
The compiler was complaining about ‘UILabel?’ does not have a member named ‘text’
I have fixed it by adding changing the line to:
cell.textLabel?.text = “(\indexPath.row)”
Now it compiles fine.
I am having trouble with the “class func createInManagedObjectContext”
xCode says that “Class methods may only be declared on a type”
Also, in the view controller, I get an error that “‘LogItem.Type’ does not have a member named ‘createInManagedObjectContext'”
Thanks!
Both of those are probably caused by the class fun being put in the wrong place. It should be inside of the LogItem.swift file within the curly braces defining the class.
your swift tutorial series is awesome! as someone with little-to-no app development experience, it helped me easily get through what could have been a very frustrating process. thanks for the help.
For the line
if let moc = self.managedObjectContext {
I’m getting an error
Initializer for conditional binding must have Optional type, not ‘NSManagedObjectContext’
Note: I am using Xcode 7.0 beta which may have something to do with it. Any solutions?
I was getting the same error, Change line to :
if let moc = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext
Great tutorial so far, thanks a lot! I was having a problem with the data not displaying in the table view. I checked everything: set delegate and dataSource to self, set reference to table view, check if the ‘items array’ had elements in it. cellForRowAtIndexPath was not being called. Turned out, what I did wrong was: I put the fetch() call in viewDidAppear() instead of viewDidLoad()! (Noob mistake, shame shame)
By the way, nice to learn about sortDescriptors and predicates. Very powerful stuff indeed!