You are browsing the archive for iOS.

Alternative Autocomplete UITextField

6:30 pm in Conclave, Presentation by Andrew Wimpy

For a project I am working on, I needed to implement a search bar that would automatically complete the word the user is typing into the search bar and display relevant results. However, I didn’t want this to work like the default search bar functionality in objective c where the search bar is tied to a table view that takes up most of the on screen real estate. Instead I needed this auto complete search bar to visually be more like the search bar on Google. I have attached an example Xcode Project that demonstrates this functionality on an iPhone.

As can be seen in the image above the only two objects needed to make this work are a UITextField and a UITableView. The UITextField will act as our search bar, and the UITableView will be populated with our auto complete elements. Other than that you just need to have the correct logic.

Step 1: Create Objects

First, we will be creating the UITextField and the UITableView. The table and text fields can be configured according to what is needed in your app, but I would always create the UITable with a width of about 2 to 4 pixels less than the text field. This will allow the auto complete functionality to be visually more like the Google search bar. The one thing to note is that the table is initially hidden. This is done because we only want the table to display when the user is typing into the text field.

//Search Bar
txtField = [[UITextField alloc] initWithFrame:CGRectMake(5, 20, 261, 41)];
txtField.borderStyle = 3; // rounded, recessed rectangle
txtField.autocorrectionType = UITextAutocorrectionTypeNo;
txtField.textAlignment = UITextAlignmentLeft;
txtField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
txtField.returnKeyType = UIReturnKeyDone;
txtField.font = [UIFont fontWithName:@"Trebuchet MS" size:22];
txtField.textColor = [UIColor blackColor];
[txtField setDelegate:self];
[self.view addSubview:txtField];
[txtField release];

//Autocomplete Table
autoCompleteTableView = [[UITableView alloc] initWithFrame:CGRectMake(6, 56, 259, tableHeight) style:UITableViewStylePlain];
autoCompleteTableView.delegate = self;
autoCompleteTableView.dataSource = self;
autoCompleteTableView.scrollEnabled = YES;
autoCompleteTableView.hidden = YES;
autoCompleteTableView.rowHeight = tableHeight;
[self.view addSubview:autoCompleteTableView];
[autoCompleteTableView release];

We now need to create an array that will be what we are searching through to populate the autoCompleteTableView. I have included a text file with sample elements to populate this array. We first need to pull the contents of the file into memory and then convert the bytes from the file into a string. This string will then be split around newline characters to create our array. Finally, we will be creating our auto complete array that will hold the elements that correspond with what is being typed in the search bar.

//Pull the content from the file into memory
NSString *filePath = [NSString stringWithFormat:@"/Users/drewnextpression/AutoCompleteTextField/elements.txt"];
NSData* data = [NSData dataWithContentsOfFile:filePath];

//Convert the bytes from the file into a string
NSString* string = [[[NSString alloc] initWithBytes:[data bytes]
length:[data length]
encoding:NSUTF8StringEncoding] autorelease];

//Split the string around newline characters to create an array
NSString* delimiter = @"\n";
NSArray *item = [string componentsSeparatedByString:delimiter];
elementArray = [[NSMutableArray alloc] initWithArray:item];
autoCompleteArray = [[NSMutableArray alloc] init];

Step 2: Text Field Logic

As the user types into the text field, the text will be converted into a substring by using UITextfield’s delegate method shouldChangeCharactersInRange. Once this substring has been created, it will be passed to the method searchAutocompleteEntriesWithSubstring.

The method searchAutocompleteEntriesWithSubstring will compare the substring of the text field against the elements in the element array to see if any of the element’s beginning characters are the same as the substring. This is done by grabbing each element one at a time from the array of elements. An NSRange value is then created that will have the range and location of the substring in the element. Since I wanted the search to be case insensitive I have two different ranges. One NSRange will check for lowercase and another will check for uppercase.

Once these ranges are found, their length can be used to determine if the substring occurred at the beginning of the element. Since NSRange will return a length of 0 if the substring isn’t in the beginning of the current element, we can use this logic to only add elements to the autoCompleteArray that have an NSRange of length != 0.

Finally, the table needs to be unhidden since the user is typing into the text field, and it needs to be reloaded since it is being drawn based on the elements in the autoCompleteArray.

// String in Search textfield
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSString *substring = [NSString stringWithString:textField.text];
substring = [substring stringByReplacingCharactersInRange:range withString:string];
[self searchAutocompleteEntriesWithSubstring:substring];
return YES;
}

