Saturday
Feb252012

UIAlertView is dangerous

And so is UIActionSheet. Here's why. Starting with iOS 4, more and more of the standard library replaced its old delegate patterns with block-based handlers. Here's an example of animating UIViews pre iOS 4:

- (IBAction)buttonTapped:(id)sender {
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(viewDidMove:finished:context:)];

    view.center = CGPointMake(50, 50);

    [UIView commitAnimations];
}

...

- (void)viewDidMode:(NSString *)animationId finished:(BOOL)finished context:(void *)context {
    [self updateState];
}

And for iOS 4+ you'd just do this:

- (IBAction)buttonTapped:(id)sender {
    [UIView animateWithDuration:1.0 animations:^{
        view.center = CGPointMake(50, 50);
    } completion:^(BOOL finished) {
        [self updateState];
    }];
}

Block-based handlers made it so that you'd write less code. And the code you wrote for your handler was you attached to the code that triggered the animation. Not scattered throughout your code's delegate methods.

However, UIActionSheet and UIAlertView have not been update to use blocks instead of delegates. I think it has to do with their delegate protocols having so many methods. Still, for the most part I only use - (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated.

There are a few dangers of using delegates of UIAlertView and UIActionSheets. Aside from forcing you to spread out the code that's relevant to the alert view, I came across a weird bug in one of my apps. I had a UITableViewController with a list of items, which implemented UIAlertViewDelegate. I would pop a UIAlertView asking the user "Do you really want to remove the item named ? Yes/No". Here's the implementation:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 0) {
        [self deleteItemAtIndex:self.selectedIndex];
    }
}

Way later I made a subclass of the UITableViewController, and wrote some code which popped an UIAlertView on network errors:

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Couldn't connect to the server." message:@"Try again in later." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alertView show];
[alertView release];

Did you spot where I went wrong? I just filled out the initializer without really thinking about it. This was a fire-and-forget UIAlertView, but I accidentally put delegate:self in there. The subclass didn't implement UIAlertViewDelegate, but its super class did. So whenever I hit "OK", super's didDismissWithButtonIndex would be called with buttonIndex 0.

So you need to always, always, always save your UIAlertView and compare it in your delegate method:

