Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Getting Started on Your First iOS App – Part III

This blog was co-authored by Samuel Serrano. The full source can be found here.

In our last post we set up a basic RootViewController with a NavController, allowing us to swipe from view to view. Today, we will put a model behind the views and create a simple form to populate a TableView. For now, we don’t plan on persisting the data (have to save something for the next blog post).

We’re going to do some refactoring to make our RootViewController be a UITableViewController. This will allow us to get rid of the xib file, and make the first screen look better. Step one, is to move the “Create New Scoresheet” button into the navigation bar. While we’re at it, we’re going to put a title into the nav bar as well.

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationItem.title = @"Scoresheets";
    self.createNewScoresheetButton = [UIButton buttonWithType:UIButtonTypeContactAdd];
    [self.createNewScoresheetButton addTarget:self action:@selector(touchUpCreateNewScoresheet:) forControlEvents:UIControlEventTouchUpInside];
    self.navigationItem.rightBarButtonItem  = [[UIBarButtonItem alloc] initWithCustomView:self.createNewScoresheetButton];
} 

Additionally change the signature of touchUpCreateNewScoresheet to return “void” instead of IBAction. Now that you have this manual wiring, you should no longer have any references to the XIB file… delete it. You’ll notice the specs still pass [1], woohoo!

Now to make our RootView a table view, simply change the header file class signature to subclass UITableViewController:

@interface RootViewController : UITableViewController

Now when you run the app, you should see empty rows of the table. Time to fill them up! We’ll start by modeling the ScoresheetCollection, which in reality is just a mutable array. The idea being the RootViewController will create a ScoresheetCollection to pass around and act as the backing datasource for the table. Resist the temptation to subclass NSMutableArray, instead subclass NSObject and use composition to get your mutable array. NSMutableArray isn’t friendly to subclassing (we burnt a few hours on this). To drive out the implementation, we add the following test to RootViewControllerSpec in the “after Create New Scoresheet is tapped” describe block:

it(@"should create a new scoresheet", ^{
    [subject.scoresheetCollection count] should equal(1);
});

This should blow up (shouldn’t be able to compile, actually), driving out the creation of the ‘count’ method on ScoresheetCollection and the ScoresheetCollection, itself. We’ll create a simple subclass of NSObject called ScoresheetCollection and give it a private scoresheets variable which is the NSMutableArray under the hood. Time to get our first taste of creating a custom constructor:

#import "ScoresheetCollection.h"

@interface ScoresheetCollection ()

@property (strong, nonatomic) NSMutableArray* scoresheets;

@end

@implementation ScoresheetCollection

- (id)init {
    self = [super init];
    if (self) {
        self.scoresheets = [[NSMutableArray alloc] init];
    }
    return self;
}

- (int)count {
    return -1;
}

@end

The if-self is the standard for custom initializers, as it prevents the swallowing of errors if the super constructor fails. And for now, the collection’s count method is returning -1, because why not? We’ll drive out the real implementation in a few. In The header file of RootViewController, we can simply forward declare (instead of importing) ScoresheetController – this helps XCode remove the red squiggles faster, and prevents unnecessarily importing of the whole file.

#import <UIKit/UIKit.h>

@class ScoresheetCollection;

@interface RootViewController : UITableViewController

@property (strong, nonatomic) UIButton *createNewScoresheetButton;
@property (strong, nonatomic) ScoresheetCollection *scoresheetCollection;

-(id)initWithScoresheetCollection: (ScoresheetCollection *)scoresheetCollection;

@end

The compiler should be happy now, but the test cases, expectedly, fail. The idea now is to pass along a reference to the scoresheetCollection into the CreateScoresheetViewController and let the CreateScoresheetViewController own the responsibility of adding to the collection. For now, our actual scoresheets can be of NSDictionary type as we can vaguely see in the future that it will have keys and values (no need to over-engineer).

In CreateScoresheetViewController,

- (id)initWithScoresheetCollection:(ScoresheetCollection *)scoresheetCollection {
    self = [super init];
    if (self) {
        self.scoresheetCollection = scoresheetCollection;
    }
    return self;
}

In ScoresheetCollection, make the “count” method actually return the number of scoresheets in the scoresheets array:


- (int)count {
    return [self.scoresheets count];
}

Run the specs again- they should still be failing, as we’re not actually adding anything to the ScoresheetCollection. Since the CreateScoresheetController has a reference to the scoresheet, we can have it create and add a scoresheet when the controller loads:

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSDictionary *scoresheet = [[NSDictionary alloc] init];
    [self.scoresheetCollection addScoresheet:scoresheet];
}

This assumes you have an addScoresheet method on scoresheetCollection, which we wrote to simply add to the underlying mutable array:

- (void)addScoresheet:(NSDictionary *)scoresheet {
    [self.scoresheets addObject:scoresheet];
}

Your specs should pass now, and if you throw in some debugging you should notice that every time you tap to create a scoresheet, we add something to the underlying collection.

