© Radoslava Leseva Adams and Hristo Lesev 2016

Radoslava Leseva Adams and Hristo Lesev, Migrating to Swift from Flash and ActionScript, 10.1007/978-1-4842-1666-8_6

6. Adding a More Complex UI

Radoslava Leseva Adams and Hristo Lesev2

(1)London, UK

(2)Kazanlak, Bulgaria

In this chapter we dig deeper in designing user interfaces (UIs) for iOS with Swift by making another app. There is a lot of ground to cover both on the UI front and on the language front. To make things easier, we will point out some of the Swift concepts that may look new or strange, explain them along the way, and let you know where you can find more information.

In this chapter you will do the following:

  • Learn how to add views to your app and transition between them.

  • Try out different navigation patterns.

  • Learn how to pass data between view controllers.

  • Learn one more way of making adaptive UIs.

  • Learn how to work with a table view and use it to present interactive choices.

  • Handle user settings.

  • Learn how to customize your app’s launch screen.

  • Learn about the Observer design pattern.

  • Create and manage a UI programmatically.

  • Learn how to use annotations to make your code easier to navigate.

When you are done, you will have an app that lets you collect votes from your friends when trying to decide where to go for dinner or which film to see.

Swift reference

Look for notes like this one throughout the chapter. They contain brief descriptions of Swift language features that we use in the code and will point you to the relevant chapters in Part IV, which is dedicated to language migration. Or, if you would prefer to dig into the language first, you can go to Part IV of this volume (chapters 17 to 22) and come back to the tutorials in this chapter later with an even better understanding of Swift.

Setting Up the App

In the course of this chapter we will develop two versions of the same app:

  • a Lite version, which lets users vote on a single topic and shows them the results of the vote; and

  • a Pro version, which adds a settings screen and also allows for new voting topics and choices to be created.

Both apps will start off with the same logic and UI and on top of that we will add Lite and Pro functionality. In this section we will build this common logic and UI in a project, which we will later clone to create the two different versions of the app.

Start by making a new Single View Application iOS project in Xcode (FileNewProject…, then iOSApplicationSingle View Application). Call it SimpleVote and set its language to Swift. This will create the boilerplate project, with which you are intimately familiar by now (Figure 6-1).

A371202_1_En_6_Fig1_HTML.jpg
Figure 6-1. The boilerplate Single View Application project

Defining a Data Model

The voting app will use a simple data model, which contains a topic for the vote, a list of choices, and the number of participants who cast their votes or abstained. Each choice has a title and keeps track of how many votes it receives.

Let us define a structure that will represent a choice first. Create a new group in your project and call it Data model. Select the new group and create a new Swift file in it (FileNewFile… ➤ iOSSourceSwift File). Call the file Choice.swift. Then add the following definition (Listing 6-1).

Listing 6-1. Defining a Structure to Represent a Choice for Voting
struct Choice {
    let title: String
    var votes: Int = 0


    init(title: String) {
        self.title = title
    }
}

The Choice structure represents a single option users can vote on. It has a constant member, called title, which we can set to “Pizza,” “Thai food,” and so on, and a variable member votes, which keeps track of how many votes the choice has received. The init method we have added is similar to a constructor in ActionScript: in Swift it is called initializerand its purpose is to ensure that all properties of Choice have initial values.

Swift reference

For the moment you can think of structures as similar to classes: they are types, which keep together data (member variables and constants) and behavior (methods). We look at structures and initializers in detail in Chapter 21.

Add another Swift file to the Data model group and call it VoteData.swift. Let us define another structure in it, called VoteData. This one will store the vote topic and choices, as well as information on a voting session: how many participants there were and how they voted (Listing 6-2).

Listing 6-2. Defining a Structure for Keeping Data on the Voting Session
struct VoteData {
    // Set a default topic for voting:
    var topic = "Which one would you choose?"


    // An array of choices for voting:
    var choices: [Choice] = []


    // The number of participants, who voted or abstained:
    var participants = 0


    var votes: Int {
        get {
            var numVotes = 0


            // Each choice keeps track of the number of votes
            // it receives, so just sum them all up:
            for choice in choices {
                numVotes += choice.votes
            }


            return numVotes
        }
    }


    var abstains: Int {
        get {
            // Work out how many participants abstained:
            return participants - votes
        }
    }


    mutating func resetVotes() {
        // Clear the vote information for a new session:
        participants = 0


        for i in 0 ..< choices.count {
            choices[i].votes = 0
        }
    }
}

Before we put this structure to use, let us look at its members and what each of them does, clarifying a few Swift concepts along the way.

The first property in VoteData is topic and is initialized with a literal string. The initialization helps the Swift compiler infer the type of topic.

Swift reference

Swift is a strongly typed language, but you do not have to explicitly declare types if they can be inferred. For more information on type inference, see Chapter 19.

The second property, choices, is an array of Choice instances. The following line,

var choices: [Choice] = []

declares choice and also initializes it with an empty array.

Swift reference

For details on arrays and other container types see Chapter 19.

The votes and abstains members are computed properties.

Swift reference

A computed property is like a hybrid between a property and a method: it does not store its value but rather works it out it on the fly. For more details see Chapter 21.

The resetVotes method has been defined with the keyword mutating, because inside it we change the properties of VoteData. Its task is to clear the data about the last voting session, so the user can start a new one.

Swift reference

By default, methods defined in a structure have read-only access to the structure’s properties. To allow modification, a method must be declared as mutating. See Chapter 21 for more information.

The for loop in resetVotes uses the half-open range operator (..<). The syntax you see is equivalent to this for loop declaration in ActionScript :

for (var i : int = 0; i < choices.count; i++)
Swift reference

Range operators in Swift define intervals of values you can iterate over. For details on how they work see Chapter 18, and for more information on loops see Chapter 20.

Adding a Custom View Controller

If you open the main storyboard of the project, Main.storyboard, you will see that there is currently a view controller on it: it controls the view of the app’s main screen. In Project navigator there is also a source file, called ViewController.swift. It contains the definition of ViewController: a class, which inherits UIViewController—the base class for view controllers in the iOS SDK. The view controller on the storyboard and the ViewController class are not one and the same, but they are linked—we will see how shortly.

So far in the examples in this book we have modified and added code to ViewController. This time we will create our own class and will link it with the view controller on the storyboard.

In Project navigator create a new group , call it View controllers and add a new Swift file to it. Name the file VoteViewController.swift. Your project should now look like Figure 6-2.

A371202_1_En_6_Fig2_HTML.jpg
Figure 6-2. The app project with the new view controller

Open VoteViewController.swift and replace the following line,

import Foundation

with Listing 6-3, which declares VoteViewControlleras a subclass of UIViewController.

Listing 6-3. Declaring a Custom View Controller
import UIKit

class VoteViewController: UIViewController {
}
Tip

Foundation and UIKit are both iOS SDK frameworks. The Foundation framework contains the basis of Objective-C classes and UIKit defines classes that deal with the user interface, such as UIViewController.

Next we will connect the main screen of the app with our custom VoteViewController class. Open Main.storyboard in the Editor and select the view controller, which is on it. With the controller selected, open the Identity Inspector, locate the Custom Class section, and set Class to VoteViewController(Figure 6-3).

A371202_1_En_6_Fig3_HTML.jpg
Figure 6-3. Assigning a custom class to the view controller on the storyboard
Tip

It may be easier to select the view controller in the Document Outline (Figure 6-3).

We have now replaced the ViewController class. Let us delete it. In Project navigator right-click ViewController and select Delete from the pop-up menu (or press Backspace). You will see a dialog, which asks whether you want to delete the file or to remove its reference from the project. Go ahead and click Move to Trash (Figure 6-4).

A371202_1_En_6_Fig4_HTML.jpg
Figure 6-4. Deleting the view controller file generated by Xcode

Now run the app on your iOS device or in the simulator to make sure it compiles and runs without errors.

Designing the Vote View UI

We will now design the UI of the view, for which our Vote View Controller is responsible. This time we will use stack views: yet another technique for making UIs adaptive, which is good to have under your belt.

Open Main.storyboard in the Editor. In Object library find a Vertical Stack View and drag it onto the view on the storyboard (Figure 6-5).

A371202_1_En_6_Fig5_HTML.jpg
Figure 6-5. Adding a Vertical Stack View to the storyboard

Add constraints for the stack view: set the distance between it and the top and bottom layout guides to 20 and set zero for the distance between it and the parent view leading (left) and trailing (right) margins. Have a look at Chapter 2 if you need a reminder for how to do that. When you are done, four constraints should appear under the stack view in the Document outline (Figure 6-6).

A371202_1_En_6_Fig6_HTML.jpg
Figure 6-6. Adding constraints for the vertical stack view

With the stack view selected, open the Attributes inspector to set it up. Under Stack View leave Alignment and Distribution both to Fill and set Spacing to 10 (Figure 6-7).

A371202_1_En_6_Fig7_HTML.jpg
Figure 6-7. Set up the vertical stack view in the Attributes inspector .

Now let us add more UI elements : drag a Label, a Table View, and a Horizontal Stack View from the Object library and position them in this order from top to bottom in the vertical stack view.

Constrain the height of the label and the height of the horizontal stack view to 30.

Select the horizontal stack view and set Stack ViewDistribution to Fill Equally and its Spacing to 10.

Add two buttons to the horizontal stack view and set their titles to Abstain and Vote: you can double-click each button on the storyboard and type a title directly. Alternatively, you can set their titles in the Attributes inspector under ButtonTitle.

Have a look at Figure 6-8 to check if your storyboard looks like it.

A371202_1_En_6_Fig8_HTML.jpg
Figure 6-8. The vote view on the storyboard

If you run your app now it should look like Figure 6-9.

A371202_1_En_6_Fig9_HTML.jpg
Figure 6-9. The vote view at runtime

The buttons we added show just titles by default. To make them more visible, let us add borders. Back on the storyboard in Xcode, if you select one of the buttons and have a look at the Attributes inspector , however, you will notice that there are no properties exposed that allow us to control borders. This is a good excuse to get familiar with another of Xcode’s features: the User Defined Runtime Attributes, which you will find in the Identity inspector. Here you can set properties that would be available at runtime. The format is key-type-value, where the key, called Key Path, is the fully qualified property name: it could be a property of the class we are modifying or a property of one of its members.

In this case we will be setting up properties of UIButton’s layer member . This is an instance of the CALayer class, which manages the visual content of a view. UIButton inherits its layer property from UIView—the base class for user interface elements in iOS.

Select the Abstain button and in the Identity inspector find the User Defined Runtime Attributessection. Click the + button to add an attribute and set its Key Path to layer.borderWidth, its Type to Number, and Value – to 1. Add another Number attribute, called layer.cornerRadius, and set its value to 6 (Figure 6-10).

