Use Your Loaf

Creating an OS X Core Data Helper App

Do you ever find you need to pre-populate an iOS Core Data app with some data and wanted to create and edit that data with a full OS X desktop app? If you have only ever developed for iOS you may not consider this approach because OS X seems at first to be more complicated. Convinced that it could not be that hard I thought I would give it a go and share my work.

I have an iOS Core Data example app in my GitHub repository which will serve as a test. The WorldFacts project uses a Core Data store to hold a list of Country objects containing some common facts about each country such as its capital, population, area, etc. The Core Data store is created on startup from a plist file. Our aim is to create an editable version of that core data store that we can managed on the Mac and then imported into the iOS app.

Creating An OS X Target

The first step is to add an OS X target to the WorldFacts Xcode project. With the project highlighted in the Xcode Navigator pane use the “+” button to add a target:

Choose the “Cocoa Application” template for the new target from the OS X section:

The product name for the target needs to be unique within the Xcode project so we will call the OS X target WorldFactsBuilder. We will also stick to Objective-C for now and use both Storyboards and Core Data as with the iOS project:

The Xcode project should now contain a new OS X target named WorldFactsBuilder with the basic template files for a core data OS X app stored in a new folder named after the target. (There is also be an OS X unit test target created by default but I will remove that for now).

OS X Application Architecture

It is interesting to take a look at the objects in the storyboard created by the Xcode template to get a feel for the architecture of a modern OS X application:

Application Object (NSApplication): Like the UIApplication object of an iOS application you do not normally subclass this object instead you work with the application delegate. Note that the main menu (NSMenu) is a property of the application object.

Application Delegate: The applicate delegate object implements the NSApplicationDelegate protocol in much the same way as an iOS application delegate implements the UIApplicationDelegate protocol. Many of the methods such as application[Will|Did]FinishLaunching: and applicationWillTerminate: will be recognisable to an iOS developer.

Window Controller (NSWindowController): This takes care of loading and managing a single window (it is the window delegate). You do not need to create the window as it is already contained in the Storyboard. In OS X 10.10 Yosemite the window controller has properties for an associated content view controller and the storyboard that loaded it:

@property (strong) NSViewController *contentViewController NS_AVAILABLE_MAC(10_10);
@property(readonly, strong) NSStoryboard *storyboard NS_AVAILABLE_MAC(10_10);

View Controllers (NSViewController): As with a UIViewController an NSViewController manages a view hierarchy. We will have a single view controller to manage a table view for our country data.

If you want to dig deeper see this earlier post on useful OS X resources.

Building the Storyboard

The Main Menu

Let’s start with the easy part and simplify the main menu by deleting the Format and View menus which we will not need:

We also need to fix a small problem with the template code for the File > Save command. In the template this command sends the SaveDocument: action to the first responder. The method in the application delegate that saves the core data stack is named SaveAction:. Delete the SaveDocument: action and control drag from the Save menu command to the first responder action and select SaveAction:.

Creating a Table View

For the main interface we have some choices. The iOS app uses a split view controller to display a summary list of the countries in a table view with a separate detail view controller. Yosemite also has an NSSplitViewController but I am going to keep it simple. Since we have more screen space we will show everything in a single table view.

To create the table view interface drag a table view from the object library into the view of the generic view controller. The result is, at first glance, a complex hierarchy of unfamiliar views:

If you compare with a UITableView on iOS much more of the view hierarchy is shown in Interface Builder. The top view is the scroll view which contains a clip view which contains the actual table view. The scroll bar objects and table header view are also directly visible in Interface Builder.

The other major difference with iOS is that our table has columns. Each column contains an NSTableCellView with a single NSTextField to display the country property. We have eight properties so we need eight columns in the table.

Click on the table view in the Interface Builder document outline and then in the attributes inspector increase the number of columns to “8”. Whilst in the attributes inspector you can also adjust the appearance and behaviour of the table view.

