Porting Apple’s Core Data Books Sample Project to RubyMotion

end-result-root

In this post we’re going to reconstruct the Apple sample project Core Data Books, building it step by step from scratch using RubyMotion. This will take us through:

  • Use of NSFetchedResultsController to manage a collection of objects to be displayed in a table view,
  • Providing undo and redo functionality via the device shake gesture,
  • Use of a nested context to isolate changes made in an add operation.

The app presents a table view of books following the master-detail pattern; tapping on a book in this main (master) list takes you to its detail view. From there you can edit the book, or cancel (undo) the edit and return to the book list. From there you can also tap the add button to add a new book to the list. All of these changes are persisted to the database using the conventional patterns for doing so, as suggested by Apple.

This is an excerpt from the book, Beginning Core Data with RubyMotion. All code samples are available on GitHub.

NSFetchedResultsController

When displaying data stored in Core Data using a UITableView, you can write the code to manage the fetching of records yourself but you will end up writing very similar code in each app you build, to handle:

  • The fetching of only the subset of records need for what is to be shown on the screen,
  • Filtering and sorting,
  • And perhaps also caching.

Luckily though, Apple have abstracted this access out into a controller class, NSFetchedResultsController, that provides efficient access to Core Data objects including ways to easily track changes to these objects. In this chapter we will explore how we can use this class to reduce the amount of application code we need to write while providing a more efficient, responsive application experience to the user.

The Model

For ease of development we will use Core Data Query (CDQ) and ruby-xcdm to define our model.

The model itself is simply an entity called Book with three fields:

  • A title of type String
  • An author of type String
  • A copyright of type Date (optional)

Let’s create our project and define the model:

$ motion create CoreDataBooks
    Create CoreDataBooks
    Create CoreDataBooks/.gitignore
    Create CoreDataBooks/app/app_delegate.rb
    Create CoreDataBooks/Gemfile
    Create CoreDataBooks/Rakefile
    Create CoreDataBooks/resources/Default-568h@2x.png
    Create CoreDataBooks/spec/main_spec.rb

Add Core Data Query to your Gemfile:

gem 'cdq', '0.1.8'

And install:

$ bundle install

This should also pull in all required dependencies, such as ruby-xcdm.

We can now kickstart our Core Data development using the cdq init command on the shell which will:

  • Set up an empty schema for us to define our model(s) in,
  • Add in a spec (test) helper for CDQ that will help us later,
  • Add in the schema:build rake task into our Rakefile such that it will run every time we build and run the app for the simulator.
    $ cdq init
         Creating init:

      Δ  Creating directory: schemas
      Δ  Creating file: CoreDataBooks/schemas/0001_initial.rb
      Δ  Creating directory: spec/helpers
      Δ  Creating file: CoreDataBooks/spec/helpers/cdq.rb

         Done
      Δ  Checking bundle for cdq... 
      Δ  Adding schema:build hook to Rakefile... Done.

      Now edit schemas/0001_initial.rb to define your schema,
      and you're off and running.

We can now open up schemas/0001_initial.rb and create our Book model:

schema "0001 initial" do
  entity "Book" do
    string :title, optional: false
    string :author, optional: false
    datetime :copyright, optional: true
  end
end

The Core Data Stack

Next we need a Core Data stack set up for our application. At this stage we have no special requirements so we will make use CDQ to set this up automatically. Change app/app_delegate.rb to match the following:

class AppDelegate
  include CDQ

  def application(application, didFinishLaunchingWithOptions:launchOptions)
    cdq.setup
    true
  end
end

At this point we can run our application and create and save Book records as in previous examples, though we need to access the Book model via the cdq helper, i.e. cdq('Book') as we have not yet defined a model class to map to this entity. We can do this with another of CDQ’s helpers:

$ cdq create model Book

     Creating model: Book

  Δ  Creating directory: app/models
  Δ  Creating file: app/models/Book.rb
  Δ  Creating directory: spec/models
  Δ  Creating file: spec/models/Book.rb

     Done

Now we can refer to our Book instances via the Book class, e.g. Book.all.count.

The Root View Controller

Next we need our root view controller, which will be a UITableViewController, our ‘master’ table view. In the first instance we want to set up our NSFetchedResultsController, ready to populate our table view and later respond to events such as new books being created or existing books edited.

Create a directory called controllers; this is where we will keep all of our view controllers:

$ mkdir app/controllers

Next create a new file, app/controllers/root_view_controller.rb, setting up an empty UITableViewController:

class RootViewController < UITableViewController
end

This will be our root view controller, though the main controller of the app is going to be a UINavigationController. We set both controllers up in our app delegate (app/app_delegate.rb):

