Use Your Loaf

[[brain engage] write]

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.

Stanford iOS 8 Course With Swift

The iOS grapevine has been buzzing the last few days with news that the latest version of the Stanford iOS course has hit iTunes U. It is once again being taught by the excellent Paul Hegarty and has been updated not only for iOS 8 but will also use Swift.

This is an interesting if not totally surprising decision as Paul mentions in the first lecture that there simply was not time to teach both Swift and Objective-C. The first two videos are already posted and immediately dig into Swift including an introduction to Swift Optionals. So whatever your level of Swift expertise I think this will be well worth following along.

You can find it on iTunes U at Developing iOS 8 Apps with Swift

Using a Launch Screen Storyboard

Static Launch Images

Launch images are what iOS displays whilst loading an App to give the impression of a responsive system. Creating these static launch images for the growing number of screen resolutions has become something of a pain in recent years. At the time of writing the list of possible launch image sizes is below (sizes include the status bar region). I have omitted the landscape versions for brevity:

  • iPad 2 and iPad mini (@1x): 768 x 1024
  • iPad and iPad mini (retina @2x): 1536 x 2048
  • iPhone 5 (@2x): 640 x 1136
  • iPhone 6 (@2x): 750 x 1334
  • iPhone 6 Plus (@3x): 1242 x 2206

There is some good news with Xcode 6 and iOS 8 which allow a NIB or storyboard launch screen file to be used. By taking advantage of auto layout and size classes a single NIB or storyboard file automatically creates the launch images at runtime. Note that if you want to properly support the larger iPhone 6 and iPhone 6 Plus screen sizes in fullscreen mode without scaling you must supply the appropriate launch images either as static images of dimensions as listed above or with a storyboard launch screen file.

(Updated 26-Dec-2014: made it clearer that supporting iPhone 6 and iPhone 6 Plus requires you to provide the launch images but they can be static image files or from a launch screen file).

Using a Launch Image File

Xcode 6 adds a LaunchScreen.xib file by default to new projects. For an existing project add a new file using the Launch Screen template:

Launch Screen Template

Note that this will add a NIB file to the project which is fine if you have a single view or view controller on the initial launch screen. If you have multiple views you will need to ignore the launch screen template and add a storyboard. You should also specify the launch screen file in the project settings for the target:

Project Settings

This will add the Launch screen interface file base name (UILaunchStoryboardName) key to the application plist file:

UILaunchStoryboardName

At this point you can layout the launch view in Interface Builder using autolayout and size classes as necessary to create suitable images for each screen resolution. The Xcode template does not provide a very good example as it provides a splash screen style layout with the app name and copyright statement that you will probably want to delete before adding your own view layout:

LaunchScreen.xib

You can preview the storyboard in Xcode or test it in the simulator or on an actual device. Since the launch screen is only briefly displayed you may find it useful to set a breakpoint on application:didFinishLaunchingWithOptions: in the App delegate.

Launch Screen Constraints

The system loads the launch file before launching the app which creates some constraints on what it can contain (some of which may force you back to static image files):

  • The app is not yet loaded so the view hierarchy does not exist and the system can not call any custom view controller setup code you may have in the app (e.g. viewDidLoad)

  • You can only use standard UIKit classes so you can use UIView or UIViewController but not a custom subclass. If you try to set a custom class you will get an Illegal Configuration error in Xcode.

  • The launch file can only use basic UIKit views such as UIImageView and UILabel. You cannot use a UIWebView.

  • If you are using a storyboard you can specify multiple view controllers but there are again some limitations. For example you can embed view controllers in a navigation or tab bar controller but more complex container classes such as UISplitViewController do not work (at least not yet).

  • Localizing the launch file does not currently seem to have any effect. The base localization is always used so you will probably want to avoid text on the launch screen.

  • You cannot specify different launch files for iPad and iPhone. This may be a problem if you have significantly different interfaces for those devices as there is only so much you can do with auto layout and size classes.

Note that if you are deploying to iOS 7 you will still need to include the static launch image files. You can include both a launch image file and static launch images. Devices such as the iPhone 6 running iOS 8 will use the launch image file whilst iOS 7 devices will fallback to the launch images.

Split View Controllers

If your root view controller is a split view controller you do not have too many options at least with iOS 8.1. If you add a split view controller to the launch screen storyboard it will not load. The increased flexibility of split view controllers in iOS 8 also makes me suspect they will not be supported any time soon.

Other than going back to static launch images the only alternative seems to be to simplify the user interface by ignoring the split screen. For example consider the following iPhone and iPad launch screens that use a split view controller. On the iPhone (compact width) device the initial screen shows the master view controller (a table view controller embedded in a navigation controller in this case):

Initial Screen iPhone

On the iPad (regular width) device the initial screen after launch shows the master and detail view controllers in a split screen layout:

Initial Screen iPad

This is a very common setup but there is no good way to use a Launch screen file in this case. I am open to suggestions but the closest I can get is to ignore the split screen and use a view controller embedded in a navigation controller as the launch screen.

Launch Storyboard

It is far from perfect but it does as least more or less match the initial user interface on all devices (albeit without the split on the iPad). As a launch image intended to give the user the impression that the app is loading it may just about be good enough but you will have to judge for yourself.

Runtime Generation (added 28-Dec-2014)

The Apple documentation does not make it clear but the required launch images are generated by the system at runtime. This was briefly mentioned in the WWDC 2014 Platform State of the Union session (at about 1hour,22minutes). You can verify it by looking at the application container of an App deployed to a device or the simulator. The launch images required for a specific device are cached in Library/Caches/LaunchImages. The following screenshot shows the launch images generated on an iPad Air 2:

For comparison the following screenshot shows the launch images generated for an iPhone 6 Plus: