Creating a master-detail iOS application

Having covered how classes, protocols, and enums are defined in Swift, a more complex master-detail application can be created. A master-detail application is a specific type of iOS application that initially presents a table view, and when an individual element is selected, a secondary details view will show more information about the selected item.

Using the Create a new Xcode project option from the welcome screen, navigate to File | New | Project... or press Command + Shift + N, and select Master-Detail Application from the iOS Application category:

Creating a master-detail iOS application

In the subsequent dialog, enter appropriate values for the project, such as the name (MasterDetail), the organizational identifier (typically based on the reverse DNS name), ensure that the Language drop-down reads Swift and that it is targeted for Universal devices.

Creating a master-detail iOS application

When the project is created, an Xcode window will open, containing all the files created by the wizard itself, including the MasterDetail.app and MasterDetailTests.xctest products. The MasterDetail.app is a bundle that is executed by the simulator or a connected device, while the MasterDetailTests.xctest product is used to execute unit tests for the application's code.

The application can be launched by pressing the triangular play button on the top-left corner of Xcode or by pressing Command + R, which will run the application against the currently selected target.

Creating a master-detail iOS application

After a brief compile and build cycle, the iOS Simulator will open with a master page that contains an empty table:

Creating a master-detail iOS application

The default MasterDetail application can be used to add items to the list, by clicking on the add (+) button on the top-right corner of the screen. This will add a new timestamped entry to the list.

Creating a master-detail iOS application

When this item is clicked, the screen will switch to the details view, which in this case presents the time in the center of the screen:

Creating a master-detail iOS application

This kind of master-detail application is common in iOS applications to display a top-level list (such as a shopping list, a set of contacts, to-do notes, and so on) while allowing the user to tap to see the details.

There are three main classes in the master-detail application:

  • AppDelegate: This class is defined in the AppDelegate.swift file, and is responsible for starting the application and setting up the initial state
  • MasterViewController: This class is defined in the MasterViewController.swift file, and is used to manage the first (master) screen's content and interactions
  • DetailViewController: This class is defined in the DetailViewController.swift file, and is used to manage the second (detail) screen's content

In order to understand what the classes do in more detail, the next three sections will present each one of them in turn.

Tip

The code generated in this section was created from Xcode 6.1, so the templates might differ slightly if using a different version of Xcode. An exact copy of the corresponding code can be acquired from the Packt website or from the book's GitHub repository at https://github.com/alblue/com.packtpub.swift.essentials/.

The AppDelegate class

The AppDelegate class is the main entry point to the application. When a set of Swift source files are compiled, if the main.swift file exists, it is used as the entry point for the application by running that code. However, to simplify setting up an application for iOS, a special attribute @UIApplicationMain exists which will both synthesize the main method and set up the associated class as the application delegate.

The AppDelegate class for iOS extends the UIResponder class, which is the parent of all the UI content on iOS. It also adopts two protocols, UIApplicationDelegate and UISplitViewControllerDelegate, which are used to provide callbacks when certain events occur:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,
   UISplitViewControllerDelegate {
  var window: UIWindow?
  ...
}

Note

On OS X, the AppDelegate class will be a subclass of NSApplication and will adopt the NSApplicationDelegate protocol.

The synthesized main function calls the UIApplicationMain method that reads the Info.plist file. If the UILaunchStoryboardName key exists and points to a suitable file (the LaunchScreen.xib interface file in this case), it will be shown as a splash screen before doing any further work. After the rest of the application has loaded, if the UIMainStoryboardFile key exists and points to a suitable file (the Main.storyboard file in this case), the storyboard is launched and the initial view controller is shown.

The storyboard has references to the MasterViewController and DetailViewController classes. The window variable is assigned to the storyboard's window.

Once the application has been loaded, the application:didFinishLaunchingWithOptions: callback is called with a reference to the UIApplication instance and a dictionary of options that notifies how the application has been started:

func application(
 application: UIApplication,
 didFinishLaunchingWithOptions launchOptions:
  [NSObject: AnyObject]?) -> Bool {
  // Override point for customization after application launch.
  ...
}

In the sample MasterDetail application, the application:didFinishLaunchingWithOptions: method acquires a reference to the splitViewController from the explicitly unwrapped optional window, and the AppDelegate is set as its delegate:

let splitViewController = 
 self.window!.rootViewController as UISplitViewController
splitViewController.delegate = self

Tip

The syntax … as UISplitViewController performs a type cast so that the generic rootViewController can be assigned to the more specific type; in this case, UISplitViewController.

Finally, the navigationController is acquired from the splitViewController, which stores an array of viewControllers. This allows the DetailView to show a button on the left-hand side to expand the details view, if necessary:

let navigationController = splitViewController.viewController
 [splitViewController.viewControllers.count-1]
 as UINavigationController
navigationController.topViewController
 .navigationItem.leftBarButtonItem =
 splitViewController.displayModeButtonItem()

The only difference this makes is when running on a wide-screen device, such as an iPhone 6 Plus or an iPad, where the views are shown side-by-side in landscape mode. This is a new feature in iOS 8 applications.

The AppDelegate class

Otherwise, when the device is in portrait mode, it will be rendered as a standard back button:

The AppDelegate class

The method concludes with return true to let the OS know that the application opened successfully.

The MasterViewController class

The MasterViewController class is responsible for coordinating the data shown on the first screen (when the device is in portrait orientation) or the left-half of the screen (when a large device is in landscape orientation). This is rendered with a UITableView, and data is coordinated through the parent UITableViewController class:

class MasterViewController: UITableViewController {
  var objects = NSMutableArray()
  override func viewDidLoad() {…}
  func insertNewObject(sender: AnyObject) {…}
  …
}

The viewDidLoad method is used to set up or initialize the view after it has loaded. In this case, a UIBarButtonItem is created so that the user can add new entries to the table. The UIBarButtonItem takes a @selector in Objective-C, and in Swift, is treated as a string literal convertible (so that "insertNewObject:" will result in a call to the insertNewObject method). Once created, the button is added to the navigation on the right-hand side, using the standard .Add type which will be rendered as a + sign on the screen:

override func viewDidLoad() {
  super.viewDidLoad()
  let addButton = UIBarButtonItem(
    barButtonSystemItem: .Add, target: self, 
    action: "insertNewObject:")
  self.navigationItem.rightBarButtonItem = addButton
  self.navigationItem.leftBarButtonItem = self.editButtonItem()
}

The objects are NSDate values and are stored inside the class as an NSMutableArray. The insertNewObject method is called when the + button is pressed, and creates a new NSDate instance, which is then inserted into the array. The event sender is passed as an argument of the AnyObject type, which will be a reference to the UIBarButtonItem (although it is not needed or used here):

func insertNewObject(sender: AnyObject) {
  objects.insertObject(NSDate.date(), atIndex: 0)
  let indexPath = NSIndexPath(forRow: 0, inSection: 0)
  self.tableView.insertRowsAtIndexPaths(
   [indexPath], withRowAnimation: .Automatic)
}

Note

The UIBarButtonItem class was created before blocks were available on iOS devices, so it uses the older Objective-C @selector mechanism. A future release of iOS might provide an alternative that takes a block, in which case Swift functions can be passed instead.

The parent class contains a reference to the tableView, which is automatically created by the storyboard. When an item is inserted, the tableView is notified that a new object is available. Standard UITableViewController methods are used to access the data from the array:

override func numberOfSectionsInTableView(
 tableView: UITableView) -> Int {
  return 1
}
override func tableView(tableView: UITableView,
 numberOfRowsInSection section: Int) -> Int {
  return objects.count
}
override func tableView(tableView: UITableView,
 cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
  let cell = tableView.dequeueReusableCellWithIdentifier(
   "Cell", forIndexPath: indexPath) as UITableViewCell
  let object = objects[indexPath.row] as NSDate
  cell.textLabel?.text = object.description
  return cell
}
override func tableView(tableView: UITableView,
 canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
  return true
}

The numberOfSectionsInTableView function returns 1 in this case, but a tableView can have multiple sections, for example, to permit a contacts application having a different section for A, B, C through Z. The numberOfRowsInSection method returns the number of elements in each section; in this case, since there is only one section, the number of objects in the array.

Note

The reason why each method is called tableView and takes a tableView argument is a result of the Objective-C heritage of UIKit. The Objective-C convention combined the method name as the first named argument, so the original method was [delegate tableView:UITableView, numberOfRowsInSection:NSInteger]. As a result, the name of the first argument is reused as the name of the method in Swift.

The cellForRowAtIndexPath method is expected to return a UITableViewCell for an object. In this case, a cell is acquired from the tableView using the dequeueReusableCellWithIdentifier method (which caches cells as they go off screen to save object instantiation) and then the textLabel is populated with the object's description (which is a String representation of the object; in this case, the date).

This is enough to display elements in the table, but in order to permit editing (or just removal, as in the sample application), there are some additional protocol methods that are required:

override func tableView(tableView: UITableView,
 canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
  return true
}
override func tableView(tableView: UITableView,
 commitEditingStyle editingStyle: UITableViewCellEditingStyle,
 forRowAtIndexPath indexPath: NSIndexPath) {
  if editingStyle == .Delete {
    objects.removeObjectAtIndex(indexPath.row)
    tableView.deleteRowsAtIndexPaths([indexPath],
     withRowAnimation: .Fade)
  }
}

The canEditRowAtIndexPath method returns true if the row is editable; if all the rows can be edited, then this will return true for all the values.

The commitEditingStyle method takes a table, a path, and a style which is an enumeration that indicates which operation occurred. In this case, UITableViewCellEditingStyle.Delete is passed in order to delete the item from both the underlying object array and also from the tableView. (The enumeration can be abbreviated to .Delete because the type of the editingStyle is known to be UITableViewCellEditingStyle.)

The DetailViewController class

The detail view is shown when an element is selected in the MasterViewController. The transition is managed by the storyboard controller; the views are connected with a segue (pronounced segway; the product of the same name based it on the word segue which is derived from the Italian word for follows).

To pass the selected item between controllers, a property exists in the DetailViewController class called detailItem. When the value is changed, additional code is run, which is implemented in a didSet property notification:

class DetailViewController: UIViewController {
  var detailItem: AnyObject? {
    didSet {
      self.configureView()
    }
  }
  … 
}

When the DetailViewController has the detailItem set, the configureView method will be invoked. The didSet body is run after the value has been changed, but before the setter returns to the caller. This is triggered by the segue in the MasterViewController:

class MasterViewController: UIViewController {
  …
  override func prepareForSegue(
   segue: UIStoryboardSegue, sender: AnyObject?) {
    super.prepareForSegue(segue, sender: sender)
    if segue.identifier == "showDetail" {
      if let indexPath = 
       self.tableView.indexPathForSelectedRow() {
        let object = objects[indexPath.row] as NSDate
        let controller = (segue.destinationViewController 
         as UINavigationController)
         .topViewController as DetailViewController
        controller.detailItem = object
        controller.navigationItem.leftBarButtonItem =
         self.splitViewController?.displayModeButtonItem()
        controller.navigationItemleftItemsSupplementBackButton =
         true
      }
    } 
  }
}

The prepareForSegue method is called when the user selects an item in the table. In this case, it grabs the selected row index from the table and uses this to acquire the selected date object. The navigation controller hierarchy is searched to acquire the DetailViewController, and once this has been obtained, the selected value is set with controller.detailItem = object, which triggers the update.

The label is ultimately displayed in the DetailViewController through the configureView method, which stamps the description of the object onto the label in the center:

class DetailViewController {
  ...
  @IBOutlet weak var detailDescriptionLabel: UILabel!
  function configureView() {
    if let detail: AnyObject = self.detailItem {
      if let label = self.detailDescriptionLabel {
        label.text = detail.description
      }
    }
  }
}

The configureView method is called both when the detailItem is changed and when the view is loaded for the first time. If the detailItem has not been set, then this has no effect.

The implementation introduces some new concepts, which are worth highlighting:

  • The @IBOutlet attribute indicates that the property will be exposed in interface builder and can be wired up to the object instance. This will be covered in more detail in Chapter 4, Storyboard Applications with Swift and iOS, and in Chapter 5, Creating Custom Views in Swift.
  • The weak attribute indicates that the property will not store a strong reference to the object; in other words, the detail view will not own the object but merely reference it. Generally, all @IBOutlet references should be declared as weak to avoid cyclic dependency references.
  • The type is defined as UILabel!, which is an implicitly unwrapped optional. When accessed, it performs an explicit unwrapping of the optional value; otherwise the @IBOutlet would be wired up as a UILabel? optional type. Implicitly unwrapped optional types are used when the variable is known to never be nil at runtime, which is usually the case for @IBOutlet references. Generally, all @IBOutlet references should be implicitly unwrapped optionals.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset