iOS App Postmortems – 20 Rep Squats

20 Rep Squats is now available on the app store.

I am particularly proud of it as I believe it is my most well-designed, aesthetically pleasing application yet.

Development Time:

~18h, the bulk of which was spent trying to get Core Plot to display my charts properly.

Review Time (time spent in the iOS app submission queue):

12 days; my longest wait yet.

New technologies I learned and used:

As mentioned above, I used the Core Plot framework for generating the charts.

Challenges:

The biggest challenge – by far – was trying to get Core Plot to display the charts the way I wanted it to. This may be the subject of a blog post in the future, but suffice it to say, it should have much better defaults. I don’t think what I want to do is really that unusual; I just want to display all of my data, in only the postitive X/Y quadrant, with custom labels for points on each axis. But [graph.defaultPlotSpace scaleToFitPlots:[graph allPlots]] didn’t do the trick, and getting the right orthogonalCoordinateDecimals and plotRangeWithLocations was a painstaking process.

Future plans:

This will probably depend primarily on user requests. It does pretty much everything that I want it to do, and there’s only so much a ’20 Rep Squats’ app can do without turning it into something else (such as a full workout tracking app). So as always, please let me know if there’s anything you would like to see in it!

A tape-delay app store in a real-time world

Since I’m currently playing the app store review process waiting game once again, I started thinking about how different the app store is from the rest of the technology world.

As we all know, the time it takes from the moment you submit your application to the moment it is live on the app store can be days, weeks, or even more. I find it quite amusing that the app store is essentially moving in the opposite direction as the rest of the industry.

We live in a world of push notifications, live streaming, real-time everything, instant deployment via git push, and ultra-short feedback loops between the developer and the user. Comparatively speaking, Apple’s app store processes are stuck in the 80’s. Nothing says agile and proactive like waiting two weeks for your app to be reviewed, only to have it denied by Apple due to a two-minute bug fix, right?!? Yeah. Not quite.

But doesn’t Apple need these processes to protect its users? What about security? Reliability? Spam? Blah blah blah. All I hear are excuses.

I want to be able to push new versions of my apps with git. I want to be able to fix bugs without a two week lag time. I want Apple to use their vast brains and pocketbooks to figure out how to make things better. I don’t want more excuses. Besides, it’s not like the current process are perfect anyway.

In-App Rating Prompts With iRate

I recently updated all of my apps to include in-app rating prompts using Nick Lockwood’s ridiculously awesome iRate library. It will be a while before I find out if this results in an increase in sales, but for now, here’s a quick guide to using iRate.

Step 1: Download iRate.h and iRate.m and include them in your project.

That’s it! You’re ready to go. Of course, you may want to do a bit of extra configuration, but iRate’s defaults are set up in such a way that you might not even need to. Nevertheless, you’ll probably want to at least turn on iRate’s debug flag so that it prompts you for a rating every time you launch the app. This lets you see that the rating prompt and the link to the app store are working correctly. To do that, just add the following to your app delegate:

#import "iRate.h"

...

+ (void)initialize
{
    [iRate sharedInstance].debug = YES;
}

Now, if you run your app, you’ll see the rating prompt. Note that if you are running in the simulator, the ‘rate’ link won’t actually work – it only seems to work on a device. Just don’t forget to turn off the debug flag before you ship your app!

By default, the rating prompt will appear after the app has been installed for 10 days, and has been used 10 times. If you want to change these defaults (I switched them to 5 and 5), you can do so in the initialize method mentioned above.

+ (void)initialize
{
    [iRate sharedInstance].daysUntilPrompt = 5;
    [iRate sharedInstance].usesUntilPrompt = 5;
}

iRate offers an absurd number of configuration options, so no matter what your scenario is, it’s probably doable. Check out the documentation for all the details.

Good luck on getting those downloads and ratings!

Filtering a UITableView with a UISearchBar using Core Data

Continuing the tradition of UITableView filtering articles on this blog, I thought it would be fun to do one on filtering with Core Data.

This won’t be a Core Data tutorial though, so I won’t be going in to much detail on how Core Data itself works, or how to set it up.

We’ll start off with the UITableView filtering sample from my original blog post. We’ll then create a very simple object model containing our Food class, and give it a name and a desc property.

<insert boring Core Data boilerplate code here>

Now that that’s all done, we’ll start off by populating our data model with some default foods. We can do that in our App Delegate. We want to make sure the initial data is only added once, so we’ll use a key in NSUserDefaults to keep track of whether or not we’ve added our data yet. So we’ll add some code to application:didFinishLaunchingWithOptions.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    bool hasAddedData =  [prefs boolForKey:@"HasAddedInitialData"];
    if(!hasAddedData)
    {
        [self addInitialData];
        [prefs setBool:true forKey:@"HasAddedInitialData"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }

    return YES;
}

We call the addInitialData method if we haven’t done it before. The addInitialData method looks like this:

-(void)createFoodWithName:(NSString*)name andDescription:(NSString*)description
{
    Food* food = (Food*)[NSEntityDescription insertNewObjectForEntityForName:@"Food" inManagedObjectContext:self.managedObjectContext];
    food.name = name;
    food.details = description;
}