I kept the defaults apart from two settings as follows:

  • Alternating rows to highlight alternate rows.
  • Autosave including Column Information. Entering a name for the Table View causes the table setup such as column widths, order, etc. to be saved to the user defaults system and restored across application runs.

At this point you will probably need to resize the table view columns and the overall size of the view to make everything visible. I found it easier to set the table column widths to 100 and then resize the containing view so that the table view fits within the layout guidelines. Leave some extra space below the table view so we can add some buttons. I also set the name of each column in the table header:

Number Formatters

The Population and Area columns both show some large numbers which we want to format correctly.

  • Drag a Number Formatter from the Xcode object library to the NSTextFieldCell
  • In the Atributes inspector set the Style to Decimal and the minimum value to zero.

Adding buttons

We want to be able to add and delete rows so drag two round buttons below the table view and position them in the bottom left of the view. It does not matter which style of button you use as you can change it in the attributes inspector to match the screenshot below.

Pay attention to the following settings:

  • Style: Round
  • Title: Blank
  • Font: System Regular
  • Image: NSAddTemplate or NSRemoveTemplate
  • Position: Image Only (second option from the left, see screenshot)

If you end up with a wrongly sized button select it and use Editor > Size to Fit Content (⌘=) to make it match the image size. At this point the view with the buttons should look something like this:

Setting Autolayout Constraints

If you have used autolayout for iOS applications this is nothing new. We need constraints to make the scroll view and the two buttons fit inside the containing superview. I find it easiest to add the constraints from the document outline and then if necessary use the size inspector to set the spacing for each of the constraints to the “Standard” space.

  • Control drag from the Scroll View to the containing superview and add constraints for the leading, trailing and top space to container:

  • Control drag from the Add button to the containing superview and add leading and bottom space to container constraints. Then add vertical spacing between the Add button and the scroll view. Finally add horizontal spacing between the Add and Remove buttons.

  • Control Drag from the Remove button to the containing superview and add a bottom and trailing space to container constraints. Also add a vertical spacing constraint from the button to the scroll view. Use the size inspector to change the relation of the horizontal constraint to “Greater Than or Equal” to allow the space between the remove button and the right hand edge of the window to grow.

  • Finally select the Window in the Window Controller Scene and use the size inspector to set a minimum width and height for the window. (Note that you can also set the initial size and position of the window at this point if you wish):

If Xcode is showing autolayout issues you may need to update frames to position each of the views. At the time of writing I did find one autolayout issue with the clip view that I could not resolve.

Misplaced View: Frame for “Clip View” will be different at run time.

According to this forum post it is a known Xcode bug.

We also need some contraints for the text field in our NSTableCellView to make the text fill the view. Repeat the following step for the text field in each column.

  • Control drag from the NSTextField to the containing NSTableCellView and add leading, trailing, top and botton space constraints. I set the top and bottom space constraints to 0 and the leading and trailing space to 2.

Configuring Core Data

We already have a Core Data model (WorldFacts.xdatamodeld) and NSManagedObject subclasses (Country.m and Country+Extensions.m) as part of the iOS application. Those files are not yet included when Xcode builds the OS X WorldFactsBuilder target. With each file selected you can add them to the new target from the file inspector:

The Xcode OS X template includes the boilerplate code to setup the core data stack in the application delegate. It is very similar to setting up a core data stack for iOS so I will not go into details. By default the name of the model and persistent store files are based on the name of the target. Since we want to use the same names as the iOS target we need to fix the template code.

Delete the WorldFactsBuilder.xcdatamodeld file from the project and correct the URL for the managed object model file:

NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"WorldFacts" withExtension:@"momd"];

iOS 7 and OS X Mavericks changed the default journalling mode for Core Data SQLite stores to Write-Ahead Logging (WAL). This results in extra -wal and -shm files which would need to be included when we copy the Core Data store generated by the OS X app to the iOS app. To avoid these extra files we need to change to rollback journalling mode when we add the persistent store. Note that the template code also uses an XML store when we need an SQLite store:

NSURL *url = [applicationDocumentsDirectory URLByAppendingPathComponent:@"WorldFacts.sqlite"];
NSDictionary *options = @{NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}};
if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
                               configuration:nil
                                         URL:url
                                     options:options
                                       error:&error]) {

Note: A better way to do this would have been to leave the WAL journalling enabled and add an export button that used migratePersistentStore:toURL:options:withType:error.

Subclassing the Window Controller

We need to subclass the NSWindowController to add a property for the core data context. When adding the new class to the project make sure you select the Cocoa Class from the OS X section of the Xcode template dialog:

I named the class CountryWindowController as it will manage the display of Country objects. Note that there is no need to create a XIB file as the user interface is already in the storyboard:

Also make sure that the new class is only added to the OS X WorldFactsBuilder target. Do not add it to the iOS target:

With the class added remember to update the storyboard. Select the window controller object in the Window Controller Scene and using the Identity inspector change the class from NSWindowController to our custom CountryWindowController class:

Now in the public interface of the window controller we need a property for the managed object context:

@interface CountryWindowController : NSWindowController
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@end

The NSWindowController class is not a view controller it is a window controller. It does not have viewWillLoad or viewDidLoad methods like a view controller but it does have windowWillLoad and windowDidLoad methods. The windowWillLoad method is a good place to retrieve the managed object context from the application delegate.

- (void)windowWillLoad {
  [super windowWillLoad];
  AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
  self.managedObjectContext = delegate.managedObjectContext;
  [Country importDataToMoc:self.managedObjectContext];
}

I have also borrowed the importDataToMoc: method from the iOS app to populate our empty core data stack with some data. (The countries.plist file must be added to the OS X target). A better approach would have been to add a menu command that allows the user to select an external file to import but I will take a shortcut here to save space.

If we run the application at this point not much will happen as we still have not connected the user interface (make sure you have the OS X target selected) but the core data store should be created in the application support directory:

~/Library/Application%20Support/com.useyourloaf.WorldFactsBuilder/WorldFacts.sqlite

Undo Manager

This bit had me confused for a while. When a user tries to undo/redo a change either from the edit menu or with ⌘Z an undo/redo message is sent to the responder chain. When the undo message reaches the window, the NSWindow object tries the windowWillReturnUndoManager: method in its delegate - the NSWindowController. At that point we need to return the NSUndoManager from the core data managed object context (unlike on iOS the OS X managed object context provides an undo manager by default).

So in the CountryWindowController we need to implement windowWillReturnUndoManager as follows:

- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window {
  return self.managedObjectContext.undoManager;
}

Country Table View Controller

A final piece of setup code. We need to subclass our generic view controller and pass it the managed object context from our window controller. To do that I deleted the view controller created by the Xcode template and added a new one named CountryTableViewController. As with the window controller make sure you only add this file to the OS X target. The public interface for the class has a single property for the managed object context:

@interface CountryTableViewController : NSViewController
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@end

One thing to note that differs from iOS is that we do not have an equivalent of UITableViewController. For OS X you subclass NSViewController and then implement the NSTableViewDelegate and NSTableViewDataSource methods as required. Before we discuss that we just need to remember to update the storyboard to change the class of the view controller to CountryTableViewController.

Then back in the windowDidLoad method of our window controller we can retrieve our view controller from the contentController property and set the managed object context:

- (void)windowDidLoad {
  [super windowDidLoad];
  CountryTableViewController *countryTVC = (CountryTableViewController *)self.contentViewController;
  countryTVC.managedObjectContext = self.managedObjectContext;
}

Cocoa Bindings

Now things start to get interesting.

At this point in an iOS app we would probably add a subclass for the table view controller and implement the various table view delegate and data source methods. A similar approach will also work here but there is another more interesting way which saves us a lot of code.

Cocoa Bindings is an OS X only feature that allows you to connect a data object to a view object. An acronym soup of Key-Value Binding (KVB), Key-Value Coding (KVC) and Key-Value Observing (KVO) provide the underlying technologies that ensure that the value displayed by the view and stored in the data object stay in sync without the need for additional glue code. For further details see Cocoa Bindings Programming Topics.

It is possible to bind a view directly to a model object. In practise it is useful to have a controller object. AppKit supplies a number of controller classes (all subclasses of NSController) specifically to work with Cocoa Bindings. These classes all provide easy ways to bind views to the currently selected object and also to supply placeholders values when the selection is empty.

The AppKit supplied subclasses of NSController:

  • NSUserDefaultsCollection binds views to the user preferences system
  • NSObjectController binds views to a single model object
  • NSArrayController binds views to a collection of objects
  • NSTreeController binds views to an hierarchical collection of objects

In our case we have a simple flat collection of objects so we will use an NSArrayController. We want to bind the array controller to the columns in our table view so we need to add it to the view controller. Drag an Array Controller object from the Xcode object library to the view controller scene.

It is not required but it can help to rename the Array Controller label that appears in Interface Builder. For some reason the label does not always update in the document outline. If that happens a restart of Xcode seems to force the update:

Binding to the model

We want the array controller to be bound to Country objects from our core data managed object context:

  • Using the Attributes inspector change the Object Controller mode from Class to Entity Name and then set the Entity Name to Country.
  • Set the Prepares Content flag - this makes the array controller fetch the Country entity data when it is loaded from the storyboard.

  • In the Bindings inspector (second from last tab) in the parameters section expand the Managed Object Context and bind it to the Country Table View Controller with a model key path of managedObjectContext:

At this point we have our array controller bound to the managed object context in our view controller so that it can fetch, add and remove Country objects.

Binding to the View

In the storyboard document outline expand the table view and select the Country column. In the Bindings inspector bind the value to the CountriesArrayController (this is where changing the label of the array controller is useful). Make sure the Controller Key is set to arrangedObjects (the NSArrayController property that has all of our Country objects) and the Model Key Path is name which is the property of the Country object we want to display in this column:

In the document outline expand the Country table column and navigate down to the text field (NSTextField) below the Table Cell View. In the Bindings inspector bind the value to the Table Cell View and set the Model Key Path to objectValue.name:

With the text field still selected switch to the Attributes inspector and change the “Behavior” to Editable. Repeat this process for each of the columns and text fields using the appropriate property name for the model key path.

Finally we can connect our add and remove buttons to the array controller. Control drag from the add button to the array controller and connect the add: action. Repeat for the remove button connecting to the remove: action.

For an extra bonus we can also bind the enabled state of the remove button to the canRemove: method of the array controller. The remove button will then be disabled when there are no entries to remove. In the Binding inspector for the remove button look under the Availability section for the Enabled property and bind to the CountriesArrayController with a Controller key of canRemove:

Running the app at this point you should be able to add, edit and remove entries to the table. If you have problems go back and check that you correctly added the core data files to the OS X target. You may also want to delete the core data file (~/Library/Application%20Support/com.useyourloaf.WorldFactsBuilder/WorldFacts.sqlite).

Initial Column Sorting

Clicking on any column header sorts that column and the table setting is automatically saved to the user defaults to preserve the setting. However on first run, before the user clicks on a column header the table is unsorted. To fix that we need to give the country array controller a sort descriptor that it will use by default. Add a property to the country table view controller private interface to hold the sort descriptors:

@property (nonatomic, strong, readonly) NSArray *sortDescriptors;

Then implement the getter to return a sort descriptor for the name property:

- (NSArray *)sortDescriptors {
  if (_sortDescriptors == nil){
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    _sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
  }
  return _sortDescriptors;
}

Then in the Bindings inspector for the array controller, in the Controller Content Parameters section:

  • Bind Sort Descriptors to Country Table View Controller
  • Model Key Path: sortDescriptors

Good Enough?

There is a lot more we could do to improve and polish but for the purposes of this post I think we have done enough. Given how little code we have written our application has a lot of functionality to view and edit our core data store:

Using the Core Data Store with iOS

To complete the exercise we can take the WorldFacts.sqlite file from the Application Support directory (~/Library/Application Support/com.useyourloaf.WorldFactsBuilder) and add it to the iOS target. We need a minor change to our Core Data setup code in the iOS app to use this new store straight from the application bundle. Since we will never change the store in the iOS app we open it in read only mode:

NSURL *storeURL = [[NSBundle mainBundle] URLForResource:@"WorldFacts" withExtension:@"sqlite"];
NSDictionary *storeOptions = @{NSReadOnlyPersistentStoreOption: @YES};
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                               configuration:nil
                                                         URL:storeURL
                                                     options:storeOptions
                                                       error:&error]) {

Room for Improvement

If you are still reading at this point you deserve a medal! This has been a long post as I have covered a lot of detail but I hope it will show how easy it can be to make the jump from iOS to OS X when you need to.

The full Xcode project including both the iOS and OS X applications can be found in my GitHub Code Examples repository.

Useful OS X Resources for iOS Developers

I realised the other day that I was coming up to the five year anniversary of this blog and I have never once covered creating an OS X application. I hope to fix that in the near future but it got me looking for good resources for iOS developers looking to make the leap.

One of the problems is that, compared to iOS, there is a lack of information on developing “modern” OS X apps. That should improve in April when Big Nerd Ranch are scheduled to publish the 5th edition of Cocoa Programming for OS X which I have on pre-order and will of course review. In the mean time there are three presentations from WWDC 2014 that I found useful to describe the changes that came with OS X 10.10 Yosemite that will appeal to iOS developers:

Two other Apple resources if you are just getting started:

If you are trying to get your head around the magic of Cocoa Data Bindings especially with Core Data I still like what Marcus Zarra has to say on the topic:

The ever dependable Ray Wenderlich also has some good tutorials aimed at iOS developers. If you have other good (beyond Stack Overflow) resources feel free to let me know in the comments.

Updating to the iOS 8 Search Controller

When I first wrote about adding a search bar to a table view we were still using iOS 5. Not much changed with search bars until Apple deprecated UISearchDisplayController in iOS 8 and replaced it with UISearchController. This post will revisit that original project to update it for the new UISearchController.

App Modernization

As a sidenote I thought I would also take the opportunity to modernise the code and make the app iOS 8 only. The list of changes is a reminder of how much has changed since iOS 5:

  • Use modules and remove precompiled header
  • Convert to a Universal app and Storyboard
  • Use Auto Layout
  • Use Dynamic Type
  • Use Asset Catalogs for icons and images
  • Use Base Internationalization
  • Remove old orientation handling

The updated user interface uses a split view controller on both an iPad and iPhone - something that was not possible in the old days of iOS 5. The master view shows a list of countries that can be filtered by the search bar. The detail view shows the selected country (screenshot is cropped to top left corner):

The search bar when active hides the navigation view and shows the search results (the screenshot this time is for the iPhone interface):

A Recap of UISearchDisplayController

Before looking at the new approach to managing a search bar here is quick recap of the now deprecated UISearchDisplayController class and delegates:

You initialise a search display controller with a search bar and a view controller to manage the content. Once the user activates the search the search display controller overlays the search results view over the original content. When the content view controller was a table view controller it was typical to also make it the search display controller delegate, data source and delegate for the search results table view and delegate for the search bar. You can see this approach in my original post.

The WWDC 2014 session 228 - A Look Inside Presentation Controllers describes the limitations and problems with UISearchDisplayController. I could summarise by saying that the search bar and search results views tended to have a mind of their own…

The New and Shiny UISearchController

The UISearchController class replaces UISearchDisplayController and has a simpler set of protocols:

Creating the Search Display Controller

In the past if you wanted to construct a search bar interface with Interface Builder you would drag the Search Bar and Search Display Controller objects into the Storyboard. This created UISearchBar and UISearchDisplayController objects with the various delegates already hooked up.

These objects still exist in the Xcode 6.1 object library even though iOS 8 deprecates UISearchDisplayController. Unfortunately, at the time of writing, Interface Builder is not able to create the new UISearchController so you must create it in code. First we will add a property to the UYLCountryTableViewController class for the search controller:

@property (strong, nonatomic) UISearchController *searchController;

Now in the viewDidLoad method we can create and setup the search controller.

self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.searchResultsUpdater = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.searchBar.scopeButtonTitles = @[NSLocalizedString(@"ScopeButtonCountry",@"Country"),
                                                      NSLocalizedString(@"ScopeButtonCapital",@"Capital")];
self.searchController.searchBar.delegate = self;

A few words of explanation for the above code:

  • When creating the search controller we do not need a separate search results controller as we will use the table view controller itself.
  • Likewise we will also use the table view controller to update the search results by having it implement the UISearchResultsUpdating protocol.
  • We do not want to dim the underlying content as we want to show the filtered results as the user types into the search bar.
  • The UISearchController takes care of creating the search bar for us.
  • The table view controller will also act as the search bar delegate for when the user changes the search scope.

Next we add the search bar view to the table view header:

self.tableView.tableHeaderView = self.searchController.searchBar;

Finally since the search view covers the table view when active we make the table view controller define the presentation context:

self.definesPresentationContext = YES;

UISearchResultsUpdating Delegate

With the search controller configured the rest is mostly boilerplate code. We need to implement the UISearchResultsUpdating delegate to generate the new filtered results anytime the search text changes:

- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
  NSString *searchString = searchController.searchBar.text;
  [self searchForText:searchString scope:searchController.searchBar.selectedScopeButtonIndex];
  [self.tableView reloadData];
}

