Delegation and Text Input - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

7. Delegation and Text Input

In this chapter, we will introduce delegation, a recurring design pattern of Cocoa Touch development. In addition, you will see how to use the debugger that Xcode provides to find and fix problems in your code.

By the end of the chapter, the HypnoNerd user will be able to display hypnotic messages on the screen using a text field (Figure 7.1).

Figure 7.1 Finished HypnoNerd

Finished HypnoNerd

Text Fields

Open the HypnoNerd application that you started in the previous chapter.

You have already seen one way to display text on your user interfaces using a UILabel. Now let’s take a look at another way to display text using a UITextField. An instance of UITextField allows the user to modify the text, much like a username or password field on a website.

Open BNRHypnosisViewController.m and modify the loadView to add a UITextField to its view.

- (void)loadView

{

CGRect frame = [UIScreen mainScreen].bounds;

BNRHypnosisView *backgroundView = [[BNRHypnosisView alloc] initWithFrame:frame];

CGRect textFieldRect = CGRectMake(40, 70, 240, 30);

UITextField *textField = [[UITextField alloc] initWithFrame:textFieldRect];

// Setting the border style on the text field will allow us to see it more easily

textField.borderStyle = UITextBorderStyleRoundedRect;

[backgroundView addSubview:textField];

self.view = backgroundView;

}

Build and run the application and you should see the text field on the Hypnotize tab. Tap on the text field, and the keyboard will slide up from the bottom of the screen, allowing you to input text. To understand how this is happening under the hood, you need to understand the first responder.

UIResponder

UIResponder is an abstract class in the UIKit framework. It is the superclass of three classes that you have already encountered:

· UIView

· UIViewController

· UIApplication

UIResponder defines methods for handling (or “responding to”) events: touch events, motion events (like a shake), and remote control events (like pausing or playing). Subclasses override these methods to customize how they respond to events.

With touch events, it is obvious which view the user has touched. Touch events are sent directly to that view. You saw an example of this in Chapter 5.

What about the other types of events? The UIWindow has a pointer called firstResponder which indicates who should respond to the other types of events. When you select a text field, for example, the window moves its firstResponder pointer to that text field. Motion and remote control events are sent to the first responder.

Figure 7.2 firstResponder

firstResponder

When a text field or a text view becomes firstResponder, it shows its keyboard. When it loses first responder status, it hides its keyboard. If you want one of these views to become first responder, you send it the message becomeFirstResponder and the keyboard appears. When you want to hide the keyboard, you send it the message resignFirstResponder.

Most views refuse to become first responder; they do not want to steal focus from the currently selected text field or text view. An instance of UISlider, for example, handles touch events but will never accept first responder status.

Configuring the keyboard

The keyboard’s appearance is determined by a set of the UITextField’s properties called UITextInputTraits. Let’s modify some of these to give the text field some placeholder text and to modify the keyboard’s return type.

- (void)loadView

{

CGRect frame = [UIScreen mainScreen].bounds;

BNRHypnosisView *backgroundView = [[BNRHypnosisView alloc] initWithFrame:frame];

CGRect textFieldRect = CGRectMake(40, 70, 240, 30);

UITextField *textField = [[UITextField alloc] initWithFrame:textFieldRect];

// Setting the border style on the text field will allow us to see it more easily

textField.borderStyle = UITextBorderStyleRoundedRect;

textField.placeholder = @"Hypnotize me";

textField.returnKeyType = UIReturnKeyDone;

[backgroundView addSubview:textField];

self.view = backgroundView;

}

Build and run the application. Now the text field has a placeholder string that will be displayed until the user types in some text. Also, the return key now says Done instead of the default Return. Figure 7.3 shows what the interface looks like with these changes.

Figure 7.3 Configured text field

Configured text field

If you tap the Done key, you will notice that nothing happens. Changing the return key type has no impact on the functionality of the return key. In fact, the return key does not do anything automatically; you have to implement the return key functionality yourself. Before you do that, though, let’s take a look at some of the other useful properties that you can use to configure the keyboard.

autocapitalizationType

This determines how capitalization is handled. The options are none, words, sentences, or all characters.

autocorrectionType

This will suggest and correct unknown words. This value can be YES or NO.

enablesReturnKeyAutomatically

This value can be YES or NO. If set to yes, the return key will be disabled if no text has been typed. As soon as any text is entered, the return key becomes enabled.

keyboardType