Now we want to actually create a “scoresheet” so that we can see a new scoresheet in our table view. To do this we’ll add a text field on CreateScoresheetViewController to put in a name of the scoresheet.

Screen Shot 2014-04-11 at 10.06.35 AM

Right click and drag the textfield and button to create outlets in the CreateScoresheetViewController header (refer to the previous blogpost if you need more detail). Before we go any further, let’s write a test to drive out the behavior.

describe(@"after filling out a name and tapping save", ^{
  beforeEach(^{
    CreateScoresheetViewController *createController = (CreateScoresheetViewController *)navController.topViewController;
    createController.nameTextField.text = @"I am a banana";
    [createController.saveScoresheetButton sendActionsForControlEvents:UIControlEventTouchUpInside];
  });

  it(@"should create a new scoresheet", ^{
    [subject.scoresheetCollection count] should equal(1);
  });

  describe(@"the new scoresheet", ^{
    __block NSDictionary *scoresheet;

    beforeEach(^{
      scoresheet = [subject.scoresheetCollection lastObject];
    });

    it(@"should save the name", ^{
      scoresheet[@"name"] should equal(@"I am a banana");
    });
  });
});

Here we are calling lastObject on scoresheetCollection, which is an NSArray method. Since we are going to want to use a lot of NSArray methods, we are going to make the scoresheets array a forwarding target. This mechanic requires a bit of boilerplate:

- (BOOL)respondsToSelector:(SEL)selector {
    return [super respondsToSelector:selector] || [self.scoresheets respondsToSelector:selector];
}

- (id)forwardingTargetForSelector:(SEL)selector {
    if ([self.scoresheets respondsToSelector:selector]) {
        return self.scoresheets;
    }
    return [super forwardingTargetForSelector:selector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    if ([self.scoresheets respondsToSelector:selector]) {
        return [self.scoresheets methodSignatureForSelector:selector];
    }
    return [super methodSignatureForSelector:selector];
}

The forwardingTargetForSelector method is a magical method that will take all calls on undefined methods for scoresheetCollection and call it on the returned object instead. In this case, our array acts as the forwarded target. The other methods act as support for forwardingTargetForSelector. Your forwarded methods still need to be in the header file, but there is no need to implement them. Note that you’ll get compiler warnings for header definitions with no implementation. You can get around this by using categories to hide your definitions in. It’s kind of a cheaty way to add more methods that have implementation elsewhere. To declare a category, reopen the interface block of your class with the name of your category in parenthesis after the class name. That’s it. Categories are hugely complex and important so if you’re curious google around.

#import <Foundation/Foundation.h>

@interface ScoresheetCollection : NSObject

-(void)addScoresheet:(NSDictionary *)scoresheet;

@end

@interface ScoresheetCollection (ProxiedMethods)

- (NSUInteger)count;
- (id)lastObject;

@end

Moving the count signature into the category means we can remove the count implementation from ScoresheetCollection.m. Additionally, we can move addScoresheet into the category and rename it as addObject to conform to the underlying NSArray. The pros are that we have less of our own code. The cons are that we now have a clunky interface into ScoresheetCollection that talks about “Objects” and not “Scoresheets.” Your call, and remember you can always rewrite it later.

At this point, we are still creating empty scoresheets on the viewDidLoad for CreateScoresheetViewController. To get our tests to pass, we need to create a touchUpInside outlet from our CreateScoresheetViewController XIB. After using the nifty interface builder to create a new touchUpInsideSaveButton method, we’ll write out our last piece of this implementation.

- (IBAction)touchUpInsideSaveButton:(id)sender {
    NSMutableDictionary *scoresheet = [[NSMutableDictionary alloc] init];
    scoresheet[@"name"] = self.nameTextField.text;
    [self.scoresheetCollection addObject:scoresheet];
}

This should make our test cases pass, but running the app is still pretty abrasive. There is no feedback on the save, and saving data seems to disappear. What we’d really like is once we tap Save, we’d navigate back to the table view and see the name in a list along with all other names we’ve entered. Let’s update our RootViewControllerSpec for that.

describe(@"after filling out a name and tapping save", ^{
  beforeEach(^{
    CreateScoresheetViewController *createController = (CreateScoresheetViewController *)navController.topViewController;
    createController.nameTextField.text = @"I am a banana";
    [createController.saveScoresheetButton sendActionsForControlEvents:UIControlEventTouchUpInside];
  });

  it(@"should create a new scoresheet", ^{
    [subject.scoresheetCollection count] should equal(1);
  });

  it(@"should navigate back to the RootViewController", ^{
    navController.topViewController should be_instance_of([RootViewController class]);
  });

  describe(@"the new scoresheet", ^{
    __block NSDictionary *scoresheet;

    beforeEach(^{
      scoresheet = [subject.scoresheetCollection lastObject];
    });

    it(@"should save the name", ^{
      scoresheet[@"name"] should equal(@"I am a banana");
    });
  });

  describe(@"the updated table view", ^{
    it(@"should show our scoresheet name in the list", ^{
      RootViewController *rootViewController = (RootViewController *)navController.topViewController;
      [rootViewController.tableView numberOfRowsInSection:0] should equal(1);
    });
  });
});

The two new tests fail and error out, respectively. Tapping Save did not send us back to the RootView and since we’re not on the RootView, then numberOfRowsInSection melts down on our CreateScoresheetViewController. To get the first test to pass, we pop the current view from our navigationController after we tap Save (in CreateScoresheetViewController):

- (IBAction)touchUpInsideSaveButton:(id)sender {
    NSMutableDictionary *scoresheet = [[NSMutableDictionary alloc]init];
    scoresheet[@"name"] = self.nameTextField.text;
    [self.scoresheetCollection addObject:scoresheet];

    [self.navigationController popViewControllerAnimated:YES];
}

Cool, one failing test down, one to go. This last one is a bit more involved. To get a UITableView to properly display a backing collection one has to configure both a delegate and a dataSource to the table. Both of these roles will be fulfilled by the ScoresheetCollection. We wire up this responsibility in two halves: the RootViewController and the ScoresheetCollection. In RootViewController, after the page loads, we assign both the delegate and dataSource:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.navigationItem.title = @"Scoresheets";
    self.createNewScoresheetButton = [UIButton buttonWithType:UIButtonTypeContactAdd];
    [self.createNewScoresheetButton addTarget:self action:@selector(touchUpCreateNewScoresheet:) forControlEvents:UIControlEventTouchUpInside];
    self.navigationItem.rightBarButtonItem  = [[UIBarButtonItem alloc] initWithCustomView:self.createNewScoresheetButton];

    self.tableView.delegate = self.scoresheetCollection;
    self.tableView.dataSource = self.scoresheetCollection;
}

