Search This Blog

Monday, April 4, 2011

iPhone Programming: Who Just Called Me?

The best way to learn programming is to solve tasks that irk you. Well, here is something that irked the heck out of me. I often receive calls from outside of the US (India, Korea, Japan etc.). Very often, the incoming caller id leaves out the prefix. Example, instead of "+91 981234 5678"  I get just "9812345678", or, instead of "+82 111 11111" I just get "82 111 11111". The irritating part is that the iPhone cannot map it to my address book. Fine. But there is the additional irritant - If I search the iPhone address book, I can't search by phone number :-(

Damn. So now I am left guessing who just called me. It happened so often, I finally decided to do something about it. Or, I decided to solve the damn problem.



So I had to learn how to:

1. Access the call log, so I could check my recent call list and select the number I wanted to check

2. Access my address book and search for that number (substring search)

Well, as it turns out, "1" is impossible, unless the phone is jailbroken. There are no APIs to access call log. Well, not much harm done - you can always enter the number in the app.

So here is what I came up with: (I've blurred the numbers)





And now, an explanation of the code:

Note: I am just going to explain the main parts of the program, the "WhoCalledMeViewController" .m and .h files. The rest are standard stuff. You can download my sources and explore.

WhoCalledMeViewController.h

[objc]
//
// WhoCalledMeViewController.h
// WhoCalledMe
//
// Created by Arjun on 4/1/11.
// Copyright 2011 Hughes Systique Corp. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>
#import <AddressBookUI/AddressbookUI.h>
#import "MBProgressHUD.h"
[/objc]

A few notes here: You need to include the AddressBook headers as well as add the AddressBook framework to your project. I don't really think you need the AddressBookUI framework as we are not using the address book contact picker UI here. You can try removing it. Finally, MBProgressHUD is a great 3rd party activity indicator that is really easy to use. All you need to do is drop MBProgressHUD.h/m into your project and use away.

[objc]
@interface WhoCalledMeViewController : UIViewController <MBProgressHUDDelegate> {
IBOutlet UITextView *listOfMatches; // will contain all the names & phones that match
IBOutlet UITextField *number; // holds the number you enter
NSMutableArray* _addressBookNames; // holds a list of all the address book names
NSMutableArray* _addressBookPhones; // holds a list of all the phone #s (there is a 1:1 between _addressBookPhones and Names)
MBProgressHUD *HUD; // used to display an activity indicator
}
@property (nonatomic, retain) UITextView *listOfMatches;
@property (nonatomic, retain) UITextField *number;
@property(nonatomic, retain)NSMutableArray *_addressBook;
- (IBAction) bkgrndClick:(id)sender; // make sure keyboard disappears on tap
- (IBAction) findPhone:(id)sender; // the function that searches your address book for the number
-(void)loadAddressBook; // called at the start to load the contact book into an array for faster search
-(IBAction)initHudAndAB; // loadAddressBook is called by this function, actually
@end
[/objc]

I think the comments in the header file are clear. the bkgrndClick function is a typical trick people use to make keyboards disappear when you tap on the screen. Basically it involves add a custom button equal to the size of the screen behind everything and then detect when the user taps it - when tapped, we will have the keyboard disappear.

One quick note: When you use MBProgressHUD, you need to implement some of its required methods in your file. The <MBProgressHUDDelegate> instruction tells iOS that this file will implement these methods.

Okay, now lets get into the implementation:

WhoCalledMeViewController.m

[objc]
//
// WhoCalledMeViewController.m
// WhoCalledMe
//
// Created by Arjun on 4/1/11.
// Copyright 2011 Hughes Systique Corp. All rights reserved.
//
#import "WhoCalledMeViewController.h"
@implementation WhoCalledMeViewController
@synthesize number;
@synthesize listOfMatches;
@synthesize _addressBook;
// This takes care of the keypad disappearing if you tap outside of it
-(IBAction) bkgrndClick:(id) sender
{
[number resignFirstResponder];
}
[/objc]

Gee. I just explained this. The 'tap in the background to make the keyboard go away' trick.

[objc]
- (void)dealloc
{
[super dealloc];
}
- (void)didReceiveMemoryWarning
{
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)viewDidUnload
{
[super viewDidUnload];
[_addressBookPhones release];
[_addressBookNames release];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
[/objc]

Standard callbacks. Just making sure we release the global arrays on exit, and we only support portrait mode.

[objc]
// MBProgressHUD calls this when the progress bar is over
- (void)hudWasHidden {
// Remove HUD from screen when the HUD was hidded
[HUD removeFromSuperview];
[HUD release];
}
[/objc]

MBProgressHUD has some mandatory callbacks. This one is called when the HUD activity completes. the HUD is removed from the view and deallocated.

[objc]
// this is called when you press the address book button
// it is also called by initHudAndAB for the first time
// the goal of this function is to iterate through the address book
// and suck out ALL phone #s associated to a ALL names
-(void)loadAddressBook
{
// remember this function will be called each time you refresh the address book
// by pressing the address book button, so make sure you release before alloc again
if ( _addressBookNames) { [_addressBookNames release]; }
if ( _addressBookPhones) { [_addressBookPhones release]; }
HUD.labelText = @"Loading Address Book...";
ABAddressBookRef _addressBookRef = ABAddressBookCreate();
NSArray* allPeople = (NSArray *)ABAddressBookCopyArrayOfAllPeople(_addressBookRef);
_addressBookNames = [[NSMutableArray alloc] initWithCapacity:[allPeople count]];
_addressBookPhones = [[NSMutableArray alloc] initWithCapacity:[allPeople count]];
HUD.labelText=[NSString stringWithFormat:@"Loading %d records",[allPeople count]];
// now iterate though all the records and suck out phone numbers
for (id record in allPeople) {
CFTypeRef phoneProperty = ABRecordCopyValue((ABRecordRef)record, kABPersonPhoneProperty);
NSArray *phones = (NSArray *)ABMultiValueCopyArrayOfAllValues(phoneProperty);
CFRelease(phoneProperty);
for (NSString *phone in phones) {
NSString* compositeName = (NSString *)ABRecordCopyCompositeName((ABRecordRef)record);
//so, if a name has multiple phone numbers, we create duplicate records
// of the name and each phone #
[_addressBookNames addObject:compositeName];
[_addressBookPhones addObject:phone];
[compositeName release];
}
[phones release];
}
CFRelease(_addressBookRef);
[allPeople release];
allPeople = nil;
}
[/objc]

Okay, so what we do is that when the app first loads, we read the entire address book and copy it into two global arrays. The first one contains the names and the second contains the phone numbers. We ensure they both have the same # of entries. In other words, if 'Bob' has 3 numbers, we populate the Names array with 3 'Bobs' each with a single number.
You need to use the ABxxx (AddressBook) APIs of iOS to read/manipulate the address book. Also,
kABPersonPhoneProperty is used to retrieve _all_ the phone numbers associated to an entry. Note that a single records can have many phone numbers. So first, we iterate the address book and get each records, and then iterate each record to retrieve the phone #s associated to it. As we get each phone#, we populate our global arrays appropriately.

[objc]
// This is called when you press the address book icon. Basically, it shows and activity indicator
// and loads the address book in a separate thread. It calls loadAddressBook
-(IBAction)initHudAndAB
{
// Now lets show a progress dialog
HUD = [[MBProgressHUD alloc] initWithWindow:[UIApplication sharedApplication].keyWindow];
HUD.mode = MBProgressHUDModeIndeterminate;
// Add HUD to screen
[self.view.window addSubview:HUD];
// Regisete for HUD callbacks so we can remove it from the window at the right time
HUD.delegate = self;
HUD.labelText = @"Working...";
// Show the HUD while the provided method executes in a new thread
[HUD showWhileExecuting:@selector(loadAddressBook) onTarget:self withObject:nil animated:YES];
}
[/objc]

Note however that we don't directly call loadAddressBook when the app starts. This is because we want to display a HUD while the contact book is loading (because it may take some time). MBProgressHUD requires that we display the indicator, and then launch the 'time consuming operation' in a separate thread. So instead, when the user starts the app, we call initHudAndAB, which basically displays a progressHUD and then launches a thread which calls loadAddressBook. Caution: Make sure you don't do an UI updates directly in a background thread - you will face all sorts of data sync and mutex lock problems. Google around if you want to know more.

[objc]
// This function is called when you press the phone icon. It searches the address book (actually, array that
// was preloaded) for any substring match of the phone number you entered
-(IBAction)findPhone:(id) sender
{
NSString *foundNames=@"";
listOfMatches.text = @"Searching for Matches...";
int maxCount=0;
for (int i=0; i<[_addressBookPhones count];i++)
{
NSString *item = [_addressBookPhones objectAtIndex:i];
// nifty trick to remove all other characters in a phone #
// remember 3015271111 can be stored in many ways using +, (, ), . etc characters.
// we want to eliminate all characters that are not digits and them compare
NSCharacterSet* nonDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet]; // all characters that are not digits
NSString *strippedPhone = [[item componentsSeparatedByCharactersInSet: nonDigits] componentsJoinedByString: @""]; // take them off
NSRange textRange = [strippedPhone rangeOfString:number.text]; // substring search
if(textRange.location != NSNotFound) // means found
{
foundNames = [foundNames stringByAppendingString:@"\n"];
foundNames = [NSString stringWithFormat:@"%@%@ :%@",foundNames, [_addressBookNames objectAtIndex:i], item];
maxCount++;
}
if (maxCount >1000) { i = [_addressBookPhones count]; } // just take the first 1000 matches
} //for
listOfMatches.text = foundNames;
}
@end
[/objc]

Okay, and this function does the job of searching the address book for the number you entered in the text box. Basically, it iterates through the global phone list array that was populated when the app started and checks if the number entered occurs anywhere as a substring. This is a further detail to this: the iPhone can store numbers in various formats like +1 (240) 1112222 or 1 (240) (111) (22) 22 or 1.240.111.222 or whatever else. Obviously, if the user enters 1222 we want it to match these records. So to do that, we strip the phone # of all non digits using a cute NSCharacter operation and then compare.

Finally, I took another short cut - I just used a UITextView to display the results - it is very convenient because it allows us to have a scrollable list display of data. If you prefer, convert this to a TableView yourself. Its a little more work.

Get the Source

Download the project HERE

4 comments:

  1. [...] will not be explaining how to use the HUD. For that take a look at my other tutorial here. Watch the latest videos on [...]

    ReplyDelete
  2. Would you please to tell me that which xcode and ios version you used in this tutorial? I got the error message in Device mode "*** -[UIWindow setRootViewController:]: unrecognized selector sent to instance 0x13ae20".
    Thanks.
    Steve

    ReplyDelete