In the previous chapter, you built your first multiview application. In this chapter, you’ll build another one—this time, creating a full tab bar application with five different tabs and five different content views. Building this application reinforces a lot of what I covered in Chapter 6. You’ll use those five content views to demonstrate a type of iOS control not yet covered, a picker view, or just a picker. You may not be familiar with the name, but you’ve almost certainly used a picker if you’ve owned an iPhone or iPod touch for more than ten minutes. Pickers contain controls with dials that spin. You use them to input dates in the Calendar application or to set a timer in the Clock application, as shown in Figure 7-1. It is not quite as common on an iPad, since the larger display lets you present other ways of choosing among multiple items, but even there, it’s used by the Calendar application.
Figure 7-1. The Clock app uses a picker to set the time the alarm should go off
Pickers provide a bit more complexity than the iOS controls you’ve seen so far; as such, they deserve a little more attention. Pickers can be configured to display one dial or many. By default, pickers display lists of text, but they can also be made to display images.
The Pickers Application
This chapter’s application , Pickers, features a tab bar, and when you build Pickers, you’ll change the default tab bar so that it has five tabs, add an icon to each of the tab bar items, and then create a series of content views and connect each view to a tab. The application’s content views feature five different pickers:
Date picker: The first content view you’ll build uses a date picker , the easiest type of picker to implement (see Figure 7-2). The view also has a button that, when tapped, displays an alert showing the date that was picked.
Figure 7-2. The first tab displays a date picker
Single-component picker: The second tab features a picker with a single list of values, as shown in Figure 7-3, and provides a little more work to implement than the date picker. You’ll learn how to specify the values to be displayed in the picker by using a delegate and a data source .
Figure 7-3. A picker displaying a single list of values
Multicomponent picker: In the third tab, you’ll create a picker with two separate wheels. The technical term is picker component. Here, you create a picker with two components. You’ll see how to use the data source and delegate by providing two independent lists of data to the picker (see Figure 7-4), each changeable without impacting the other one.
Figure 7-4. A two-component picker, showing an alert that reflects your selection
Picker with dependent components: In the fourth content view, you’ll build another picker with two components . But this time, the values displayed in the component on the right change based on the value selected in the component on the left. In this example, you’re going to display a list of states in the left component and a list of that state’s ZIP codes in the right component, as shown in Figure 7-5.
Figure 7-5. In this picker, one component depends on the other. As you select a state in the left component, the right component changes to a list of ZIP codes in that state.
Custom picker with images: In the fifth content view , I’ll demonstrate how to add image data to a picker, and you’re going to do it by writing a little game that uses a picker with five components. Apple’s documentation describes the picker’s appearance as looking a bit like a slot machine. So, you’ll be creating a slot machine, as shown in Figure 7-6. For this picker, the user won’t be able to manually change the values of the components but will be able to select the Spin button to make the five wheels rotate to a new, randomly selected value. If three copies of the same image appear in a row, the user wins.
Figure 7-6. Your fifth component picker uses the picker like a slot machine
Delegates and Data Sources
Before you start building the application , let’s look at what makes pickers more complex than the other controls you’ve used so far. With the exception of the date picker, you can’t use a picker by just grabbing one in the Object Library, dropping it on your content view, and configuring it. You also need to provide each picker with both a delegate and a data source.
You’ve already used application delegates, and the basic idea works the same for pickers. The control itself defers several jobs to its delegate, the most important of these being the determination of what to actually draw for each of the rows in each of its components. The picker asks the delegate for either a string or a view that will be drawn at a given spot on a given component. The picker gets its data from the delegate.
In addition to the delegate, pickers must have a data source. The data source tells the picker how many components it will be working with and how many rows make up each component. The data source works like the delegate in that its methods are called at certain, prespecified times. Without a data source and a delegate, pickers cannot do their job; in fact, they won’t even be drawn.
It’s common for the data source and the delegate to be the same object and exist in the same actual Swift file, typically the view controller for the picker’s enclosing view. You will use that approach in this application: the view controllers for each of your application’s content panes will be the data source and the delegate for their picker.
Note
The question often arises as to whether the picker data source is part of the model, view, or controller portion of the application. A data source sounds like it must be part of the model, but it’s actually part of the controller. The data source isn’t usually an object designed to hold data. In simple applications, the data source might hold data, but its true job is to retrieve data from the model and pass it along to the picker.
Creating the Pickers Application
Although Xcode provides a template for tab bar applications, you’re going to build yours from scratch. It’s not much extra work, and it will be good practice.
Create a new project, select the Single View App template again, and choose Next to go to the next screen. In the Product Name field, type Pickers. Make sure the Use Core Data check box is deselected, and set Language to Swift and the Devices pop-up to Universal. Then choose Next again. Xcode will let you select the folder where you want to save your project.
I’m going to walk you through the process of building the whole application, but at any step of the way, if you feel like challenging yourself by moving ahead, by all means do so. If you get stumped, you can always come back.
Creating the View Controllers
In the previous chapter, you created a root view controller (root controller for short) to manage the process of swapping your application’s other views. You’ll be doing that again this time, but you won’t need to create your own root view controller class. Apple provides a good class for managing tab bar views, so you’re just going to use an instance of UITabBarController as your root controller. First, you need to create five new classes in Xcode: the five view controllers that the root controller will swap in and out. Expand the Pickers folder in the Project Navigator. There, you’ll see the source code files that Xcode created to start off the project. Single-click the Pickers folder, and press ⌘N.
Select iOS and then select Source in the left pane of the new file assistant. Then, select the Cocoa Touch Class icon and click Next to continue. The next screen lets you give your new class a name. Enter DatePickerViewController in the Class field. Ensure the Subclass of field contains UIViewController. Make sure that the “Also create XIB file” check box is deselected, set Language to Swift, and then click Next.
You’ll be shown a folder selection window, which lets you choose where the class should be saved. Choose the Pickers directory, which already contains the AppDelegate class and a few other files. Make sure also that the Group pop-up has the Pickers folder selected and that the target check box for Pickers is selected. After you click the Create button, the file DatePickerViewcontroller.swift will appear in the Pickers folder.
Repeat those steps four more times, using the names SingleComponentPickerViewController, DoubleComponentPickerViewController, DependentComponentPickerViewController, and CustomPickerViewController. At the end of all this, the Pickers folder should contain all the view controller class files, as shown in Figure 7-7.
Figure 7-7. The Project Navigator should contain all these files after creating the five view controller classes
Creating the Tab Bar Controller
Now, let’s create your tab bar controller . The project template already contains a view controller called ViewController, which is a subclass of UIViewController. To convert it to a tab bar controller, all you need to do is change its base class. Open ViewController.swift and make the following change shown in bold:
class ViewController: UITabBarController {
Next, to set up the tab bar controller in the storyboard, open Main.storyboard. The template added an initial view controller, which you’re going to replace, so select it in the Document Outline or the Editor Area, and delete it by pressing the Delete key. In the Object Library, locate a tab bar controller and drag it to the editing area (see Figure 7-8).
Figure 7-8. Dragging a tab bar controller from the library onto the canvas
While you’re dragging, you’ll see that, unlike the other controllers I’ve been asking you to drag out from the Object Library, this one actually pulls out three complete view controller pairs at once, all of which are connected to each other with curved lines. This is actually more than just a tab bar controller; it’s also two child controllers, already connected and ready to use.
Once you drop the tab bar controller onto the editing area, three new scenes are added to the storyboard. If you expand the document view on the left, you will see a nice overview of all the scenes contained in the storyboard (see Figure 7-8). You’ll also see the curvy lines still in place connecting the tab bar controller with each of its children. Those lines will always adjust themselves to stay connected if you move the scenes around, which you are always free to do. The on-screen position of each scene within a storyboard has no impact on your app’s appearance when it runs.
This tab bar controller will be your root controller. As a reminder, the root controller controls the very first view that the user will see when your program runs, and it is responsible for switching the other views in and out. Since you’ll connect each of your views to one of the tabs in the tab bar, the tab bar controller makes a logical choice as a root controller. You need to tell iOS that the tab bar controller is the one that it should load from Main.storyboard when the application starts. To do this, select the Tab Bar Controller icon in the Document Outline and open the Attributes Inspector; then in the View Controller section, select the Is Initial View Controller check box. With the view controller still selected, switch to the Identity Inspector and change the class to ViewController.
Tab bars can use icons to represent each of the tabs, so you should also add the icons you’re going to use before editing the storyboard. You can find some suitable icons in the ImageSets folder of the source code archive for this book. Each subfolder of ImageSets contains three images (one for devices with a standard display, two for Retina devices). In the Xcode Project Navigator, select Assets.xcassets and drag each subfolder from the ImageSets folder and drop it into the left column of the editing area, underneath AppIcon, to copy them all into the project (see Figure 7-9).
Figure 7-9. Drag the images below AppIcon within Assets.xcassets in Xcode
If you want to make your own icons instead, there are some guidelines for how they should be created. The icons you use should be 24 × 24 pixels and saved in PNG format. The icon file should have a transparent background. Don’t worry about trying to color the icons so that they match the appearance of the tab bar. Just as it does with the application icon, iOS will take your image and make it look just right.
Tip
An image size of 24 × 24 pixels is actually for standard displays; for Retina displays on iPhone 4 and later and for the new iPads, you need a double-sized image, or it will appear pixelated. For the iPhone 7/6s Plus, you need to provide an image that’s three times the size of the original. This is easy: for any image foo.png, you should also provide an image named [email protected] that is doubled in size and another called [email protected] that is three times the size. Calling UIImage(named:"foo") will return the normal-sized image or the double-sized image automatically to best suit the device on which your app is currently executing.
Back in the storyboard, you can see that each of the child view controllers shows a name like “Item 1” at the top and has a single bar item at the bottom of its view, with a label matching what is present in the tab bar. You might as well set these two up so that they have the right names from the start, so select the Item 1 view controller and then click the tab bar item labeled Item 1 in the Document Outline. Open the Attributes Inspector and you’ll see a text field for setting the title of the bar item, which currently contains the text Item 1. Replace the text with Date and press the Enter key. This immediately changes the text of the bar item at the bottom of this view controller, as well as the corresponding tab bar item in the tab bar controller. While you’re still in the inspector, click the Image pop-up and select “clockicon” to set the icon, too. Now repeat the same steps for the second child view controller, but name this one Single and use the “singleicon” image for its bar item.
Your next step is to complete your tab bar controller so it reflects the five tabs shown in Figure 7-2. Each of those five tabs will contain one of your pickers. The way you’re going to do this is by simply adding three more view controllers to the storyboard (in addition to the two that were added along with the tab bar controller) and then connecting each of them so that the tab bar controller can activate them. Get started by dragging out a normal view controller from the Object Library and dropping on the storyboard. Next, Control-drag from the tab bar controller to your new view controller, release the mouse button, and select view controllers from the Relationship Segue section of the small pop-up window that appears. This tells the tab bar controller that it has a new child to maintain, so the tab bar immediately acquires a new item. Your new view controller gets a bar item in the bottom of its view and in the Document Outline, just like the others already had. Now do the same steps outlined previously to name this latest view controller’s bar item Double with “doubleicon” as its image.
Drag out two more view controllers and connect each of them to the tab bar controller, as described previously. One at a time, select each of their bar items, naming one of them Dependent with “dependenticon” as its image and naming the other Custom with “toolicon” as its image. When finished, you should have one view controller with your tab bar at the bottom and five connected view controllers, as shown in Figure 7-10.
Figure 7-10. Adding your five view controllers that you’ll access using the tab bar from your root view controller
Now that all your view controllers are in place, it’s time to set up each of them with the correct controller class from the set you created earlier. This will let you implement different functionality for each controller. In the Document Outline, select the Date view controller and bring up the Identity Inspector. In the Custom Class section of the inspector, change the class to DatePickerViewController, and press Return or Tab to finish (see Figure 7-11).
Figure 7-11. Connecting your Date view to its view controller
Repeat this same process for the other four view controllers, in the order in which they appear at the bottom of the tab bar controller. You can select each view controller in turn by clicking it in the storyboard, making sure to click in the bar at the top of the controller that contains the controller’s name. In the Identity Inspector for each, use the class names SingleComponentPickerViewController, DoubleComponentPickerViewController, DependentComponentPickerViewController, and CustomPickerViewController, respectively. Before moving on to the next bit of GUI editing, save your storyboard file .
Initial Simulator Test
At this point, the tab bar and the content views should all be hooked up and working. Compile and run, and your application should launch with a tab bar that functions, as shown in Figure 7-12. Click each of the tabs in turn. Each tab should be selectable.
Figure 7-12. The application with five empty but selectable tabs
There’s nothing in the content views now, so the changes won’t be very dramatic. In fact, you won’t see any difference at all, except for the highlighting tab bar items. But if everything went correctly, the basic framework for your multiview application is now set up and working. You can start designing the individual content views.
Tip
If your simulator crashes when you click one of the tabs, most likely you’ve either missed a step or made a typo. Go back and make sure the connections are right and the class names are all set correctly.
If you want to make doubly sure everything is working, you can add a different label or some other object to each of the content views and then relaunch the application. In that case, you should see the content of the different views change as you select different tabs.
Implementing the Date Picker
To implement the date picker, you’ll need a single outlet and a single action. The outlet will be used to grab the value from the date picker . The action will be triggered by a button and will put up an alert to show the date value pulled from the picker. You’ll add both of these from inside Interface Builder while editing the Main.storyboard file, so select it in the Project Navigator if it’s not already front and center.
The first thing you need to do is find a date picker in the Object Library and drag it to the date scene in the editing area. Click the Date icon in the Document Outline to bring the correct view controller to the front and then drag the date picker from the Object Library and place it at the top of the view, right up against the top of the display. It’s okay if it overlaps the status bar because this control has so much built-in vertical padding at the top that no one will notice.
Now you need to apply Auto Layout constraints so that the date picker is correctly sized and placed when the application runs on any kind of device. You want the picker to be horizontally centered and anchored to the top of the view. You also want it to be sized based on its content, so you need three constraints. With the date picker selected, first select Editor ➤ Size to Fit Content from the Xcode menu bar. If this option is not enabled, move the picker slightly and try again. Next, click the Align button below the storyboard , select the Horizontally in Container box, and then click Add 1 Constraint. Click the Pin button (which is next to the Align button). Using the four distance boxes at the top of the pop-up, set the distance between the picker and the top of edge of the view above it to zero by entering 0 in the top box, and below it so that it becomes a solid line. At the bottom of the pop-up, set Update Frames to Items of New Constraints and then click Add 1 Constraint. The date picker will resize and move to its correct position, as shown in Figure 7-13.
Figure 7-13. The date picker is positioned at the top of its view controller’s view
Single-click the date picker, if it’s not already selected, and go to the Attributes Inspector. As you can see in Figure 7-14, a number of attributes can be configured for a date picker. You’re going to leave most of the values at their defaults (but feel free to play with the options when you’re finished, to see what they do). The one thing you will do is limit the range of the picker to reasonable dates. Look for the Constraints heading and select the Minimum Date check box. Leave the value at the default of 1/1/1970. Also select the Maximum Date box and set that value to 12/31/2200.
Figure 7-14. The Attributes Inspector for a date picker. Set the maximum date, but leave the rest of the settings at their default values.
Now let’s connect this picker to its controller. Press ⌥⌘Enter to open the Assistant Editor and make sure the jump bar at the top of the Assistant Editor is set to Automatic. That should make DatePickerViewController.swift show up there. Next, Control-drag from the picker to the blank line between the class declaration and the viewDidLoad() method , releasing the mouse button when the Insert Outlet, Action, or Outlet Collection tooltip appears. In the pop-up window that appears after you let go, make sure Connection is set to Outlet, enter datePicker as the name, and then press Enter to create the outlet and connect it to the picker.
Next, grab a button from the library and place it a small distance below the date picker. Double-click the button and give it a title of Select. You want this button to be horizontally centered and to stay a fixed distance below the date picker. With the button selected, click the Align button at the bottom of the storyboard, select the Horizontally in Container box, and then click Add 1 Constraint. To fix the distance between them, Control-drag from the button to the date picker and release the mouse. In the pop-up that appears, select Vertical Spacing. Finally, click the Resolve Auto Layout Issues button at the bottom of the storyboard and then click Update Frames in the top section of the pop-up (if this item is not enabled, it means that the button is already in its correct location). The button should move to its correct location, and there should no longer be any Auto Layout warnings.
Make sure that DatePickerViewController.swift is still visible in the Assistant Editor; if it’s not, use the Manual selection in the jump bar to locate and open it. Now Control-drag from the button to the line above the closing brace at the end of the class in the Assistant Editor, until you see the Insert Outlet, Action, or Outlet Collection tooltip appear. Change the Connection type to Action, name the new action onButtonPressed, and press Enter to connect it. Doing so creates an empty method called onButtonPressed () , which you should complete with the code in Listing 7-1.
Listing 7-1. Your Select Button Action Code
@IBAction func onButtonPressed(_ sender: UIButton) {
let date = datePicker.date
let message = "The date and time you selected is (date)"
let alert = UIAlertController(
title: "Date and Time Selected",
message: message,
preferredStyle: .alert)
let action = UIAlertAction(
title: "That's so true!",
style: .default,
handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
In viewDidLoad(), you create a new NSDate object. An NSDate object created this way will hold the current date and time. You then set datePicker to that date, which ensures that every time this view is loaded from the storyboard, the picker will reset to the current date and time; see Listing 7-2.
Listing 7-2. Setting Your Date in the viewDidLoad() Method
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let date = NSDate()
datePicker.setDate(date as Date, animated: false)
}
And that is all there is. Go ahead and build and run to make sure your date picker checks out. If everything went okay, your application should look like Figure 7-2 when it executes. If you choose the Select button, an alert will pop up, telling you the date and time currently selected in the date picker .
Note
The date picker does not allow you to specify seconds or a time zone. The alert displays the time with seconds and in Greenwich mean time (GMT). You could have added some code to simplify the string displayed in the alert, but isn’t this chapter long enough already? If you’re interested in customizing the formatting of the date, take a look at the NSDateFormatter class.
Implementing the Single-Component Picker
Your next picker lets the user select from a list of values . In this example, you’ll use an array to hold the values you want to display in the picker. Pickers don’t hold any data themselves. Instead, they call methods on their data source and delegate to get the data they need to display. The picker doesn’t really care where the underlying data lives. It asks for the data when it needs it, and the data source and delegate (which are often, in practice, the same object) work together to supply that data. As a result, the data could be coming from a static list, as you’ll do in this section, or it could be loaded from a file or a URL, or even made up or calculated on the fly.
For the picker class to ask its controller for data, you must ensure that the controller implements the right methods. One part of doing that is declaring in the controller’s class definition that it will implement a couple of protocols. In the Project Navigator, single-click SingleComponentPickerViewController.swift. This controller class will act as both the data source and the delegate for its picker, so you need to make sure it conforms to the protocols for those two roles. Add the following code:
class SingleComponentPickerViewController: UIViewController,
UIPickerViewDelegate, UIPickerViewDataSource {
When you do this, you’ll see an error appear in the editor. That’s because you haven’t yet implemented the required protocol methods. You’ll do that soon, so just ignore the error for now.
Building the View
Select Main. storyboard again since it’s time to edit the content view for the second tab in your tab bar. In the Document Outline, click the Single icon to bring the view controller into the foreground in the Editor Area. Next, bring over a picker view from the library (see Figure 7-15) and add it to your view, placing it snugly into the top of the view, as you did with the date picker view.
Figure 7-15. Adding a picker view from the library to your second view
Now you need to apply Auto Layout constraints so that the picker is correctly sized and placed when the application runs on any kind of device. You want the picker to be horizontally centered and anchored to the top of the view. You also want it to be sized based on its content, so you need three constraints. With the picker selected, first select Editor ➤ Size to Fit Content from the Xcode menu bar. If this option is not enabled, move the picker slightly and try again. Next, click the Align button below the storyboard, select the Horizontally in Container box, and then click Add 1 Constraint. Click the Pin button (which is next to the Align button). Using the four distance boxes at the top of the pop-up, set the distance between the picker and the top of edge of the view above it to zero by entering 0 in the top box, and then click the dashed red line below it so that it becomes a solid line. At the bottom of the pop-up, set Update Frames to Items of New Constraints and then click Add 1 Constraint. The picker will resize and move to its correct position, as shown in Figure 7-16.
Figure 7-16. The picker, positioned at the top of its view controller’s view
Now let’s connect this picker to its controller. The procedure here is just like for the previous picker view: open the Assistant Editor, set the jump bar to show the SingleComponentPickerViewController.swift file, Control-drag from the picker to the top of the SingleComponentPickerViewController class, and create an outlet named singlePicker.
Next, with the picker selected, press ⌥⌘6 to bring up the Connections Inspector. If you look at the connections available for the picker view, you’ll see that the first two items are dataSource and delegate. If you don’t see those outlets, make sure you have the picker selected, rather than the UIView. Drag from the circle next to dataSource to the View Controller icon at the top of the scene in the storyboard or in the Document Outline (see Figure 7-17) and then drag from the circle next to delegate to the View Controller icon. Now this picker knows that the instance of the SingleComponentPickerViewController class in the storyboard is its data source and delegate, and the picker will ask it to supply the data to be displayed. In other words, when the picker needs information about the data it is going to display, it asks the SingleComponentPickerViewController instance that controls this view for that information.
Figure 7-17. Connecting the dataSource to the view controller
Drag a button to the view and place it just below the picker. Double-click the button and name it Select. Press Return to commit the change. In the Connections Inspector, drag from the circle next to Touch Up Inside to code in the Assistant Editor, releasing it just above the closing bracket at the end of the class definition to make a new action method. Name this action onButtonPressed and you’ll see that Xcode fills in an empty method. You’ve just seen another way to add an action method to a view controller and link it to its source view. You want this button to be horizontally centered and to stay a fixed distance below the date picker. With the button selected, click the Align button at the bottom of the storyboard , select the Horizontally in Container box, and then click Add 1 Constraint (see Figure 7-18).
Figure 7-18. Center the button horizontally in the view
To fix the distance between them, Control-drag from the button to the picker and then release the mouse. In the pop-up that appears, select Vertical Spacing (see Figure 7-19). Finally, if you have any layout issues as shown by the Issue Inspector, click the Resolve Auto Layout Issues button at the bottom of the storyboard and then click Update Frames in the top section of the pop-up (if this item is not enabled, it means that the button is already in its correct location). The button should move to its correct location, and there should no longer be any Auto Layout warnings .
Figure 7-19. Set a consistent vertical spacing from the button to the picker
Implementing the Data Source and Delegate
To make your controller work properly as the picker’s data source and delegate, you’ll start with some code you should feel comfortable with and then add a few methods that you’ve never seen before.
Single-click SingleComponentPickerViewController.swift in the Project Navigator and add the following property at the top of the class definition. This gives you an array with the names of several well-known movie characters.
@IBOutlet weak var singlePicker: UIPickerView!
private let characterNames = [
"Luke", "Leia", "Han", "Chewbacca", "Artoo",
"Threepio", "Lando"]
Then change the onButtonPressed() method to what’s shown in Listing 7-3.
Listing 7-3. The onButtonPressed Method for Your Single Picker View
@IBAction func onButtonPressed(_ sender: UIButton) {
let row = singlePicker.selectedRow(inComponent: 0)
let selected = characterNames[row]
let title = "You selected (selected)!"
let alert = UIAlertController(
title: title,
message: "Thank you for choosing",
preferredStyle: .alert)
let action = UIAlertAction(
title: "You're welcome",
style: .default,
handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
As you saw earlier, a date picker contains the data you need to get to, but here, your regular picker hands off that job to the delegate and data source. The onButtonPressed() method needs to ask the picker which row is selected and then grabs the corresponding data from your pickerData array. Here is how you ask it for the selected row:
let row = singlePicker.selectedRow(inComponent: 0)
Notice that you needed to specify which component you want to know about. You have only one component (i.e., one spinning wheel) in this picker, so you simply pass in 0, which is the index of the first (and only) component.
In the class declaration, you created an array of character names so that you have data to feed the picker. Usually, your data will come from other sources, like a property list or a web service query. By embedding an array of items in your code the way you’ve done here, you are making it much harder on yourself if you need to update this list or if you want to have your application translated into other languages. But this approach is the quickest and easiest way to get data into an array for demonstration purposes. Even though you won’t usually create your arrays like this, you will almost always configure some form of access to your application’s model objects here in the viewDidLoad() method so that you’re not constantly going to disk or to the network every time the picker asks you for data.
Tip
If you’re not supposed to create arrays from lists of objects in your code, as you just did, how should you do it? Embed the lists in property list files and add those files to your project. Property list files can be changed without recompiling your source code, which means there is little risk of introducing new bugs when you do so.
Finally, insert the code shown in Listing 7-4 at the bottom of the file.
Listing 7-4. Picker Data Source and Delegate Methods
// MARK:-
// MARK: Picker Data Source Methods
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView,
numberOfRowsInComponent component: Int) -> Int {
return characterNames.count
}
// MARK: Picker Delegate Methods
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return characterNames[row]
}
These three methods are required to implement the picker. The first two methods are from the UIPickerViewDataSource protocol , and they are both required for all pickers (except date pickers). Here’s the first one:
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
Pickers can have more than one spinning wheel, or component, and this is how the picker asks how many components it should display. You want to display only one list this time, so you return a value of 1. Notice that a UIPickerView is passed in as a parameter. This parameter points to the picker view that is asking you the question, which makes it possible to have multiple pickers being controlled by the same data source. In this case, you know that you have only one picker, so you can safely ignore this argument because you already know which picker is calling you.
The second data source method is used by the picker to ask how many rows of data there are for a given component.
func pickerView(_ pickerView: UIPickerView,
numberOfRowsInComponent component: Int) -> Int {
return characterNames.count
}
Once again, you are told which picker view is asking and which component that picker is asking about. Since you know that you have only one picker and one component, you don’t bother with either of the arguments and simply return the count of objects from your sole data array .
After the two data source methods, you implement one delegate method. Unlike the data source methods, all of the delegate methods are optional. The term optional is a bit deceiving because you do need to implement at least one delegate method. You will usually implement the method that you are implementing here. However, if you want to display something other than text in the picker , you must implement a different method instead, as you’ll see when you get to the custom picker later in this chapter.
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return characterNames[row]
}
In this method, the picker asks you to provide the data for a specific row in a specific component. You are provided with a pointer to the picker that is asking, along with the component and row that it is asking about. Since your view has one picker with one component, you simply ignore everything except the row argument and use that to return the appropriate item from your data array.
Build and run the application. When the simulator comes up, switch to the second tab—the one labeled Single—and check out your new custom picker, which should look like Figure 7-3.
In the next section you’ll implement a picker with two components. If you feel up to a challenge, this next content view is actually a good one for you to attempt on your own. You’ve already seen all the methods you’ll need for this picker, so go ahead if you’d like to give it a try. You might want to start with a good look at Figure 7-4, just to refresh your memory. When you’re finished, read on, and you’ll see how I tackled this problem.
Implementing a Multicomponent Picker
The next tab will have a picker with two components , or wheels, each independent of the other. The left wheel will have a list of sandwich fillings, and the right wheel will have a selection of bread types. You’ll write the same data source and delegate methods that you did for the single-component picker. You’ll just need to write a little additional code in some of those methods to make sure you’re returning the correct value and row count for each component. Start by single-clicking DoubleComponentPickerViewController.swift and adding the following code:
class DoubleComponentPickerViewController: UIViewController,
UIPickerViewDelegate, UIPickerViewDataSource {
Here, you simply conform your controller class to both the delegate and data source. Save this and click Main.storyboard to work on the GUI.
Building the View
Select Double Scene in the Document Outline and click the Double icon to bring its view controller to the front in the Editor Area. Now add a picker view and a button to the view, change the button label to Select, and then make the necessary connections. I’m not going to walk you through it this time, but you can refer to the previous section if you need a step-by-step guide, since the two view controllers are identical in terms of connections in the storyboard. Here’s a summary of what you need to do:
Create an outlet called doublePicker in the class extension of the DoubleComponentPickerViewController class to connect the view controller to the picker .
Connect the dataSource and delegate connections on the picker view to the view controller (use the Connections Inspector).
Connect the Touch Up Inside event of the button to a new action method called onButtonPressed on the view controller (use the Connections Inspector).
Add Auto Layout constraints to the picker and the button to pin them in place.
Make sure that you save your storyboard before you dive back into the code. You may want to bookmark this page because you may be referring to it in a bit.
Implementing the Controller
Select DoubleComponentPickerViewController.swift and add the code in Listing 7-5 at the top of the class definition, below your picker outlet .
Listing 7-5. Parameters Needed for Your Two-Component Picker
@IBOutlet weak var doublePicker: UIPickerView!
private let fillingComponent = 0
private let breadComponent = 1
private let fillingTypes = [
"Ham", "Turkey", "Peanut Butter", "Tuna Salad",
"Chicken Salad", "Roast Beef", "Vegemite"]
private let breadTypes = [
"White", "Whole Wheat", "Rye", "Sourdough",
"Seven Grain"]
As you can see, you start by defining two constants that will represent the indices of the two components, which is just to make your code easier to read. Picker components are referred to by number, with the leftmost component being assigned zero and increasing by one with each move to the right. Next, you declare two arrays that hold the data for your two picker components .
Now implement the onButtonPressed() method, as shown in Listing 7-6.
Listing 7-6. What to Do When the Select Button Is Pressed
@IBAction func onButtonPressed(_ sender: UIButton) {
let fillingRow =
doublePicker.selectedRow(inComponent: fillingComponent)
let breadRow =
doublePicker.selectedRow(inComponent: breadComponent)
let filling = fillingTypes[fillingRow]
let bread = breadTypes[breadRow]
let message = "Your (filling) on (bread) bread will be right up."
let alert = UIAlertController(
title: "Thank you for your order",
message: message,
preferredStyle: .alert)
let action = UIAlertAction(
title: "Great",
style: .default,
handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
Also, add the data source and delegate methods at the bottom of the class, as shown in Listing 7-7.
Listing 7-7. The dataSource and delegate Methods
// MARK:-
// MARK: Picker Data Source Methods
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == breadComponent {
return breadTypes.count
} else {
return fillingTypes.count
}
}
// MARK:-
// MARK: Picker Delegate Methods
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == breadComponent {
return breadTypes[row]
} else {
return fillingTypes[row]
}
}
The onButtonPressed() method is a bit more involved this time, but there’s very little there that’s new to you. You just need to specify which component you are talking about when you request the selected row using those constants you defined earlier—breadComponent and fillingComponent.
let fillingRow =doublePicker.selectedRow(inComponent: fillingComponent)
let breadRow = doublePicker.selectedRow(inComponent: breadComponent)
You can see here that using the two constants instead of 0 and 1 makes your code considerably more readable. From this point on, the onButtonPressed() method is fundamentally the same as the last one you wrote.
When you get down to the data source methods, that’s where things start to change a bit. In the first method, you specify that your picker should have two components rather than just one.
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
This time, when you are asked for the number of rows, you need to check which component the picker is asking about and return the correct row count for the corresponding array.
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == breadComponent {
return breadTypes.count
} else {
return fillingTypes.count
}
}
Next, in your delegate method, you do the same thing. You check the component and use the correct array for the requested component to fetch and return the correct value.
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == breadComponent {
return breadTypes[row]
} else {
return fillingTypes[row]
}
}
That wasn’t so hard, was it? Compile and run your application, and make sure the Double content pane looks like Figure 7-4.
Notice that the wheels are completely independent of each other. Turning one has no effect on the other. That’s appropriate in this case, but there will be times when one component is dependent on another. A good example of this is in the date picker. When you change the month, the dial that shows the number of days in the month may need to change because not all months have the same number of days. Implementing this isn’t really hard once you know how, but it’s not the easiest thing to figure out on your own, so let’s do that next.
Implementing Dependent Components
As you’re picking up momentum, I’m not going to hold your hand quite as much when it comes to material I’ve already covered. Instead, I’ll focus on addressing new features . Your new picker will display a list of U.S. states in the left component and a list of corresponding ZIP codes in the right component.
You’ll need a separate list of ZIP code values for each item in the left component. You’ll declare two arrays, one for each component, as you did last time. You’ll also need a dictionary. In the dictionary, you’re going to store an array for each state (see Figure 7-20). Later, you’ll implement a delegate method that will notify you when the picker’s selection changes. If the value in the left picker wheel changes, you will grab the correct array out of the dictionary and assign it to the array being used for the right-side picker wheel. Don’t worry if you didn’t catch all that; I’ll talk about it more as you get into the code.
Figure 7-20. Your application’s data. For each state, there will be one entry in a dictionary with the name of the state as the key. Stored under that key will be an Array<String> instance containing all the ZIP codes from that state.
Add the following code to your DependentComponentPickerViewController.swift file:
class DependentComponentPickerViewController: UIViewController,
UIPickerViewDelegate, UIPickerViewDataSource {
private let stateComponent = 0
private let zipComponent = 1
private var stateZips:[String : [String]]!
private var states:[String]!
private var zips:[String]!
Now it’s time to build the content view. That process is identical to the previous two component views you built. If you get lost, flip back to the “Building the View” section for the single-component picker and follow those step-by-step instructions. Here’s a hint: start by opening Main.storyboard, find the view controller for the DependentComponentPickerViewController class, and then repeat the same basic steps you’ve done for all the other content views in this chapter. You should end up with an outlet property called dependentPicker connected to a picker, an empty onButtonPressed: method connected to a button, and both the delegate and dataSource properties of the picker connected to the view controller. Don’t forget to add the Auto Layout constraints to both views! When you’re finished, save the storyboard.
Now you’ll implement this controller class. This implementation may seem a little complicated at first. To make one component dependent on the other, you need to add a whole new level of complexity to your controller class. Although the picker displays only two lists at a time, your controller class must know about and manage 51 lists. The technique you’re going to use here actually simplifies that process. The data source methods look almost identical to the one you implemented for the DoublePickerViewController. All of the additional complexity is handled elsewhere, between viewDidLoad and a new delegate method called pickerView(_:didSelectRow:inComponent:).
Before you write the code, you need some data to display. Up until now, you’ve created arrays in code by specifying a list of strings. Because you don’t want to type in several thousand values, you’re going to load the data from a property list. Both NSArray and NSDictionary objects can be created from property lists.
The data that you need is included in a property list called statedictionary.plist in the project archive, under the Picker Data folder. Drag that file into the Pickers folder in your Xcode project. If you single-click the .plist file in the Project Navigator, you can see and even edit the data that it contains (see Figure 7-21).
Figure 7-21. The statedictionary.plist file, showing your list of states. Within Ohio, you can see the start of a list of ZIP codes.
In DependentComponentPickerViewController.swift, I’m going to first show you some whole methods to implement, and then I’ll break it down into more digestible chunks. Start with the implementation of onButtonPressed() ,as shown in Listing 7-8.
Listing 7-8. The onButtonPressed Method for Your ZIP Code View
@IBAction func onButtonPressed(_ sender: UIButton) {
let stateRow =
dependentPicker.selectedRow(inComponent: stateComponent)
let zipRow =
dependentPicker.selectedRow(inComponent: zipComponent)
let state = states[stateRow]
let zip = zips[zipRow]
let title = "You selected zip code (zip)"
let message = "(zip) is in (state)"
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: .alert)
let action = UIAlertAction(
title: "OK",
style: .default,
handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
Next, add the code in Listing 7-9 to the existing viewDidLoad() method .
Listing 7-9. Add to the viewDidLoad() Method
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let bundle = Bundle.main
let plistURL = bundle.urlForResource("statedictionary",
withExtension: "plist")
stateZips = NSDictionary.init(contentsOf: (plistURL)!) as! [String : [String]]
let allStates = stateZips.keys
states = allStates.sorted()
let selectedState = states[0]
zips = stateZips[selectedState]
}
Finally, add the data source and delegate methods at the bottom of the file, as shown in Listing 7-10.
Listing 7-10. Your dataSource and delegate Methods for Displaying State ZIP Codes
// MARK:-
// MARK: Picker Data Source Methods
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == stateComponent {
return states.count
} else {
return zips.count
}
}
// MARK:-
// MARK: Picker Delegate Methods
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == stateComponent {
return states[row]
} else {
return zips[row]
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == stateComponent {
let selectedState = states[row]
zips = stateZips[selectedState]
dependentPicker.reloadComponent(zipComponent)
dependentPicker.selectRow(0, inComponent: zipComponent,
animated: true)
}
}
There’s no need to talk about the onButtonPressed() method since it’s fundamentally the same as the previous one. I should talk about the viewDidLoad() method, though. There’s some stuff going on there that you need to understand, so pull up a chair and let’s chat. The first thing you do in this new viewDidLoad() method is grab a reference to your application’s main bundle.
let bundle = Bundle.main
A bundle is just a special type of folder, the contents of which follow a specific structure. Applications and frameworks are both bundles , and this call returns a bundle object that represents your application.
Note
In the latest revision of Xcode and the iOS libraries, Apple provided a more user-friendly way of referring to elements such as NSBundle from Swift. Instead of something like let bundle = NSBundle.mainBundle(), you use the much easier and more readable preceding versions.
One of the primary uses of a bundle (NSBundle) is to get to resources that you added to your project. Those files will be copied into your application’s bundle when you build your application. If you want to get to those resources in your code, you usually need to use a bundle. You use the main bundle to retrieve the URL of the resource in which you’re interested.
let plistURL = bundle.urlForResource("statedictionary",
withExtension: "plist")
This will return a URL containing the location of the statedictionary.plist file. You can then use that URL to load your dictionary . Once you do that, the entire contents of that property list will be loaded into the newly created Dictionary object; that is, it is assigned to stateZips.
stateZips = NSDictionary.init(contentsOf: (plistURL)!) as! [String : [String]]
The Swift Dictionary type has no convenient way to load data from an external source, but the Foundation class NSDictionary does. This code takes advantage of that by loading the content of the statedictionary.plist file into an NSDictionary, which you then cast to the Swift type [String : [String]] (that is, a dictionary in which each key is a string representing a state and the corresponding value is an array containing the ZIP codes for that state, as strings). This reflects the structure shown in Figure 7-18.
To populate the array for the left component of the picker, which will display the states, you get the list of all keys from your dictionary and assign those to the states array. Before you assign it, though, you sort it alphabetically.
let allStates = stateZips.keys
states = allStates.sorted()
Unless you specifically set the selection to another value, pickers start with the first row (row 0) selected. To get the zips array that corresponds to the first row in the states array, you grab the object from the states array that’s at index 0. That will return the name of the state that will be selected at launch time. You then use that state name to grab the array of ZIP codes for that state, which you assign to the zips array that will be used to feed data to the right-side component.
let selectedState = states[0]
zips = stateZips[selectedState]
The two data source methods are practically identical to the previous version. You return the number of rows in the appropriate array. The same is true for the first delegate method you implemented. The second delegate method is the new one, which is where the magic happens.
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == stateComponent {
let selectedState = states[row]
zips = stateZips[selectedState]
dependentPicker.reloadComponent(zipComponent)
dependentPicker.selectRow(0, inComponent: zipComponent,
animated: true)
}
}
In this method, which is called any time the picker’s selection changes, you look at the component and see whether the left component is the one that changed, which would mean that the user selected a new state. If it is, you grab the array that corresponds to the new selection and assign it to the zips array. Next, you set the right-side component back to the first row and tell it to reload itself. By swapping the zips array whenever the state changes, the rest of the code remains pretty much the same as it was in the DoublePicker example.
You’re not quite finished yet. Build and run the application and then check out the Dependent tab, as shown in Figure 7-22. The two components are equal in size. Even though the ZIP code will never be more than five characters long, it has been given equal billing with the state. Since state names like Mississippi and Massachusetts won’t fit in half of the picker on most iPhone screens, this seems less than ideal.
Figure 7-22. Do you really want the two components to be of equal size? Notice the clipping of a long state name
Fortunately, there’s another delegate method you can implement to indicate how wide each component should be. Add the method in Listing 7-11 to the delegate section of DependentComponentPickerViewController.swift. You can see the difference in Figure 7-23.
Figure 7-23. With the adjustment in width of your picker components, your UI has become more visually useful
Listing 7-11. Setting the Width of Your Picker’s Components
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
let pickerWidth = pickerView.bounds.size.width
if component == zipComponent {
return pickerWidth/3
} else {
return 2 * pickerWidth/3
}
}
In this method, you return a number that represents how many pixels wide each component should be, and the picker will do its best to accommodate this. I’ve chosen to give the state component two-thirds of the available width and the rest goes to the ZIP component. Feel free to experiment with other values to see how the distribution of space between the components changes as you modify them. Save, build, and run; the picker on the Dependent tab will look more like the one shown in Figure 7-5.
By this point, you should be fairly comfortable with both pickers and tab bar applications. You have one more thing to do with pickers.
Creating a Simple Game with a Custom Picker
Next up, you’re going to create a simulated slot machine. Take a look back at Figure 7-6 before proceeding so you know what you’re building.
Preparing the View Controller
Begin by adding the following code to CustomPickerViewController.swift:
class CustomPickerViewController: UIViewController,
UIPickerViewDelegate, UIPickerViewDataSource {
private var images:[UIImage]!
At this point, all you’ve added to the class is a property for an array that will hold the images to use for the symbols on the spinners of the slot machine. The rest will come a little later.
Building the View
Even though the picker in Figure 7-6 looks quite a bit fancier than the other ones you’ve built, there’s actually very little difference in the way you’ll design your storyboard. All the extra work is done in the delegate methods of your controller.
Make sure that you’ve saved your new source code and then select Main.storyboard in the Project Navigator and use the Document Outline to select the Custom icon in the Custom Scene to edit the GUI. Add a picker view, a label below that, and a button below that. Name the button Spin.
With the label selected, bring up the Attributes Inspector. Set the alignment to centered. Then click Text Color and set the color to something bright. Next, let’s make the text a little bigger. Look for the Font setting in the inspector, and click the icon inside it (it looks like the letter T inside a little box) to pop up the font selector. This control lets you switch from the device’s standard system font to another if you like, or you can simply change the size. For now, just change the size to 48 and delete the word Label since you don’t want any text displayed until the first time the user wins. With the label selected, click Editor ➤ Size to Fit Content to make sure the label is always large enough to display its content.
Now add Auto Layout constraints to center the picker, label, and button horizontally and to fix the vertical gaps between them, between the label and the picker, and between the picker and the top of the view. You’ll probably find it easiest to drag from the label in the Document Outline when adding its Auto Layout constraints because the label on the storyboard is empty and difficult to find.
After that, make all the connections to outlets and actions. Create a new outlet called picker to connect the view controller to the picker view and another called winLabel to connect the view controller to the label. Again, you’ll find it easiest to use the label in the Document Outline than the one on the storyboard. Next, connect the button’s Touch Up Inside event to a new action method called spin(). After that, just make sure to connect the delegate and data source for the picker.
There’s one additional thing you need to do. Select the picker and bring up the Attributes Inspector. You need to deselect the User Interaction Enabled within the View settings check box so that the user can’t manually change the dial and cheat. Once you’ve done all that, save the changes you’ve made to the storyboard.
Implementing the Controller
There is a bunch of new stuff to cover in the implementation of this controller . Select CustomPickerViewController.swift and get started by filling in the contents of the spin() method, as shown in Listing 7-12.
Listing 7-12. The spin() Method
@IBAction func spin(_ sender: UIButton) {
var win = false
var numInRow = -1
var lastVal = -1
for i in 0..<5 {
let newValue = Int(arc4random_uniform(UInt32(images.count)))
if newValue == lastVal {
// numInRow++ *** NOTE THAT increment/decrement operators are deprecated in Swift 3
numInRow += 1
} else {
numInRow = 1
}
lastVal = newValue
picker.selectRow(newValue, inComponent: i, animated: true)
picker.reloadComponent(i)
if numInRow >= 3 {
win = true
}
}
winLabel.text = win ? "WINNER!" : " " // Note the space between the quotes
}
Note
The common use of unary increment (foo++) and decrement (foo--) was deprecated in Swift 3 to the use of += and -=, respectively.
Change the viewDidLoad() method to what’s shown in Listing 7-13.
Listing 7-13. The Modifications to viewDidLoad() to Set Up the Images and Label
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
images = [
UIImage(named: "seven")!,
UIImage(named: "bar")!,
UIImage(named: "crown")!,
UIImage(named: "cherry")!,
UIImage(named: "lemon")!,
UIImage(named: "apple")!
]
winLabel.text = " " // Note the space between the quotes
arc4random_stir()
}
Finally, add the dataSource and delegate code to the end of the class declaration, before the closing brace, as shown in Listing 7-14.
Listing 7-14. Your dataSource and delegate Methods
// MARK:-
// MARK: Picker Data Source Methods
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 5
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return images.count
}
// MARK:-
// MARK: Picker Delegate Methods
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let image = images[row]
let imageView = UIImageView(image: image)
return imageView
}
func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
return 64
}
The spin( ) Method
The spin() method executes when the user taps the Spin button. In it, you first declare a few variables that will help you keep track of whether the user has won. You’ll use win to keep track of whether you’ve found three in a row by setting it to true if you have. You’ll use numInRow to keep track of how many of the same value you have in a row so far. You will keep track of the previous component’s value in lastVal so that you have a way to compare the current value to the previous value. You initialize lastVal to -1 because you know that value won’t match any of the real values.
var win = false
var numInRow = -1
var lastVal = -1
Next, you loop through all five components and set each one to a new, randomly generated row selection. You get the count from the images array to do that, which is a shortcut you can use because you know that all five columns use the same number of images.
for i in 0..<5 {
let newValue = Int(arc4random_uniform(UInt32(images.count)))
You compare the new value to the previous value and increment numInRow if it matches. If the value didn’t match, you reset numInRow to 1. You then assign the new value to lastVal, so you’ll have it to compare the next time through the loop.
if newValue == lastVal {
numInRow += 1
} else {
numInRow = 1
}
lastVal = newValue
After that, you set the corresponding component to the new value, telling it to animate the change; you tell the picker to reload that component .
picker.selectRow(newValue, inComponent: i, animated: true)
picker.reloadComponent(i)
The last thing you do each time through the loop is check whether you have three in a row and then set win to true if you do.
if numInRow >= 3 {
win = true
}
Once you’re finished with the loop, you set the label to say whether the spin was a win.
winLabel.text = win ? "WINNER!" : " "
// Note the space between the quotes
The viewDidLoad() Method
The first thing was to load six different images, which you added to Images.xcassets back at the beginning of the chapter. You did this using the imageNamed() convenience method of the UIImage class.
images = [
UIImage(named: "seven")!,
UIImage(named: "bar")!,
UIImage(named: "crown")!,
UIImage(named: "cherry")!,
UIImage(named: "lemon")!,
UIImage(named: "apple")!
]
The next thing you did in this method was to make sure the label contains exactly one space. You want the label to be empty, but if you really make it empty, it collapses to zero height. By including a space, you make sure the label is shown at its correct height.
winLabel.text = " " // Note the space between the quotes
Finally, you called the arc4random_stir() function to seed the random number generator so that you don’t get the same sequence of random numbers every time you run the application.
So, what do you do with those six images? If you scroll down through the code you just typed, you’ll see that two data source methods look pretty much the same as before; however, if you look further into the delegate methods, you’ll see that you’re using completely different delegate code to provide data to the picker. The one that you’ve used up to now returned a string, but this one returns a UIView.
Using this method instead, you can supply the picker with anything that can be drawn into a UIView. Of course, there are limitations on what will work here and look good at the same time, given the small size of the picker. But this method gives you a lot more freedom in what you display, although it is a bit more work.
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let image = images[row]
let imageView = UIImageView(image: image)
return imageView
}
This method returns one UIImageView object initialized with one of the images for the symbols. To do that, you first get the image for the symbol for the row. Next, create and return an image view with that symbol. For views more complex than a single image, it can be beneficial to create all needed views first (e.g., in viewDidLoad()) and then return these precreated views to the picker view when requested. But for your simple case, creating the needed views dynamically works fine.
You got through all of it in one piece, and now you get to take it for a test. So, build and run the application and see how it works (see Figure 7-24).
Figure 7-24. It’s not the prettiest slot machine app, but it gives you an idea of the versatility of working with pickers
Additional Details for Your Game
Your game works okay, especially when you think about how little effort it took to build it. Now let’s improve it with a couple more tweaks. There are two things about this game right now that I should address.
It’s so quiet. Real slot machines aren’t quiet, so yours shouldn’t be either.
It tells the user that they’ve won before the dials have finished spinning, which is a minor thing, but it does tend to eliminate the anticipation. To see this in action, run your application again. It is subtle, but the label really does appear before the wheels finish spinning.
The Picker Sounds folder in the project archive that accompanies the book contains two sound files: crunch.wav and win.wav. Drag both of these files to your project’s Pickers folder. These are the sounds you’ll play when the users tap the Spin button and when they win, respectively.
To work with sounds, you’ll need access to the iOS Audio Toolbox classes. Insert the following AudioToolbox line at a position after the existing import line at the top of CustomPickerViewController.swift:
import UIKit
import AudioToolbox
Next, you need to add an outlet that will point to the button. While the wheels are spinning, you’re going to hide the button. You don’t want users tapping the button again until the current spin is all done. Add the following bold line of code to CustomPickerViewController.swift:
class CustomPickerViewController: UIViewController,
UIPickerViewDelegate, UIPickerViewDataSource {
private var images:[UIImage]!
@IBOutlet weak var picker: UIPickerView!
@IBOutlet weak var winLabel: UILabel!
@IBOutlet weak var button: UIButton!
After you type that and save the file, click Main.storyboard to edit the GUI. Open the Assistant Editor and make sure it shows the CustomPickerViewController.swift file. Click and drag from the little ball to the left of the outlet you just added to the button on the storyboard, as shown in Figure 7-25.
Figure 7-25. Connecting the button outlet to the button on the storyboard canvas
Now, you need to do a few things in the implementation of your controller class . First, you need some instance variables to hold references to the loaded sounds. Open CustomPickerViewController.swift again and add the following new properties (shown in bold):
class CustomPickerViewController: UIViewController,
UIPickerViewDelegate, UIPickerViewDataSource {
private var images:[UIImage]!
@IBOutlet weak var picker: UIPickerView!
@IBOutlet weak var winLabel: UILabel!
@IBOutlet weak var button: UIButton!
private var winSoundID: SystemSoundID = 0
private var crunchSoundID: SystemSoundID = 0
You also need a couple of methods added to your controller class. Add the two methods in Listing 7-15 to the CustomPickerViewController.swift file .
Listing 7-15. Hiding the Spin Button and Playing Sounds in Your Slot Machine Game
func showButton() {
button.isHidden = false
}
func playWinSound() {
if winSoundID == 0 {
let soundURL = Bundle.main.urlForResource(
"win", withExtension: "wav")! as CFURL
AudioServicesCreateSystemSoundID(soundURL, &winSoundID)
}
AudioServicesPlaySystemSound(winSoundID)
winLabel.text = "WINNER!"
DispatchQueue.main.after(when: .now() + 1.5) {
self.showButton()
}
}
You use the first method to show the button. As noted previously, you’ll need to hide the button when the user taps it because if the wheels are already spinning, there’s no point in letting them spin again until they’ve stopped.
The second method will be called when the user wins. First, you check whether you have already loaded the winning sound. The winSoundID and crunchSoundID properties are initialized as zero, and valid identifiers for loaded sounds are not zero, so you can check whether a sound is loaded yet by comparing its identifier to zero. To load a sound, you first ask the main bundle for the path to the sound, in this case win.wav, just as you did when you loaded the property list for the Dependent picker view. Once you have the path to that resource, the next three lines of code load the sound file in and play it. Next, you set the label to WINNER! and call the showButton() method; however, you call the showButton() method in a special way using a function called DispatchQueue(when: ). This is a handy function that lets you run code sometime in the future—in this case, one-and-a-half seconds in the future, which will give the dials time to spin to their final locations before telling the user the result.
Note
You may have noticed something a bit odd about the way you called the AudioServicesCreateSystemSoundID() function. That function takes a URL as its first parameter, but it doesn’t want an instance of NSURL. Instead, it wants a CFURL (previously CFURLRef), which is a pointer to a structure that belongs to the C-language Core Foundation framework. NSURL is part of the Foundation framework, which is written in Objective-C. Fortunately, many of the C components in Core Foundation are “bridged” to their Objective-C counterparts in the Foundation framework so that a CFURL is functionally equivalent to an NSURL pointer. That means certain kinds of objects created in Swift or Objective-C can be used with C APIs simply by casting them to the corresponding C type using the as keyword.
You also need to make some changes to the spin() method . You will write code to play a sound and to call the playWinSound method if the player wins. Make the changes to the spin() method shown in Listing 7-16.
Listing 7-16. Your Updated spin() Method to Add Sounds
@IBAction func spin(sender: AnyObject) {
var win = false
var numInRow = -1
var lastVal = -1
for i in 0..<5 {
let newValue = Int(arc4random_uniform(UInt32(images.count)))
if newValue == lastVal {
numInRow += 1
} else {
numInRow = 1
}
lastVal = newValue
picker.selectRow(newValue, inComponent: i, animated: true)
picker.reloadComponent(i)
if numInRow >= 3 {
win = true
}
}
if crunchSoundID == 0 {
let soundURL = Bundle.main.urlForResource(
"crunch", withExtension: "wav")! as CFURL
AudioServicesCreateSystemSoundID(soundURL, &crunchSoundID)
}
AudioServicesPlaySystemSound(crunchSoundID)
if win {
DispatchQueue.main.after(when: .now() + 0.5) {
self.playWinSound()
}
} else {
DispatchQueue.main.after(when: .now() + 0.5) {
self.showButton()
}
}
button.isHidden = true
winLabel.text = " " // Note the space between the quotes
}
First, you load the crunch sound if needed, just as you did with the win sound before. Now play the crunch sound to let the player know the wheels have been spun. Next, instead of setting the label to WINNER! as soon as you know the user has won, you do something tricky. You call one of the two methods you just created, but you do it after a delay using DispatchQueue.main.after(when: ). If the user won, you call your playWinSound() method half a second into the future, which will give time for the dials to spin into place; otherwise, you just wait a half-second and reenable the Spin button . While waiting for the result, you hide the button and clear the label’s text.
Now you’re done, so build and run the app and then click the final tab to see and hear this slot machine in action. Tapping the Spin button should play a little cranking sound, and a win should produce a winning sound.
Summary
By now, you should be comfortable with tab bar applications and pickers. In this chapter, you built a full-fledged tab bar application containing five different content views from scratch. You practiced using pickers in a number of different configurations and creating pickers with multiple components. You even know how to make the values in one component dependent on the value selected in another component. You also saw how to make the picker display images rather than just text.
Along the way, you talked about picker delegates and data sources and saw how to load images, play sounds, and create dictionaries from property lists. It was a long chapter, so congratulations on making it through. In the next chapter, you’ll start working with one of the most common elements for iPhone devices: table views.