This determines the type of keyboard that will be displayed. Some examples are the ASCII keyboard, email address keyboard, number pad, and the URL keyboard.

secureTextEntry

Setting this to YES makes the text field behave like a password field, hiding the text that is entered.

Delegation

You have already seen the Target-Action pattern. This is one form of callbacks that is used by UIKit: When a button is tapped, it sends its action message to its target. This typically triggers code that you have written.

A button’s life is relatively simple. For objects with more complex lives, like a text field, Apple uses the delegation pattern. You introduce the text field to one of your objects: “This is your delegate, when anything interesting happens in your life, send a message to him.” The text field keeps a pointer to its delegate. Many of the message it sends to its delegates are informative: “OK, I am done editing!”. Here are some of those:

- (void)textFieldDidEndEditing:(UITextField *)textField;

- (void)textFieldDidBeginEditing:(UITextField *)textField;

Notice that it always sends itself as the first argument to the delegate method.

Some of the message it sends to its delegate are queries: “I am about to end editing and hide the keyboard. OK?” Here are some of those:

- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;

- (BOOL)textFieldShouldClear:(UITextField *)textField;

- (BOOL)textFieldShouldReturn:(UITextField *)textField;

You are now going to make your BNRHypnosisViewController the delegate of the text field. You will implement the textFieldShouldReturn: method. When you run it, you will see that the method gets called automatically when the user taps the Done button.

Figure 7.4 BNRHypnosisViewController as UITextField delegate

BNRHypnosisViewController as UITextField delegate

In BNRHypnosisViewController.m, update loadView to set the delegate property of the UITextField to point at the BNRHypnosisViewController.

- (void)loadView

{

CGRect frame = [UIScreen mainScreen].bounds;

BNRHypnosisView *backgroundView = [[BNRHypnosisView alloc] initWithFrame:frame];

CGRect textFieldRect = CGRectMake(40, 70, 240, 30);

UITextField *textField = [[UITextField alloc] initWithFrame:textFieldRect];

// Setting the border style on the text field will allow us to see it more easily

textField.borderStyle = UITextBorderStyleRoundedRect;

textField.placeholder = @"Hypnotize me";

textField.returnKeyType = UIReturnKeyDone;

// There will be a warning on this line. We will discuss it shortly.

textField.delegate = self;

[backgroundView addSubview:textField];

self.view = backgroundView;

}

The method textFieldShouldReturn: takes in just one argument: the text field whose return key was tapped. For now, the application will just print the text of the text field to the console.

In BNRHypnosisViewController.m, implement the textFieldShouldReturn:. Be very careful that there are no typos or capitalization errors, or the method will not be called. The selector of the message the text field sends must exactly match the selector of the method implemented.

- (BOOL)textFieldShouldReturn:(UITextField *)textField

{

NSLog(@"%@", textField.text);

return YES;

}

Build and run the application, type some text into the text field, and tap the return key. The text should print to the console.

Notice that you did not need to implement all of text field’s delegate methods, just the one that you cared about. At runtime the text field will ask its delegate if it implements a method before calling it.

Protocols

For every object that can have a delegate, there is a corresponding protocol that declares the messages that the object can send its delegate. The delegate implements methods from the protocol for events it is interested in. When a class implements methods from a protocol, it is said to conform to the protocol.

