Apps would be static, linear, and boring if the user couldn’t interact with them. In this chapter, we’ll look at three different ways an app can respond to user interaction:
A control is a special type of view that’s designed for user interaction. Because controls come with UIKit and are available for everyone to use in their apps, they have a consistent and familiar look across different apps, making controls in your interface more intuitive for your users.
As with gestures, control events can trigger actions in your code. Let’s look at several different types of controls, and how to receive notification of different control events.
One of the most common controls in UIKit is the button. See figure 5.1 for default looks for several different button types.
You’ll use system buttons to make your distance converter app interactive. The user can convert the number of miles they’ve entered to kilometers or vice versa (see figure 5.2). Okay, we’re not going to win any design awards, but we’re focused on functionality for the moment.
Open the distance converter app where you left it at the end of the last chapter, or check it out at https://github.com/iOSAppDevelopmentwithSwiftinAction/DistanceConverter.git.
Let’s add the two convert buttons to the interface.
State |
Description |
---|---|
Default | The default state for the button |
Highlighted | Active while the user is touching the button |
Selected | Active if the button’s selected property is set to true |
Disabled | Active if the button’s enabled property is set to false |
One important thing to understand with buttons is that attributes below the State Config attribute only apply to the currently selected state. If you don’t set specific attributes for a state, appropriate defaults for that state will be implemented.
For the default state, change the text of the button (called its title) to “Convert to km.” While you’re here, drag on a second button beside the kilometers label, and give it the title “Convert to miles.”
Run it on the iPhone 6s Plus simulator. The interface should look something like figure 5.2.
Nothing happens yet when you tap the buttons. You’re going to connect the convert buttons with methods in the view controller code that will perform the conversion and display the result.
To connect a method to a control event in Interface Builder, you’ll need to define the method as an action. An action in simple terms is a method that will be triggered when something happens. In this case, you’ll create an action method in your view controller code that will be triggered when a convert button is touched using the following steps:
If you don’t see the view controller source code, double-check you have the Automatic mode selected in the Assistant Editor jump bar.
if let miles = Double(milesTextField.text!) { }
distance.miles = miles kmTextField.text = "(Int(distance.km))"
Follow the same process to create a convertToMiles() method, triggered by the Convert to Miles button, that converts the value in the kilometers text field to miles, and displays the result in the miles text field.
Compare your solution with mine at https://github.com/iOSAppDevelopmentwithSwiftinAction/DistanceConverter.git (3.ConvertDistanceWithButtons).
But wait—are the conversion buttons necessary? Maybe the conversion could happen automatically as the user types the distance into the text field.
As it happens, text fields are types of controls too, and can also trigger actions in your code. Let’s take a look.
Text fields display one line of text that the user can edit using the pop-up software keyboard.
Select a text field now, and examine the attributes available in the Attributes Inspector.
There are three main categories of keyboards, with different variations, as explained in table 5.2.
Category |
Use |
---|---|
ASCII | Text, emails, URLs, and so on. The Numbers and Punctuation keyboard can be accessed if necessary. |
Numbers and Punctuation | Numbers and punctuation, where the ASCII keyboard can be accessed if necessary. |
Number Pad | For when numbers and relevant symbols are required, and the ASCII keyboard isn’t required. |
To make the different variations of keyboards, the bottom layer of keys in the ASCII keyboard is swapped out, and one of the Number Pad keys is swapped out. See figure 5.4 for all keyboard type variations.
That’s not all the different keyboard types! Keyboard keys and layout vary depending on the language, the orientation of the device, and the device type itself!
Which type of keyboard is most appropriate for your miles and kilometers text fields? Choose an appropriate type and make the adjustment in the Attributes Inspector.
You’ll modify your distance converter app to automatically calculate the distance conversion as the user types it into the text field.
You may have noticed when you connected the action to the text field that you didn’t have a choice of event. Connecting actions in this way assumes a control’s default event. The Editing Did End event is the text field’s default, which only triggers after a user stops editing a text field, for instance, by tapping on another text field.
This isn’t the event we’re looking for, so delete the Editing Did End connection by selecting the X in the Connections Inspector (see figure 5.6).
The Editing Changed event triggers whenever the text in a text field is modified. This sounds more like it! Connect the Editing Changed event to the convertToKm method.
You could connect the Editing Changed event for the kilometers text field to the convertToMiles() method in the same way, but this time let’s connect the action in code.
Use the UIControl’s addTarget() method to specify the target and the action. You’ll also need to need to specify the control event itself that you’re listening for (editingChanged). These steps show you how:
kmTextField.addTarget(self, action: #selector(convertToMiles), for: .editingChanged)In English, this line says, “When the editingChanged event is triggered, call the convertToMiles() action on self (that is, instance method of the view controller). And that’s all that’s necessary to connect the action in code! Now that you’ve made the conversion happen automatically, you’ve made the buttons redundant!
If you want to check out my version of the app at this point, you can do that at https://github.com/iOSAppDevelopment-with-Swift-inAction/DistanceConverter.git (4.ConvertDistanceWhenText-Changes).
UIKit provides several controls for different purposes. We’re not going to discuss all of them in detail now, but table 5.3 has a summary of what’s available and what they’re useful for.
Control |
Default interface |
Use |
Example usage |
---|---|---|---|
Switch | Modify a Boolean value between an on or off state. Similar to a toggle button or checkbox. | Turn sound off or on. | |
Slider | Modify a numeric value between a continuous range of values, such as between 0 and 1. | Adjust the sound volume. | |
Stepper | Modify a numeric value by increasing or decreasing by a defined amount. | Select the quantity of a product in a shopping cart. | |
Picker | Select a value from a set of values. Similar to a drop-down or combo box, but allows for multiple selectors. | Select a language from a set of languages. | |
Date Picker | Select a date and/or time. | Select a departure date in a travel app. | |
Segmented Control | Select one value from a small set of values. | Select a travel class (Economy, Business) in a travel app. |
If we’re being pedantic, Picker isn’t a control, although Date Picker is! How could this be possible? While the UIDatePicker class subclasses UIControl, the UIPickerView class subclasses UIView directly, bypassing UIControl. Therefore, UIPickerView doesn’t have access to connect events to action methods. Rather, it uses what’s called the delegation pattern to be customized and receive user interaction (we’ll look at the delegation pattern shortly). The user has no idea of the internal implementation of a view, so if it looks like a control and works like a control, it’s a control! I’ve therefore included the Picker in this list of controls.
Controls are a useful high-level way to implement user interaction. But sometimes you don’t need all the bells and whistles of controls—you might have a simple view, and you need to receive information on touch events.
In this section, you’ll create an app called Touch Views that displays simple views that change color when the user touches them. See figure 5.8 for the interface on the left, and the view hierarchy of the app on the right.
To spare you the headache of setting up this interface, check out the TouchViews project repository at https://github.com/iOSAppDevelopmentwithSwiftinAction/TouchViews.git (1.Initial Setup).
When you have the project open in Xcode, open the ViewController.swift file and examine how the views are constructed in code, passing in a CGRect structure to the UIView initializer, the way you did in the ViewsInCode project in the previous chapter.
To distinguish the views from each other, they each have a different random background color. The random property is already set up for you in a UIColor extension that you can find in the UIColorExtension.swift file.
Notice that while views A, B, and C are being added to the subviews of the root view, view D is added to the subviews of view C. Have another look at this view hierarchy in figure 5.8. Note that a view that is added after another view appears in front. This is why view B appears to be in front of view A.
Whenever an app receives a touch event, it first follows a path down the view hierarchy performing what is called hit-testing to determine the lowest-level view that was touched.
For example, if the user touches within the bounds of view D (see figure 5.9), iOS first checks the root views’ subviews from front to back (that is, views C, B, and then A) until it finds a view that contains the touch. When it finds the touch in view C, it doesn’t need to continue looking in views B and A. It then looks inside the subviews of view C and finds that view D contains the touch. As view D doesn’t have any subviews, it determines view D is the lowest-level view that was touched.
After drilling down to view D, iOS will then call this view’s touchesBegan() method, passing in a Set of touches. If you create a custom subclass of UIView and override this method, you can provide custom implementation for this view when it’s touched.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) self.backgroundColor = UIColor.random }Now, back in the ViewController class, instead of creating instances of UIView, create instances of your new view subclass, ColoredView.
Notice that when you touch view B where it overlaps view A, only view B changes color. iOS checks a view’s subviews in the order that they’re displayed, from front to back. When a view returns a successful hit test, iOS stops checking other views at this level. In the example, view B is closer to the front than view A because it was added last. Because it’s the front view, when it returns a successful hit test, iOS stops there, and doesn’t perform a hit test on view A.
Notice that when you touch view D, its superview, view C, also changes color. Why?
When a view receives an event such as a touch event, it passes this event up to its superclass, and so on. When the event arrives at the root view of a scene, it’s passed to the scene’s view controller. The view controller in turn passes the event on to the superview of its root view. In this example, the superview of the root view of the scene is the window of the app. The dotted line in figure 5.10 illustrates the path of the event in our app, called the responder chain.
Every object that can receive these events is called a responder and every responder (that is, UIView and UIViewController) subclasses the UIResponder class. The UIResponder class is where you’ll find the touchesBegan() method.
Let’s demonstrate that the view controller is on the responder chain.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) self.view.backgroundColor = UIColor.random }
Other UIResponder methods can be overridden to receive other touch events, as explained in table 5.4.
Touch method |
Trigger |
---|---|
touchesBegan() | One or more fingers touched down on a view. |
touchesMoved() | One or more fingers moved within a view. |
touchesEnded() | One or more fingers lifted off a view. |
touchesCancelled() | A touch is interrupted by a system event. |
touchesEstimatedPropertiesUpdated() | To ensure touch events are presented in a timely manner, sometimes touch attributes are estimated. These estimated values are later updated in this method. |
Make the views also change color when the user lifts their finger off the view. You can check out the completed app at https://github.com/iOSAppDevelopmentwithSwiftinAction/TouchViews.git (2.ColoredView).
With all these triggers for touches, you could easily respond to taps—but what if you want your view to also respond to double taps? Should you wait a short period before responding to the tap, in case it was going to be a double tap? How long would that short period of time be?
What if you want your app to pinch to zoom in and out on an image? Are you brushed up on your Pythagoras theorem?
Not to worry, Apple has you covered with another type of user interaction called gesture recognizers.
Gesture recognizers go one step further than merely reporting touch information. Gesture recognizers interpret the touches and recognize the intention of the movement the way humans would. They can tell the difference between a swipe and a pan, a double tap and two single taps, or a pinch and a rotation.
Without needing to program or understand the complicated underlying code defining the gesture recognition algorithms, your app can detect and respond to all sorts of complex predefined gestures.
Having standardized gesture recognizers has the added benefit of consistency with other apps in the App Store, which should make your interface more intuitive for your users. Apple provides several gesture recognizers, each of which detects different types of gestures. See table 5.5 for different gestures, the relevant recognizer, and how this gesture can be used in your app.
Gesture |
Recognizer |
Example usage |
---|---|---|
Tap | UITapGestureRecognizer | Selecting a control or item. |
Double tap | UITapGestureRecognizer | Zooming in (or out if already zoomed in). |
Pinch | UIPinchGestureRecognizer | Zooming in/out. |
Pan | UIPanGestureRecognizer | Dragging or panning content in any direction. |
Flick | UIPanGestureRecognizer | Scrolling or panning content in any direction quickly. |
Drag from edge of display | UIScreenEdgePanGestureRecognizer | Drag in additional content from off-screen. |
Swipe (left, right, up, or down) | UISwipeGestureRecognizer | Returning to previous screen, revealing hidden view or button. |
Two fingers circular movement | UIRotationGestureRecognizer | Rotating content. |
Touch and hold | UILongPressGestureRecognizer | Positioning cursor in text fields. |
You’ll explore the possibilities with gestures by creating a simple image viewer app. In this app, you’ll pan, zoom, and rotate an image, or tap to view the next image. The starter project is bare bones, with only an image view in the main scene ready for viewing.
Check out a starter project for your image viewer app at https://github.com/iOSAppDevelopmentwithSwiftinAction/Image-Viewer.git (1.InitialSetup).
Image views are straightforward—they’re a special type of view that can display an image stored in your project.
If you open the project folder in Finder, you’ll also find a folder called Images with three photos. Feel free to use these images or replace them with your own photos. Now let’s add these files to your project!
When you add files to your project, you have a few options to consider:
Destination: Copy items if needed. If you check this option, any files or folders you drag into your project will physically be copied into your project folder. You can theoretically include references in your project to files that aren’t in your project folder. This could make sense, for example, if you’re sharing resources with another project, though it’s recommended to keep all relevant files within your project folder—it helps organize your resources into one place. The image folder is already in the project folder, so in this case checked or unchecked is irrelevant.
Added folders: Create groups/Create folder references. When you add a folder, you have the choice to add it as a reference to the folder, or a reference to each individual file bundled into a group. If you have a reference to a folder (blue icon), whenever you update the contents of the physical folder on disk, Xcode will automatically update its contents in the Project Navigator. A group, on the other hand (yellow icon), is no longer connected to the folder itself after being added to the project. Any files you add to the folder will not be reflected in the group, and any files you remove from the folder will be highlighted as missing files. Generally, it makes sense to go with groups, but cases exist where folder references can come in handy. For example, perhaps you’re sharing a folder with a graphic designer and want the resources to update automatically.
Add to targets: Choose which target you would like to add the files to. Every resource and source file is explicitly included in the appropriate target. You can check this by selecting a file in the Project Navigator and noting the Target Membership section in the File Inspector. Generally, files are divided into their target groups in the Project Navigator, but files can also be shared between targets.
File categories
Xcode determines what to do with each file when building your app by categorizing files into Compile Sources, Bundle Resources, and Frameworks and Libraries.
Compile Sources—This category refers to all source files, such as Swift or Objective-C code. Source files are compiled into your app executable, called the binary.
Bundle Resources—This category refers to all sorts of resources and media you may want to include. Certain resource files such as images, audio files, or even PDF files are copied directly into your app bundle. Other resource files, such as the asset catalog or storyboards, are converted in different ways when copied into the app bundle.
Frameworks and Libraries—This category refers to frameworks that your app will link to. Distinct from third-party frameworks, frameworks from Apple are automatically linked with your project, and they don’t need to be physically added. We’ll look more at third-party frameworks and libraries in chapter 11.
You can examine the categories of the files in your app in the Build Phases tab of your project target settings. For more details about project settings, check appendix A.
For a view to respond to a gesture, you need to add a gesture recognizer to the view. You can do this in code, or in Interface Builder. Let’s start by adding a pan gesture recognizer to the image view in Interface Builder.
Add a pan gesture recognizer to your image view to be able to pan the image around.
let translation = sender.translation(in: self.view)
imageView.center = CGPoint( x: imageView.center.x + translation.x, y: imageView.center.y + translation.y)Because gesture recognizers report on movement since the moment a gesture began, and the center property reports on the current location of the image view, if we continue adding the gesture movement to the image view location every time the gesture recognizer reports a movement, the image view will move exponentially. To illustrate this, consider if the image view begins at (x:0, y:0). The first time the gesture recognizer is called, the translation may be a movement of (x:1, y:1), so the image view is moved to (x:1, y:1). The second time the gesture recognizer is called, the translation may have moved another 1 point in the x direction and 1 point in the y direction, so the translation (representing the movement from the moment the gesture began) will be (x:2, y:2). The new location of the image view should be (x:2, y:2) but following the code above, instead it will be (x:3, y:3). What can be done about this? There are two possible solutions:
sender.setTranslation(CGPoint.zero, in: self.view)Your handlePan method should now look like the following code.
@IBAction func handlePan(_ sender:UIPanGestureRecognizer) { let translation = sender.translation(in: self.view) 1 imageView.center = CGPoint( 2 x: imageView.center.x + translation.x, 2 y: imageView.center.y + translation.y) 2 sender.setTranslation(CGPoint.zero, in: self.view) 3 }
A good image viewer can zoom in on the image as well.
Follow the same steps that you followed for the pan gesture recognizer, but with the pinch gesture recognizer:
imageView.transform = imageView.transform.scaledBy( x: sender.scale, y: sender.scale)
sender.scale = 1
In the end, your handlePinch method should look like the following:
@IBAction func handlePinch(_ sender: UIPinchGestureRecognizer) { imageView.transform = 1 imageView.transform.scaledBy( 2 x: sender.scale, y: sender.scale) 2 sender.scale = 1 3 }
Run the app and confirm you can pinch the image to zoom.
If you’re running the app in the simulator, you can simulate two fingers if you hold down the Alt key.
Your image viewer app is coming along!
To round out your image viewer app, how about adding rotation to the mix?
After going through the process twice already, you should be familiar enough to try it yourself without following instructions. Add a rotate gesture recognizer to the image view. When you’re done, compare your results with the code in listing 5.1 for the handleRotate method.
Use the rotated method to transform the rotation of the transformation matrix.
@IBAction func handleRotation(_ sender: UIRotationGestureRecognizer) { imageView.transform = 1 imageView.transform.rotated(by:sender.rotation) 2 sender.rotation = 0 3 }
Run your app again, and you can rotate your view as well!
You may have noticed a limitation of the recognizers. By default, only one gesture can be performed at a time. If the system recognizes that you’re pinching to zoom, for example, you can’t rotate the image until you stop zooming by taking your fingers off the screen.
You can change this default behavior, however. You could be zooming and rotating and panning all at the same time! But to change this default behavior, you’ll need to use the delegation pattern.
We’ve looked at the target-action pattern, where one object can call a method on another object. The delegation pattern is like the target-action pattern on steroids—in the delegation pattern, an object contains a property called the delegate, which contains a list of methods that the object can call. You can then implement this delegate object, providing custom responses to the methods the object calls.
An object can call methods on its delegate for various purposes:
The list of methods in a delegate is defined by a protocol—in fact, all an object knows or cares about its delegate is that it can handle the methods in the delegate protocol. By convention, the delegate protocol has the suffix “Delegate.”
You can create a delegate object that adopts the delegate protocol, and then set your object as the delegate property. Often, for simplicity, a view controller is used as a delegate object.
You’ll find the delegation pattern is used frequently in the iOS SDK, including gesture recognizers!
All gesture recognizers have a property delegate with a list of methods defined by the UIGestureRecognizerDelegate protocol. This protocol contains methods such as
Oh! That method sounds like exactly what you need to be able to zoom, rotate, and pan at the same time! How about using it?
To define your view controller as the delegate for a gesture recognizer, your view controller would need to
See figure 5.14 for a visual representation of the relationships when a gesture recognizer uses a view controller as its delegate.
Implement the delegation pattern here by doing the following:
extension ViewController:UIGestureRecognizerDelegate { 1 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 2 return true 3 } }
After implementing three gesture recognizers using Interface Builder, you could probably implement another gesture recognizer blindfolded! Let’s explore an alternative approach to setting up a gesture recognizer: implementing it purely with code.
You’re going to implement a single-finger double-tap gesture that will navigate to the next image.
let images = ["CradleMountain.JPG", "Laguna69.JPG", "PatagoniaSky.JPG"] var imageNo = 0
@objc func handleTap(_ sender: UITapGestureRecognizer) { imageNo += 1 if imageNo == images.count {imageNo = 0} imageView.image = UIImage(named: images[imageNo]) }
Unlike the other action methods, this action method doesn’t begin with @IBAction. Because you’re not going to trigger this method from Interface Builder, this attribute isn’t necessary. Instead, this method will need to begin with the @objc keyword to make it available to Objective-C—more on that in a moment.
Parameter |
Description |
---|---|
target | Specifies the object to receive any gesture events, which in this case will be self—the view controller. |
action | Specifies the method to receive notification of the gesture event. You set up the handleTap() method to receive these notifications. The action is specified using a special expression called #selector. Use #selector to pass in the name of the method. Because the #selector expression uses the Objective-C runtime to connect to the associated method, the method will need to be exposed with the @objc keyword. |
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))You can now customize the recognizer.
tapGestureRecognizer.numberOfTouchesRequired = 1 1 tapGestureRecognizer.numberOfTapsRequired = 2 2
imageView.addGestureRecognizer(tapGestureRecognizer)
Well, you added double tapping to go to the next image, but how about double tapping with two fingers to go to the previous image? Because each tap recognizer only recognizes taps of a specific number of fingers and taps, you’ll have to set up another tap gesture recognizer.
Add a double-tap-with-two-fingers gesture recognizer to go to the previous image. If you want to peek at the answer, you can download the completed image viewer from https://github.com/iOSAppDevelopmentwithSwift-inAction/ImageViewer.git (2.ImageViewerComplete).
In this chapter, you learned the following: