Simple UIProgressHUD replacement

The private API UIProgressHUD shows an activity indicator (spinner) over a shaded round rect. Although you can use fancy and flexible replacements like MBProgressHUD, there is a simpler way if you don’t need all the extra functionality: Take a regular UIActivityIndicatorView and add a partly transparent UIView behind it.

The simplest is to subclass of UIActivityIndicatorView so you can use it like any activity indicator.

The interface file:

//  ActivityHUD.h
#import 

@interface ActivityHUD : UIActivityIndicatorView
- (ActivityHUD *)initInView:(UIView *)view;
@end

And the implementation file:

//  ActivityHUD.m
#import "ActivityHUD.h"
#import 

@implementation ActivityHUD

- (ActivityHUD *)initInView:(UIView *)view
{
    self = [super initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
    if (self) {
        // add background view
        CGFloat border = self.bounds.size.height / 2;
        CGRect hudFrame = CGRectInset(self.bounds, -border, -border);
        UIView *hud = [[UIView alloc] initWithFrame:hudFrame];
        hud.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.67];
        hud.layer.cornerRadius = border;
        [self addSubview:hud];
        [self sendSubviewToBack:hud];

        // center in parent view
        [view addSubview:self];
        self.center = [self convertPoint:view.center fromView:view];
    }
    return self;
}

@end

To use, init the ActivityHUD in a view, and later call startAnimating to show it, and stopAnimating to hide.

The Three Underscores Idiom

#define ___
// defines three underscores as nothing

This is something I learned on my first programming job (eons ago…) and found useful but underused.

Useful for what? To solve the persistent tension between the robustness of single-exit point and the complexity of using extra state variables and state checking code.

Let me show you what I mean.

Continue reading

Dynamic UIActionSheet

Some actions may sometimes have more than one possible target, and other times only one.

For example: Calling a contact. If the contact has just one phone number, you openURL with it and you’re done. But if she has several numbers, you want to open a UIActionSheet to let the user choose which number to call.

Or picking an image. If you’re running on a camera-less iPod, you just show a UIImagePickerController to pick from the photo library. But with a camera, and maybe an existing image to delete, you’ll want to show an action sheet to choose the actual action that your “Photo” button performs.

So you need to do two things:

  1. Decide whether to show an action sheet at all or just do the one possible action.
  2. If you need an action sheet, fill it dynamically with the possible targets for action.

The code to do that doesn’t have to be complicated. It can actually be as simple as this:

@interface MyViewController() :  {
	NSArray *possibleTargets; // action sheet buttons go here
}
@end

@implementation MyViewControllerMyViewController

- (IBAction)selectTargetForAction:(id)sender
{
    possibleTargets = SomeWayToFindPossibleTargets();

    if ([possibleTargets count] == 1) {
        // just call the one number, or show the one image picker
        [self doAction:[possibleTargets lastObject]];
    }
    else if ([possibleTargets count] > 1) {
        // show a dynamically filled action sheet
        UIActionSheet *targetsSheet =
            [[UIActionSheet alloc] initWithTitle:__(@"Select")
                delegate:self
                cancelButtonTitle:nil // since we want it last
                destructiveButtonTitle:nil
                otherButtonTitles:nil]; // since we can't hard-code them
        for (NSString *target in possibleTargets) {
            [targetsSheet addButtonWithTitle:__(target.title)];
        }
        targetsSheet.cancelButtonIndex = [targetsSheet addButtonWithTitle:__(@"Cancel")];
        [targetsSheet showInView:self.view];
    }
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex != actionSheet.cancelButtonIndex) {
        [self doAction:[possibleTargets objectAtIndex:buttonIndex]];
    }
}

@end

Notes:

  1. The @interface code above is part of the .m file, as described in my earlier post How to Keep Your Protocols Private.
  2. If you’re puzzled by the meaning of __(@"String") then see my earlier post on How To Make NSLocalizedString Fun To Use.
  3. The objects in possibleTargets can hold a selector (SEL) of the actual method to be executed, or they can hold just relevant data for the action (like a phone number to call).

Reordering a UITableView

I like the simple and straightforward interface of the Reminders.app: edit reminders inline, and add a reminder by tapping and typing in the next empty line. So I wrote a UITableViewController subclass that does just that, but adds Edit mode with reordering.

Coding reordering requires implementing methods from both of the two entangled protocols UITableViewDataSource and UITableViewDelegate.

In UITableViewDataSource we have the methods:
tableView:commitEditingStyle:forRowAtIndexPath: (for insert and delete)
tableView:canMoveRowAtIndexPath: (for reorder)
tableView:moveRowAtIndexPath:toIndexPath: (for reorder)

And in UITableViewDelegate:
tableView:editingStyleForRowAtIndexPath: (for insert and delete)
tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: (for reorder)

The UITableViewDelegate methods are not strictly necessary in the simplest case where all rows are deletable, no row is insertable, and all rows are reorderable. In fact, in this simplest case you don’t need canMoveRowAtIndexPath either, only this:

- (void)tableView:(UITableView *)tableView
    moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
    toIndexPath:(NSIndexPath *)toIndexPath
{
    // Update the model with the change.
    // The view updates are already handled by UIKit.
}

But of course, if the last row (or the row past last) is an insert row, and is not reorderable, than you need to implement the other methods as well. You can get the code for that (along with the inline editable UITextField-s) at GitHub: http://github.com/yonat/EditableList.