Without the next step, we have compiler errors. In the ScoresheetCollection header file, we need to declare ourselves as following the delegate and dataSource protocols.

#import <Foundation/Foundation.h>

@interface ScoresheetCollection : NSObject <UITableViewDelegate, UITableViewDataSource>
@end

@interface ScoresheetCollection (ProxiedMethods)

- (NSUInteger)count;
- (id)lastObject;
- (void)addObject:(NSDictionary *)object;

@end

The protocols (listed in the angle brackets) act as a contract that ScoresheetCollection will implement a few key methods defined by the protocol. If those methods are not defined, we can’t even compile (which is where we’re at now). XCode helps us out a little in the Issue Navigator (left panel, triangle with a bang in it) and tells us what we’re missing. Additionally, protocols can suggest optional methods the contracted class may implement. If we look up the UITableViewDelegate’s docs we see a huge list of methods, but all of them are optional. To be a delegate of UITableView is to override a number of default behaviors. We will implement none of these.

The other protocol, UITableViewDataSource, has two required methods to the protocol (along with optional ones). The first method is to determine how many rows exist per section in the table. We only wish to have one section, so this is pretty easy to implement:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.scoresheets count];
}

Our second required method gets into the guts of how tables work. Each row in our table is drawn by a UITableViewCell object. This object knows how to draw itself and react to user input, but we only need a few (usually a couple more than fit on the screen), if each cell is drawn the same way (other than the text itself). If we had a table of 100 words, there’d be no point in creating 100 UITableViewCell objects that are all doing the exact same work. Instead we reuse a small pool of objects to redraw with different backing data. The documentation for our required method (tableView:cellForRowAtIndexPath) gives us a hint to ask the tableView if it already has a free cell constructed. With this hint,

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *identifier = NSStringFromClass([self class]);
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];

    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }

    NSDictionary *scoresheet = [self.scoresheets objectAtIndex:indexPath.row];
    NSString *scoresheetName = scoresheet[@"name"];
    [cell.textLabel setText:scoresheetName];

    return cell;
}

In the last three lines we pull the scoresheet out of our backing data structure, grab the name out of that, and tell the cell it has some text it should render. Running the specs right now should pass, but don’t celebrate too early. Running the app we notice that the table view still isn’t updating. What gives? If we look at our specs, we’re making an awkward assumption: if we directly ask the tableView how many rows it has (using a method we implemented) it should respond with how many rows have been displayed on the screen. To fix this in implementation, we need to extend RootViewController’s viewWillAppear method. The two methods viewDidLoad and viewWillAppear are hooks for after the view is instantiated(ish) and before the view is about to be painted, respectively. We want to tell the UITableView to reload its data.

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  [self.tableView reloadData];
}

That fix should make your specs still pass and your app properly work now. Congrats, your iOS app almost looks legit!

Next time we’ll persist the data so that the user can close the app without losing their list of scoresheet names.


Footnotes

#1 You probably have some sort of error referring to a nib. This is your cached xib file. Open the Simulator’s main menu and click on “Reset Content and Settings.” After that, clean your project (XCode > Product > Clean).

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *