Welcome to the sixth installment in our series on how to both design and build a 1980s version of the iOS “Phone” app. In this tutorial I'll be demonstrating how to programmatically launch a phone call from within the iOS SDK and how to respond to user touch events on the keypad.
Final App Preview
This is a snapshot of what we will be building over the course of this series:
Before You Begin. . .
This is a multi-part series designed to teach intermediate iOS SDK topics. The content will become increasingly complex as the series progresses. If at any point you find yourself lost in following this series, you might need to take a step back and work your way through our Learn Objective-C series or our Beginning iOS SDK Development series.
In the last tutorial in this series, I demonstrated how to skin the phone screen in Xcode 4. In this tutorial, we'll proceed with connecting the interface to our class files and actually code the phone screen logic.
Step 1: Setup the IBOutlets and IBActions
Before we can start coding the phone screen logic, we need to connect the Interface Builder objects added in part four of this series with the XIB "File's Owner" class PhoneViewController
. Doing so will allow us to reference the IB objects directly in our PhoneViewController
code.
Begin by opening the PhoneView.xib
file. Next, select View > Editor > Assistant or just click the "Show the Assistant Editor" button near the top-right of the Xcode toolbar. The assistant window should now be displaying the PhoneViewController.h
file, and your screen should look something like this:
In the code editor pane, create an IBOutlet
UILabel
to reference the label that will display the phone number entered by the user:
@interface PhoneViewController : UIViewController { } @property(nonatomic, retain) IBOutlet UILabel *phoneNumberLabel; @end
Next, connect the UILabel
to the outlet by CTRL + Clicking on the label in Interface Builder, holding, and dragging to the IBOutlet
in the code editor:
After you've connected the object in IB, go ahead and synthesize this property in the PhoneViewController.m
file and be sure to clean up after the memory in the dealloc:
and viewDidUnload:
methods:
@implementation PhoneViewController @synthesize phoneNumberLabel; - (void)viewDidUnload { [self setPhoneNumberLabel:nil]; [super viewDidUnload]; } - (void)dealloc { [phoneNumberLabel release]; [super dealloc]; }
While we're modifying the phoneNumberLabel
code, go ahead and remove the dummy phone number value currently set in Interface Builder. This field should be blank until the user begins tapping on the keypad.
Next, create the following three IBAction
declarations in PhoneViewController.h
:
@interface PhoneViewController : UIViewController { } @property(nonatomic, retain) IBOutlet UILabel *phoneNumberLabel; -(IBAction)numberButtonPressed:(UIButton *)pressedButton; -(IBAction)deleteButtonPressed:(UIButton *)pressedButton; -(IBAction)dialButtonPressed:(UIButton *)pressedButton; @end
CTRL + Click on the "1" keypad button to reveal a list of actions available for this button, and then drag from the "Touch up inside" circle to the -(IBAction)numberButtonPressed:
method declaration.
Repeat this process for numbers 2 - 9 and 0, connecting each to the same IBAction
method.
Next connect the "Del" button to -(IBAction)deleteButtonPressed:
and the "Dial" button to -dialButtonPressed:
.
With the necessary connections made, the buttons should now configured to call the appropriate method when tapped. To test this, save your work in Interface Builder and PhoneViewController.h
and switch to the PhoneViewController.m
file. Add in the following code:
-(IBAction)numberButtonPressed:(UIButton *)pressedButton { NSLog(@"Number keypad button pressed."); } -(IBAction)deleteButtonPressed:(UIButton *)pressedButton { NSLog(@"Delete keypad button pressed."); } -(IBAction)dialButtonPressed:(UIButton *)pressedButton { NSLog(@"Dial keypad button pressed."); }
Save the file and run the application. Give a tap on each of the buttons in the keypad. Did you see the appropriate console message logged in the Xcode debug area? If so, you've completed this step successfully!
Step 2: Display Dialed Numbers
With the IBAction
and IBOutlet
connections made, our next step is to display the keypad numbers in phoneNumberLabel
when tapped.
This can be done with just one line of code:
-(IBAction)numberButtonPressed:(UIButton *)pressedButton { self.phoneNumberLabel.text = [self.phoneNumberLabel.text stringByAppendingString: pressedButton.titleLabel.text]; }
Because the text
property of phoneNumberLabel
is of type NSString
, we are able to simply use the stringByAppendingString:
method from the NSString
class to concatenate the tapped button's title with the existing value of the phoneNumberLabel
text property.
Simple enough. However, wouldn't it be nice if phone number formatting was auto-applied to the label as the user taps along? This is how the official Apple iOS Phone app works, and it is what we will achieve in our application as well.
In order to customize the display, it would be useful to have a temporary NSString
object that we can store just the phone digits within. Otherwise, we'd need to constantly strip special characters like "( )" and "-" directly from the text property of phoneNumberLabel
, making for kludgy code.
Go back to PhoneViewController.h
and add the following property:
@interface PhoneViewController : UIViewController { } @property(nonatomic, retain) NSString *phoneNumberString; @property(nonatomic, retain) IBOutlet UILabel *phoneNumberLabel;
Then synthesize this property in the implementation file:
@implementation PhoneViewController @synthesize phoneNumberString; @synthesize phoneNumberLabel;
Finally, we want this value to be allocated into memory when the PhoneViewController
object is initially unarchived from the XIB file, and we want it to be deallocated from memory when the PhoneViewController
object is destroyed.
To allocate the property when the XIB is unarchived, add the following to PhoneViewController.m
:
-(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if(self) { phoneNumberString = [[NSString alloc] init]; } return self; }
Next, clean up the memory we just added by adding the following line to the dealloc
method:
- (void)dealloc { [phoneNumberString release]; [phoneNumberLabel release]; [super dealloc]; }
Now back to formatting the values placed in the label.
First, update the numberButtonPressed
function to use the newly created phoneNumberString
variable instead of the label:
-(IBAction)numberButtonPressed:(UIButton *)numberButton { self.phoneNumberString = [self.phoneNumberString stringByAppendingString:pressedButton.titleLabel.text]; self.phoneNumberLabel.text = self.phoneNumberString; }
This is a good time to save your files, build, and run the application again. The result should be the same as when you were only using the UILabel
, but iterative testing throughout the development cycle is often the best way to find bugs in your own code.
Before moving on to actually formatting the text prior to display in phoneNumberLabel
, let's take a few minutes to play with the official Phone app on an iPhone device to determine how our application should behave.
As an aside, a ten-digit U.S. number can be broken down into three consituent parts: the area code, the prefix number, and the line number (for more on what these numbers represent, see here). We'll use these naming conventions in our source code.
If you were watching closely while tapping in numbers on an iPhone, you'll notice that a single dash (i.e. "-") character appears when the user enters the fourth digit, and that this dash is replaced with area code parentheses and a second dash character when the user enters the eighth digit. This makes perfect sense as it successfully accounts for both seven digit local calls (i.e. 555-5555) and ten digit long-distance calls (i.e. (555) 555-5555).
To replicate this behavior, we'll need to alter the formatting of the phone number label when the user reaches a number between four and ten digits long or when the user reaches a number between seven digits and ten digits long. Per the functionality of the iPhone Phone app, anything entered that is either longer or shorter than those values will simply be displayed as a single, solid line of digits.
Let's begin by formatting numbers entered between four and seven digits long:
-(IBAction)numberButtonPressed:(UIButton *)pressedButton { self.phoneNumberString = [self.phoneNumberString stringByAppendingString:pressedButton.titleLabel.text]; if([self.phoneNumberString length] >= 4 && [self.phoneNumberString length] <= 7) { NSRange prefixRange = {0, 3}; NSString *prefixString = [self.phoneNumberString substringWithRange:prefixRange]; NSString *lineNumberString = [self.phoneNumberString substringFromIndex:3]; self.phoneNumberLabel.text = [NSString stringWithFormat:@"%@-%@", prefixString, lineNumberString]; }
In the code above, we first use the length
method of NSString
, which returns a count of the total numbers in the receiving string, to craft a conditional that will only be executed when the phone number is between four and seven digits long.
Next we create an NSRange
to hold the prefix position entered, that is, placeholder characters 1 - 3 in the phoneNumberString
. The next line is a temporary variable to actually hold the number prefix data. Then we use the substringFromIndex:
method to take the portion of the phone string from the index number supplied to the end of the string. Finally, we use the stringWithFormat:
method to craft our custom display string and assign it to the text
property of the UILabel
.
We need to take similar steps for digits entered in the eight to ten range:
else if([self.phoneNumberString length] >= 8 && [self.phoneNumberString length] <= 10) { NSRange areaCodeRange = {0, 3}; NSString *areaCodeString = [self.phoneNumberString substringWithRange:areaCodeRange]; NSRange prefixRange = {3, 3}; NSString *prefixString = [self.phoneNumberString substringWithRange:prefixRange]; NSString *lineNumberString = [self.phoneNumberString substringFromIndex:6]; self.phoneNumberLabel.text = [NSString stringWithFormat:@"(%@) %@-%@", areaCodeString, prefixString, lineNumberString]; }
The same basic pattern is used again above. The phoneNumber
string is split into several substrings using the substringWithRange:
and substringFromIndex:
methods of NSString
before they are joined together again in the desired format by stringWithFormat:
.
Of course, if the number of digits entered by the user is either greater than ten or less than four, we want to just display them without any special formatting:
else { self.phoneNumberLabel.text = self.phoneNumberString; } }
The code above should correctly format the digits pressed by the user as they are entered on the keypad. Save, build, and run your application once again now.
If all is well so far, congrats! Next we're going to make the code a bit more modular by placing the display logic we just wrote into a function named -(void)displayPhoneNumber
. Why? Because this display logic will be needed both when adding new digits to the display and when deleting them, so it's best not to repeat ourselves.
In the end, the displayPhoneNumber
and numberButtonPressed:
methods should look like this:
-(void)displayPhoneNumber { if([self.phoneNumberString length] >= 4 && [self.phoneNumberString length] <= 7) { NSRange prefixRange = {0, 3}; NSString *prefixString = [self.phoneNumberString substringWithRange:prefixRange]; NSString *lineNumberString = [self.phoneNumberString substringFromIndex:3]; self.phoneNumberLabel.text = [NSString stringWithFormat:@"%@-%@", prefixString, lineNumberString]; } else if([self.phoneNumberString length] >= 8 && [self.phoneNumberString length] <= 10) { NSRange areaCodeRange = {0, 3}; NSString *areaCodeString = [self.phoneNumberString substringWithRange:areaCodeRange]; NSRange prefixRange = {3, 3}; NSString *prefixString = [self.phoneNumberString substringWithRange:prefixRange]; NSString *lineNumberString = [self.phoneNumberString substringFromIndex:6]; self.phoneNumberLabel.text = [NSString stringWithFormat:@"(%@) %@-%@", areaCodeString, prefixString, lineNumberString]; } else { self.phoneNumberLabel.text = self.phoneNumberString; } } -(IBAction)numberButtonPressed:(UIButton *)pressedButton { self.phoneNumberString = [self.phoneNumberString stringByAppendingString:pressedButton.titleLabel.text]; [self displayPhoneNumber]; }
Of course, you'll also need to add a method declaration in PhoneViewController.h
as well:
-(IBAction)deleteButtonPressed:(UIButton *)pressedButton; -(IBAction)dialButtonPressed:(UIButton *)pressedButton; -(void)displayPhoneNumber;
With these forward thinking steps completed, get ready to delete some digits!
Step 3: Activate the Delete Button
Removing a button from the phoneNumberString
variable is easy enough:
self.phoneNumberString = [self.phoneNumberString substringToIndex:([self.phoneNumberString length] - 1)];
We just need to calculate the current length of the string and subtract one character from the end. This is done above with the substringToIndex:
method, which essentially does the reverse: it returns all the characters up to a certain point, in this case, one minus the length of the string.
This works well enough, but what would happen with this code snippet if the user pressed delete button before entering a digit? We need to account for that edge case, and we also need to call our displayPhoneNumber
method again to properly format the output. When these two tasks are complete, the code should look like this:
-(IBAction)deleteButtonPressed:(UIButton *)numberButton { if([self.phoneNumberString length] > 0) { self.phoneNumberString = [self.phoneNumberString substringToIndex:([self.phoneNumberString length] - 1)]; } [self displayPhoneNumber]; }
You know the drill: save, build, and run the app again to see this step in action.
Step 4: Making Phone Calls
Our last step is likely also the most anticipated: launching a phone call from within our app! Doing so will only take three lines of code:
-(IBAction)dialButtonPressed:(UIButton *)numberButton { NSString *phoneLinkString = [NSString stringWithFormat:@"tel:%@", self.phoneNumberString]; NSURL *phoneLinkURL = [NSURL URLWithString:phoneLinkString]; [[UIApplication sharedApplication] openURL:phoneLinkURL]; }
First, we create a special URL string that is prefixed with "tel:". This indicates to the iOS SDK that we want to call the Phone application and pass in the proceeding variable, in this case the value stored in phoneNumberString
.
The next line actually creates an NSURL
object that we will use to launch the call, and the final line sends a message to the UIApplication
object to actually launch the call. The details of UIApplication
are beyond the scope of this tutorial, but it is often used when you want to open applications outside of your own, including Mobile Safari and the Maps app.
If everything went well, you should now be able to enter and delete numbers on the keypad as well as launch a phone call from within our app!
In case you didn't notice, there is one small but very important detail missing: tapping on an individual digit is a silent event, and what kind of an 80s phone would be complete without sound effects? This issue will be addressed in a future tutorial.
Conclusion
Over the course of this tutorial, I've shown you how to respond to touch events on the keypad and programmatically initiate a phone call with the iOS SDK. If you have any questions, feel free to leave them in the comment section below!
Comments