If you're new to iOS/Objective-C and have a .Net/C# background you may want to first read some or all of the previous posts i've written, before continuing on:
- Strings
- Arrays
- Creating Classes, Properties, and Methods
- Interfaces
- Enums
- Reflection
- Namespaces
- Memory Management
I remember the first time I sat down to actually write an app for the iPhone. I had an array of data and simply wanted to put it into a scrollable list. I knew the control to use was the UITableView, which is built into iOS. So I took my array and without even looking at the documentation I set the dataSource property of the UITableView and I ran my application.
It looked something like this:
@interface ViewController ()
{
UITableView* _tableView;
NSMutableArray* _data;
}
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
_data = [[NSMutableArray alloc]initWithObjects:@"One", @"Two", @"Three", @"Four", nil];
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
_tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_tableView];
_tableView.dataSource = _data;
}
@end
I then ran the application, and immediately got an exception:
'NSInvalidArgumentException', reason: '-[__NSArrayM tableView:numberOfRowsInSection:]: unrecognized selector sent to instance
My immediate thought process: Hmm... thats strange. Why didn't it work? Maybe i messed up creating the array? Or maybe i need to set a property on the UITableView? And whats with the exception, what method is: "tableView:numberOfRowsInSection" and why is it trying to call it on an array? Guess i'll google it!
So what is actually wrong with the code above?
If I had actually spent a second to look at the documentation for dataSource property on the UITableView, I would have saw that dataSource isn't expecting an array, its expecting a protocol of type UITableViewDataSource. So what does that mean?
Basically, every control that connects to data, has its own data source protocol. And each protocol asks for data in its own way. Generally, data source protocols have 2 - 3 required methods that you must implement, such as "tableView:numberOfRowsInSection", and a bunch of optional methods that you can choose to or not to implement.
In this case, the UITableViewDataSource has 2 required methods that must be implemented:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
The first method is asking for how many "rows" will be displayed in the table view, and the second method is asking for the particular cell that should be displayed for a given index.
I can still clearly remember my reaction to this: "Ugh...seriously, I have to implement all this stuff myself, I hate this platform/language, its so primitive... why can't this be as simple as it is on other platforms such as Silverlight"
Heh, yea I guess you can say I was spoiled by .NET/C# :). However, now that I know how powerful this approach can be, i've changed my tune. If you're still reading, great! Don't quit yet, at least read to the end of this post before jumping to any conclusions, I know i'm definitely glad I didn't quit when I first saw this!
Lets start out by looking at how we would implement the first method:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _data.count;
}
Well thats really simple, right? We just need to return the amount of items that we want to be displayed in the table.
Note: You might have noticed that one of the parameters is "section". In the tableView's datasource there is an optional method where you can return the number of sections that will be displayed in the table. (The default is 1) If you've ever used an iPhone and went into the music app, you would have undoubtably seen that the music is organized alphabetically, and that the artistis are grouped by letter, such as "A", "B", "C", etc... Those letters being displayed are sections. I'm not going to go into too much detail in this post about sections, however I didn't want to ignore sections altogether, as they are obviously mentioned in each one of these 2 required methods.
Now lets take a look at the other required method:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if(!cell)
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
NSString* val = [_data objectAtIndex:indexPath.row];
cell.textLabel.text = val;
return cell;
}
I know, I know...there is a lot more code in this method! Stay with me though :)!
It's important to know that the tableView, like any well written control that displays lots of data has the concept of virtualization and recycling. If you're unfamiliar with these terms, it basically means that the tableView only displays the cells that are currently in view. Any cells that aren't in the current viewport are essentially placed into a queue. When a new cell comes into view the queue gets checked first, if there is a cell available, that cell gets "reused" and no new cell is created.
Note:In .NET the same concept is followed, however its just generally not exposed to the consumer of the control like it is in iOS.
Keeping that in mind, the first line is essentially checking the tableView's "recycling" queue. If we don't get a cell back, then its on us to create a new cell. Its very important to notice that the queue uses a string identifier system. So, the identifier that you pass into dequeue must match the identifier you used to create the cell.
The next line, is simply looking up the data from our array and setting the text of the cell to match it.
Note: indexPath is made of a section and a row. Again since we didn't explicitly tell the tableView how many sections to display, it assumes one, so there is no need to write additional logic here.
We then return that cell, and it gets displayed in the table. Its also important to know that this method will get called constantly as you scroll, b/c its called for every cell when it comes back into the viewport.
Just so you have a complete picture, here's what the entire source would look like:
@interface ViewController ()
{
UITableView* _tableView;
NSMutableArray* _data;
}
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
_data = [[NSMutableArray alloc]initWithObjects:@"One", @"Two", @"Three", @"Four", nil];
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
_tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_tableView];
_tableView.dataSource = self;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _data.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if(!cell)
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
NSString* val = [_data objectAtIndex:indexPath.row];
cell.textLabel.text = val;
return cell;
}
@end
As you've undoubtably deduced by now, this is certainly not as easy as setting the tableView's dataSource property to an array. And i'm sure you're thinking: Ok, i kept reading like he asked me to, but I still don't see how this can be a good thing
The reason why its worth implementing a datasource like this, is b/c of the pure power you as a developer has when it comes to displaying data in a control. Sure, if all you want to do is display a list of data, its certainly a lot more code to write. However, if you want to build highly complex and highly performant lists without compromising your data, you can absolutely do that. For instance, say you wanted to insert a row at the top of your table. Generally with any control where you're just assigning an array, you have to modify your array to handle such a task. And then somewhere/somehow you have to hope that the control has a way to style that appended row, which actually has nothing to do with your data.
If you're using this approach though, it comes remarkably easy. Simply return that you want to display your data count + 1. And in the method where you return a cell, you simply check if the indexPath.row is zero, if so thats your "appended" row. Otherwise you subtract one from the index and use that to lookup your data.
But don't just take my word for it, you should try it out yourself and keep an open mind, I know i'm certainly glad I did.
Now I couldn't write this article without telling you about our own data driven control :). The control I talked about above is the UITableView and if all you want to do is display a single column list, then you really don't have to read any further as its built into iOS, so you get it for free. However, if you want a multi column list (aka a Grid) then you'll be sad to know there isn't one built into the platform. However, my team and I have written such a control and it follows the exact same protocol driven data source model. And you can try it for FREE. Its also worth noting, that although it uses a protocol for data, we've actually included some built in data source helper classes for you that implement the common data source methods. So all you have to do is assign your array, just like you would in .NET!
#import "ViewController.h"
#import <IG/IG.h>
@interface ViewController ()
{
IGGridView* _gridView;
IGGridViewDataSourceHelper* _ds;
NSMutableArray* _data;
}
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
_data = [[NSMutableArray alloc]initWithObjects:@"One", @"Two", @"Three", @"Four", nil];
_gridView = [[IGGridView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
_gridView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_gridView];
_ds = [[IGGridViewDataSourceHelper alloc]init];
_ds.data = _data;
_gridView.dataSource = _ds;
}
@end
You can even play with some samples that show off the control with our app for free:
As always, I hope this was helpful,
-SteveZ