-(void)addInitialData
{
    [self createFoodWithName:@"Steak" andDescription:@"Rare"];
    [self createFoodWithName:@"Steak" andDescription:@"Medium"];
    [self createFoodWithName:@"Salad" andDescription:@"Caesar"];
    [self createFoodWithName:@"Salad" andDescription:@"Bean"];
    [self createFoodWithName:@"Fruit" andDescription:@"Apple"];
    [self createFoodWithName:@"Potato" andDescription:@"Baked"];
    [self createFoodWithName:@"Potato" andDescription:@"Mashed"];
    [self createFoodWithName:@"Bread" andDescription:@"White"];
    [self createFoodWithName:@"Bread" andDescription:@"Brown"];
    [self createFoodWithName:@"Hot Dog" andDescription:@"Beef"];
    [self createFoodWithName:@"Hot Dog" andDescription:@"Chicken"];
    [self createFoodWithName:@"Hot Dog" andDescription:@"Veggie"];
    [self createFoodWithName:@"Pizza" andDescription:@"Pepperonni"]; 

    [self.managedObjectContext save:nil];
}

It simply populates the data model with our default foods.

Up next comes the changes to our UITableViewController. We’ll change the viewDidLoad method so that it looks like this. The primary difference between the old version and this version is that this version sets up its managedObjectContext so that it can make Core Data calls. It also calls the shiny new filter method that does the actual filtering.

- (void)viewDidLoad
{
    [super viewDidLoad];

    UITableViewFilterDemoAppDelegate* del = [UIApplication sharedApplication].delegate;
    self.managedObjectContext = del.managedObjectContext;

    searchBar.delegate = (id)self;

    [self filter:@""];
}

The filter method is where we do the real work.

-(void)filter:(NSString*)text
{
    filteredTableData = [[NSMutableArray alloc] init];

    // Create our fetch request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

    // Define the entity we are looking for
    NSEntityDescription *entity = [NSEntityDescription
                                   entityForName:@"Food" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    // Define how we want our entities to be sorted
    NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc]
                                        initWithKey:@"name" ascending:YES];
    NSArray* sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];

    // If we are searching for anything...
    if(text.length > 0)
    {
        // Define how we want our entities to be filtered
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(name CONTAINS[c] %@) OR (details CONTAINS[c] %@)", text, text];
        [fetchRequest setPredicate:predicate];
    }

    NSError *error;

    // Finally, perform the load
    NSArray* loadedEntities = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    filteredTableData = [[NSMutableArray alloc] initWithArray:loadedEntities];

    [self.tableView reloadData];
}

It’s a little complicated, but the comments essentially describe the flow. The important part – the actual filtering – is the NSPredicate* part. We create a predicate which says we want to find all foods with either a name or details that contains our search string. The [c] is used to indicate that the match be case insensitive.

The results of the filtering are stored in the class’s filteredTableData object.

Finally, there are a few more details we have to worry about. We’ll have to add our usual UITableView data source methods. The methods simply return the appropriate data from the filteredTableData array. We also want to set up the textDidChange searchBar delegate method so that it calls our filter method.

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

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    int rowCount = filteredTableData.count;
    return rowCount;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];

    Food* food = [filteredTableData objectAtIndex:indexPath.row];
    cell.textLabel.text = food.name;
    cell.detailTextLabel.text = food.details;

    return cell;
}

-(void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)text
{
    [self filter:text];
}

And there you have it – a UISearchBar filtering a UITableView with the power of Core Data!

Download the demo project

 

iOS App Postmortems – Drink Menu

Drink Menu is now available on the app store. Like always, that means it’s postmortem time!

Development Time:

  • ~3h. Drink Menu was essentially a total conversion of BBQ Menu, so I wasn’t starting from scratch.

Review Time (time spent in the iOS app submission queue):

New technologies I learned and used:

  • There wasn’t really anything new here

Challenges:

  • Fully renaming an Xcode project is surprisingly difficult. There are a lot of hidden strings and plist entries to watch out for. Other than that, it was smooth sailing.

Future plans:

Drink Menu, along with BBQ Menu, do pretty much everything I want them to do. Unless there are specific requests from users, they likely won’t get too many more features. So as always, please let me know if there’s anything you would like to see in it!

iOS Pain Points – the App Review Waiting Game

My latest app was uploaded on July 9. It’s now July 19 – almost two full weeks worth of business days later – and approval is nowhere to be found. Granted, I’ve had pretty good luck so far, with most of my apps being approved in a week or so, but this is getting annoying.

What exactly am I paying $99 per year for? Forced upgrades, woefully inadequate development tools, constantly changing rules, a black-box review process? In short, why is Apple’s entire developer ecosystem so incredibly mediocore?

Am I wrong to expect better from one of the richest companies in the history of the world? Why can’t they hire a few more temps to help get through the app backlog and speed up the review times?

I believe that to a certain extent, Apple itself has caused many of these problems themselves , as the current app store rules encourage quick hacks and one-and-done applications.

As always, none of this matters, as Apple is simply not interested in the opinions of a small-time app developer. They are content to sit on their laurels and rake in their billions.

(And yes, there’s definitely a lot of bitterness in this post. Sorry.)

EDIT: Ironically, my app was approved the day after posting this. Thanks for listening, Apple!