📜 ⬆️ ⬇️

Creating a universal UIAlertController for various versions of iOS

One of the most popular classes in UIKit before the release of iOS version 8 was UIAlertView and UIActionSheet. Probably every developer of applications for the mobile platform from Apple, sooner or later came across them. Displaying messages or an action selection menu is an integral part of almost any user application. To work with these classes, or rather, to handle keystrokes, the programmer needed to implement in its class the methods of the corresponding delegate - UIAlertViewDelegate or UIActionSheetDelegate (if you didn’t need anything beyond, then it was enough to implement the clickedButtonAtIndex). In my opinion, this is very inconvenient: if several dialog boxes with different sets of actions were created inside the object, they were still processed in the same method with a bunch of conditions inside. With the release of iOS 8, UIKit now includes the UIAlertController class, which replaced the UIAlertView and UIActionSheet. And one of its main distinguishing features is that instead of delegates it uses the block approach:

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Hello" message:@"Habr!" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"Action" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { //    }]]; 

This approach allows you to write more structured and logical code. From now on, the programmer no longer needs to separate the creation of a dialog box and event handling - UIAlertController eliminates this misunderstanding, but at the same time introduces historical injustice due to the impossibility of using it in iOS 7 and earlier versions. There are several ways to solve this problem:


The last option is the most logical, and most of the developers, I am sure, would choose it, but this method has a significant drawback - the condition for checking the version of the operating system will have to be written every time you need to display a dialog box. Faced with this in practice, I created a special wrapper class UIAlertDialog, which allows you to forget about this problem.

The idea is that the convenient block syntax UIAlertController'a can be used in their projects, not limited to the latest versions of iOS.
')
Defining the style of the dialog box

 typedef NS_ENUM(NSInteger, UIAlertDialogStyle) { UIAlertDialogStyleAlert = 0, UIAlertDialogStyleActionSheet }; 

and block type

 typedef void(^UIAlertDialogHandler)(NSInteger buttonIndex); 

You can go to the class structure:

 @interface UIAlertDialog : NSObject <UIAlertViewDelegate, UIActionSheetDelegate> - (instancetype)initWithStyle:(UIAlertDialogStyle)style title:(NSString *)title andMessage:(NSString *)message; - (void)addButtonWithTitle:(NSString *)title andHandler:(UIAlertDialogHandler)handler; - (void)showInViewController:(UIViewController *)viewContoller; @end 

Inside the constructor
 - (instancetype)initWithStyle:(UIAlertDialogStyle)style title:(NSString *)title andMessage:(NSString *)message { if (self = [super init]) { self.style = style; self.title = title; self.message = message; self.items = [NSMutableArray new]; } return self; } 


passed parameters are saved in

internal variables
 @interface UIAlertDialog () @property (nonatomic) UIAlertDialogStyle style; @property (copy, nonatomic) NSString *title; @property (copy, nonatomic) NSString *message; @property (strong, nonatomic) NSMutableArray *items; @end 


and initialized an array (items) that will store the actions of the buttons.

Adding a new button:

 - (void)addButtonWithTitle:(NSString *)title andHandler:(UIAlertDialogHandler)handler { UIAlertDialogItem *item = [UIAlertDialogItem new]; item.title = title; item.handler = handler; [self.items addObject:item]; } 

where UIAlertDialogItem is

special inner class (analogous to UIAlertAction)
 @interface UIAlertDialogItem : NSObject @property (copy, nonatomic) NSString *title; @property (copy, nonatomic) UIAlertDialogHandler handler; @end 


which stores the text of the button and the action associated with it.

And finally, the showInViewController method, which encapsulates the creation of a dialog box depending on the version of the operating system:

 - (void)showInViewController:(UIViewController *)viewContoller { if ([[UIDevice currentDevice].systemVersion intValue] > 7) { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self showAlertControllerInViewController:viewContoller]; }]; return; } if (self.style == UIAlertDialogStyleActionSheet) { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self showActionSheetInView:viewContoller.view]; }]; } else { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self showAlert]; }]; } } 

I focus your attention on the fact that each corresponding method is not executed immediately, but is added to the main queue for execution. This is due to the fact that if another dialog box is created in the button handler, it will be displayed only after the entire animation of the previous dialog is completed.

Let us consider in detail the methods for creating dialog boxes:

UIAlertController

 - (void)showAlertControllerInViewController:(UIViewController *)viewController { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:self.title message:self.message preferredStyle:self.style == UIAlertDialogStyleActionSheet ? UIAlertControllerStyleActionSheet : UIAlertControllerStyleAlert]; NSInteger i = 0; for (UIAlertDialogItem *item in self.items) { UIAlertAction *alertAction = [UIAlertAction actionWithTitle:item.title style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { NSInteger buttonIndex = i; if (item.handler) { item.handler(buttonIndex); } }]; [alertController addAction:alertAction]; i++; } UIAlertAction *closeAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"close", nil) style:UIAlertActionStyleCancel handler:nil]; [alertController addAction:closeAction]; [viewController presentViewController:alertController animated:YES completion:nil]; } 

In this listing, I would like to mark the line

 NSInteger buttonIndex = i; 

or rather its position in the code. Due to the property of the block to store the context in which it was created, it becomes possible to transfer the index of the pressed button to the block handler. This method is necessary: ​​UIAlertAction does not contain the desired parameter.

UIAlertView and UIActionSheet

According to the UIAlertDialog description, now the creation of the dialog box looks like this:

 - (void)showMessage:(NSString *)message { UIAlertDialog *alertDialog = [[UIAlertDialog alloc] initWithStyle:UIAlertDialogStyleAlert title:message andMessage:nil]; [alertDialog showInViewController:self]; } 

due to the fact that this class is a delegate of UIAlertView and UIActionSheet

 @interface UIAlertDialog : NSObject <UIAlertViewDelegate, UIActionSheetDelegate> 

it is necessary to clarify one point.

As you know, delegates in a class are described as properties with the weak modifier. This means that if the strong references to the delegate object no longer exist, then an attempt to call delegate methods will throw an EXC_BAD_ACCESS exception.

In our case, this is exactly what will happen - the ARC will remove the alertDialog , since there are no external links to it. The problem can be solved by creating the heir classes UIAlertView and UIActionSheet by adding a link to the dialog object in them:

 @interface UIAlertViewDialog : UIAlertView @property (strong, nonatomic) UIAlertDialog *alertDialog; @end 

and

 @interface UIActionSheetDialog : UIActionSheet @property (strong, nonatomic) UIAlertDialog *alertDialog; @end 

Thanks to this manipulation, the code for creating dialog boxes will look like this:

 - (void)showActionSheetInView:(UIView *)view { UIActionSheetDialog *actionSheet = [[UIActionSheetDialog alloc] initWithTitle:self.title delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil]; actionSheet.alertDialog = self; for (UIAlertDialogItem *item in self.items) { [actionSheet addButtonWithTitle:item.title]; } [actionSheet addButtonWithTitle:NSLocalizedString(@"close", nil)]; actionSheet.cancelButtonIndex = actionSheet.numberOfButtons - 1; [actionSheet showInView:view.window]; } 

similarly for UIAlertView
 - (void)showAlert { UIAlertViewDialog *alertView = [[UIAlertViewDialog alloc] initWithTitle:self.title message:self.message delegate:self cancelButtonTitle:nil otherButtonTitles:nil]; alertView.alertDialog = self; for (UIAlertDialogItem *item in self.items) { [alertView addButtonWithTitle:item.title]; } [alertView addButtonWithTitle:NSLocalizedString(@"close", nil)]; alertView.cancelButtonIndex = alertView.numberOfButtons - 1; [alertView show]; } 


and the final touch is the handling of button actions, which happens in the corresponding delegate method:

 - (void)actionSheet:(UIActionSheetDialog *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == actionSheet.numberOfButtons - 1) { return; } UIAlertDialogItem *item = self.items[buttonIndex]; if (item.handler) { item.handler(buttonIndex); } } 

UIAlertViewDelegate
 - (void)alertView:(UIAlertViewDialog *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == alertView.numberOfButtons - 1) { return; } UIAlertDialogItem *item = self.items[buttonIndex]; if (item.handler) { item.handler(buttonIndex); } } 


Conclusion

The result is a simple and compact solution that will significantly reduce the time to work with dialog boxes ( source code ).

Thanks for attention!

Source: https://habr.com/ru/post/242801/


All Articles