This post compatible with Xcode 6.3 Beta, Updated on February 16, 2014
This is part three 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.
In this (somewhat lengthy) section of the tutorial, we’ll implement deleting rows, adding rows, and persisting the data so it stays put even after app close. When we’re done we’ll have something that works like the video here:
Implementing swipe-to-delete
Before we get started, let’s go ahead and wipe out the filtering we were toying around with in fetchLog(). We’ll just simplify fetchLog to look like this:
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 } }
First, we just need to implement the canEditRowAtIndexPath callback from the UITableViewDataSource protocol. In our case, we’ll be able to delete anything, so we can just return true all the time. But, if for example we had some entries locked, we could check the indexPath and return false instead.
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { return true }
Now, this tells the UITableView that we can edit these rows (deleting a row is a form of editing it.) But iOS will look for one more callback, tableView:commitEditingStyle:forRowAtIndexPath.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if(editingStyle == .Delete ) { // Find the LogItem object the user is trying to delete let logItemToDelete = logItems[indexPath.row] // Delete it from the managedObjectContext managedObjectContext?.deleteObject(logItemToDelete) // Refresh the table view to indicate that it's deleted self.fetchLog() // Tell the table view to animate out that row logTableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } }
This method gets called on the UITableViewDataSource as well, and it’s called upon performing the swipe-to-delete action on iOS. Without this method implemented swipe-to-delete will simply be disabled.
So, we add it in here and we take action based on the deleted row. When the editingStyle is .Delete, we know that the intention is the delete the row. We identify the row data from our logItems array by accessing the indexPath.row index (this is the integer row number of the deleted item in the table view)
Next, we delete the object in Core Data using the deleteObject method on the managedObjectContext we computed earlier.
Now, we can update the table view itself by calling self.fetchLog() again. We could stop here and called reloadData() on the tableview, but instead we can use the deleteRowsAtIndexPaths:withRowAnimation: method of the table view to get some free table view animations.
Try running the app now, and you can swipe left on a table view row to delete it. You’ll get some free animations, and this data is updated in our Core Data object graph.
Being able to delete objects is great, but we also need to be able to add items if this is going to be a real app. Let’s make an adjustment to our viewDidLoad() code.
We have a line in our code right now setting the height of the tableview:
// Reduce the total height by 20 points viewFrame.size.height -= 20
If we’re going to add a button we’ll just to adjust this even further, by the amount of the button height, and create the button. So we’ll set that up now:
// Add in the "+" button at the bottom let addButton = UIButton(frame: CGRectMake(0, UIScreen.mainScreen().bounds.size.height - 44, UIScreen.mainScreen().bounds.size.width, 44)) addButton.setTitle("+", forState: .Normal) addButton.backgroundColor = UIColor(red: 0.5, green: 0.9, blue: 0.5, alpha: 1.0) addButton.addTarget(self, action: "addNewItem", forControlEvents: .TouchUpInside) self.view.addSubview(addButton) // Reduce the total height by 20 points for the status bar, and 44 points for the bottom button viewFrame.size.height -= (20 + addButton.frame.size.height)
First, we create a UIButton instance, and set it’s size to be equal to 44×44 points. The position will be at an x coordinate of 0, and a y coordinate of whatever the screen height is, minus the size of the button (44 points) This puts it at the bottom of the screen.
We set the title to simply “+” to indicate adding a new item, set the background color, and add a target.
The target is a selector that fires when the button is clicked. In our case, the selector is called “addNewItem”, which means we need to create this as a function…
let addItemAlertViewTag = 0 let addItemTextAlertViewTag = 1 func addNewItem() { var titlePrompt = UIAlertController(title: "Enter Title", message: "Enter Text", preferredStyle: .Alert) var titleTextField: UITextField? titlePrompt.addTextFieldWithConfigurationHandler { (textField) -> Void in titleTextField = textField textField.placeholder = "Title" } titlePrompt.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (action) -> Void in if let textField = titleTextField { println(textField.text) } })) self.presentViewController(titlePrompt, animated: true, completion: nil) }
This function will create a UIAlertController object with a text field by using the addTextFieldWithConfigurationHandler method. I’ll probably go over this as a separate blog post at some point, but for now it’s enough to know that when the handler on the “Ok” action gets called, the textField’s text value is inside of textField.text. We can use this to take input from the user without adding too much more view work.
Okay, so now that we can detect the text entered, we can call a function to save the new item. We’ll call it saveNewItem:title
func saveNewItem(title : String) { // Create the new log item var newLogItem = LogItem.createInManagedObjectContext(self.managedObjectContext!, title: title, text: "") // Update the array containing the table view row data self.fetchLog() // Animate in the new row // Use Swift's find() function to figure out the index of the newLogItem // after it's been added and sorted in our logItems array if let newItemIndex = find(logItems, newLogItem) { // Create an NSIndexPath from the newItemIndex let newLogItemIndexPath = NSIndexPath(forRow: newItemIndex, inSection: 0) // Animate in the insertion of this row logTableView.insertRowsAtIndexPaths([ newLogItemIndexPath ], withRowAnimation: .Automatic) } }
Our method will take in the title as the only argument, and use the createInManagedObjectContext function we created earlier to add a new record with the title of title. For now, we can leave the text blank.
Similar to how we did before with the deletion, we need to call fetchLog() to update our table view’s backing array, logItems.
Finally, we can create our NSIndexPath for the new item, and called insertRowsAtIndexPaths to animate in the new row.
Now, we just need to call the new method from the handler closure in the titlePrompt’s “Ok” button:
titlePrompt.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (action) -> Void in if let textField = titleTextField { self.saveNewItem(textField.text) } }))
Running the app you can now add and delete rows to our table, backed by Core Data. If you close the app and restart it, you may notice that the data is reset every time. This is happening because Core Data doesn’t persist automatically, you must explicitly call save for that behavior. So as a final step in this tutorial section, let’s create a save() method in our ViewController.swift file.
func save() { var error : NSError? if(managedObjectContext!.save(&error) ) { println(error?.localizedDescription) } }
The API is pretty simple just call save() on the managedObjectContext. Optionally you can include an NSError pointer to capture errors. The save() function returns true on success, and false on failure. If the save succeeds, your data will now persist.
Let’s call our save() method after the user adds or removes an item.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { ... // Tell the table view to animate out that row logTableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) save() ...
func saveNewItem(title : String) { ... if let newItemIndex = find(logItems, newLogItem) { // Create an NSIndexPath from the newItemIndex let newLogItemIndexPath = NSIndexPath(forRow: newItemIndex, inSection: 0) // Animate in the insertion of this row logTableView.insertRowsAtIndexPaths([ newLogItemIndexPath ], withRowAnimation: .Automatic) save() ... }
Running the app we now have a super nifty app where we can add or remove items from a list with neat animations!
This concludes part 3 of the Core Data tutorial for now. The full source code can be found here.
In the next section, we’ll set up migrations so you can make changes to the schema of your live apps. Core Data Migrations Tutorial in Swift ».
As always, be sure to subscribe to my newsletter for tutorial updates and more, and take a look at my upcoming book, which is now in early access.
Hi Jameson ! I have followed this nice tutorial, but, when I write the code for deleting a row, my simulator does not show the red “Delete” button when I swipe left a row 😮
When i Download your code, everything works fine, but mine don’t, there is a thing to do in storyboard to allow this button?
And strange enough when I watch your storyboard from my Xcode, he is completely empty (often happens to me this time, maybe a bug with Xcode) but the app works fine.
Thanks for your help and your time 😉
Nice work! I will pre-order your book as soon I have the money 🙂
Anthony
It’s not a bug, there is no content in the storyboard, everything is added programmatically as shown in the tutorial.
If your delete button isn’t showing then your two callback methods aren’t present. The first is the method containing “canEditRowAtIndexPath” and the second containing “commitEditingStyle”.
You need to implement both of these functions as defined in the tutorial in order to see the delete button.
ok, thanks for the answer.
I understand for your code (the empty storyboard).
For the other issue, I had actually implemented the two methods that you describe “canEditRowAtIndexPath” and “commitEditingStyle”, this is why I don’t understand this problem :/ Probably a bug from Xcode, I only see that.
Thanks for your help ! 🙂
I ran into this same problem. However, I was adding my TableView through Interface Builder – following Jameson’s “Developing iOS 8 Apps Using Swift” tutorial, and I don’t know enough about Size Classes to know if adding the TableView through code (and setting its width to the View’s width) does the trick or not.
Regardless, the simplest answer seems to be disabling Size Classes. If you go into Interface Builder, click on the ViewController in question, then make sure the File Inspector tab is open in the Utilities area. Now look under the “Interface Builder Document” section and you can toggle the “Use Size Classes” option. After doing this you should be able to resize your TableView as needed and, after re-running your app, see the Delete button on swipe.
All that said, in the long run an understanding of how Size Classes and Auto Layout work is the way to go, but I’m still learning that myself. (Note: not sure if Jameson covers this in his tutorial series or book, but here is a pretty good article I found discussing this: https://www.codefellows.org/blog/size-classes-with-xcode-6-one-storyboard-for-all-sizes).
update data tutorial upload please…… thanks in advance
That’s a fair request 🙂
Hi, i have a question [logTableView .deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)] why do we need [] ??
thanks 😀
[] means to create an array with whatever is inside. Since the API wants “rows” and not “row”, it’s expecting between 1 and.. um.. more. So you have to say its an array of indexPaths… with just 1 record.. indexPath.
Is it possible to save and fetch on separate view controllers? Would it ever be advised to do so? Thanks in advance
In practice I would recommend saving and fetching in their own controllers, not tied to a View Controller. The View Controller should just call out to a class set up to handle this kind of stuff asyncronously.
Big thanks for your tutorial, Jameson! It is really easy to follow you, which, I think, is quite important when we talk about tutorials 🙂
I have one question! Each time I rerun the app, I get the managedObjectContext repopulated with these 1st, 2nd, etc. items, that are created in viewDidLoad (excuse me, if I explained something incorrect, if I did, please ask a question:)). So with each start my app gets more and more of these objects. Here’s a screenshot http://imageshack.com/a/img537/7789/YBadZ9.png
Which is the best way to escape this undesired effect?
Well, I came up with a solution: I created NumberOfReruns entity in Core Data where I put a counter for reruns and now the app checks this counter before executing the array population. Not sure if it is the best way.
This is a case of pre-populating data basically. In practice you would either load in a default database, or just check to see if the data is already in-place by counting records, or looking for a specific record. Your method works fine too, except that if a save fails for whatever reason, you might end up with NumberOfReruns greater than 1, but without the records actually inserted.
Thank you!
I’m fairly new to Xcode and IOS programming but have been programming in other environments for many years. As a means of learning programming for this “new” environment I have been working on developing an app which relies heavily on locally stored, persistent data. I have gone through many blogs and tutorials and I find your treatment of the subject matter to be extremely well thought out and easy to follow. You provide insight into why things are done without overloading the novice with unnecessary detail or theory. You also don’t simply regurgitate the information from Apple resources and instead restate the pertinent points in a manner that is far less intimidating and much more easily understood.
Keep up the great work! I look forward to reviewing your posts on other topics.
why you initialize the buttton by coding ?
why you didn’t add the button form the Object library
It’s an arbitrary decision. I like to show both methods in my tutorials.
Jameson
I worked through your tutorial. Thanks for posting it. I’m fairly green so I would like to go through it again and maybe a third time to really soak up your knowledge. Once I’m more comfortable I would like to extend this example. I would like to add a counter for each item/cell. I was thinking it would be nice to give the options to swipe right to add a value (+ 1) and swipe left to decrease a value (- 1). So the user would start out by adding items (like your example). Example maybe they are counting cars. If they add a Honda Civic and than see another Honda Civic they wouldn’t add it again they would just swipe right and in the cell it would show “Honda Civic 2”
Hopefully you understand what I want to accomplish. I’m just curious is using Core Data the best approach for an application like this or is Core Data overkill. Any insight would be super useful and appreciated. Thanks again great stuff.
Thanks
Christine
Core Data is probably overkill for your app, but I don’t think there’s a much simpler alternative that won’t a bunch of it’s own problems. I’d say go with Core Data.
I read parts 1-3 and this was fantastic! Thanks for doing this programmatically, too! I needed to create a categories UITableView application and this was indispensable over the last few days, just got the app done.
Awesome!
Hi Jameson,
I’m trying to understand how the swipe to delete works without us needing to write any gesture recognizer code. Is there some magic under the hood that makes the editingStyle of a cell equal to Delete when you swipe to the left?
Great tutorials!
–Veronica
It’s not really magic, you could manually implement the code yourself. If you want you can dig in to exactly what Apple did here. If you put a breakpoint in your code somewhere and open up the Variables View (the left side of the console output on the bottom) you can expand the tableView object and see that it contains a UIScrollView, and that scrollView contains a UITableView called ‘_swipe’ and *that* cell contains a ‘_swipeToDeleteGobblerGestureRecognizer’, which is an UIGobblerGestureRecognizer that Apple uses internally. The main difference I can see in that class is that it will prevent gestures from transmitting to other recognizers, mostly I’m sure it’s the same as any gesture recognizer though. Digging deeper you can actually see the delete button listed as an ‘_excludedView’, which I’m sure is to avoid confusing tapping of the delete button, and swiping of the delete button. (http://i.imgur.com/aQ9YNla.png)
This may not have answered your question, but the short answer is that Apple did a lot of the work for you already, and when you implement the protocols listed, such as UITableViewDataSource and UITableViewDelegate, what you’re really doing is intercepting some of the things that will happen by default from the iOS SDK.
Got it. I used magic in the way people use the word magic to describe Rails. While you could implement it yourself most people don’t and most don’t understand what it being done for you. Apple is doing enough work here that if your target audience was experienced engineers in other languages/platforms I would suggest you add an explanation.
Yeah the iOS SDK is about 80% magic in that regard 🙂
maybe its an oversight but after creating/deleting objects from the datastore, you never call save on the managedObjectContext to commit.
As a side note this tutorial could use a little more explanation.
a.) why/how to create the main view programmatically ( i knew the main view is the main container from doing windows programming, else ordinary user will never guess the hierarchy of UI elements)
b.) deletion – why do you have to fade? best practice or just to show you could do it?
c.) creating initial data. i would imagine you dont want to create data every time the application runs. a simple explanation to put in viewDidload, run app to create the data, comment out viewDidLoad, then re-run the app.
I stand corrected.
thank you for sharing the knowledge. am just standing to learn IOS after doing windows for years. i have tried using CoreData to create master/detail records in past using NSManagedObject. Until i saw this i was using a viewmodel object to transfer thing in and out of my array. also the samples i have seen from so called “experts” for deleting records was to add a new record at the old index then delete the old record ( i did understand the logic as we are dealing with array; old record will be pushed down) I just question why Apple will create an API that works like that. IOS takes getting used to but am beginning to adjust and put up with some of the annoying ways that Swift compiler and API have.
thank you