A371202_1_En_6_Fig10_HTML.jpg
Figure 6-10. Adding a runtime attribute to show a border around the button

Do the same for the Vote button.

Run the app to see the buttons styled with a border (Figure 6-11).

A371202_1_En_6_Fig11_HTML.jpg
Figure 6-11. The buttons with borders

Linking the UI with the View Controller

In this section we will wire the UI we created on the storyboard to code by adding outlets and actions to the VoteViewController class . Have a look back at Chapter 2 if you need a reminder about what outlets and actions are, how to create them, and how to work with the Assistant editor.

Open Main.storyboard and VoteViewController.swift side by side in the Assistant editor. Ctrl-drag the label from the storyboard into the definition of VoteViewController to create an outlet for it and call the outlet topicLabel. Create an outlet for the table view and call it choicesTable. Create handlers for the Touch Up Inside action for the Vote and Abstain buttons. When you are done, the definition of VoteViewController should look similar to Listing 6-4.

Listing 6-4. VoteViewController with Outlets and Actions Added to It
class VoteViewController: UIViewController {
    // MARK: Outlets
    // The label shows the topic of the vote.
    @IBOutlet weak var topicLabel: UILabel!


    // The table lists the choices for voting
    // and lets the user select one.
    @IBOutlet weak var choicesTable: UITableView!


    // MARK: Actions
    @IBAction func vote(sender: AnyObject) {
        // User has voted.
        // TODO: Increment the votes for the selected choice.
        // TODO: Increment the number of participants.
        // TODO: Clear the selection to prepare for the next participant.
     }


    @IBAction func abstain(sender: AnyObject) {
        // User has decided not to vote.
        // TODO: Increment the number of participants.
        // TODO: Clear the selection to prepare for the next participant.
    }
}

What your version is probably missing are the comments. Some of those deserve a special mention here: the MARK and TODO annotations can help you navigate your code in the Jump bar. The Jump bar is located at the top of the Editor and is called thus for a reason: it lets you quickly jump to specific places in your project and in your code (Figure 6-12).

A371202_1_En_6_Fig12_HTML.jpg
Figure 6-12. Using annotations to organize your code

For an annotation to show up in the Jump bar it must appear in a comment (this can be a single-line comment or a comment block) and must be followed by a colon; it does not have to be the first string in the comment. You can use the following annotations:

  • // MARK: Heading. This will create a section heading in the Jump bar. It is good practice to keep related methods under the same heading.

  • // MARK: - Heading, // MARK: Heading –, or // MARK: - Heading -. These will add separators before, after, or before and after a heading.

  • // TODO: Reminder about an unfinished task.

  • // FIXME: Reminder about a known issue.

Tip

Use MARK, FIXME, and TODO annotations to organize your code and make it easy to navigate.

Let us replace a couple of the TODO annotations with code. Use the Jump bar to go to the following line in the vote action:

// TODO: Clear the selection to prepare for the next participant.

Add a call to a function, named clearSelection: we will implement it later on, when we learn how to work with the table view. Remove the TODO annotation, but leave the comment in as documentation. Do the same in the abstain action (Listing 6-5).

Listing 6-5. Replacing TODO Annotations
// MARK: Actions
@IBAction func vote(sender: AnyObject) {
    // User has voted.
    // TODO: Increment the votes for the selected choice.
    // TODO: Increment the number of participants.


    // Clear the selection to prepare for the next participant:
    clearSelection()
}


@IBAction func abstain(sender: AnyObject) {
    // User has decided not to vote.
    // TODO: Increment the number of participants.


    // Clear the selection to prepare for the next participant:
    clearSelection()
}

Before we move on with implementing how choices will appear on the screen, let us add another call to clearSelection to make the compiler properly angry.

Shortly we will add another view controller to our app to show results from the vote. We want the vote screen to be clear of any selected choices whenever we return to it. To achieve that, a good place to call clearSelection is in the view controller’s viewWillAppear method. It is inherited from the base class, UIViewController, and is called every time the view is about to be displayed.

Let us add an override for viewWillAppear. Add another MARK annotation, which will let us keep base class overrides together (Listing 6-6). Let us also add a TODO annotation as a reminder for something else we will want to do when the view appears: restart voting by clearing the results of the last vote.

Listing 6-6. Override the View Controller’s viewWillAppear Method and Clear the Vote Screen
// MARK: UIViewController
override func viewWillAppear(animated: Bool) {
    // Call the base class method first
    // to make sure we won't miss out on any preparation done by it:
    super.viewWillAppear(animated)


    // Then clear the selection:
    clearSelection()


    // TODO: reset the vote results
}

Handling Choices with a Table View

As an ActionScript developer you may be used to presenting mutually exclusive choices with radio buttons, especially if you have developed desktop apps. However, radio buttons are well suited to a precise mouse cursor and less so to a chunky thumb on a small phone screen. The iOS SDK offers several different controls that help implement the same logic and feel more natural under the hand: you can use a segmented control ( UISegmentedControl)—like the All/Missed filter controls in your recent iPhone calls list; a picker ( UIPickerView) like the date picker we used in Chapters 2 and 3 ; or a table view (UITableView), which we are about to use.

The UITableView control is designed for presenting lists of items and receiving1 user interaction, including making mutually exclusive selections, so it will do very well for handling votes.

Let us start setting up our table view by making sure it only allows one item to be chosen at a time: with the table view selected on the storyboard, open the Attributes inspector find Table ViewSelection and set it to Single Selection.

The next thing we will set up would be the table cells.

Designing the Cells

Select the table view on the storyboard. In the Attributes inspector set TableViewPrototype Cells to 1. This will add a Table View Cell entry under Choices Table in the Document outline. Select the new cell (this might be easier to do in the Document outline than on the storyboard) and in the Attributes inspector set Table View CellStyle to Basic (Figure 6-13).

A371202_1_En_6_Fig13_HTML.jpg
Figure 6-13. Adding a basic prototype cell

When you change the cell style, you will see the cell change on the storyboard: now it says “Title.” Cells in a table view can have very sophisticated design with various other UI controls added to them (image views, blocks of styled text, rating controls, etc.). A basic cell only has a label in it: this is what we will use to display choices for voting.

Next, set the cell’s Identifier to ChoiceCellin the Attributes inspector (Figure 6-14).

A371202_1_En_6_Fig14_HTML.jpg
Figure 6-14. Setting up the prototype cell

In the next section we will see what we have just done and why.

How Cells Work in a Table View

Table views in iOS are optimized for displaying large lists of ordered items. Also, as we mentioned earlier, each cell in the table can be designed to have a very custom look: you can add all sorts of UI elements to it. Cells in the same table don’t even have to all look the same, but can each have a different layout.

The way to achieve this is to design what a prototype cell for each separate layout you want to display in the table. Think of it as designing a custom view that will be shown when a cell is displayed. The cell Identifier property we set in the previous section is used to distinguish between these custom views.

For performance reasons table views are designed to reuse cell objects. Say you have a list of 100 items you want to display in a table view. Out of the 100 only a few would be visible on the screen at any time, so memory-wise it would not be very effective to keep cell instances for all of them. On the other hand, each time you scroll up or down to see more items, creating cells from scratch for those that have just come into view might hurt performance.

A table view balances those time and memory costs by reusing cells: once they have been requested and created, the table view keeps a number of cell objects at hand (in a queue) that can be used again and again. It is only their content that changes: for example, instead of recreating a label in the cell, the table view will change its text.

Displaying data in the table view

UITableView is designed to be used as the View in the now familiar Model-View-Controller (MVC) pattern, which we discussed in Chapter 5 . Being merely a view, its responsibility is to display data but not to provide it. For that it relies on an external source. We will make our VoteViewController that source. “But isn’t VoteViewController the controller in the MVC pattern?” you might be thinking. . . . You are right, this sounds confusing. But bear with me: in this case the view controller’s role as a data source is to be the vehicle for the data from the data model it contains.

To start with, let us assign the view controller to the dataSource property of the table view (Listing 6-7). For that purpose we will override UIViewController’s viewDidLoad method and do the assignment in it. You have already used viewDidLoad in previous chapters: it is called when the view has been loaded for displaying for the first time.

Listing 6-7. Set VoteViewController as the Data Source for the Table View
// MARK: UIViewController
override func viewDidLoad() {
    // Call the base class method first
    // to make sure we won't miss out on any preparation done by it:
    super.viewDidLoad()


    // This will cause the methods required
    // by the UITableViewDataSource to be called:
    choicesTable.dataSource = self
}

If you Cmd + click dataSource in the line of code we just added to see its definition, you will see that it is of type UITableViewDataSource?. UITableViewDataSource is a protocol from the iOS SDK, which provides an interface that the table view knows how to use to query for data. For VoteViewController to be used as the data source for the table view, it needs to conform to this protocol.

Swift reference

-For details on protocols see Chapter 21 and for the meaning of the question mark after UITableViewDataSource see Chapter 19.

To make VoteViewController conform to UITableViwDataSource, we will first add the protocol to the view controller’s inheritance list:

class ViewController: UIViewController, UITableViewDataSource {

Then we will implement two of the protocol’s methodsw , which the table view will call when it is about to display its cells and needs to know what data to put in them.

The first method, tableView(_:numberOfRowsInSection:), tells the table view how many rows to display in each of its sections. Here a section represents a group of rows, for example, the group for contacts starting with the letter “A” in your address book.

The second method, tableView(_:cellForRowAtIndexPath:), fires each time a cell from the table needs to be shown on the screen. This is where we take advantage of the reusing of cells we saw in the previous section: instead of creating a new cell each time, we call the table view’s dequeueReusableCellWithIdentifier method , which will only create a cell from scratch if there is not one available in the queue. Every time after that we will have a ready-made instance we can just populate with data. Listing 6-8 shows the implementation of both methods.

Listing 6-8. Implementing Methods of the UITableViewDataSource Protocol
// MARK: UITableViewDataSource
func tableView(tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
    // Query the data model for the number of choices to present for voting:
    return choiceCount
}


func tableView(tableView: UITableView,
    cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // Ask the table view for a cell that looks like
    // the one we prototyped ("ChoiceCell"):
    let cell = tableView.dequeueReusableCellWithIdentifier("ChoiceCell",
        forIndexPath: indexPath)


    // Fill in the cell with data from the data model:
    setUpCell(cell: cell, atRow: indexPath.row)


    return cell
}

The two methods access a property, choiceCount and a method, setUpCell, which we have not implemented yet. We will take care of this in the next section.

Adding Placeholders for Querying the Data Model

The property and the method that we now need to define will both access the data model: choiceCount is responsible for telling the table view how many rows to display and setUpCell populates a given cell with data. The Lite and the Pro versions of our app will each deal with the data model differently and so will need slightly different implementations for choiceCount and setUpCell. So at this stage of the app development we will just add placeholders and provide implementation later on for each app version.

Let us define choiceCount as a computed property to VoteViewController and annotate it with MARK: Data. For the moment we will have it return 1 (Listing 6-9).

Listing 6-9. Adding a Placeholder for the Number of Options for Voting
// MARK: Data
var choiceCount: Int {
    get {
        // Query the data model for the number of choices for voting.
        // TODO: Replace this row with a query to the data model:
        return 1
    }
}

Next, add a placeholder for the setUpCell method under the same annotation (Listing 6-10). It takes two parameters: an instance of UITableViewCell and the row that the cell is on. The row number lets find the relevant item in the choices array in the data model we defined back in Listing 6-2.

Listing 6-10. Adding a Placeholder for Filling in Vote Options
func setUpCell(cell cell: UITableViewCell, atRow row: Int) {
    // TODO: Populate the cell with data
}

Clearing the Selection in the Table

Remember the calls to clearSelection we added earlier, when we were setting up the UI? We will implement clearSelection here to do what it says: if the user has tapped a cell in the table to select it, this method will clear this selection, so that the next user can vote. Add this method under MARK: Helper methods(Listing 6-11).

Listing 6-11. Clearing the Selection in the Table View
// MARK: Helper methods                                                                  
func clearSelection() {
    if let selectedIndexPath = choicesTable.indexPathForSelectedRow {
        choicesTable.deselectRowAtIndexPath(selectedIndexPath, animated: true)
    }
}

Adding a Second View

Add a new Swift file to the View controllers group in your project and name it ResultsViewController.swift. Open the new file and replace the import Foundation line with the class declaration from Listing 6-12: we are declaring another UIViewController subclass.

Listing 6-12. Declaring a View Controller Class , Which Will Be in Charge of the Results View
import UIKit

class ResultsViewController: UIViewController {
}
Tip

You can also let Xcode declare the class for you when you add the new file. To do that, from the main menu select FileNew…, then iOSSourceCocoa Touch Class and in the wizard set Subclass of to UIViewController. This will create the new class and stub out the following two methods: viewDidLoad and didReceiveMemoryWarning.

Next, we will add a view controller to the storyboard , which we will link with this new class. Open Main.storyboard, find a View Controller element in the Object library, and add it to the storyboard. Then add a Vertical Stack View to the new view controller and set the same constraints we used for the vertical stack view in Vote View Controller : set the distance between the stack view and the top and bottom layout guides to 20 and set zero for the distance between it and the parent view leading and trailing margins. In the Attributes inspector set the stack view’s Spacing property to 10.

Add a Text View to the vertical stack view and in the Attributes inspector leave its Text property to Plain, but delete the default text it comes with. Uncheck Editable and Selectable in the Behavior section: we will use this text view as read-only (Figure 6-15).

A371202_1_En_6_Fig15_HTML.jpg
Figure 6-15. Setting up the text view

Let us link the new UI elements we just added with the ResultsViewController class . On the storyboard select the new view controller (or select it in the Document outline) and in the Identity inspector, under Custom Class, set its Class to ResultsViewController.

Open Main.storyboard and ResultsViewController.swift side by side in the Assistant editor and Ctrl + drag the text view from the storyboard into the definition of the ResultsViewController class to create an outlet for it. Name the outlet resultsTextView (Listing 6-13).

Listing 6-13. Adding an Outlet for the Text View
class ResultsViewController: UIViewController {
    // MARK: Outlets
    @IBOutlet weak var resultsTextView: UITextView!
}

Add a placeholder method for presenting the results of a vote and call it displayVoteResults: we will add different implementations for it in the two versions of the app. Then add an override for UIViewController’s viewWillAppear method and in it call displayVoteResults—we want to refresh the information every time we switch to this view. The definition of ResultsViewControllershould now look like Listing 6-14.

Listing 6-14. Adding a Placeholder for Displaying Vote Results
class ResultsViewController: UIViewController {
    // MARK: Outlets
    @IBOutlet weak var resultsTextView: UITextView!


    // MARK: Data
    func displayVoteResults() {
        // TODO: Display a vote summary in resultsTextView
    }


    // MARK: UIViewController
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)


        // Results will be refreshed every time the view appears on screen:   
        displayVoteResults()
    }
}

Working with the Launch Screen

You have probably noticed that the SimpleVote project contains two storyboard files created by Xcode: Main.storyboard and LaunchScreen.storyboard. We have so far been working with Main.storyboard to design the user interface of the app. LaunchScreen.storyboard is what you would guess by its name: a storyboard, where you can design the launch screen of your app.

In Project navigator find LaunchScreen.storyboard and open it. It should look familiar: this is what Main.storyboard looked like before we added UI elements to it. We will not do anything fancy with the launch screen, just a little tweak to demonstrate that you can work with it the way you do with the rest of your app’s user interface.

Drag a Label from the Object library and drop it onto the view on LaunchScreen.storyboard. Change the label’s title to Cast your vote! and click the Align button at the bottom of the editor to constrain the label to the middle of its containing view both vertically and horizontally. When you are done, your storyboard and Document outline should look like Figure 6-16.

A371202_1_En_6_Fig16_HTML.jpg
Figure 6-16. Designing the launch screen in LaunchScreen.storyboard

If you run your app now , you should see Cast your vote! for a few seconds before the main screen of the app loads.

Developing VoteLite

In the Lite version of our application we will hard-code data for voting on, enable voting, and show results. For transitioning between the vote view and the results view we will initially set up segues and will then see how adding a navigation bar can save some of the work.

Setting up the VoteLite Project

We will reuse the code and user interface we created in our SimpleVote project. This requires a bit of work, as there is no straightforward way to duplicate a project in Xcode. Following are the steps for setting up the new project and adding the files we created or modified in the first part of this chapter.

  1. Create a new Single View Application project (FileNewProject…iOSApplicationSingle View Application) and call it VoteLite.

  2. In Project navigator select and delete ViewController.swift the way you did in SimpleVote (Figure 6-4).

  3. Open both projects in Finder and copy the following files and folders from SimpleVote/SimpleVote to VoteLite/VoteLite (Figure 6-17):

    • The Base.lproj folder, which contains the storyboard files. When asked, agree to replace the Base.lproj folder in VoteLite.

    • Choice.swift

    • VoteData.swift

    • VoteViewController.swift

    • ResultsViewController.swift

    A371202_1_En_6_Fig17_HTML.jpg
    Figure 6-17. Copying the reusable source and storyboard files in Finder
  4. Now let us add the newly copied files in Xcode and structure the project:

    1. Back in Xcode create two file groups and name them Data model and View controllers.

    2. Right-click the Data model group and from the popup menu select Add Files to Vote Lite… (or, with the group selected, use Xcode’s main menu: FileAdd Files to Vote Lite…).

    3. Then, from the dialog select Choice.swift and VoteData.swift. Add VoteViewController.swiftand ResultsViewController.swiftto the View controllers group.

    Your project’s structure should now resemble Figure 6-18.

    A371202_1_En_6_Fig18_HTML.jpg
    Figure 6-18. VoteLite in Project navigator
  5. Clean the project (ProductClean). This will remove any files that normally result from a build or are created by Xcode when it initially sets up your project . (If you are curious, have a look at the project’s build and DerivedData folders beforehand. Even before you build the project for the first time they contain intermediate and cached files.)

  6. Build and run the VoteLite app. It should now look identical to SimpleVote.

Adding Buttons to Trigger Transitions

We will add a button to each of the screens in our app and will use the buttons for navigating between the two screens.

Open Main.storyboard and from the Object library drag a Button control and drop it at the bottom of the vertical stack view in Vote View Controller. Title the button See results and add a border and corner radius to it the way we did for the buttons in the SimpleVote project.

Tip

It might be easier to drop the button in the tree in the Document outline.

Do the same in Results View Controller: add a button at the bottom of the vertical stack view and style it with a border and corner radius. Title this one Back to voting (Figure 6-19). Add a height constraint to each of the buttons and set it to 30.

A371202_1_En_6_Fig19_HTML.jpg
Figure 6-19. The two buttons on the storyboard and in the Document outline
Tip

If a newly added UI element initially appears in a funny place on the storyboard, select the view controller whose view you added it to and click the Resolve Auto Layout Issues button. Then from the pop-up menu select All Views in * View Controller ➤ Update Frames.

A371202_1_En_6_Figa_HTML.jpg

We will now add segues: transitions between the two views. A segue can be defined programmatically or in Interface builder. We will use Interface builder in this tutorial, so we can visualize the transitions.

First, let us make the See results button trigger a transition to the results view. Open Main.storyboard; in the Document outline select the See results button and Ctrl + drag from it to the Results View Controller (you can also do this on the storyboard). From the pop-up menu select Action SegueShow (Figure 6-20).

A371202_1_En_6_Fig20_HTML.jpg
Figure 6-20. Adding a segue between the two app screens

Transitioning between views with segues

You will see the segue on the storyboard show up as an arrow between the two view controllers. It will also appear in the Document outline (Figure 6-21).

A371202_1_En_6_Fig21_HTML.jpg
Figure 6-21. The segue on the storyboard and in the Document outline

Run your app and tap the See results button to test the segue. This should take you to the results view. But hey, you are locked there and you can’t go back!

We will remedy that with an unwind segue. An unwind segue is a way of “exiting” a view to return to the view that was shown before it. It is set up somewhat differently from the show segue we created earlier. While we did not have to write any code to make that segue work, to implement an unwind segue, we will first need to create an action that handles it. Note that this action needs to be in the view controller we want to transition back to, not the one that triggers the transition.

Open VoteViewController.swift, the view controller our unwind segue will lead back to, and add this action method to the VoteViewControler class (Listing 6-15). We do not need to add any more code to make the segue work.

Listing 6-15. Adding an Unwind Segue to VoteViewController
// MARK: Segues
@IBAction func unwindToVoting(unwindSegue: UIStoryboardSegue) {
}

Now let us create the unwind segue and connect it with this action. Open Main.storyboard. In the Document outline select the Back to voting button and Ctrl + drag from it to the Exit icon of Results View Controller. When you release the mouse, you should see a pop-over, which lists the unwindToVoting action under Action segue (Figure 6-22).

A371202_1_En_6_Fig22_HTML.jpg
Figure 6-22. Creating an unwind segue

In order to let you select the appropriate action, Interface Builder scans your code for methods with this signature and adds them to the pop-over list:

@IBAction func unwindMethodName(unwindSegue: UIStoryboardSegue)

Select unwindToVotingand the unwind segue will appear in the Document outline at the bottom of the Results View Controller Scene. Run the app again to test going back and forth between the two views.

Displaying Data

It is time to make this app do something useful. To start with, we will hard-code some data in VoteViewController.