The -searchForText:scope: method is unchanged so I will not cover it again here.

UISearchBarDelegate - Scope Bar

The UISearchBarDelegate protocol defines optional methods for when the user edits the search text, clicks a search bar button or changes the search bar scope. We are already handling updates to the search results as the user enters the search text using the UISearchResultsUpdating delegate. What is missing is reloading the search results when the user changes the search scope which we can do in searchBar:selectedScopeButtonIndexDidChange:

- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
{
  [self updateSearchResultsForSearchController:self.searchController];
}

This method uses the UISearchResultsUpdating delegate method we saw before to update the search results.

Scrolling To Top

There is one more change to the table view controller that while not directly related to the search bar is worth mentioning here. The table view has a section index built in sectionIndexTitlesForTableView: from an array whose first element is the table view index search icon:

 NSMutableArray *index = [NSMutableArray arrayWithObject:UITableViewIndexSearch];

In earlier versions of the code the UITableViewDataSource method tableView:sectionForSectionIndexTitle: would force the scroll view to the top of the table by setting the content offset to zero:

self.tableView.contentOffset = CGPointZero;
return NSNotFound;

This no longer works with the new view layout instead we scroll the table view to make the search bar frame visible:

CGRect searchBarFrame = self.searchController.searchBar.frame;
[self.tableView scrollRectToVisible:searchBarFrame animated:NO];
return NSNotFound;

Wrapping Up

I find the new UISearchController interface to be much easier to use compared to the old UISearchDisplayController. I assume Apple will eventually update Interface Builder to support it directly. In the meantime it is trivial to create in code and no longer seems to suffer from the strange and unpredictable bugs of its predecessor.

You can find the updated Xcode project is in my Coding Examples GitHub repository.