sample

Word Prediction App Part 1: IOS TableView Filter Application

As one of my first apps in learning how to program in IOS with Objective-C, I decided to take a look into list filtering. Coming from learning Android first, I was interested in how IOS would tackle the entire design process. To keep things simple, the application is a single column, single section, multi-row tableview. This is just a listview in most other frameworks but under the hood it is just a tableview.

Using Apple’s MVC design scheme, this application includes a model that will handle list filtering and creating the initial data from a plist. The model will also provide a pointer to a filtered list which the controller will use to update the view.

All code can be found here: TableView Filter Application BitBucket


Let’s start out with the layout since Apple makes design-first so easily and fun. Here is the layout I chose for the application:

Storyboard Capture

We place a textview as our filter at the top of the application, a label to display all matches we find and finally the tableview as the body of the application.

In our controller class we need to tie our UI to the controller outlets and actions. The IBAction for the textview we are looking for is valueChanged, this will simulate a “key up” event most people are used too.


@interface ListFilterViewController ()

//our outlets for the UI objects
@property (nonatomic, readwrite, weak)IBOutlet UITextField *tvFilter;
@property (strong, nonatomic)IBOutlet UITableView *tableView;
@property (nonatomic, readwrite, weak)IBOutlet UILabel *matchLbl;

//our model objects
@property (strong, nonatomic)ListModel *listModel;
@property (strong, nonatomic)NSMutableArray *filterdList;

//actions from filter
- (IBAction)filterChanged:(UITextField *)sender;

@end

In order to connect the tableview we need to extend the following:

@interface ListFilterViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
//in more complex apps, each should have its own controller 
@end

This will allow us to implement the following 3 methods in our controller:
Utilizing the below, a simple list view should be viewable with 5 rows of “Cell Text”.

//table view

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    //return the sections we need, just 1 for this example since we want a single list
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    //return the number of rows in section, we are just going to put our array size here
    return 5;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //here we create a simple identifier for reusability 
    static NSString *cellID = @"SimpleID";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
    
    if (cell == nil) { //check if its nil, if it is we need to create a simple cell with the default style
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
    }
     
    cell.textLabel.text = "Cell Text"; //set the inner cell label text

    return cell; //finally return the individual cell 
}

For the model I created a simple ListModel class that creates an NSMutableArray and initializes with a plist containing approximately 140,000 words.

Here is the .h file

#import <Foundation/Foundation.h>

@interface ListModel : NSObject

@property (nonatomic, readwrite, strong) NSMutableArray *tableData;
@property (nonatomic, readwrite, strong) NSMutableArray *filteredList;

- (NSMutableArray *)sortWithSquence:(NSString *)sequence;

@end

And here is the .m file


#import "ListModel.h"

@implementation ListModel

@synthesize tableData = _tableData;
@synthesize filteredList = _filteredList;

- (id) init
{
    if(self == [super init])
    {
        NSLog(@"Creating table data");
        
        // Path to the plist (in the application bundle)
        NSString *path = [[NSBundle mainBundle] pathForResource:
                          @"words" ofType:@"plist"];
        // Build the array from the plist
        _tableData  = [[NSMutableArray alloc] initWithContentsOfFile:path];
        //create a 2nd array that will contain the filtered version 
        _filteredList = [[NSMutableArray alloc] init];
        NSLog(@"Created %lu rows", _tableData.count);
    }
    return self;
}

- (NSMutableArray *)sortWithSquence:(NSString *)sequence
{
    if([sequence isEqualToString:@""]) //if no filter, return the regular list
    {
        return _tableData;
    }
    
    //remake the filtered list
    [_filteredList removeAllObjects];
    
    //O(n)
    //for simplicity purpose, the app first used brute force methods for the array
    //future renditions of this code will utilize core data for data sorting as core data stores the data
    //in memory for quickness and will provide a wrapper to encapsulate the need for arrays
    for(NSString *str in _tableData) //loop through entire set each time for now ..
    {
        NSRange textRange;
        textRange =[str rangeOfString:sequence options:NSCaseInsensitiveSearch];
        
        if(textRange.location != NSNotFound)
        {
            [_filteredList addObject:str];
        }
    }
    return _filteredList;
}

@end

For the filter method, we first check if the sequence is empty and if it is, return the pointer to the NSMutableArray that contains all the words. This prevents us from remaking a list that is full.
Next the filtered list calls a remove all objects method to clear itself (but not deallocate it). Since we have a strong pointer and the object itself is no longer pointing to the previous memory locations, they can be cleaned up. The ARC value should be 0 at this point. To verify this, I ran through each letter to re-create 26 filtered lists and watched the memory inspector provided by XCode.

Memory Utilization

We can see that this filter method does have a high initial memory usage since we start the app around 32mb. But creating/recreating the filtered lists brings it up to about 36mb peak and then drops back down after a short period of time. Using tools like this during your application coding process is very important to capture memory leaks and poor memory usage.


The listmodel will be allocated and initialized within the controller class along with the table data source and delegate in the viewDidLoad event. Notice I have also added the filtered list initializations here as well as defaulted the match label text.


- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"View did load, init of data");
    _listModel = [[ListModel alloc] init];
    _filterdList = _listModel.tableData; //init to data source at first
    _matchLbl.text = [NSString stringWithFormat:@"Matches: %lu", _filterdList.count];
    _tableView.dataSource = self;
    _tableView.delegate = self;
    
}

Next we can setup the filter event to re-filter our array every time the textfield value changes, this is also where the label can be updated.

- (IBAction)filterChanged:(UITextField *)sender
{
    _filterdList = [_listModel sortWithSquence:sender.text]; //get our sorted list
    _matchLbl.text = [NSString stringWithFormat:@"Matches: %lu", _filterdList.count]; //update our label
    [_tableView reloadData]; //reload the table data
}

Finally we can implement the 3 table methods properly:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return _filterdList.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //here we create a simple identifier for reusability 
    static NSString *cellID = @"SimpleID";
     
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
     
    if (cell == nil) { //check if its nil, if it is we need to create a simple cell with the default style
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
    }
     
    NSString *cellText = [_filterdList objectAtIndex:indexPath.row]; 
    cell.textLabel.text = cellText; //set the inner cell label text

    return cell;
}

The final product will look like this:

App Sample

App Sample Filtered

Not sure what nonpariello is but it has my name in it so it must be something important.