How can I perform the handler of a UIAlertAction?
Asked Answered
F

3

6

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?

Fauver answered 21/4, 2015 at 13:42 Comment(2)
It's maybe not the answer you're looking for (which I'm not sure is possible anyway), but have you tried passing in some blocks for your actions rather than using the handlers? So keep another datasource of the actions you want to run based on the index selected.Architect
@Architect would I also then need to provide a way to pass the text, style, and enabledness of the actions?Fauver
C
7

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);
Chun answered 28/4, 2016 at 22:59 Comment(4)
Is this using non-public APIs? Apple won't let the app on the Store if it is.Fauver
I don't know if Apple will ding an App for it. I use it for unit tests (which are not built as part of the App binary).Chun
If you're trying to execute the same block of code both outside and inside the handle, I would refactor the code to a separate method, or move the code to a block and execute the block in the handler. Outside of unit testing, I can't think of a reason why one would need to execute a UIAlertAction handler.Chun
I'm trying to make a wrapper class so I can keep iOS 7 compatibility, using both UIAlertController and UIAlertView.Fauver
F
2

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.

Fauver answered 21/4, 2015 at 18:46 Comment(1)
Please explain any downvotes with an accompanying comment, so I know how to better answer in the future :)Fauver
E
1

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 }
    )
}
Eyeleteer answered 25/11, 2022 at 16:19 Comment(1)
Surprisingly, this should have been the right answer. This approach is way more safe and also easily testable.Monoxide

© 2022 - 2024 — McMap. All rights reserved.