class AppDelegate
  include CDQ

  def application(application, didFinishLaunchingWithOptions:launchOptions)
    cdq.setup

    root_view_controller = RootViewController.alloc\
                                          .initWithNibName(nil, bundle: nil)

    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.makeKeyAndVisible
    @window.rootViewController = UINavigationController.alloc.\
                            initWithRootViewController(root_view_controller)

    true
  end
end

If we run the app now we will just see an empty table view. What we would like to do next is to display the books in the database in our table view, with each author having their own section. Here is what the finished product will look like:

end-result-root

We will use an NSFetchedResultsController to greatly reduce the amount of code we need to write in order to make our app fetch data efficiently and handle changes to the underlying data correctly, and with a minimum of extra coding. Our first step is to set up our NSFetchedReultsController instance with everything it needs to make the initial fetch. Extend app/controllers/root_view_controller.rb as follows:

class RootViewController < UITableViewController
  def viewDidLoad
    super

    error_ptr = Pointer.new(:object)

    if not self.fetchedResultsController.performFetch(error_ptr)
      # NOTE: We will need to replace this with proper error handling
      #       code.
      #       abort() causes the application to generate a crash log
      #       and terminate; useful during development but should not
      #       appear in a production application.
      NSLog("Unresolved error %@, %@", error_ptr, error_ptr.userInfo)
      abort
    end
  end

  # Methods related to NSFetchedResultsController

  def fetchedResultsController
    if not @fetched_results_controller.nil?
      return @fetched_results_controller
    end

    fetch_request = NSFetchRequest.alloc.init
    fetch_request.setEntity(Book.entity_description)

    author_sort_descriptor = NSSortDescriptor.alloc\
                                             .initWithKey("author",
                                                          ascending: true)
    title_sort_descriptor = NSSortDescriptor.alloc\
                                            .initWithKey("title",
                                                         ascending: true)

    fetch_request.setSortDescriptors([ author_sort_descriptor,
                                       title_sort_descriptor ])

    @fetched_results_controller = \
          NSFetchedResultsController\
              .alloc\
              .initWithFetchRequest(fetch_request,
                                managedObjectContext:cdq.contexts.current,
                                sectionNameKeyPath:"author",
                                cacheName:"Root")

    @fetched_results_controller.delegate = self

    @fetched_results_controller
  end
end

Breaking it Down

  • In viewDidLoad we start by calling super to perform the default initialisation and then make a call to self.fetchedResultsController to set up our fetched results controller.
  • In self.fetchedResultsController we:
    • First check if we have already set the controller up: we are storing the instance in an instance variable @fetched_results_controller–if that is not nil then we simply return it. This function is therefore only expensive to call the first time.
    • We then set up an NSFetchRequest as we have done before, setting the entity to that of Book.
    • We add in two sort descriptors, which together will cause books to be returned sorted first by author name and then by book title name.
    • We now have enough to initialise our NSFetchedResultsController, passing in:
      • our fetch request
      • the managed object context (where we take our only context, the main/current context as set up by CDQ)
      • a sectionNameKeyPath of ‘author’, meaning that in determining which section a given Book should go in, the controller will use the Book‘s author as the key.
      • Finally we specify a cache name so that we can benefit from NSFetchedResultsController‘s built-in caching mechanism.
    • Before returning our now-initialised controller we set its delegate to self, in preparation for the delegate methods that we will add so that we can configure the view controller’s response to changes made to our Book records (e.g. animating addition and removal of books from the table view).

We have set up and performed the initial fetch of Book records into memory but we are not yet displaying them in the table view because we have not yet set up the data source of the table view.

As this controller is a subclass of UITableViewController we already have a table view set up at self.tableView and its delegate and data source are already set to self, so all we need to do is provide the methods for the UITableViewDataSource (and later UITableViewDelegate) protocols, which we add to the definition of our RootViewController:

# UITableView data source methods

def numberOfSectionsInTableView(tableView)
  self.fetchedResultsController.sections.count
end

def tableView(tableView, numberOfRowsInSection:section)
  section_info = self.fetchedResultsController.sections\
                                              .objectAtIndex(section)
  section_info.numberOfObjects
end

def configureCell(cell, atIndexPath:indexPath)
  book = self.fetchedResultsController.objectAtIndexPath(indexPath)

  cell.textLabel.text = book.title
  cell
end

def tableView(tableView, cellForRowAtIndexPath:indexPath)
  cell = tableView.dequeueReusableCellWithIdentifier("CELL")

  cell ||= UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault,
                                               reuseIdentifier: "CELL")
  self.configureCell(cell, atIndexPath: indexPath)

  return cell
end

def tableView(tableView, titleForHeaderInSection:section)
  self.fetchedResultsController.sections.objectAtIndex(section).name
end

