Escribiendo

un poquito

UIActionSheet + Automatic Actions

One of our iOS development teams at Prolific Interactive was recently constructing a view controller with a series of UIActionSheets, each of which presents a variable set of buttons.

This post introduces my experimental approach to seamlessly handle this situation in Objective-C. The result is UIActionSheet+AutomaticActions.

Constraints (self.imposed)

As I envisioned the solution, I gave myself the following criteria:

  1. Keep the solution entirely within a category.
  2. Do not subclass.
  3. Remove the requirement for the view controller to conform to UIActionSheetDelegate.
  4. Keep the interface as simple as possible.

How to use it?

The final implementation is available as a CocoaPod:

pod "UIActionSheet+AutomaticActions"

First, add this import:

#import "UIActionSheet+AutomaticActions.h"

Then, create an array of actions. Each action is represented as a NSDictionary object in the form: @{ NSString : selector as NSString }.

NSArray *actionSheetItems = @[@{@"Action1":NSStringFromSelector(@selector(sheet1Action1))},
                              @{@"Action2":NSStringFromSelector(@selector(sheet1Action2))},
                              @{@"Action3":NSStringFromSelector(@selector(sheet1Action3))},
                              @{@"Cancel":[NSNull null] }];

Finally, instantiate a UIActionSheet using -jlm_initWithItems:delegate: (note that delegate can be any object– no protocols necessary) and show as usual.

UIActionSheet *as = [[UIActionSheet alloc] jlm_initWithItems:items delegate:self];

[as showInView:self.view];

That’s it. :)

Implementation details

Let’s take a quick tour of what’s going on inside the category.

It all starts in - (instancetype)initWithItems:delegate:. We instantiate a new UIActionSheet that we’ll be returning to the view controller.

UIActionSheet *actionSheet = [[UIActionSheet alloc] init];

We also set actionSheet as its own delegate. This is so that we can internally receive UIActionSheetDelegate’s -actionSheet:clickedButtonAtIndex

actionSheet.delegate = actionSheet;

Next, we dynamically add all the buttons to actionSheet and set the cancel button index:

for (NSDictionary *sheetItem in items) {
    NSString *title = [[sheetItem allKeys] firstObject];
    [actionSheet addButtonWithTitle:title];
}
actionSheet.cancelButtonIndex = items.count - 1;

We retain the array of actions so we can later invoke the proper selector and, to avoid subclassing, we have to resort to associated objects. Check this NSHipster post for a description of Associated Objects.

objc_setAssociatedObject(actionSheet,
                         kJLMActionSheetItems,
                         items,
                         OBJC_ASSOCIATION_RETAIN);

Finally, we also need a weak reference to the delegate and we return actionSheet:

objc_setAssociatedObject(actionSheet,
                         kJLMActionSheetDelegate,
                         delegate,
                         OBJC_ASSOCIATION_ASSIGN);

At this point, we are essentially all set and just need to retrieve the proper associated objects and invoke the selector when -actionSheet:clickedButtonAtIndex: is send to the delegate. Here’s that implementation:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
  id delegate = objc_getAssociatedObject(self, kAOActionSheetDelegate);
  if (!delegate) {
      return;
  }

  NSArray *items = objc_getAssociatedObject(self, kAOActionSheetItems);
  NSString *selectorString = [[items[buttonIndex] allValues] firstObject];

  if (![selectorString isEqual:[NSNull null]]) {
      SEL selector = NSSelectorFromString(selectorString);
      if (selector) {
  #pragma clang diagnostic push
  #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [delegate performSelector:selector];
  #pragma clang diagnostic pop
      }
  }
}

Note: Don’t be alarmed by the #pragma lines. This is a trick to suppress warnings. Read this StackOverflow discussion to understand why this is necessary.

Conclusion

I started by saying that this was all an experiment. This is, in part, because UIActionsheet is officially deprecated starting iOS 8, in favor of UIAlertController. Now that over 78% of the iOS user are running iOS 8, you should seriously consider using UIAlertController.