(If you are coming from Java or C#, you would use the word “interface” instead of “protocol”.)

The protocol for UITextField’s delegate looks like this:

@protocol UITextFieldDelegate <NSObject>

@optional

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField;

- (void)textFieldDidBeginEditing:(UITextField *)textField;

- (BOOL)textFieldShouldEndEditing:(UITextField *)textField;

- (void)textFieldDidEndEditing:(UITextField *)textField;

- (BOOL)textField:(UITextField *)textField

shouldChangeCharactersInRange:(NSRange)range

replacementString:(NSString *)string;

- (BOOL)textFieldShouldClear:(UITextField *)textField;

- (BOOL)textFieldShouldReturn:(UITextField *)textField;

@end

This protocol, like all protocols, is declared with the directive @protocol followed by its name, UITextFieldDelegate. The NSObject in angled brackets refers to the NSObject protocol and tells us that UITextFieldDelegate includes all of the methods in the NSObject protocol. The methods specific toUITextFieldDelegate are declared next, and the protocol is closed with an @end directive.

Note that a protocol is not a class; it is simply a list of method declarations. You cannot create instances of a protocol, it cannot have instance variables, and these methods are not implemented anywhere in the protocol. Instead, implementation is left to each class that conforms to the protocol.

The UITextFieldDelegate protocol is part of the iOS SDK. Protocols in the iOS SDK have reference pages in the developer documentation where you can see what methods are declared. You can also write your own protocol. You will do that in Chapter 22.

Methods declared in a protocol can be required or optional. By default, protocol methods are required. If a protocol has optional methods, these are preceded by the directive @optional. Looking back at the UITextFieldDelegate protocol, you can see that all of its methods are optional. This is typically true of delegate protocols.

Before sending an optional message, the object first asks its delegate if it is okay to send that message by sending another message, respondsToSelector:. Every object implements this method, which checks at runtime whether an object implements a given method. You can turn a method selector into a value that you can pass as an argument with the @selector() directive. For example, UITextField could implement a method that looks like this:

- (void)clearButtonTapped

{

// textFieldShouldClear: is an optional method,

// so we check first

SEL clearSelector = @selector(textFieldShouldClear:);

if ([self.delegate respondsToSelector:clearSelector]) {

if ([self.delegate textFieldShouldClear:self]) {

self.text = @"";

}

}

}

If a method in a protocol is required, then the message will be sent without checking first. This means that if the delegate does not implement that method, an unrecognized selector exception will be thrown, and the application will crash.

To prevent this from happening, the compiler will insist that a class implement the required methods in a protocol. But for the compiler to know to check for implementations of a protocol’s required methods, the class must explicitly state that it conforms to a protocol. This is done either in the class header file or the class extension: the protocols that a class conforms to are added to a comma-delimited list inside angled brackets in the interface declaration.

In BNRHypnosisViewController.m, declare that BNRHypnosisViewController conforms to the UITextFieldDelegate protocol in the class extension. The reason for adding it to the class extension rather than the header file is the same reason as always: add to the class extension if the information (conforming to a particular protocol in this case) does not need to be publicly visible, and add it to the header file if other objects do need to know about the information.

@interface BNRHypnosisViewController () <UITextFieldDelegate>

@end

Build the application again. Now that you have declared that BNRHypnosisViewController conforms to the UITextFieldDelegate protocol, the warning from the line of code where you set the delegate disappears. Furthermore, if you want to implement additional methods from theUITextFieldDelegate protocol in BNRHypnosisViewController, those methods will now be auto-completed by Xcode.

Many classes have a delegate pointer, and it is nearly always a weak reference to prevent strong reference cycles. In this case, for example, your view controller indirectly owns the text field. If the text field owned its delegate, you would have a strong reference cycle that would cause a memory leak.

Figure 7.5 Preventing strong reference cycle

Preventing strong reference cycle

Adding the Labels to the Screen

To make things a little interesting, you are going to add instances of UILabel to the screen at random positions. In BNRHypnosisViewController.m, implement a new method that will draw a given string on the screen twenty times at random positions.

- (void)drawHypnoticMessage:(NSString *)message

{

for (int i = 0; i < 20; i++) {

UILabel *messageLabel = [[UILabel alloc] init];

// Configure the label's colors and text

messageLabel.backgroundColor = [UIColor clearColor];

messageLabel.textColor = [UIColor whiteColor];

messageLabel.text = message;

// This method resizes the label, which will be relative

// to the text that it is displaying

[messageLabel sizeToFit];

// Get a random x value that fits within the hypnosis view's width

int width =

(int)(self.view.bounds.size.width - messageLabel.bounds.size.width);

int x = arc4random() % width;

// Get a random y value that fits within the hypnosis view's height

int height =

(int)(self.view.bounds.size.height - messageLabel.bounds.size.height);

int y = arc4random() % height;

// Update the label's frame

CGRect frame = messageLabel.frame;

frame.origin = CGPointMake(x, y);

messageLabel.frame = frame;

// Add the label to the hierarchy

[self.view addSubview:messageLabel];

}

}

In BNRHypnosisViewController.m, update the textFieldShouldReturn: method to call this new method, passing in the text field’s text, clear the text that the user typed, and then dismiss the keyboard by calling resignFirstResponder.

- (BOOL)textFieldShouldReturn:(UITextField *)textField

{

NSLog(@"%@", textField.text);

[self drawHypnoticMessage:textField.text];

textField.text = @"";

[textField resignFirstResponder];

return YES;

}

Build and run the application, and enter some text into the text field. After tapping the return key, the text should be displayed on instances of UILabel across the view.

Motion Effects

iOS devices have a lot of powerful components embedded within them. A few of these – the accelerometer, magnetometer, and gyroscope – help determine the orientation of the device. They are how the device knows, for example, whether to display in portrait or landscape orientation. Starting in iOS 7, Apple introduced a way for applications to easily take advantage of these sensors by adding built-in parallax.

When you drive down the road, the signs along the shoulder appear to move much more quickly than trees in the distance. Your brain interprets this difference in apparent speed as movement in space. This visual effect is called “parallax”. With iOS 7, you have probably noticed this on the home screen where the icons appear to move relative to the wallpaper when you tilt the device. It is used subtly (and not so subtlety) in various places across the operating system and bundled apps, including the red badges on Home screen icons, the volume changer pop-up, and alert views.

Applications can access the same technology that powers those effects by using the UIInterpolatingMotionEffect class. Instances are given an axis (either horizontal or vertical), a key path (which property of the view do you want to impact), and a relative minimum and maximum value (how much the key path is allowed to sway in either direction).

In BNRHypnosisViewController.m, modify the drawHypnoticMessage: method to add a vertical and horizontal motion effect to each label that allows its center to sway 25 points in either direction.

[self.view addSubview:messageLabel];

UIInterpolatingMotionEffect *motionEffect;

motionEffect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x"

type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];