Open VoteViewController.swift and add a property of type VoteData to the ViewController class. Then add a method, called createVoteData, in which we will populate the VoteData instance with a topic for voting and some choices (Listing 6-16).

Listing 6-16. Hard-Coding Data in the VoteViewController Class
// MARK: Data
var voteData = VoteData()


func createVoteData(){
    voteData.topic = "Gandalf or Dumbledore?"
    voteData.choices = å
        [Choice(title: "Gandalf"), Choice(title: "Dumbledore")]
}
Swift reference

In Listing 6-16 you can see the syntax for initializing an array in Swift: [item1, item2, ..., itemN]. For details on how to create and use arrays see Chapter 19.

Then, let us make a call to createVoteData in the view controller’s viewDidLoad method we overrode earlier. We will also set the label at the top of the view to display the vote topic (Listing 6-17).

Listing 6-17. Calling the createVoteData to Initialize
override func viewDidLoad() {
    // Call the base class method first
    // to make sure we won't miss out on any preparation done by it:
    super.viewDidLoad()


    // This will cause the methods required
    // by the UITableViewDataSource to be called:
    choicesTable.dataSource = self


    // Populate the model with data:
    createVoteData()


    // Get the label to display the vote topic
    // that was set in the data model:
    topicLabel.text = voteData.topic
}

Earlier in the chapter, when we set up the SimpleVote project , we added a couple of placeholders, whose role is to query the data model for data (see Listing 6-9 if you need a reminder). One was a property, choiceCount, which needs to work out how many choices there are to display in the table view. The other one was a method, setUpCell—it populates a given cell from the table view with data. Add the following implementation (Listing 6-18). Note the textLabel property of the cell that we set to display a choice: this comes from the label that the Basic cell style makes available to us.

Listing 6-18. Implementing Methods for the UITableViewDataSource Protocol
var choiceCount: Int {
    get{
        // Query the data model for the number of choices for voting.
        // Replace this row with a query to the data model:
        return voteData.choices.count
    }
}


func setUpCell(cell cell: UITableViewCell, atRow row: Int) {
    // Populate the cell with data
    if row >= 0 && row < voteData.choices.count {
        let choice = voteData.choices[row]


        // textLabel is a property of the Basic table view cell:
        cell.textLabel?.text = choice.title
    }
}

Taking Votes

Now that we have data and a topic with two choices for voting, let us implement the voting functionality. Use the Jump bar to locate the TODO annotations we left in the vote and abstain methods and let us replace these with code (Listing 6-19).

Listing 6-19. Implementing the Logic for the Voting
// MARK: Actions
@IBAction func vote(sender: AnyObject) {
    // User has voted.
    // Increment the votes for the selected choice:
    if let selectedIndexPath = choicesTable.indexPathForSelectedRow {
        assert(selectedIndexPath.row < voteData.choices.count)
        voteData.choices[selectedIndexPath.row].votes += 1
    }


    // Increment the number of participants:
    voteData.participants += 1


    // Clear the selected choice
    // to prepare the screen for the next participant:
    clearSelection()
}


@IBAction func abstain(sender: AnyObject) {
    // User has decided not to vote.
    // Increment the number of participants.
    voteData.participants += 1


    // Clear the selected choice
    // to prepare the screen for the next participant:
    clearSelection()
}

When the Vote button is tapped, we check if a row in the table has been selected. If it has, we find the corresponding choice and increment its votes. Then we increment the total number of participants in the vote. If the user taps Abstain, we just increment the total number of participants. At the end of both methods we clear the selection, so that the next user can vote.

With this functionality the user has two ways to abstain: by tapping the Abstain button or by leaving all choices unselected and tapping the Vote button. This implicit abstaining is not obvious and may even be confusing. Later on in the chapter we will improve this by alerting the users, so that they don’t abstain accidentally.

Swift reference

The if let construct in Listing 6-19 does conditional binding. It is effectively a shortcut for doing several operations: first, declaring a constant, selectedIndexPath, then checking if choicesTable.indexPathForSelectedRow is nil and if it is not, making an assignment to the constant. Conditional binding works with Swift’s Optional type. For more details see Chapter 19.

The line in vote, on which we call assert, is worth noting:

assert(selectedIndexPath.row < voteData.choices.count)

This is a basic check, which ensures that the selected cell is on a row that has a match in the voteData.choices array—that is, that we will not access an index in the array, which is out of bounds2. We will look at asserts in Chapter 8 .

Using Segues to Pass Data Between View Controllers

Let us take stock of the app we have created so far: it has a couple of views, has ways to transition back and forth between them, displays data, and lets the user vote. There is one thing missing: we have not yet implemented a way of showing the results of a vote. We will do so in this section. The implementation will use one of the segues to get the results to the relevant screen, which is one way of passing data between view controllers.

In order to be able to access a segue programmatically, we need to give it an identifier. Open Main.storyboard and select the segue we created for transitioning to the Results View Controller. In the Attributes inspector set Storyboard SegueIdentifier to ShowResults (Figure 6-23).

A371202_1_En_6_Fig23_HTML.jpg
Figure 6-23. Adding an identifier to the segue

Next, we will change the ResultsViewControllerto keep a copy of the vote data and display it. Open ResultsViewController.swift and add a VoteData member. Then replace the TODO annotation we added in displayVoteResults earlier in Listing 6-14 with the implementation from Listing 6-20.

Listing 6-20. Displaying the Vote Results in a Text View
// MARK: Data
var voteData = VoteData() // This will receive a copy of the data


func displayVoteResults() {
    // Check if we have something to report and return if not:
    if voteData.participants == 0 {
        resultsTextView.text = "No voting took place"
        return
    }


    // Create a string, which will accumulate a summary of the votes:
    var voteInformation = "Vote topic: (voteData.topic) "


    voteInformation += " Participants: (voteData.participants)"
    voteInformation += " Abstained: (voteData.abstains) "


    // Iterate through the options array
    // to read the votes each option received:
    for choice in voteData.choices {
        voteInformation +=
            " - (choice.title) received (choice.votes) votes"
    }


    // Display the string in the text view:
    resultsTextView.text = voteInformation
}

Now let us see how ResultsViewControllercan get a copy of the data for displaying. We will override a method, called prepareForSegue in VoteViewController,that will trigger the transition . This is a method of the base class UIViewController, so add its implementation under the MARK: UIViewController annotation in VoteViewController.swift. prepareForSegue is called before the segue passes navigation to its destination view controller and gives us access to this destination view controller (Listing 6-21).

Listing 6-21. Overriding prepareForSegue in VoteViewController.swift
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Check if we are preparing for the "ShowResults" segue:
    if segue.identifier == "ShowResults" {
        // Check if our destination is the ResultsViewController:
        if let resultsController =
            segue.destinationViewController as? ResultsViewController {
            // Copy data over to the results controller:
            resultsController.voteData = voteData
        }
    }
}
Swift reference

In Listing 6-21, in addition to the conditional binding in the second if statement, you can see an example of type casting. For more details and what the question mark after the as operator means see Chapter 19.

Finally, let us make sure that the voting session is reset every time we go back to the vote screen. Find the TODO annotation you added to VoteViewController’s viewWillAppear method and replace it with a call to voteData.resetVotes() to clear the data from the last vote (Listing 6-22).

Listing 6-22. Clear Vote Results Every Time We Go Back to the Vote Screen
override func viewWillAppear(animated: Bool) {
    // Call the base class method first
    // to make sure we won't miss out on any preparation done by it:
    super.viewWillAppear(animated)


    // Then clear the selection
    clearSelection()


    // Reset the vote results
    voteData.resetVotes()
}

Run your app and test it. You should be able to cast votes and see the results.

Using a Navigation View Controller

As you might guess, the way we implemented navigation between our two views is not necessarily the most efficient. If we add more views to the app, it will quickly become hard work to create and maintain segues between all of them. Instead of doing all of this work ourselves, we can use a view controller, which manages some or all of the transitions between the views.

A navigation view controller is an example of that. It is a container view controller: instead of managing a single view, it manages many views and the transitions between them. The views are treated like a stack: there is a root view, which sits at the bottom of the stack and other views can be pushed on top (shown on the screen) or popped out of the stack (dismissed from the screen). The navigation view controller also decorates its child views with space for navigation controls—you will see what this looks like in a bit.

We will add a navigation view controller to the VoteLite app. Open Main.storyboard, select the Vote View Controller , and from Xcode’s main menu select EditorEmbed inNavigation Controller. This adds the navigation view controller to the storyboard, moves the storyboard entry point to it, and makes Vote View Controller a child of the navigation view controller. You will also see a navigation bar appear on top of the Vote View Controller (Figure 6-24).

A371202_1_En_6_Fig24_HTML.jpg
Figure 6-24. Embedding the Vote View Controller in a navigation view controller

If you ran the app now, you would notice a difference immediately: the Back to voting button has become obsolete, as a Back button appears on the navigation bar by default when we go to the vote results screen (Figure 6-25).

A371202_1_En_6_Fig25_HTML.jpg
Figure 6-25. The navigation view controller adds a Back navigation button

Let us make the Show results button obsolete too. First, we will add a title that will appear in the navigation bar of the vote view—this title will be used in navigation later. If you look at Vote View Controller in the Document outline, you will see that a Navigation Item has appeared in the tree. Select it and in the Attributes inspector set Navigation ItemTitle to Vote. Alternatively, you can double-click the navigation bar that has appeared on top of the Vote View Controller and set the title there (Figure 6-26).

A371202_1_En_6_Fig26_HTML.jpg
Figure 6-26. Adding a title to the navigation bar of Vote View Controller

Next, find a Bar Button Item in the Object Library and drop it onto the right end of the navigation bar in the Vote View Controller or under Right Bar Button Items in the Document outline (Figure 6-27). Double-click to title it Results.

A371202_1_En_6_Fig27_HTML.jpg
Figure 6-27. Adding a bar button

As magical as our navigation view controller is, this time we will need to help it work out where the bar button should lead. Select the bar button and Ctrl + drag from it toward the Results View Controller: you know how to do this two ways now: on the storyboard and in the Document outline. From the pop-up menu select Action SegueShow to create a segue to the results view. Select the segue and in the Attributes inspector set its Identifier to ShowResults. This is the same identifier we gave the segue triggered by the Show results button: reusing it will allow us to take advantage of the prepareForSegue method we implemented earlier in Listing 6-21.

If you run the app now, you should be able to go to the results screen by tapping Results on the navigation bar. When the results screen shows up, there should be a button for going back to the vote view, which reads < Vote (Figure 6-28).

A371202_1_En_6_Fig28_HTML.jpg
Figure 6-28. Using the navigation view controller

This completes our work on VoteLiteand we are ready to move on to developing its Pro version.

Developing VotePro

In the Pro version of the voting app we will add more screens to allow composing new votes and managing user settings. Even with only four screens using the navigation approach we applied to VoteLite would be counterproductive for managing the transitions between them.

The iOS SDK offers various container view controllers, which can manage multiple views. For example, your default mail client app uses master-detail design: the master view contains e-mail snippets and the detail view—the contents of the e-mail selected in the master view. The default weather app organizes its views as pages. Another popular app design is to make views accessible via tabs—this is what we are going to use for VotePro.

Setting Up the VotePro Project

Once again we will reuse the code and UI we created in the SimpleVote project . Create a new Single View Application project and name it VotePro. Then repeat the steps we took for setting up VoteLite: delete ViewController.swift and add the files with custom code and UI from SimpleVote. Organize the files in two groups: View controllers and Data model. Add another group under View controllers and name it Containers. Your project should have the following structure (Figure 6-29).

A371202_1_En_6_Fig29_HTML.jpg
Figure 6-29. VotePro in the Project navigator

Adding a Tab Bar Controller

Open Main.storyboard and select the Vote View Controller, then from Xcode’s main menu select EditorEmbed InTab Bar Controller. This adds another view controller to the storyboard and moves the storyboard entry point to it (Figure 6-30).

A371202_1_En_6_Fig30_HTML.jpg
Figure 6-30. Adding a tab bar controller to the storyboard

Notice also how a tab bar appears at the bottom of the Vote View Controller with an icon placeholder and a title Item. Double-click the title and change it to Vote (Figure 6-31).

A371202_1_En_6_Fig31_HTML.jpg
Figure 6-31. Changing the title of the tab

So far we have a tabbed application with one tab. Let us add the Results View Controller to the list of controllers managed by the tab bar controller; this will make both our views accessible via tabs. Select the Tab Bar Controller either on the storyboard or in the Document outline and Ctrl + drag from it to the Results View Controller. From the pop-up menu select Relationship Segueview controllers (Figure 6-32).

A371202_1_En_6_Fig32_HTML.jpg
Figure 6-32. Adding the Results View Controller to the list that the tab view controller manages

This will cause a tab bar item to appear at the bottom of Results View Controller too. Change its title to Results . Then run the app to test the navigation. You should see two tabs at the bottom of the app and be able to switch between the Vote and the Results screens (Figure 6-33).

A371202_1_En_6_Fig33_HTML.jpg
Figure 6-33. Switching between views using tabs

Passing Data Between Tabs

A tab bar makes views in an app accessible in a random order. This means that when we add more views to our app, the user will be able to go from any view to any other view. So a transition between the Vote View Controller and the Results View Controller is no longer guaranteed and thus not a good place to pass data between them, unlike it was in VoteLite. Instead, we will keep the data someplace where both view controllers can access it.

One candidate is the tab bar controller. By adding the two view controllers to its list of managed view controllers we created parent-child relationships, where the tab bar controller is the parent. The base class UIViewControllerhas a property, which will help us access this parent from our two child view controllers.

Let us create a class that we will link with the tab bar controller first. Add a new Swift file to the View controllers/Containers group in your project (FileNewFile… ➤ iOSSourceSwift File) and call it VoteTabBarController.swift.

Open VoteTabBarController.swift and replace the line

import Foundation

with the definition of the VoteTabBarController class from Listing 6-23. This class inherits UITabBarController and overrides its viewDidLoad method to initialize an instance of VoteData. It is the voteData property of VoteTabBarController that we want its child view controllers to access.

Listing 6-23. Define the VoteTabBarController Class
import UIKit

class VoteTabBarController: UITabBarController {

    var voteData = VoteData()

    override func viewDidLoad() {
        super.viewDidLoad()


        voteData.topic = "Who would you have lunch with?"
        voteData.choices = [Choice(title: "Batman"),
            Choice(title: "Wonder Woman"), Choice(title: "Superman")]
    }
}

On the storyboard select the tab bar controller and in the Identity inspector set Custom ClassClass to VoteTabBarController. This will link the tab bar controller on the storyboard with the class we just defined.

As a next step we will have both VoteViewController and ResultsViewController access their tab view controller parent. Open VoteViewController.swift and add a weakly referenced implicitly unwrapped optional for VoteTabBarControllerunder the MARK: UIViewController annotation (Listing 6-24) and call it tabManager. Initialize tabManager in VoteViewController’s viewDidLoad method. Each UIViewController has a tabBarController property, which gives it access to a tab bar controller, if it is embedded in one—this is what we will use to initialize tabManager with.

Listing 6-24. Add a Reference to the VoteTabBarController Instance in the VoteViewController Class
class VoteViewController: UIViewController, UITableViewDataSource {

    // MARK: UIViewController
    weak var tabManager: VoteTabBarController!


    override func viewDidLoad() {
        super.viewDidLoad()


        // Get a reference to the tab bar controller
        // and cast it to the class we created for it:
        tabManager = self.tabBarController as! VoteTabBarController


        // Access the vote data in tabManager
        // to initialize the topic label:
        topicLabel.text = tabManager.voteData.topic


        // This will cause the methods required
        // by the UITableViewDataSource to be called:
        choicesTable.dataSource = self
    }


    // The rest of the class definition
}
Swift reference

We threw in a couple of terms in the previous paragraph that need explaining. “Implicitly unwrapped” is term used for Swift variables and constants declared as Optional. An optional type has the option of having a value or having no value (i.e., being nil). Getting to the value, or finding out its absence, is called unwrapping. For details on how all this works and the syntax used see Chapter 19.

Another term that might need an explanation is “weak reference.” In Chapter 21 we will see that, much like in ActionScript, class instances in Swift are always passed by reference, while other types are passed by value. In the example above this means that the assignment to tabManager does not cause a new instance of VoteTabBarController to be created; instead, it makes tabManager refer to the same instance that self.tabBarController does. A weak reference has memory-management implications. When the instance of VoteViewController gets deallocated, normally this will result in its properties being deallocated too. Making a reference weak prevents that. For more details on memory management, see Chapter 21.

Let us use tabManagerto display choices in the table view. First, find the two placeholders, which we defined earlier to help display options in the table view: setUpCell and optionCount. Their implementation in Listing 6-25 will look very familiar: it is almost the same as what we provided in VoteLite, only this time voteData is accessed via tabManager.

Listing 6-25. Display Vote Data in the Table View in VoteViewController
// MARK: Data
func setUpCell(cell cell: UITableViewCell, atRow row: Int) {
    // Populate the cell with data:
    if row >= 0 && row < tabManager.voteData.choices.count {
        let choice = tabManager.voteData.choices[row]
        cell.textLabel?.text = choice.title
    }
}


var choiceCount: Int {
    get{
        // Query the data model for the number of choices
        // to present for voting:
        return tabManager.voteData.choices.count
    }
}

Still in VoteViewController.swift find the viewWillAppear method and add a call to reset the number of votes, so that each transition to the vote screen will start a new voting session. The method should now look like Listing 6-26.

Listing 6-26. Refresh the Topic Label and Start a New Session Every Time the View Appears on Screen
override func viewWillAppear(animated: Bool) {
    // Call the base class method first
    // to make sure we won't miss out on any preparation done by it:
    super.viewWillAppear(animated)


    // Then clear the selection
    clearSelection()
    tabManager.voteData.resetVotes()
}

We will implement the vote logic for the two action handlers we added earlier: vote and abstain. Use the Jump bar to find the two actions and add the code from Listing 6-27. This is again the same as the implementation we had in VoteLite, except for how we access voteData, which now lives in VoteTabBarController.

Listing 6-27. Implement the Vote and Abstain Actions
@IBAction func vote(sender: AnyObject) {
    // User has voted.
    // Increment the votes for the selected choice:
    if let selectedIndexPath = choicesTable.indexPathForSelectedRow {
        assert(selectedIndexPath.row < tabManager.voteData.choices.count)
        tabManager.voteData.choices[selectedIndexPath.row].votes += 1
    }


    // Increment the number of participants:
    tabManager.voteData.participants += 1


    // Clear the selected choice to prepare the screen
    // for the next participant:
    clearSelection()
}


@IBAction func abstain(sender: AnyObject) {
    // User has decided not to vote.
    // Increment the number of participants.
    tabManager.voteData.participants += 1


    // Clear the selected choice to prepare the screen
    // for the next participant:
    clearSelection()
}

Next, open ResultsViewController.swiftand add a reference to the tab bar controller, called tabManager. Then, add an overload for viewDidLoad and initialize tabManager in it (Listing 6-28).

Listing 6-28. Add a Reference to the VoteTabBarController Instance in the ResultsViewController Class
class ResultsViewController: UIViewController{

    // MARK: UIViewController
    weak var tabManager: VoteTabBarController!


    override func viewDidLoad() {
        super.viewDidLoad()


        // Get a reference to the tab bar controller
        // and cast it to the class we created for it:
        tabManager = self.tabBarController as! VoteTabBarController
    }


    // The rest of the class definition
}

Now let us implement displayVoteResults—this method is currently a placeholder, which we added when we set up SimpleVote. The implementation looks the same as the one in the Lite version of the app, again, the only difference being how we access the data (Listing 6-29).

Listing 6-29. Implement the Method That Will Display Vote Results in ResultsViewController
func displayVoteResults() {
    // Check if we have something to report and return if not:
    if tabManager.voteData.participants == 0 {
        resultsTextView.text = "No voting took place"
        return
    }


    // Create a string, which will accumulate a summary of the votes:
    var voteInformation = "Vote topic: (tabManager.voteData.topic) "


    voteInformation += " Participants: (tabManager.voteData.participants)"
    voteInformation += " Abstained: (tabManager.voteData.abstains) "


    // Iterate through the options array
    // to read the votes each option received:
    for choice in tabManager.voteData.choices {
        voteInformation +=
            " - (choice.title) received (choice.votes) votes"
    }


    // Display the string in the text view:
    resultsTextView.text = voteInformation
}     

This is a good time to run the app, cast some votes, and switch between the tabs to see them update in real time.

Showing Alerts

You are familiar with alerts: these are modal pop-up views, which you can use to show messages to the user or ask them to make a choice. We will create an alert to make the voting process a bit more user-friendly.

In the logic we implemented for voting in Listing 6-27 we increment the number of participants in the session when the Vote button gets tapped even if the user has not made a choice. And this, as we pointed out when we used the same logic in VoteLite, means that the user implicitly abstains, possibly without even realizing it. Showing an alert that warns the user about this abstention and asks if the user does want to abstain will make things more obvious and prevent abstaining by accident.

Let us modify the vote action to only increment the number of participants when an actual vote has been cast. Then make it show an alert if the user has not made a choice (Listing 6-30). To show the alert we will add a call to showAbsentVoteAlert—a function we will implement shortly.

Listing 6-30. Modifying the Vote Action in VoteViewController to Alert the User If No Choice Was Made
@IBAction func vote(sender: AnyObject) {
    // User has voted.
    // Increment the votes for the selected choice:
    if let selectedIndexPath = choicesTable.indexPathForSelectedRow {
        assert(selectedIndexPath.row < tabManager.voteData.choices.count)
        tabManager.voteData.choices[selectedIndexPath.row].votes += 1


        // Increment the number of participants:
        tabManager.voteData.participants += 1
    } else {
        // The user tapped Vote, but didn't select an option to vote for.
        showAbsentVoteAlert()
    }


    // Clear the selected choice
    // to prepare the screen for the next participant:
    clearSelection()
}

The next piece of code in Listing 6-31 shows the implementation of showAbsentVoteAlert. It creates an instance of UIAlertController, an iOS SDK class, and sets it up to show a title and a message to the user.

Note that you can choose a style for the alert: you have a choice between UIAlertControllerStyle.Alertand UIAlertControllerStyle.ActionSheet. Alerts are like small dialogs: they pop up in the middle of the screen and are suited for short messages or choices between two options. Action sheets, on the other hand, emerge from the bottom of the screen on narrow screens or show as pop-overs on larger screens and are useful for longer lists of choices.

We will use the Alert style for our purposes. The alert will warn users that no choice was made and ask if they want to abstain, showing two choices: “No” and “Yes.” These choices are represented by instances of the UIAlertAction class, which we add to the alert. An alert action shows up as a button that the user can tap. It has a title, a style, and a handler, which we can optionally implement to execute code when the option is tapped.

The style we can set for an action in the alert is of type UIAlertActionStyleand can be Default, Cancel, or Destructive. These styles define what the action buttons look like and in what order they appear in the alert. A default action represents the most likely choice a user would make (e.g., save her progress through a game). An example of a destructive action would be deleting photos from Camera Roll or cleaning up your inbox.

Add the code in Listing 6-31 to the VoteViewController class under MARK: Helper methods.

Listing 6-31. Showing an Alert
func showAbsentVoteAlert()
{
    // Create an alert to ask if they want to abstain:
    let alert = UIAlertController(title: "You didn't cast a vote",
        message: "Do you want to abstain?",
        preferredStyle: UIAlertControllerStyle.Alert)


    // Add a cancel action and a default action to the alert:
    // these will appear as buttons.
    let cancelAction = UIAlertAction(title: "No",
        style: .Cancel, handler: nil)
    alert.addAction(cancelAction)


    // The default action will be trigerred when the uer wants to abstain.
    // Add a handler for that, so we can count this
    // toward the total number of participants:
    let defaultAction = UIAlertAction(title: "Yes",
        style: .Default, handler: {
        // Increment the total number of votes:
        action in self.tabManager.voteData.participants += 1 } )
    alert.addAction(defaultAction)


    // Show the alert on screen:
    self.presentViewController(alert, animated: true, completion: nil)
}
Swift reference

If you have a look at how defaultAction is instantiated in Listing 6-31, you may find syntactic strangeness, namely, the code in the curly brackets after the handler label. This is a block of code is called when the user taps the button that represents defaultAction—in other words, it is an action handler. In this case it is implemented as a closure. We will look at closures and how to use them in Swift in Chapter 22.

Run the app, leave the choices in the table unselected, and tap the Vote button to see the alert pop up (Figure 6-34). Then do an experiment and change the style of the alert to ActionSheet. What does it look like now?

A371202_1_En_6_Fig34_HTML.jpg
Figure 6-34. Showing an alert when the user taps the Vote button but has not voted

Working with User Preferences

In this section we will furnish the app with a Settings screen and learn how to use NSUserDefaults to store and read user preferences.

Start by adding another View Controller from the Object library to Main.storyboard. Then add the controller to the list of children of the tab bar controller (Ctrl + drag from the tab bar controller to the new view controller and from the pop-up menu select Relationship Segueview controllers). Set its tab bar item title to Settings.

You are now experienced with laying out user interface, so instead of walking you through each step, we will use a couple of diagrams to show you how to design the settings view.

First, find two Labels, a Switch and a Stepper in the Object library , and position them as shown in Figure 6-35.

A371202_1_En_6_Fig35_HTML.jpg
Figure 6-35. Designing the settings view

Next, add the following constraints (Figure 6-36):

  • Set the space to the leading margin to 0 for both labels.

  • Set the space to the nearest neighbor at the top to 20 for both labels.

  • Set the space to the trailing margin to 0 for the switch and for the stepper.

  • Select the top label and the switch and align their vertical centers.

  • Select the bottom label and the stepper and align their vertical centers.

A371202_1_En_6_Fig36_HTML.jpg
Figure 6-36. Adding constraints to the settings view

Select the stepper and in the Attributes inspector set its Minimum value to 2, Maximum value to 5 and Current value to 5. Leave its Step setting set to 1 (Figure 6-37).

A371202_1_En_6_Fig37_HTML.jpg
Figure 6-37. Setting up the stepper control

Now let us back this all up with code. Add a new Swift file to the View controllers group in Project navigator and call it SettingsViewController.swift. Open the new file and declare a subclass of UIViewController, called SettingsViewController(Listing 6-32).

Listing 6-32. Defining the SettingsViewController
import UIKit

class SettingsViewController: UIViewController {
}

Open Main.storyboardand SettingsViewController.swiftside by side in the Assistant editor. First select the new view controller on the storyboard and set its Class to SettingsViewController (in the Identity inspector find the Custom ClassClass setting). Then create the outlets for the switch, created the label for the maximum options setting and the stepper, and call them resultsAsPercentSwitch, maxOptionsLabel, and maxOptionsStepper, respectively (Listing 6-33).

Listing 6-33. Adding Outlets for the Settings UI
// MARK: Outlets
@IBOutlet weak var resultsAsPercentSwitch: UISwitch!
@IBOutlet weak var maxChoicesLabel: UILabel!
@IBOutlet weak var maxChoicesStepper: UIStepper!

Add two actions: one to handle the Value Changed event for the switch, called resultsAsPercentToggled, and one to handle the Value Changed event for the stepper, called maxChoicesChanged(Listing 6-34).

Listing 6-34. Adding Actions for the Settings UI
// MARK: Actions
@IBAction func resultsAsPercentToggled(sender: AnyObject) {
}


@IBAction func maxChoicesChanged(sender: AnyObject) {
}

Storing User Preferences

We will implement these actions to save the user’s preferences to the defaults system. This is Apple’s term for the storage space specifically dedicated to keeping such information. We can access the defaults system with the help of the NSUserDefaults class, which has convenience methods for storing and retrieving values of various types.

Each value stored in the defaults system must be identified by a key, so we will define a couple of keys first: one for the value of the switch and one for the value of the stepper control. A key in this case is a string, which we need to choose. We will access these keys from several view controllers in our app: SettingsViewControllerwill use them for writing to the defaults system and the ResultsViewController will need a read access, in order to decide how to present the results—as percentages or as number of votes. Later on, when we allow the user to set up his own vote topic and choices, the view controller that will help with that will need to know the maximum number of choices that the app allows.

To make the settings keys available to these view controllers, we will define them as constants in the VoteTabBarControllerclass in VoteTabBarController.swift (Listing 6-35).

Listing 6-35. Defining Keys for the User Settings in VoteTabBarController
class VoteTabBarController: UITabBarController {
    // MARK: Settings keys
    let resultsAsPercentKey = "ResultsAsPercent"
    let maxChoicesKey = "MaximumChoices"


    // The rest of the class definition
}

Go back to SettingsViewController.swiftand add a reference to the VoteTabBarController instance the way we did for the other two view controllers. Don’t forget to override UIViewController’s viewDidLoad method and initialize the reference (Listing 6-36).

Listing 6-36. Ading a Reference to the Tab Bar controller in SettingsViewController and Initializing It
// MARK: UIViewController
weak var tabManager: VoteTabBarController!


override func viewDidLoad() {
    super.viewDidLoad()


    // Get a reference to the tab bar controller
    // and cast it to the class we created for it:
    tabManager = self.tabBarController as! VoteTabBarController
}

Next we will add an implementation for the resultsAsPercentToggled action. It uses the setBool convenience method of the NSUserDefaults class to store the state of the switch (Listing 6-37).

Listing 6-37. Saving the State of the Switch
@IBAction func resultsAsPercentToggled(sender: AnyObject) {
    // Get a reference to the defaults system
    let defaults = NSUserDefaults.standardUserDefaults()


    // ... and use it to save the state of the switch:
    defaults.setBool(resultsAsPercentSwitch.on, forKey:   
        tabManager.resultsAsPercentKey)
}

The implementation of the second action, maxChoicesChanged, uses setInteger—another method of the NSUserDefaults class —to save the value of the stepper. Note that the value we read from the stepper is of type Double, so we need to convert it to an integer before storing it (allowing 4.5 choices, for example, is not very meaningful). This action does one more thing: it calls updateMaxChoicesLabel—a method we will add next to show to the user the value that was set. Listing 6-38 shows the implementation of maxChoicesChanged.

Listing 6-38. Saving the Value of the Stepper in SettingsViewController
@IBAction func maxChoicesChanged(sender: AnyObject) {
    // Get a reference to the defaults system
    let defaults = NSUserDefaults.standardUserDefaults()


    // The stepper value is of type Double,
    // but we want to store an Int, so create an Int:
    let maxChoices = Int(maxChoicesStepper.value)


    // Store the maximum preferred choices as an integer:
    defaults.setInteger(maxChoices, forKey: tabManager.maxChoicesKey)


    // Finally, update the label
    // to show the user the value they set:
    updateMaxChoicesLabel()
}

Next comes the implementation of updateMaxChoicesLabel. It is fairly simple: we read the value that the stepper has been set to and display as part of the maxOptionLabel’s text (Listing 6-39).

Listing 6-39. Updating the Label to Show the Value of the Stepper in SettingsViewController
// MARK: Helper methods
func updateMaxChoicesLabel() {
    maxChoicesLabel.text = "Maximum options: (Int(maxChoicesStepper.value))"
}

Loading User Preferences

Let us make sure that the settings screen displays the correct user preferences when it first loads. For that purpose we will use two of the convenience methods of NSUserDefaults for reading values: boolForKey and integerForKey.

The very first time we run the app there will be no keys and values stored in the system defaults for us to read. Conveniently, boolForKey and integerForKey both return values even if the relevant keys haven’t been found, so we will have default settings to start with. However, note that the default value returned by integerForKey is 0, which is not a valid value for our purposes. Later on, when we allow the user to change the topic of the vote and add choices, we will want there to be at least two choices for voting on. We can also add a number to cap choices to. Let us do that by adding a couple of constants to VoteTabBarController in VoteTabBarController.swift (Listing 6-40)—we want to keep these data constraints with the rest of the data. Later on we will need to have them accessible from a new view, in which the user will set up their own vote.

Listing 6-40. Adding Constraints to the Data Model in VoteTabBarController
// MARK: Data constraints
let minChoices = 2
let maxChoices = 5

Back in SettingsViewController.swift add a helper function, called loadSettings, which queries the system defaults for the two settings we have stored. This will use two of the convenience methods of NSUserDefaults for reading values: boolForKey and integerForKey.

The very first time we run the app there will be no keys and values stored in the system defaults to read. Conveniently, boolForKeyand integerForKeyboth return values even if keys haven’t been found, so we will have default settings to start with. A detail to pay attention to: the default value returned by integerForKey is 0, which is not a valid value for our purposes. We want the choices for voting to be at least two, so we must check and adjust the result we get from integerForKey (Listing 6-41).

Listing 6-41. Displaying the Settings When the View Is Loaded by SettingsViewController
// MARK: Helper methods
func loadSettings()
{
    // Get a reference to the defaults system
    let defaults = NSUserDefaults.standardUserDefaults()


    // Read the Boolean stored for the state of the switch.
    // If no value is found for the key,
    // boolForKey will return false by default.
    resultsAsPercentSwitch.on =
        defaults.boolForKey(tabManager.resultsAsPercentKey)


    // Read the value stored for the steper.
    // If no value is found, integerForKey will return 0.
    // When this is the case,
    // set the stepper to the maximum choices allowed.
    let maxOptions = defaults.integerForKey(tabManager.maxChoicesKey)
    maxChoicesStepper.value =
        0 == maxOptions ? Double(tabManager.maxChoices) :
        Double(maxOptions)


    // Update the label to show the value we set the switch to:
    updateMaxChoicesLabel()
}

We will call loadSettings from the view controller’s viewDidLoad method to ensure that the view can display user preferences the first time it is loaded (Listing 6-42).

Listing 6-42. Updating viewDidLoad in SettingsViewController to Call loadSettings
override func viewDidLoad() {
    super.viewDidLoad()


    // Get a reference to the tab bar controller
    // and cast it to the class we created for it:
    tabManager = self.tabBarController as! VoteTabBarController


    // Query the system defaults for user preferences:
    loadSettings()
}

Let us see what the app looks like now. Run it, go to the Settings screen, and play with the values. Then quit and start the app again: the Settings screen should show the values you last set.

Before we move on, let us put one of the settings to use: we will change the results screen to show the votes as percentages or as numbers, depending on what the user prefers. Open ResultsViewController.swift and let us add a method similar to displayVoteResults, called displayVoteResultsAsPercentages (Listing 6-43). Note the use of NSNumberFormatter: this is an iOS SDK class, which helps with converting numbers to strings. In this case it formats Double values to show up to two digits after the decimal point when interpolated in a string.

Listing 6-43. Adding a Method to ResultsViewController That Will Display the Vote Results as Percentages
func displayVoteResultsAsPercentages() {
    // Check if we have something to report and return if not:
    if tabManager.voteData.participants == 0 {
        resultsTextView.text = "No voting took place"
        return
    }


    // Create a string, which will accumulate a summary of the votes:
    var voteInformation = "Vote topic: (tabManager.voteData.topic) "
    voteInformation += " Total votes: (tabManager.voteData.participants)"


    // Work out the percent of abstain votes:
    let abstainedPercent = 100 * Double(tabManager.voteData.abstains) /
        Double(tabManager.voteData.participants)


    // Convert the percent to a string
    //  with precision of two digits after the decimal point:
    let formatter = NSNumberFormatter()
    formatter.maximumFractionDigits = 2
    let abstainedPercentString = formatter.stringFromNumber(abstainedPercent)!
    voteInformation += " Abstained: (abstainedPercentString)% "


    // Iterate through the choices array
    // to read the votes each choice received:
    for choice in tabManager.voteData.choices {
        let choiceVotePercent = 100 * Double(choice.votes) /
            Double(tabManager.voteData.participants)


        // Use the formatter to convert the percent information into a string:
        let choiceVotePercentString =
            formatter.stringFromNumber(choiceVotePercent)!
        voteInformation += " - (choice.title) received
            (choiceVotePercentString)% of the votes"
    }


    // Display the string in the text view:
    resultsTextView.text = voteInformation
}

Still in ResultsViewController, let us modify its viewWillAppear method to check the user settings and call the appropriate method for displaying the results (Listing 6-44).

Listing 6-44. Modifying ResultsViewController to Take the User Preferences into Account
override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)


    // Get a reference to the defaults system
    let defaults = NSUserDefaults.standardUserDefaults()


    // Read the Boolean stored for the state of the switch.
    // If no value is found for the key,
    // boolForKey will return false by default.
    if defaults.boolForKey(tabManager.resultsAsPercentKey) {
        displayVoteResultsAsPercentages()
    } else {
        displayVoteResults()
    }
}

Run the app and see how the results screen changes according to what you do on the settings screen.

I have not forgotten about the user preference we added for limiting the maximum number of voting options. We will see what it’s about in the next section, where we let the user set a new topic for the vote and define options.

Creating and Managing UI Programmatically

Enough of this hard-coding nonsense. In this section we will make the app more dynamic and allow the user to change the voting topic and add choices.

This will be done on a separate tab in the app’s interface, so put another view controller on the storyboard and then add it to the tab bar controller’s list of controllers. Title it Create.

Tip

You can drag and drop tab bar items in the tab view controller on the storyboard to change their order.

Add a Label, a Text Field, two Buttons, a Scroll View, and a Vertical Stack View to the view controller as shown on Figure 6-38. Note that the vertical stack view needs to be inside the scroll view.

A371202_1_En_6_Fig38_HTML.jpg
Figure 6-38. Designing the Create view controller

Title the label Topic and the two buttons – + and -. We will use the buttons for adding and removing choices. Style them the way we did with the rest of the buttons in the app: in the Identity inspector add layer.borderWidth and layer.cornerRadius under User Defined Runtime Attributes, set their types to Number and their values to 1 and 6, respectively.

Select the stack view and, in the Attributes Inspector, set Stack ViewDistribution to Fill Equally and its Spacing to 10.

Constrain the UI elements , so that

  • The vertical stack view fills up the scroll view (i.e., all of its edges go to the edges of the scroll view).

  • The label, the + button, and the scroll view’s leading edges are aligned and coincide with their parent view’s leading margin.

  • The vertical distance between the + button and the label is 20.

  • The text field and the scroll view’s trailing edges are aligned and coincide with the parent view’s trailing margin.

  • The distance between the bottom edge of the scroll view and the bottom margin is 20.

  • The vertical distance between the scroll view and the + button is 20.

  • The vertical centers of the label and the text field are aligned.

  • The vertical centers of the two buttons are aligned.

Tip

The storyboard is probably getting a bit crowded now. If you right-click somewhere outside a view controller, you can choose to zoom in and out, so that you get a better view and are able to arrange the view controllers in a convenient way. Double-clicking on the storyboard switches between 25% and 100% zoom.

Add a new Swift file to your project’s View controllers group, called CreateViewController.swift, and in it declare a class, named CreateViewController, which inherits UIViewController (Listing 6-45).

Listing 6-45. Declaring CreateViewController
import UIKit

class CreateViewController : UIViewController {
}

Back on the storyboard set the Class property of the new view controller to CreateViewController. Then create outlets for the text field, the two buttons, and the vertical stack view and name them voteTopic, addButton, removeButton, and choicesView. Add actions for the two buttons’ Touch Up Inside events and name them addButtonTapped and removeButtonTapped. Inside addButtonTapped make a call to createNewChoice and inside removeButtonTapped call removeLastChoice (Listing 6-46). The two calls we just added are to methods we will implement next.

Listing 6-46. Adding Outlets and Actions to CreateViewController
// MARK: Outlets
@IBOutlet weak var voteTopic: UITextField!
@IBOutlet weak var addButton: UIButton!
@IBOutlet weak var removeButton: UIButton!
@IBOutlet weak var choicesView: UIStackView!


// MARK: Actions
@IBAction func addButtonTapped(sender: AnyObject) {
    createNewChoice()
}


@IBAction func removeButtonTapped(sender: AnyObject) {
    removeLastChoice()
}

Creating and Removing UI Elements at Runtime

The two methods we are about to implement will dynamically add or remove a text field from the screen, allowing the user to create or delete choices for voting on. We will first add an array, which will keep references to the text fields we create and delete. Add a MARK: Dynamic UI annotation and under it declare an empty array of type UITextField (Listing 6-47).

Listing 6-47. Declaring an Array to Keep Track of Dynamic UI in CreateViewController
// MARK: Dynamic UI
// This array keeps references
// to text fields we create at runtime:
var choiceTextFields = [UITextField]()

Add the definition of createNewChoice under MARK: Dynamic UI (Listing 6-48). The first three lines in this method create a new instance of UITextField and set it up. We set a text placeholder and assign our instance of CreateViewController as the delegate for the text field. We will see later on why we need the delegation and how to make it work.

For the moment let us concentrate on the dynamic UI: after the text field has been created, we add it to the choiceTextFields array , so we can keep track of it and then we call addArrangedSubView to place it on the vertical stack view. addArrangedSubView is a method of UIStackView, which will add the text field at the bottom of the subviews that the stack view manages.

The last call we make in this method, updateButtonState, is to another method that we will define later: its role is to keep track of whether the user is allowed to add or delete options, so it can enable or disable addButton and removeButton accordingly.

Listing 6-48. Adding Text Fields at Runtime
func createNewChoice() {
    // Create and set up a new text field:
    let textField = UITextField()
    textField.placeholder = "Choice title"
    textField.delegate = self


    // Add it to the array of dynamic text fields:
    choiceTextFields.append(textField)


    // Add it to the vertical stack view:
    choicesView.addArrangedSubview(textField)


    // See if the addButton and the removeButton
    // need to be enabled/disabled:
    updateButtonState()
}

Next under the under MARK: Dynamic UI annotation comes the implementation of removeLastChoice. To remove the last text field we added on the screen, we first take its reference out of the choiceTextFields array and then we ask the vertical stack view to remove it from its list of arranged subviews. We assign nil to the reference, so that the instance it refers to it can be deallocated. Finally, we make a call to the function that will enable or disable the two buttons depending on how many choices (i.e., text fields) are left on the screen (Listing 6-49).

Listing 6-49. Deleting Text Fields at Runtime
func removeLastChoice() {
    // Take out the last text field
    // we added to the array:
    var textField = choiceTextFields.popLast()


    // Remove it from the screen:
    choicesView.removeArrangedSubview(textField!)


    // Flag it for deallocating:
    textField = nil


    // See if the addButton and the removeButton
    // need to be enabled/disabled:
    updateButtonState()
}
Swift reference

The last two methods we added both use an array to add or remove items. You can find out more about arrays in Chapter 19.

The last method we will add under MARK: Dynamic UI is updateButtonState. It will dynamically enable or disable the add and remove buttons. We want the add button to be enabled only if the choices added on the screen are below the maximum we allow and we want the remove button to be enabled only if there are more choices than the minimum.

For working out the maximum number of choices we will check the user defaults system, where the Settings view may have stored a value set by the user. If no value is found, we can default to the constraints we defined on the data in VoteTabViewController. We will check these constraints to find out the minimum number of choices too (Listing 6-50).

Listing 6-50. Enabling or Disabling the add and remove Buttons at Runtime
func updateButtonState() {
    // Check the user settings
    // for the maximum number of choices allowed:
    let defaults = NSUserDefaults.standardUserDefaults()
    var maxChoices = defaults.integerForKey(tabManager.maxChoicesKey)
    if maxChoices == 0 {
        // If no value was found,
        // take the data constraint from tabManager:
        maxChoices = tabManager.maxChoices
    }


    // Only enable the addButton if we are below the maximum:
    addButton.enabled = choiceTextFields.count < maxChoices


    // Only enable the removeButton
    // if we have more than the minimum choices allowed:
    removeButton.enabled = choiceTextFields.count > tabManager.minChoices
}

Listing 6-50 uses something we have not defined yet: tabManager. Let us fix that. Like we did in the rest of the view controllers, we will declare tabManager as a constant member and initialize it to refer to CreateViewController’s tabBarController instance inside the viewDidLoad method (Listing 6-51).

Listing 6-51. Adding a Reference to the Tab Bar Controller
// MARK: UIViewController
weak var tabManager: VoteTabBarController!


override func viewDidLoad() {
    super.viewDidLoad()


    // Get a reference to the tab bar controller:
    tabManager = self.tabBarController as! VoteTabBarController
}

We will do something else in viewDidLoad too : now that we have the means to dynamically create text fields, let us make sure that our view starts with text fields for the minimum number of choices we allow (Listing 6-52).

Listing 6-52. Creating Text Fields for the Minimum Number of Choices
override func viewDidLoad() {
    super.viewDidLoad()


    // Get a reference to the tab bar controller:
    tabManager = self.tabBarController as! VoteTabBarController


    // Create text fields for the minimum number of choices
    // that make a valid vote topic:
    for _ in 1 ... tabManager.minChoices {
        createNewChoice()
    }
}

Managing the Text Fields

Before we can build and run the app, we have to do some housekeeping. Remember that line in the createNewChoice method that we promised to explain later?

textField.delegate = self

In fact, even if you tried very hard to forget it, the compiler did not let you, as it generated a compiler error at this line. Time to put things in order.

UITextField uses the Delegation design pattern that we learned about in Chapter 5 . One of the reasons it needs a delegate is to dismiss the keyboard after the user finishes typing. We will make CreateViewController that delegate by first adding the UITextFieldDelegate protocol to its inheritance list:

class CreateViewController : UIViewController, UITextFieldDelegate {

And then we will implement UITextFieldDelegate’s method that decides whether a field should release the keyboard when the user taps the Return button (Listing 6-53).

Listing 6-53. Implementing textFieldShouldReturn to Allow a Text Field to Release the Keyboard
// MARK: UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField) -> Bool {
    // This will make the keyboard disappear
    // when the Return button is tapped in a text field:
    textField.resignFirstResponder()
    return true
}

Let us assign a delegate to the voteTopic text field. We will do this in the viewDidLoad method, which should now look like Listing 6-54.

Listing 6-54. Modifying viewDidload to Assign a Delegate to the voteTopic Text Field
override func viewDidLoad() {
    super.viewDidLoad()


    // Get a reference to the tab bar controller:
    tabManager = self.tabBarController as! VoteTabBarController


    // Create text fields for the minimum number of choices
    // that make a valid vote topic:
    for _ in 1 … tabManager.minChoices {
        createNewChoice()
    }


    // Set CreateViewController as the delegate of the text field,
    // so it can help it let go of the keyboard:
    voteTopic.delegate = self
}

You can now run the app and play with the dynamic user interface. Figure 6-39 shows you the Create screen with a new vote topic and options added at runtime.

A371202_1_En_6_Fig39_HTML.jpg
Figure 6-39. The Create screen with vote options added at runtime

Updating the Data Model

To finish off the app we will get CreateViewController to update the data model when the user creates a new topic and choices for voting, so that we can see the changes reflected in VoteViewController.

First we will add a helper method to CreateViewController, called newVoteWasComposed, which will check if the user created a valid vote: for that we want to have text in all of the text fields on the screen (Listing 6-55).

Listing 6-55. Adding a Method to CreateVewController to Check If a Valid Vote Was Created
// MARK: Helper functions
func newVoteWasComposed() -> Bool
{
    // Check if we have a valid topic and choices for voting.
    // If any text field has been left empty,
    // we assume the composing of the new topic was not finished.


    if voteTopic.text!.isEmpty {
        return false
    }


    for textField in choiceTextFields {
        if textField.text!.isEmpty {
            return false
        }
    }


    return true
}

Then let us add another helper function, which updates the data model (Listing 6-56).

Listing 6-56. Updating the Data Model from CreateViewController
func updateDataModel() {
    // Update the data in tabManager:
    tabManager.voteData.topic = voteTopic.text!


    tabManager.voteData.choices.removeAll(keepCapacity: true)

    for textField in choiceTextFields {
        tabManager.voteData.choices.append(Choice(title: textField.text!))
    }
}

We will call the two helper functions when the user exits the Create view—for that purpose we will override UIViewController’s viewWillDisappear method (Listing 6-57).

Listing 6-57. Updating the Data Model When the View in CreateViewController Is About to Disappear
override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)    


    if newVoteWasComposed() {
        updateDataModel()
    }
}

Let us add an override for viewWillAppear too. In it we will update the state of the two buttons for adding and removing options, in case the user changed preferences for the maximum number of choices (Listing 6-58).

Listing 6-58. Updating the Buttons on the UI When the View in CreateViewController Appears on Screen
override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)


    // Update the add and remove buttons,
    // in case the user changed preferences
    // on the Settings screen:
    updateButtonState()
}

Run the app and test it. You will probably notice that there is something missing: you can create a new vote topic and options, but when you switch to the vote screen, the old vote is still there. We will add the missing piece of the puzzle in the next section.

Sending Notifications

As an ActionScript developer you are intimately familiar with sending and receiving events. In this section we will see how we can send events in iOS in order to notify parts of our code about changes in other parts of the code.

To send a notification on iOS we will use NSNotificationCenter—a class from the SDK, which uses the Observer design pattern.

Note

In the Observer design pattern there are two sets of players: an object, which can send notifications, and observers that can subscribe to receive these notifications. The notifying object maintains a list of its observers and uses an event system to let them know when an event occurs that they are interested in.

NSNotificationCenter lets us send notifications, which are identifiable by their name. So let us define a name for the notification we will use to let the vote view controller know that there have been changes in the data model. We will do this in VoteTabBarController to keep the notification name together with the rest of the data-related code (Listing 6-59).

Listing 6-59. Defining a Notification Name in VoteTabBarController
class VoteTabBarController: UITabBarController {
    // MARK: Notifications
    let dataChangedNotificationName = "Vote data changed"


    // The rest of the class definition
}

Next, we will add a subscription for this notification in VoteViewController’s viewDidLoad method. Open VoteViewController.swift and modify viewDidLoad as shown in Listing 6-60.

Listing 6-60. Subscribing for Notifications in VoteViewConroller
override func viewDidLoad() {
    // Call the base class method first
    // to make sure we won't miss out on any preparation done by it:
    super.viewDidLoad()


    // Get a reference to the tab bar controller
    // and cast it to the class we created for it:
    tabManager = self.tabBarController as! VoteTabBarController


    // This will cause the methods required
    // by the UITableViewDataSource to be called:
    choicesTable.dataSource = self


    // Subscribe for notifications about changes in the data model:
    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.addObserver(self,
        selector: #selector(self.dataChanged),
        name: tabManager.dataChangedNotificationName,
        object: nil)
}

If you look at the call to notificationCenter.addObserver, you will notice strange syntax in the selector parameter:

#selector(self.dataChanged)

Let us first explain #selector.

Swift reference

Selectors are a way of identifying methods to be called and are an Objective-C legacy. Originally they could be used with string literals to select methods by their names at runtime.

Then we need to implement the method we pass to the selector parameter in addObserver. This method will be the notification handler for when the data model has changed–add it under MARK: Helper methods (Listing 6-61). In it we do two things: let the table view know that it needs to reload the data in its cells and refresh the text of topicLabel to show the new voting topic.

Listing 6-61. Adding a Notification Handler in VoteViewController
func dataChanged() {
    // This will get the table view
    // to request new data for its cells:
    choicesTable.reloadData()


    // Access the vote data in tabManager
    // to initialize the topic label:
    topicLabel.text = tabManager.voteData.topic
}

The last thing we need to do in order to enable this notification system is to send an actual notification. Let us do this after we update the data model in CreateViewController’s viewWillDisappear method (Listing 6-62).

Listing 6-62. Sending a Notification from CreateViewController
override func viewWillDisappear(animated: Bool) {
    if newVoteWasComposed() {
        updateDataModel()


        // Notify observers that data has changed:
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.postNotificationName(
            tabManager.dataChangedNotificationName, object: nil)
    }
}

Let us run the app and create a new topic for voting (Figure 6-40).

A371202_1_En_6_Fig40_HTML.jpg
Figure 6-40. The finished app

And with this we wrap up our voting app . There are more thing we could do to make it foolproof, which we will leave to you, curious reader. . . as if you need more excuses to go deeper into Swift. For example, you could modify the Create screen to reflect changes in the user preferences: if the setting for the maximum number of choices is set lower than the options that were last created, you could remove options, start with a clean slate, or show a warning.

Summary

For the sake of getting some exercise and fresh air, we hope you did not go through this chapter in one sitting. That was a lot of work to do. But hey, wasn’t it worth it!

With this last app in your arsenal you now know how to choose a navigation pattern for your applications, how to pass data between views, how to handle user settings, and how to have the flexibility of creating and managing UIs programmatically.

In the next chapter we will turn our attention to the nonvisual side of app development and see what concurrency is and how it can help you. And also when to steer clear of it.

Footnotes

1 It is worth making a distinction between receiving and handling user interaction events. The table view receives but does not handle gesture events. Instead, it uses delegation to hand over this task (we covered the Delegation design pattern in Chapter 5). A delegate needs to conform to the UITableViewDelegate protocol.

2 You can do better than calling assert here and extend the Array type to use indices in a safe way. If you are up for a little challenge, do a search on the Internet for “Swift safe array indexing” to see some cool implementations.

..................Content has been hidden....................

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