How can I perform the handler of a UIAlertAction?

3.4k Views Asked by At

I'm trying to write a helper class to allow our app to support both UIAlertAction and UIAlertView. However, when writing the alertView:clickedButtonAtIndex: method for the UIAlertViewDelegate, I came across this issue: I see no way to execute the code in the handler block of a UIAlertAction.

I'm trying to do this by keeping an array of UIAlertActions in a property called handlers

@property (nonatomic, strong) NSArray *handlers;

and then implement a delegate like such:

- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    UIAlertAction *action = self.handlers[buttonIndex];
    if (action.enabled)
        action.handler(action);
}

However, there is no action.handler property, or indeed any way I can see to fetch that, since the UIAlertAction header just has:

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIAlertAction : NSObject <NSCopying>

+ (instancetype)actionWithTitle:(NSString *)title style:(UIAlertActionStyle)style handler:(void (^)(UIAlertAction *action))handler;

@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) UIAlertActionStyle style;
@property (nonatomic, getter=isEnabled) BOOL enabled;

@end

Is there some other way to execute the code in the handler block of a UIAlertAction?

3

There are 3 best solutions below

4
Drew H On BEST ANSWER

After some experimentation I just figured this out. Turns out that the handler block can be cast as a function pointer, and the function pointer can be executed.

Like so

//Get the UIAlertAction
UIAlertAction *action = self.handlers[buttonIndex];

//Cast the handler block into a form that we can execute
void (^someBlock)(id obj) = [action valueForKey:@"handler"];

//Execute the block
someBlock(action);
1
Ky - On

Wrapper classes are great, eh?

In the .h:

@interface UIAlertActionWrapper : NSObject

@property (nonatomic, strong) void (^handler)(UIAlertAction *);
@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) UIAlertActionStyle style;
@property (nonatomic, assign) BOOL enabled;

- (id) initWithTitle: (NSString *)title style: (UIAlertActionStyle)style handler: (void (^)(UIAlertAction *))handler;

- (UIAlertAction *) toAlertAction;

@end

and in the .m:

- (UIAlertAction *) toAlertAction
{
    UIAlertAction *action = [UIAlertAction actionWithTitle:self.title style:self.style handler:self.handler];
    action.enabled = self.enabled;
    return action;
}

...

- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    UIAlertActionWrapper *action = self.helpers[buttonIndex];
    if (action.enabled)
        action.handler(action.toAlertAction);
}

All you have to do is make sure UIAlertActionWrappers are inserted into helpers instead of UIAlertActions.

This way, you can make all properties gettable and settable to your heart's content, and still retain the functionality provided by the original class.

1
Bence Pattogato On

A much more safe and future proof solution could be to have a layer on top of UIAlertAction (with you custom implementation) so you wouldn't need to call non-public APIs, which aren't considered safe. (even in tests) Something like this below and you map this to UIAlertAction in production and you can call the action handler when running tests.

final class AlertAction {
    let title: String
    let action: () -> Void
    let style: UIAlertActionStyle
}

func makeAlert(title: String, ...., actions: [AlertAction]) -> UIAlertController {
    return UIAlertController(
        title: title, 
        message: message, 
        preferredStyle: actions.map { UIAlertAction(title: $0.title, action: $0. action, style: $0.style }
    )
}