motionEffect.minimumRelativeValue = @(-25);

motionEffect.maximumRelativeValue = @(25);

[messageLabel addMotionEffect:motionEffect];

motionEffect = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y"

type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];

motionEffect.minimumRelativeValue = @(-25);

motionEffect.maximumRelativeValue = @(25);

[messageLabel addMotionEffect:motionEffect];

}

In order to test motion effects, the application must be running on a device. If you have a device provisioned for developer use, build and run the application on the device. Add some hypnotic messages to the view, and tilt the device slightly relative to your face. You will notice the magical illusion of depth that the motion effects provide.

Using the Debugger

When an application is launched from Xcode, the debugger is attached to it. The debugger monitors the current state of the application, like what method it is currently executing and the values of the variables that are accessible from that method. Using the debugger can help you understand what an application is actually doing, which, in turn, helps you find and fix bugs.

Using breakpoints

One way to use the debugger is to set a breakpoint. Setting a breakpoint on a line of code pauses the execution of the application at that line (before it executes). Then you can execute the subsequent code line by line. This is useful when your application is not doing what you expected and you need to isolate the problem.

In the project navigator, select BNRHypnosisView.m (not BNRHypnosisViewController.m). Find the line of code in initWithFrame: that sets the circleColor property to light gray. Set a breakpoint by clicking the gutter (the lightly shaded bar on the left side of the editor area) next to that line of code (Figure 7.6). The blue indicator shows where the application will “break” the next time you run it.

Figure 7.6 A breakpoint

A breakpoint

Build and run the application. The application will start and then halt before the line of code where you put the breakpoint is executed. Notice the light green indicator and shading that appear to show the current point of execution.

Now your application is temporarily frozen in time, and you can examine it more closely. In the navigator area, click the A breakpoint tab to open the debug navigator. This navigator shows a stack trace of where the breakpoint stopped execution (Figure 7.7). A stack trace shows you the methods and functions whose frames were in the stack when the application broke. The slider at the bottom of the debug navigator expands and collapses the stack. Drag it to the right to see all of the methods in the stack trace.

Figure 7.7 The debug navigator

The debug navigator

The method where the break occurred is at the top of the stack trace. It was called by the method just below it, which was called by the method just below it, and so on. Notice that the methods that you have written code for are in black text while the methods belonging to Apple are in gray.

Select the method at the top of the stack. In the debug area below the editor area, check out the variables view to the left of the console. This area shows the variables within the scope of BNRHypnosisView’s initWithFrame: method along with their current values (Figure 7.8).

Figure 7.8 Debug area with variables view

Debug area with variables view

(If you do not see the variables view, find the Debug area with variables view control in the bottom righthand corner of the console. Click the left icon to show the variables view.)

In the variables view, a variable that is a pointer shows the object’s address. You can see that self has an address because in the context of this method, self is a pointer to the instance of BNRHypnosisView, and this instance was allocated before the application halted.

Click the disclosure button next to self. The first item under self is the superclass. The superclass of BNRHypnosisView is UIView. Clicking the disclosure button next to UIView will show the variables self inherits from its superclass.

BNRHypnosisView has a variable of its own, _circleColor. The breakpoint is set to the line that sets the circleColor property. That line of code has yet to be executed, so _circleColor currently points to nil.