// Take string from Search Textfield and compare it with autocomplete array
- (void)searchAutocompleteEntriesWithSubstring:(NSString *)substring {

// Put anything that starts with this substring into the autoCompleteArray
// The items in this array is what will show up in the table view

[autoCompleteArray removeAllObjects];

for(NSString *curString in elementArray) {
NSRange substringRangeLowerCase = [curString rangeOfString:[substring lowercaseString]];
NSRange substringRangeUpperCase = [curString rangeOfString:[substring uppercaseString]];

if (substringRangeLowerCase.length != 0 || substringRangeUpperCase.length != 0) {
[autoCompleteArray addObject:curString];
}
}

autoCompleteTableView.hidden = NO;
[autoCompleteTableView reloadData];
}

Step 3: TableView Logic

As noted in step 2, the table is being drawn based on the elements in the autoCompleteArray. If the array is empty, the table will be drawn with one empty cell. If the array has one element, the table will be drawn with one cell displaying this element. If the array has two elements, the table will be drawn with two cells displaying the elements. If the array has three elements or more, the table will be drawn with three cells and will be scrollable if there are more than three objects. The code for populating the table is found in the table delegate method cellForRowAtIndexPath, and the table drawing functionality is found in the tableview delegate method numberOfRowsInSection.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger) section {

//Resize auto complete table based on how many elements will be displayed in the table
if (autoCompleteArray.count >=3) {
autoCompleteTableView.frame = CGRectMake(6, 56, 259, tableHeight*3);
return autoCompleteArray.count;
}

else if (autoCompleteArray.count == 2) {
autoCompleteTableView.frame = CGRectMake(6, 56, 259, tableHeight*2);
return autoCompleteArray.count;
}

else {
autoCompleteTableView.frame = CGRectMake(6, 56, 259, tableHeight);
return autoCompleteArray.count;
}
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = nil;
static NSString *AutoCompleteRowIdentifier = @"AutoCompleteRowIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:AutoCompleteRowIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:AutoCompleteRowIdentifier] autorelease];
}

cell.textLabel.text = [autoCompleteArray objectAtIndex:indexPath.row];
return cell;
}

When the user clicks on an element in the table, the textfield will be populated with that element and the method finishedSearching will be called. This method will close the keyboard and hide the table.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
txtField.text = selectedCell.textLabel.text;
[self finishedSearching];
}

- (void) finishedSearching {
[txtField resignFirstResponder];
autoCompleteTableView.hidden = YES;
}

Step 4: Closing the Keyboard

I wanted the user to be able to close the keyboard by either pressing the Done button on the keyboard or tapping anywhere on the background. I used the text field’s delegate method textFieldShouldReturn to close the keyboard on a Done button press. Finally, I used the method touchesBegan: withEvent: to close the keyboard when the user touches the background.

// Close keyboard when Enter or Done is pressed
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
BOOL isDone = YES;

if (isDone) {
[self finishedSearching];
return YES;
} else {
return NO;
}
}

// Close keyboard if the Background is touched
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
[super touchesBegan:touches withEvent:event];
[self finishedSearching];
}

Step 5: Clean up and Release

At this point we need to release the autoCompleteArray and elementArray. The table and text field don’t need to be released since we already released them after we initialized them.

- (void)dealloc {
[autoCompleteArray dealloc];
[elementArray dealloc];
[super dealloc];
}

That’s it! Download the AutoCompleteTextField project and give it a go. If you find any bugs or have any suggestions let me know.

Source Code: AutoCompleteTextField.zip

iOS Sprite Animations with Notifications

6:30 pm in Conclave, Presentation by Justin Leger

On one of my first projects, I needed to do simple sprite animations using UIImageView. When I got into it, I quickly realized that I didn’t have the level of control that I needed.  So I created LIImageView and LIAnimationManagerView. I added the ability to load tile based sprite sheets, numbered sequenced images, UIImage decompression on loading and most importantly Observer Notifications on animation events (Start, Stop, Pause and Loop).

I have attached an example Xcode Project that shows most of all of the functionality of these classes, but play around with it and you’ll soon see the benefit over using the standard animations of UIImageView.

As you can see below, steps 1-6 are pieces of code from the viewDidLoad selector from the projects main ViewController.

Step 1: Importing Headers into View Controller

First things first, let’s import the header files for the new image view and animation manager.

#import "LIImageView.h"
#import "LIAnimationManagerView.h"

Step 2: Load Sprite Sheet

Next, we are going to load the animation sprite sheet. This sprite sheet has all the image animation frames for all the different states of the animation sequence. They are tiled into columns and rows and spaced with a specific Rect size. First we created a string with path to the file resource. Then we load the file into a UIImage using the decompression category class extension I added. This category extension decompresses the image into memory, during the initialization of the UIImage object, so that you don’t see a lag when the animation plays for the first time. Lastly, we define a Rect variable to use with this sprite sheet.

// Load Image
NSString *fileLocation = [[NSBundle mainBundle] pathForResource:@"BrianSheet.png" ofType:nil];
UIImage *image = [[UIImage alloc] initWithContentsOfFileDecompressed:fileLocation];
CGRect spriteRect = CGRectMake(0, 0, 61, 85);

Step 3: Build Animations From Sprite Sheet

Now we are going to build the LIImageView objects. We initialize the object with a tile sprite sheet and pass a start index, end index and a rect size of the sprite. If the sprite sheet dimensions are not dividable by the Rect dimensions then a nil is returned. Indexes are zero based and start in the upper left of the sprite sheet and wrap when it reaches the end of a row. If we have a 5×5 sprite sheet, the first row would contain indexes 0-4, second row would contain 5-9 and so on.

Next we set the repeat count of the animation loops. This is the same as UIImageView, the default value is 0, which specifies to repeat the animation indefinitely.

Now we can set the frame duration of the animation. I preferred this way setting animation timing rather than setting an animation duration.  I have included several predefined values for 5, 10, 15 and 30 FPS, but all you have to do is provide a double value.

// Build Drink Animations
LIImageView *drinkIn = [[LIImageView alloc] initWithTileImage:image fromIndex:12 toIndex:15 fromRect:spriteRect];
drinkIn.animationRepeatCount = 1;
drinkIn.animationFrameDuration = ImageAnimator10FPS;

LIImageView *drinkIdle = [[LIImageView alloc] initWithTileImage:image fromIndex:15 toIndex:15 fromRect:spriteRect];
drinkIdle.animationRepeatCount = 10;
drinkIdle.animationFrameDuration = ImageAnimator5FPS;

LIImageView *drinkOut = [[LIImageView alloc] initWithTileImage:image fromIndex:16 toIndex:22 fromRect:spriteRect];
drinkOut.animationRepeatCount = 1;
drinkOut.animationFrameDuration = ImageAnimator10FPS;

Step 4: Create Animation Manager and Add Animations

Here, we are going to set up an animation manager. This manager is a UIView and is responsible for storing animations by a state name and the loading and unloading playback controls of these animations by state. The managers Rect is usually the same size as the sprite animation size and can be placed anywhere in it parents view.

Animations are added to the manager with a string defining its state. I guess I should have called it a Key, but oh well.

// Create Animation Manager
animationManager = [[LIAnimationManagerView alloc] init];
animationManager.frame = CGRectMake(129, 85, 61, 85);
[self.view addSubview:animationManager];

// Add Drink Animations to Manager
[animationManager addAnimation:drinkIn withState:@"drinkIn"];
[animationManager addAnimation:drinkIdle withState:@"drinkIdle"];
[animationManager addAnimation:drinkOut withState:@"drinkOut"];

Step 5: Add Observers to LIImageView Objects

Here is the part that I really like. Each LIImageView will dispatch a notification when the state of the animation changes (Start, Stop, Pause and Loop). This way I can define a controlled sequence of animations. “drinkIn” loops once and the triggers “drinkIdle” when done. “drinkIdle” plays 10 times then triggers “drinkOut” when it finishes. There are countless possibilities and allows a lot of customization.

So, lets define what we want to happen when a state changes. The code below defines that when the drinkIn animation stops, start playing the drinkIdle animation. The callback selector playDrinkIdleAnimation tells the animation manager to play animation by state “drinkIdle”.

// Drink Event Listeners
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playDrinkIdleAnimation)
                                             name:ImageAnimatorDidStopNotification
                                           object:drinkIn];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playDrinkOutAnimation)
                                             name:ImageAnimatorDidStopNotification
                                           object:drinkIdle];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playWalkAnimation)
                                             name:ImageAnimatorDidStopNotification
                                           object:drinkOut];

Step 6: Clean Up and Release

Now that we have built all the animations from the sprite sheet and added all animations to the manager, we can release our reference to them. It’s now the managers responsibility to release.

// Clean Up
[image release];

[drinkIn release];
[drinkIdle release];
[drinkOut release];

Step 7: Create Notification Callback Selectors

Lastly, we add a few notification callback selectors to tell the animation manager to play an animation by state.

// Drink
- (void) playDrinkInAnimation {
	[animationManager playAnimationByState:@"drinkIn"];
}
- (void) playDrinkIdleAnimation {
	[animationManager playAnimationByState:@"drinkIdle"];
}
- (void) playDrinkOutAnimation {
	[animationManager playAnimationByState:@"drinkOut"];
}

// Walk
- (void) playWalkAnimation {
	[animationManager playAnimationByState:@"walk"];
}

That’s about it. Please download the LIImageViewSample Xcode project and play around with it. Please feel free to let me know if you find any bugs, defects or have any suggestions.

Source Xcode Project: LIImageViewSample.zip