self.theAlertView = [[[UIAlertView alloc] initWithTitle:@"Warning" message:@"Do you really want to remove the item?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
[self.theAlertView show];

...

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    if (self.theAlertView == alertView) {
        if (buttonIndex == 0) {
            [self deleteItemAtIndex:self.selectedIndex];
        }
    }
}

But what if potentially pop multiple UIAlertViews in a single stack, like with a validation method:

- (void)validateInputs {
    if ([self missingTitle]) {
        self.theAlertView = [[[UIAlertView alloc] initWithTitle:@"Empty title" message:@"Continue?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
        [self.theAlertView show];
    }

    if ([self missingBody]) {
        self.theAlertView = [[[UIAlertView alloc] initWithTitle:@"Empty body" message:@"Continue?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
        [self.theAlertView show];
    }

    if ([self missingImage]) {
        self.theAlertView = [[[UIAlertView alloc] initWithTitle:@"Missing image" message:@"Continue?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
        [self.theAlertView show];
    }
}

Oops, we've overwritten the theAlertView member, so now we'll have to do something like this:

@property (readonly) NSMutableSet *alertViews;

...

- (void)validateInputs {
    @synchronized (self.alertViews) {

        if ([self missingTitle]) {
            UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"Empty title" message:@"Continue?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
            [self.alertViews addObject:alertView];
            [alertView show];
        }

        if ([self missingBody]) {
            UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"Empty body" message:@"Continue?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
            [self.alertViews addObject:alertView];
            [alertView show];
        }

        if ([self missingImage]) {
            UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"Missing image" message:@"Continue?" delegate:self cancelButtonTitle:@"Yes" otherButtonTitles:@"No", nil] autorelease];
            [self.alertViews addObject:alertView];
            [alertView show];
        }
    }
}

...

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    @synchronzed (self.alertViews) {
        if ([self.alertViews containsObject:alertView]) {
            if (buttonIndex == 0) {
                [self continueAddingItem];
            }

            [self.alertViews removeObject:alertView];
        }
    }
}

Do you see what a total pain in the ass this is? And the potential for bugs to creep in? And it's the same basic premise with UIActionSheets.

Here's what I'm proposing. Instead of delegates for alert views and action sheets I've implemented subclasses of both these classes, which use blocks as handlers. Now these blocks only cover one of the delegate methods: didDismissWithButtonIndex. If you need any of the other methods, then use the regular classes. For the majority of cases, you're good with didDismiss.

They're up on github, go ahead and do whatever. They're under the MIT License.

You use them like this:

[[[HSAlertView alloc] initWithTitle:@"Stuff" message:@"Do stuff?" dismissBlock:^(NSInteger buttonIndex) {
    // Do stuff
} cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", @"Yes", @"No", nil] show];

[[[HSActionSheet alloc] initWithTitle:@"Do stuff?" dismissBlock:^(NSInteger buttonIndex) {
    // Do stuff
} cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"No" otherButtonTitles:@"Yes", @"Maybe", nil] showInView:self.view];
Wednesday
Apr062011

2011 is the year I put out

I've got a ton of ideas for games and fun digital toys and 2011 is going to be the year for putting them out there.

Here's a start. I'm working on a puzzle game. It's not terribly original. Nothing is. But I think it'll be a fun game.

I've sketched out the concept into a rough design doc. And I'm putting it out under the Creative Commons Attribution license. That means anyone can take the idea and basically do whatever fun stuff they want with it.

You can download the design here. It's a pdf, roughly 4 mgb. Excuse my poor handwriting.

 

The idea here is to make myself beholden to the good people of The Internet to follow through with this project. But also to challenge the aforementioned good people to see who can make the best version of this game.

I will be posting semi-regularly with my progress.

 

Thursday
Nov052009

Asynchronous unit testing with OCMock

I've recently been using OCMock more and more. It's a great mocking framework for writing unit test for the mac or the iPhone.

When I mock delegate objects which wait for asynchronous callbacks I use this handy utility method

Update

There was a bug in here which made the utility return early, no matter how long the delay was. I've updated the code to fix the bug.

//
//  TestUtils.h
//
//  Created by Hans Sjunnesson on 2009-11-05.
//

#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>

@interface TestUtils : NSObject {

}

/*!
 @method waitForVerifiedMock:delay:
 @abstract Runs the current run loop for as long as specified by the delay, or until the mockobject verifies.
 @param mock the OCMockObject to verify.
 @param delay the time to wait until the mock is verified, in seconds.
*/
+ (void)waitForVerifiedMock:(OCMockObject *)mock delay:(NSTimeInterval)delay;

@end

And the implementation:

//
//  TestUtils.m
//
//  Created by Hans Sjunnesson on 2009-11-05.
//

#import "TestUtils.h"


@implementation TestUtils

+ (void)waitForVerifiedMock:(OCMockObject *)inMock delay:(NSTimeInterval)inDelay
{
  NSTimeInterval i = 0;
  while (i < inDelay)
  {
    @try
    {
      [inMock verify];
      break;
    }
    @catch (NSException *e) {}
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
    i+=0.5;
  }
}

@end

Here's an example of testing 'NSURLConnection' trying to connect to google.com.

@implementation NSObject (NSURLConnectionDelegate)
  - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {}
@end

- (void)testShouldConnect {
  id mock = [OCMockObject mockForClass:[NSObject class]];

  NSURL *url = [NSURL URLWithString:@"http://www.google.com"];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:mock startImmediately:NO];
  [[mock expect] connection:connection didReceiveResponse:OCMOCK_ANY];

  [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  [connection start];

  // Wait for five seconds
  [TestUtils waitForVerifiedMock:mock delay:5.0];

  STAssertNoThrow([mock verify], @"Should be able to connect to google.");
}
Sunday
Oct252009

Turn Core Data models into JSON

Json is a lightweight data-interchange format, says the official website. Think XML but with a format that's easier for both people and javascript interpreters to parse. Here's an easy way to serialize Core Data models into json.

First, download blakeseely's bsjonadditions project and include the source in your xcode project. Secondly, copy & paste the following snippet into a new file in your project called NSManagedObjectExtras.m:

//
//  NSManagedObjectExtras.m
//

#import "NSDictionary+BSJSONAdditions.h"
#import "NSArray+BSJSONAdditions.h"


@implementation NSManagedObject (NSObject)

- (NSDictionary *)propertiesDictionary
{
  NSMutableDictionary *properties = [[[NSMutableDictionary alloc] init] autorelease];

  for (id property in [[self entity] properties])
  {
    if ([property isKindOfClass:[NSAttributeDescription class]])
    {
      NSAttributeDescription *attributeDescription = (NSAttributeDescription *)property;
      NSString *name = [attributeDescription name];
      [properties setValue:[self valueForKey:name] forKey:name];
    }

    if ([property isKindOfClass:[NSRelationshipDescription class]])
    {
      NSRelationshipDescription *relationshipDescription = (NSRelationshipDescription *)property;
      NSString *name = [relationshipDescription name];

      if ([relationshipDescription isToMany])
      {
        NSMutableArray *arr = [properties valueForKey:name];
        if (!arr)
        {
          arr = [[[NSMutableArray alloc] init] autorelease];
          [properties setValue:arr forKey:name];
        }

        for (NSManagedObject *o in [self mutableSetValueForKey:name])
          [arr addObject:[o propertiesDictionary]];
      }
      else
      {
        NSManagedObject *o = [self valueForKey:name];
        [properties setValue:[o propertiesDictionary] forKey:name];
      }
    }
  }

  return properties;
}  

- (NSString *)jsonStringValue
{
  return [[self propertiesDictionary] jsonStringValue];
}

@end

Now all you need to do is to call [coreDataModel jsonStringValue] to get the model as a json string.

Tuesday
Aug042009

Anthony Burch rants on indie games

I'm dizzy from nodding in agreement with this video rant from Anthony Burch. He touches on the state of big-media games, with massive budgets and marketing vs. independently created, small games.

One of the point he makes is how consumers willingly fork over sixty bucks for a pile of crap, while independent game makers put out wonderfully moving pieces such as "Today I Die" and "Passage" and are having a hard time scraping by on donations.

Watch the video, take some time to play the games that Burch talks about and if you enjoy them, donate a few bucks for the good cause.