Stepping through code

In addition to giving you a snapshot of the application at a given point, the debugger also allows you to step through your code line by line and see what your application does as each line executes. The buttons that control the execution are on the debugger bar that sits between the editor area and the debug area (Figure 7.9).

Figure 7.9 Debugger bar

Debugger bar

Click the button that steps over a line. This will execute just the current line of code, which sets circleColor. Notice that the green execution indicator and shading move to the next line. Even more interesting, the variables view shows that the value of _circleColor has changed to a valid address.

At this point, you could continue stepping through the code to see what happens. Or you could click the button to continue executing your code normally. Or you could step into a method. Stepping into a method takes you to the method that is called by the line of code that currently has the green execution indicator. Once you are in the method, you have the chance to step through its code in the same way.

When you step out of a method, you are taken to the method that called it. To try it out, click the button to step out of the current method. You will be taken to the implementation of loadView in BNRHypnosisViewController.m.

Deleting breakpoints

To run your application normally again, you are going to get rid of the breakpoint. Right-click the blue indicator and select Delete Breakpoint. You can build and run to confirm that the application runs as expected.

Sometimes, a developer will set a breakpoint and forget about it. Then, when the application is run, execution stops, and it looks like the application has crashed. If an application of yours unexpectedly stops, make sure you are not halting on a forgotten breakpoint.

If you are not sure where you may have left a breakpoint, you can view a list of breakpoints in your project in the breakpoint navigator (the Deleting breakpoints tab in the navigator area).

Setting an exception breakpoint

You can also tell the debugger to break automatically on any line that causes your application to crash or that causes an exception to be thrown.

In the navigator area, select the Setting an exception breakpoint tab to open the breakpoint navigator. At the bottom of this navigator, click the + icon and select Add Exception Breakpoint....

Figure 7.10 Adding an exception breakpoint

Adding an exception breakpoint

If your application is throwing exceptions and you are not sure why, adding an exception breakpoint will help you pinpoint what is going on.

For the More Curious: main() and UIApplication

A C application begins by executing a main function. An Objective-C application is no different, but you have not seen main() in any of your iOS applications. Let’s take a look now.

Open main.m in the HypnoNerd project navigator. It looks like this:

int main(int argc, char *argv[])

{

@autoreleasepool {

return UIApplicationMain(argc, argv,

nil, NSStringFromClass([BNRAppDelegate class]));

}

}

The function UIApplicationMain creates an instance of a class called UIApplication. For every application, there is a single UIApplication instance. This object is responsible for maintaining the run loop. Once the application object is created, its run loop essentially becomes an infinite loop: the executing thread will never return to main().

Another thing the function UIApplicationMain does is create an instance of the class that will serve as the UIApplication’s delegate. Notice that the final argument to the UIApplicationMain function is an NSString that is the name of the delegate’s class. So, this function will create an instance ofBNRAppDelegate and set it as the delegate of the UIApplication object.

The first event added to the run loop in every application is a special “kick-off” event that triggers the application to send a message to its delegate. This message is application:didFinishLaunchingWithOptions:. You implemented this method in BNRAppDelegate.m to create the window and the controller objects used in this application.

Every iOS application follows this pattern. If you are still curious, go back and check the main.m file in the Quiz application that you wrote in Chapter 1.

Silver Challenge: Pinch to Zoom

Add pinch-to-zoom to the Hypnosister project from Chapter 5.

The first step is to give the scroll view a delegate:

· BNRAppDelegate should conform to the UIScrollViewDelegate protocol.

· In application:didFinishLaunchingWithOptions:, set the scroll view’s delegate property.

To perform as the scroll view’s delegate, BNRAppDelegate will need a property that points to the instance of BNRHypnosisView. Add this property in a class extension in BNRAppDelegate.m and update the rest of the code to use the property instead of the BNRHypnosisView local variable.

To set up the scroll view, you will need to give it one BNRHypnosisView as a subview and turn off the paging. The scroll view also needs limits on how much it can zoom in and out. Find the relevant UIScrollView properties to set in this class’s reference page in the documentation.

Finally, you will need to implement the scroll view delegate method viewForZoomingInScrollView: to return the BNRHypnosisView.

If you get stuck, visit the reference pages for the UIScrollView class and for the UIScrollViewDelegate protocol.

To simulate two fingers in the simulator to test your zooming, hold down the Option key while using the mouse.