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:
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.
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.
After a brief compile and build cycle, the iOS Simulator will open with a master page that contains an empty table:
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.
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:
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 stateMasterViewController
: This class is defined in the MasterViewController.swift
file, and is used to manage the first (master) screen's content and interactionsDetailViewController
: This class is defined in the DetailViewController.swift
file, and is used to manage the second (detail) screen's contentIn order to understand what the classes do in more detail, the next three sections will present each one of them in turn.
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 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? ... }
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
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.
Otherwise, when the device is in portrait mode, it will be rendered as a standard back button:
The method concludes with return
true
to let the OS know that the application opened successfully.
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) }
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.
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 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:
@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.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.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.