def tableView(tableView,
              commitEditingStyle:editingStyle,
              forRowAtIndexPath:indexPath)
  if editingStyle == UITableViewCellEditingStyleDelete
    cdq.contexts.current.deleteObject(self.fetchedResultsController\
                                          .objectAtIndexPath(indexPath))
    cdq.save
  end
end

Breaking it Down

  • The NSFetchedResultsController makes implementation of most of the methods quite straightforward, for example the table view wants to know the number of sections, and the results controller has already organised the results in sections, making it easy to count them.
  • The configureCell(cell, atIndexPath:indexPath) method is a helper method and not part of the protocol; this is here because later on when we set the controller up to respond to model changes we will use this helper method to consistently format cells after changes.
  • The final method, tableView(tableView, commitEditingStyle:editingStyle, forRowAtIndexPath:indexPath) is not used yet but is part of the data source protocol and will come into effect when a user edits the book list and taps ‘Delete’ on a book. The book will be removed from the table view (this is implemented in the table view delegate) and then here we will remove the underlying record from the context.

Running the app now we should see our Book records listed by author. Unless you have already created some however, the list will still be empty, so let’s create one in the console:

(main)> Book.create(title: "High Society", author: "Ben Elton")
=> <Book: 0x8ff1b60> (entity: Book; id: 0x8fed6d0 <x-coredata:///Book/...> ;
  data: {
    author = "Ben Elton";
    copyright = nil;
    title = "High Society";
})
(main)> cdq.save
=> true

The book won’t appear in the list until you restart the app, but it should be displayed when you do.

Responding to Changes

The books that we create via the console are now being shown in our table view, and we have linked up the delete mechanism of the table view to Core Data such that deletes made in the table view will also be reflected in our database.

We can’t at present get at this delete functionality, though it is very easy to do so, by making use of the predefined self.editButtonItem of the UITableViewController class. Let’s extend our viewDidLoad method to put this standard edit button as the left item of the navigation bar. While we’re here, let’s also place an add button in the right-hand space for later, when we set up our view for adding a new book:

class RootViewController < UITableViewController
  def viewDidLoad
    super

    # Set up the edit and add buttons
    self.navigationItem.leftBarButtonItem = self.editButtonItem;
    self.navigationItem.rightBarButtonItem = UIBarButtonItem.alloc\
                      .initWithBarButtonSystemItem(UIBarButtonSystemItemAdd,
                                                   target:self,
                                                   action:'addNewBook')
    # ...
  end
  # ...
  def addNewBook
  end
  # ..
end

Now if we build and run the app we have an edit button that will engage the table view’s row editing mode, which exposes the ability to delete individual rows. Tapping delete on any given row will cause it to be deleted from our database (as handled by our data source method), but won’t yet visibly disappear from the table view itself as we have not yet told the table view how to respond to delete events–we will do this next.

Selectors in RubyMotion

Note that in setting up our ‘add’ button we have pointed its action towards a particular method, 'addNewBook', which we have defined on the view controller. Pointing to methods in this manner would normally be handled in Objective-C using what is called a selector, but here we are instead passing in a string representation of the method name.

In Objective-C you would specify a selector for the method (void)addNewBook as @selector(addNewBook). In RubyMotion you can simply pass in the string 'addNewBook' wherever a selector is expected.

Note that if we defined our action method to also accept the sender object, i.e. def addNewBook(sender) then our selector would now be @selector(addNewBook:) in Objective-C or 'addNewBook:' in RubyMotion–the added colon signifies that there is a parameter accepted by the method in question.

At this stage we have hooked up our table view to our fetched results controller such that it can display the Book records that we create manually via the console. We have also connected the table view’s in-built deletion mechanism to the deletion mechanism of Core Data itself.

What remains in terms of connections is to implement the relevant NSFetchedResultsControllerDelegate methods such that changes at the Core Data level are reflected in the table view, and that actions taken at the table view level (e.g. tapping ‘Delete’) are effected at the Core Data level.

To complete the linkage we add the following delegate methods to the bottom of our RootViewController class:

class RootViewController < UITableViewController
  # ...
  # NSFetchedResultsController delegate methods to respond to additions,
  # removals and so on.

  def controllerWillChangeContent(controller)
    self.tableView.beginUpdates
  end

  def controller(controller,
                 didChangeObject:anObject,
                 atIndexPath:indexPath,
                 forChangeType:type,
                 newIndexPath:newIndexPath)
    case type
    when NSFetchedResultsChangeInsert
      self.tableView.insertRowsAtIndexPaths([newIndexPath],
                         withRowAnimation:UITableViewRowAnimationAutomatic)
    when NSFetchedResultsChangeDelete
      self.tableView.deleteRowsAtIndexPaths([indexPath],
                         withRowAnimation:UITableViewRowAnimationAutomatic)
    when NSFetchedResultsChangeUpdate
      self.configureCell(self.tableView.cellForRowAtIndexPath(indexPath),
                         atIndexPath:indexPath)
    when NSFetchedResultsChangeMove
      self.tableView.deleteRowsAtIndexPaths([indexPath],
                          withRowAnimation:UITableViewRowAnimationAutomatic)
      self.tableView.insertRowsAtIndexPaths([newIndexPath],
                          withRowAnimation:UITableViewRowAnimationAutomatic)
    end
  end

  def controller(controller,
                 didChangeSection:sectionInfo,
                 atIndex:sectionIndex,
                 forChangeType:type)
    case type
    when NSFetchedResultsChangeInsert
      self.tableView.insertSections(NSIndexSet.indexSetWithIndex(sectionIndex),
                          withRowAnimation:UITableViewRowAnimationAutomatic)
    when NSFetchedResultsChangeDelete
      self.tableView.deleteSections(NSIndexSet.indexSetWithIndex(sectionIndex),
                          withRowAnimation:UITableViewRowAnimationAutomatic)
    end
  end

  def controllerDidChangeContent(controller)
    self.tableView.endUpdates
  end
end

Breaking it Down

  • The key here is that when the NSFetchedResultsController receives the signal that data is going to change (controllerWillChangeContent), it tells the table view that updates are on the way (self.tableView.beginUpdates).
  • Once all updates have been made then the ‘session’ of updates is marked as complete and they are ‘committed’ by the table view (self.tableView.endUpdates). One reason for this is that by batching together several updates (some additions, some deletions, etc.) then the simplest but most informative table animation can be calculated and executed.
  • We then have two methods which respond to changes to individual books (didChangeObject) and sections (didChangeSection).
  • For individual books we check what kind of change was made (insert, delete, update or move) and make the appropriate calls to the table view. As these two classes have been designed to work together the translation from one to the other is quite straightforward.
  • When it comes to sections, the only time we need to add/remove them from the table view is when a book with a new author is created (and thus needs a new section created for it), or the last book for a particular author is deleted, thus leaving us an empty section to clean up. The methods are similarly straightforward.
  • We specify UITableViewRowAnimationAutomatic for all animations as the correct animations can be calculated automatically once all of the updates have been registered and we call self.tableView.endUpdates.

Case Statements in Ruby(Motion)

In an Objective-C switch statement you must be careful to include break statements to prevent multiple cases being matched (unless that was the desired behaviour).

In Ruby, the break is implicit at the end of each when block and you will get an error if you add break statements explicitly.

Implementing the Detail View Controller

The CoreDataBooks app has a view controller called DetailViewController that is used both as a detail view (showing the title, author and copyright of existing Book records) but also as a view for filling in the details of new Book records.

The finished result should look like this:

end-result-detail-view

The app uses a static table view to display the book’s details, with the added twist that tapping on any of the values will present another view controller which will allow the value to be edited.

The bulk of the functionality will be implemented in the DetailViewController class and then specialisations made in a subclass, AddViewController for the case where we are creating a new book record.

We start by creating a new view controller, saved in the new file app/controllers/detail_view_controller.rb:

class DetailViewController < UITableViewController
  FormFields = [ 'Title', 'Author', 'Copyright' ]

  attr_accessor :book

  def viewDidLoad
    super

    if self.book.nil?
      @values = [ 'Title', 'Author', '' ]
    else
      @values = [ self.book.title,
                  self.book.author,
                  self.book.copyright ]
    end
  end

  # UITableView data source methods

  def numberOfSectionsInTableView(tableView)
    1
  end

  def tableView(tableView, numberOfRowsInSection: section)
    3
  end

  def tableView(tableView, cellForRowAtIndexPath: indexPath)
    cell = tableView.dequeueReusableCellWithIdentifier("CELL")

    cell ||= UITableViewCell.alloc.initWithStyle(UITableViewCellStyleValue2,
                                                 reuseIdentifier: "CELL")

    cell.textLabel.text = FormFields[indexPath.row]

    cell.detailTextLabel.text = @values[indexPath.row]

    return cell
  end
end

Breaking it Down

  • We define an attr_accessor for :book, which will effectively create the property self.book which we will be able to access otuside of the class, as is common in iOS apps. This will be used when we instantiate the DetailViewController and give it a particular Book record to work with (or not, in the case of the add view).
  • In viewDidLoad we set up @values, which will contain the values for our three form fields (implemented as table cells). We check if we have been given a book or not. If not, we use some placeholder value. If we have been given a book then we initialise @values with the book’s attribute values.
  • The implementation of the UITableViewDataSource protocol is straightforward: we have just one section containing three cells, and the values for each cell are coming from the @values array that we initialised in viewDidLoad.
  • We use UITableViewCellStyleValue2 as that gives us a cell style that works well as a form, with right-aligned, highlighted field names and the rest of the space for the field value.

Displaying the Detail View

Before we get into implementing adding new books, let’s quickly link up our detail view so that when we tap on a book we are presented with the detail view, populated by the book’s attribute values.

To do this we will add a delegate method to respond to taps on the root view controller’s table cells (the books) that will instantiate a DetailViewController, pass it the book that was tapped and present it:

class RootViewController < UITableViewController
  # ...
  # Table view delegate

  def tableView(tableView, didSelectRowAtIndexPath:indexPath)
    selected_book = self.fetchedResultsController\
                        .objectAtIndexPath(indexPath)

    detailVC = DetailViewController.alloc.init
    detailVC.book = selected_book

    self.navigationController.pushViewController(detailVC, animated:true)

    tableView.deselectRowAtIndexPath(indexPath, animated: true)
  end

  # ...
end

Fairly straightforward, we retrieve the book from the fetched results controller, instantiate our DetailViewController, pass it the selected book and present it.

At this point we are successfully displaying the values for books that we have created via the console but are not yet able to create new books or edit existing ones via the user interface.

Editing Individual Fields

The next step is to set up the EditingViewController, which will display a UITextField when we are editing either the author or book title and a UIDatePicker when we are editing the copyright date, and looks like this:

end-result-edit-title

end-result-edit-copyright

 

We start by creating a new view controller, EditingViewController which will contain one UITextField and one UIDatePicker, which will be shown/hidden as appropriate depending on what we are editing.

In a new file, app/controllers/editing_view_controller.rb, place the following code:

class EditingViewController < UIViewController
  attr_accessor :editedObject
  attr_accessor :editedFieldKey
  attr_accessor :editedFieldName

  def viewDidLoad
    super

    self.view.backgroundColor = UIColor.whiteColor

    self.title = self.editedFieldName

    # This long list of assignments will give us a UITextField that looks 
    # like the example. Not to worry, with RubyMotion there are lots of
    # ways to get around verbose presentation code such as Motion Layout
    # and Teacup.
    @textField = UITextField.alloc.initWithFrame([[20, 84], [280, 31]])
    @textField.borderStyle = UITextBorderStyleRoundedRect
    @textField.font = UIFont.systemFontOfSize(15)
    @textField.autocorrectionType = UITextAutocorrectionTypeNo
    @textField.keyboardType = UIKeyboardTypeDefault
    @textField.returnKeyType = UIReturnKeyDone
    @textField.clearButtonMode = UITextFieldViewModeWhileEditing
    @textField.contentVerticalAlignment = \
                                    UIControlContentVerticalAlignmentCenter

    self.view.addSubview @textField

    @datePicker = UIDatePicker.alloc.initWithFrame([[0, 64], [320, 216]])
    @datePicker.datePickerMode = UIDatePickerModeDate

    self.view.addSubview @datePicker
  end
end

Breaking it Down

  • Thus far our view controllers have been UITableViewControllers but this one is a plain UIViewController.
  • We define three attribute accessors:
    • editedObject will be the book record being edited.
    • editedFieldKey will be the name of the attribute on the book object, which we will use to query the editedObject (a book) for the value of that field. So if the editedFieldKey is ‘title’ then we will set the field value to editedObject.title.
    • editedFieldName is the name of the field for presentation purposes, e.g. for the title field it would be presented as ‘Title’.
  • We create a UITextField and store it in the instance variable @textField. We unfortunately need to set quite a few of its attributes to make it look and behave like the field in the Objective-C version of CoreDataBooks but if we needed to do this often we could easily abstract this complexity away with our own class or helper function.
  • After adding our @textField to the view we create a UIDatePicker for picking dates (and not times), and add that to our view.

Presenting the Field Edit View

Back to our detail view controller (app/controllers/detail_view_controller.rb) we are now ready to display our editing view controller if the user taps one of the field cells:

class DetailViewController < UITableViewController
  # ...
  # UITableView delegate methods

  def tableView(tableView, didSelectRowAtIndexPath:indexPath)
    editingVC = EditingViewController.alloc.init

    editingVC.editedObject = self.book

    case indexPath.row
    when 0
      editingVC.editedFieldKey = "title"
    when 1
      editingVC.editedFieldKey = "author"
    when 2
      editingVC.editedFieldKey = "copyright"
    end

    # NOTE: The original CoreDataBooks example uses localised strings
    #       for the field name (editedFieldName) but we just
    #       capitalise the key (e.g. "author" becomes "Author" as that
    #       is sufficient, unless we want to localise the app.
    editingVC.editedFieldName = editingVC.editedFieldKey.capitalize

    self.navigationController.pushViewController(editingVC, animated: true)

    tableView.deselectRowAtIndexPath(indexPath, animated: true)
  end
end

Breaking it Down

  • When the user taps on a cell (one of our fields) then we instantiate an EditingViewController instance and pass in the three bits of information its needs:
    • The object that we are editing,
    • the method name of the attribute that we wish to edit,
    • and the presentation name of this field.

Conditionally Showing the Date Picker

If you run the app now, tapping on any of the fields should bring up the editing view controller but there are two problems: the text field and date picker aren’t showing the appropriate values from the book that was passed in, and they are both being shown at the same time.

To fix these two issues we will add a method which checks whether we have been passed in a date or not, and if so, show the date picker. Otherwise we assume that we have been passed in a string-type attribute and use the text field to display it. We make this check, show/hide elements as appropriate and set the values in viewWillAppear:

class EditingViewController < UIViewController
  # ...
  attr_accessor :hasDeterminedWhetherEditingDate
  attr_accessor :editingDate

  # ...
  def viewWillAppear(animated)
    super

    if self.isEditingDate
      @textField.hidden = true
      @datePicker.hidden = false

      date = self.editedObject.valueForKey(self.editedFieldKey)

      if date.nil?
        date = NSDate.date
      end

      @datePicker.date = date
    else
      @textField.hidden = false
      @datePicker.hidden = true

      @textField.text = self.editedObject.send(self.editedFieldKey)
      @textField.placeholder = self.title
      @textField.becomeFirstResponder
    end
  end

  protected

  def isEditingDate
    if self.hasDeterminedWhetherEditingDate
      return editingDate
    end

    entity = self.editedObject.entity
    attribute = entity.attributesByName[self.editedFieldKey]
    attributeClassName = attribute.attributeValueClassName

    if attributeClassName == "NSDate"
      self.editingDate = true
    else
      self.editingDate = false
    end

    self.hasDeterminedWhetherEditingDate = true

    return self.editingDate
  end
end
  • We determine whether the value to be edited is a date or not by accessing the entity description of the object to be edited and inspecting the relevant attribute. If the attribute class is of type NSDate then we record this fact and return true. We also record the fact that this has been determined, so that on subsequent calls we can simply return the answer immediately.
  • In viewWillAppear we use the isEditingDate helper to determine what kind of data we are dealing with, and show/hide the element appropriately so that only one is showing.
  • We then use two equivalent methods to get the value of the field:
    • For the date picker we call valueForKey(self.editedFieldKey) on the object to be edited.
    • For the text field we call send(self.editedFieldKey) on it instead.
    • Both methods have the same effect but send is more general and can be used to generalise your code; it allows you to send a specific message to an object, e.g. if self.editedFieldKey is the string 'title' then we can send 'title' to the book object as a message which will cause the method named title to be invoked; as this is the getter method for the title attribute, we are returned the value stored for the book’s title.

Adding New Books

Now that we can edit individual fields, apply those changes to our Core Data record and then persist these changes to the database we can create our add view.

In order to demonstrate how to isolate edits from the main context we will follow the example of the original sample project and handle the creation of the new Book in a child context of our main context.

It isn’t necessary to use a child context to do this, but it does demonstrate a useful pattern which you can use when you would like to provide a scratchspace, the accumulated changes of which may or may not be finally persisted to the main context, and so to the database.

Our completed add view controller should look like this:

end-result-add-view

What we will do to achieve this is:

  • Create a new controller, AddViewController which will be a subclass of DetailViewController and so inherit its field editing capabilities.
  • Add Save and Cancel buttons which will call back to its delegate (which will be the root view controller) to notify whether the user opted to save the new Book or not.
  • Configure the Save button to only be enabled if the book to be saved passes validation (i.e. has at least a title and an author).

Firstly, to make our new view appear when we tap add, we now need to implement our custom addNewBook action that we stubbed out earlier, in app/controllers/root_view_controller.rb:

def addNewBook(sender)
  addVC = AddViewController.alloc.init

  # We would like the AddViewController to notify us when it will return
  # and whether the user opted to save or discard the new Book, so we
  # register this controller instance as its delegate.
  # Note that we are not using a standard protocol for this, we will
  # be creating our own of the form `didFinishWithSave(saved)` where
  # `saved` is a boolean value.
  addVC.delegate = self

  # Here we create a child context within which to create our new record
  addingContext = cdq.contexts.new(NSMainQueueConcurrencyType)

  newBook = NSEntityDescription\
       .insertNewObjectForEntityForName("Book",
                                        inManagedObjectContext:addingContext)
  addVC.book = newBook
  addVC.managedObjectContext = addingContext

  self.navigationController.pushViewController(addVC, animated:true)
end

Here we create a new context, with a queue type of NSMainQueueConcurrencyType (more on queue types later). We create a new Book object and insert it into this context. Finally we pass this context and the book object to the add view controller.

Next we need to create our new view controller. Place the following code in a new file, app/controllers/add_view_controller.rb:

class AddViewController < DetailViewController
  attr_accessor :delegate
  attr_accessor :managedObjectContext

  def viewDidLoad
    super

    # Straight away we want the fields to be editable
    # without user interaction.
    self.setEditing(true, animated:false)

    # Add our Save and Cancel buttons
    @saveButton = UIBarButtonItem.alloc\
                            .initWithTitle("Save",
                                           style:UIBarButtonItemStylePlain,
                                           target:self,
                                           action:'save')
    self.navigationItem.rightBarButtonItem = @saveButton

    @cancelButton = UIBarButtonItem.alloc\
                            .initWithTitle("Cancel",
                                           style:UIBarButtonItemStylePlain,
                                           target:self,
                                           action:'cancel')
    self.navigationItem.leftBarButtonItem = @cancelButton
  end

  def cancel
    self.delegate.addViewController(self, didFinishWithSave:false)
  end

  def save
    self.delegate.addViewController(self, didFinishWithSave:true)
  end
end

As we are inheriting from DetailViewController there isn’t so much additional code that we need to write here. We make the fields editable straight away rather than requiring the ‘Edit’ button to be tapped. We have then configured our Save and Cancel buttons to call back to the delegate and notify that editing is finished and the user either opted to save the record or discard it.

All that remains is to persist the new record and merge it back into the main context, or else discard the child context that we created earlier, resulting in no changes to the main context.

We place the implementation of the delegate method in app/controllers/root_view_controller.rb:

class RootViewController < UITableViewController
  # ...
  # AddViewController delegate

  def addViewController(controller, didFinishWithSave:saved)
    if saved
      # The top-most context on the stack should be the
      # context that we passed to the add view controller
      # before
      addContext = cdq.contexts.pop

      error_ptr = Pointer.new(:object)

      unless addContext.save(error_ptr)
        raise "Error saving child context: #{error_ptr[0].description}"
      end

      # Rather than save the child context first we could have
      # simply called cdq.save as this will iterate through
      # all contexts from the top down, saving at each level.
      # We have presented it in this way mostly to demonstrate
      # the need for the separate saving of contexts in this order.
      cdq.save
    end

    self.navigationController.popViewControllerAnimated(true)
  end
end

CDQ makes it very easy to work with multiple contexts: in most cases you simply call cdq.save and it will step through the contexts in reverse order (main context last), saving each until all have been saved. For the sake of clarity we have split this out to show that what want to do is pop our context off the stack, save it, then save the main context.

Validating before Saving

The code as it stands has a problem, in that it allows us to attempt to save an invalid Book record (e.g. where either the title or author are blank), which will raise an exception.

What we would like to do is only enable the Save button while the data in the fields would result in a valid Book record. Luckily in Core Data this can be done with just one method call. We will perform this check in viewWillAppear; quite often in this kind of case you would need to perform the check in more than one place, but as editing occurs in a different view then performing this check as the view appears will be sufficient.

Update app/controllers/detail_view_controller.rb with the following code:

def viewWillAppear(animated)
  super

  self.updateInterface

  # Every time the view appears we want to check whether the current
  # values will result in a valid Book record.
  # This works because in order to edit an attribute the user is taken
  # to the separate editing view and returns to this view, thus ensuring
  # that this will be called whenever the field values change.
  self.updateRightBarButtonItemState
end

def updateRightBarButtonItemState
  # Conditionally enable the right bar button item
  # it should only be enabled if the book is in a valid state for saving.
  self.navigationItem\
      .rightBarButtonItem.enabled = self.book.validateForUpdate(nil)
end

Now when you run the app you should be able to add new Book records, and should automatically be prevented from saving invalid ones.

Adding Undo and Redo Capabilities

The addition of undo/redo capabilities to any app is often a welcome one; implementing such features reliably however can be tricky. By making use of the conventions of Core Data and iOS apps though we can support variables levels of undo and redo operations with relatively little additional code. The pattern we will follow can also be extended to handle the undoing of much more complex sets of edits, which is perhaps one of the principal benefits of Core Data.

In app/controllers/detail_view_controller.rb, extend the implementation of setEditing to call our helpers methods that will set up our undo manager when the user taps the Edit button, and remove it when they tap Done:

# Override setEditing so that we can manage the saving of the context
# when we finish editing, and later so that we can add in undo handling.
def setEditing(editing, animated:animated)
  super

  # Hide the back button when editing starts,
  # show it again when editing finishes.
  self.navigationItem.setHidesBackButton(editing, animated:animated)

  if editing
    self.setUpUndoManager
  else
    self.cleanUpUndoManager

    # We have finished editing, so we persist any changes to the database
    cdq.save
  end
end

Now we implement the two helper methods:

# Undo management

def setUpUndoManager
  if cdq.contexts.current.undoManager.nil?
    anUndoManager = NSUndoManager.alloc.init
    anUndoManager.setLevelsOfUndo 3

    cdq.contexts.current.undoManager = anUndoManager
  end

  # Register as an observer of the book's context's undo manager
  notification_center = NSNotificationCenter.defaultCenter

  notification_center\
                  .addObserver(self,
                               selector:'undoManagerDidUndo:',
                               name:NSUndoManagerDidUndoChangeNotification,
                               object:cdq.contexts.current.undoManager)

  notification_center\
                  .addObserver(self,
                               selector:'undoManagerDidRedo:',
                               name:NSUndoManagerDidRedoChangeNotification,
                               object:cdq.contexts.current.undoManager)
end

def cleanUpUndoManager
  NSNotificationCenter.defaultCenter\
                .removeObserver(self,
                                name:NSUndoManagerDidUndoChangeNotification,
                                object:cdq.contexts.current.undoManager)
  NSNotificationCenter.defaultCenter\
                .removeObserver(self,
                                name:NSUndoManagerDidRedoChangeNotification,
                                object:cdq.contexts.current.undoManager)

  cdq.contexts.current.undoManager = nil
end

When registering our undo management infrastructure we are specifying two helper methods, undoManagerDidUndo and undoManagerDidRedo which will be called whenever the undo manager performs an undo, or a redo, respectively. We will now add in their implementations:

# The undo manager will handle setting the values of the underlying
# object, we just need to reflect those changes in the user interface.
def undoManagerDidUndo(notification)
  self.updateInterface
  self.updateRightBarButtonItemState
end

def undoManagerDidRedo(notification)
  self.updateInterface
  self.updateRightBarButtonItemState
end

The last thing we need to do in the detail view controller is make sure that our view controller will receive shake events, which will only be sent to a view controller if it has registered itself as the first responder. We also set up our property self.undoManager so that the default shake-to-undo mechanism can find our undo manager:

# In order for the default shake-to-undo functionality to work
# we need to provide the appropriate undo manager as
# `self.undoManager'.
def undoManager
  cdq.contexts.current.undoManager
end

# The view controller must be first responder in order to be able to receive
# shake events for undo. It should resign first responder status when it
# disappears.
def canBecomeFirstResponder
  true
end

# Whenever the view appears we become first responder so that we will
# receive shake events.
def viewDidAppear(animated)
  super

  self.becomeFirstResponder
end

# And then whenever the view will disappear we resign first responder
# so that other view controller can become first responder.
def viewWillDisappear(animated)
  super

  self.resignFirstResponder
end

The last bit of additional code we should add is to give the operation to undo an appropriate name so that the user knows what will be undone, we do this in app/controllers/editing_view_controller.rb:

def save
  undoManager = cdq.contexts.current.undoManager

  undoManager.setActionName self.editedFieldName

  if self.editingDate
    self.editedObject.setValue(@datePicker.date,
                               forKey:self.editedFieldKey)
  else
    self.editedObject.setValue(@textField.text,
                               forKey:self.editedFieldKey)
  end

  self.navigationController.popViewControllerAnimated(true)
end

And with that, after editing any of the fields and before tapping Done we can shake the device to undo up to 3 edits. Shaking again will present the option to redo what was just undone; with a bit of thought you can use this pattern throughout your apps to allow even complex changes to be undone, something that your users are sure to appreciate.

While shaking to undo is a convention on iOS not all users are aware of its existence and if your use case calls for frequent use of undo and redo then you will probably want to provide more direct access to these functions. You can, anywhere in your code, call self.undoManager.undo and self.undoManager.redo to perform these operations at any time. To check whether it is possible to perform an undo at any time you can call self.undoManager.canUndo, which you can use to enable or disable your undo button as appropriate.

Wrapping Up

In this chapter we have built from scratch an app which demonstrates the most common patterns you will need to create even very complex data-driven apps which include features such as:

  • Efficient fetching and updating of records, backed by a cache (using NSFetchedResultsController).
  • Undo and redo functionality via the device shake gesture.
  • Scratch spaces where complex sets of edits can be gathered and either committed all at once or easily discarded (using child contexts).

Where to next?

Book-Cover

This post is part of a Leanpub book that I’m writing on using Core Data in RubyMotion projects. If you would like to learn more about using Core Data effectively in your RubyMotion apps please have a look.

Also coming up will be more blog posts where we will delve into some of the thornier issues that you might encounter when using Core Data. If such posts would be of interest to you then you might want to follow me on Twitter.