Одними из самых востребованных классов в UIKit до выхода iOS версии 8 являлись UIAlertView и UIActionSheet. Наверное, каждый разработчик приложений под мобильную платформу от Apple рано или поздно сталкивался с ними. Показ сообщений или меню выбора действий — это неотъемлемая часть практически любого пользовательского приложения. Для работы с этими классами, а точнее для обработки нажатий кнопок, программисту требовалось реализовывать в своем классе методы соответствующего делегата — UIAlertViewDelegate или UIActionSheetDelegate (если не требовалось чего-то сверх, то достаточно было реализовать метод clickedButtonAtIndex). На мой взгляд это очень неудобно: если внутри объекта создавалось несколько диалоговых окон с разными наборами действий, то их обработка все равно происходила в одном методе с кучей условий внутри. С выходом 8 версии iOS в составе UIKit появился класс UIAlertController, который пришел на смену UIAlertView и UIActionSheet. И одной из его главных отличительных черт является то, что вместо делегатов он использует блочный подход:
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Hello" message:@"Habr!" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Action" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
// код обработчика кнопки
}]];
Такой подход позволяет писать более структурированный и логичный код. Отныне программисту больше не требуется разделять создание диалогового окна и обработку событий — UIAlertController устраняет это недоразумение, но одновременно с этим привносит историческую несправедливость из-за невозможности использования в iOS 7 и более ранних версиях. Решить эту проблему можно несколькими способами:
- Не обращать внимание на UIAlertController и продолжать использовать устаревшие UIAlertView и UIActionSheet.
- Использовать нестандартные диалоговые окна. Программист либо пишет собственную реализацию, что приводит к увеличению временных затрат, либо подключает сторонние компоненты (например, SIAlertView), использование которых имеет ряд недостатков:
- программные модули с хорошей поддержкой можно пересчитать по пальцам (зачастую их создатели быстро забрасывают это неблагодарное дело);
- если в проекте используются несколько компонентов от разных разработчиков, то при их взаимодействии могут возникать проблемы (редко, но это возможно).
- Проверять версию iOS и создавать либо UIAlertController, либо UIAlertView или UIActionSheet.
Последний вариант наиболее логичен, и большинство разработчиков, я уверен, выбрали бы именно его, но данный метод имеет существенный недостаток — условие проверки версии операционной системы придется писать каждый раз, когда потребуется отобразить диалоговое окно. Столкнувшись с этим на практике, я создал специальный класс-обертку UIAlertDialog, который позволяет забыть об этой проблеме.
Идея заключается в том, чтобы удобный блочный синтаксис UIAlertController'а можно было использовать в своих проектах не ограничиваясь последними версиями iOS.
Определив стиль диалогового окна
typedef NS_ENUM(NSInteger, UIAlertDialogStyle) {
UIAlertDialogStyleAlert = 0,
UIAlertDialogStyleActionSheet
};
и тип блока-обработчика
typedef void(^UIAlertDialogHandler)(NSInteger buttonIndex);
можно перейти к структуре класса:
@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
- (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;
}
переданные параметры сохраняются во
@interface UIAlertDialog ()
@property (nonatomic) UIAlertDialogStyle style;
@property (copy, nonatomic) NSString *title;
@property (copy, nonatomic) NSString *message;
@property (strong, nonatomic) NSMutableArray *items;
@end
и инициализируется массив (items), который будет хранить действия кнопок.
Добавление новой кнопки:
- (void)addButtonWithTitle:(NSString *)title andHandler:(UIAlertDialogHandler)handler {
UIAlertDialogItem *item = [UIAlertDialogItem new];
item.title = title;
item.handler = handler;
[self.items addObject:item];
}
где UIAlertDialogItem — это
@interface UIAlertDialogItem : NSObject
@property (copy, nonatomic) NSString *title;
@property (copy, nonatomic) UIAlertDialogHandler handler;
@end
который хранит в себе текст кнопки и действие, связанное с ней.
И, наконец, метод showInViewController, инкапсулирующий создание диалогового окна в зависимости от версии операционной системы:
- (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];
}];
}
}
Акцентирую ваше внимание на том, что каждый соответствующий метод выполняется не сразу, а добавляется в главную очередь на выполнение. Это обусловлено тем, что если в обработчике кнопки создается другое диалоговое окно, то оно будет отображено только после завершения всей анимации предыдущего диалога.
Рассмотрим подробно методы создания диалоговых окон:
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];
}
В этом листинге хотелось бы отметить строчку
NSInteger buttonIndex = i;
а точнее ее положение в коде. Благодаря свойству блока хранить контекст, в котором он был создан, становится возможным передача индекса нажатой кнопки в блок-обработчик. Такой способ необходим: UIAlertAction не содержит в себе нужного параметра.
UIAlertView и UIActionSheet
Согласно описанию UIAlertDialog, теперь создание диалогового окна выглядит следующим образом:
- (void)showMessage:(NSString *)message
{
UIAlertDialog *alertDialog = [[UIAlertDialog alloc] initWithStyle:UIAlertDialogStyleAlert title:message andMessage:nil];
[alertDialog showInViewController:self];
}
а в связи с тем, что этот класс является делегатом UIAlertView и UIActionSheet
@interface UIAlertDialog : NSObject <UIAlertViewDelegate, UIActionSheetDelegate>
необходимо разъяснить один момент.
Как известно, делегаты в классе описывают как свойства с модификатором weak. Это означает, что если strong ссылок на объект-делегат больше не существует, то при попытке вызвать методы делегата возникнет исключение EXC_BAD_ACCESS.
В нашем случае именно это и произойдет — ARC удалит alertDialog, так как внешних ссылок на него нет. Проблему можно решить, если создать классы-наследники UIAlertView и UIActionSheet, добавив в них ссылку на объект диалога:
@interface UIAlertViewDialog : UIAlertView
@property (strong, nonatomic) UIAlertDialog *alertDialog;
@end
и
@interface UIActionSheetDialog : UIActionSheet
@property (strong, nonatomic) UIAlertDialog *alertDialog;
@end
Благодаря проделанным манипуляциям код создания диалоговых окон примет следующий вид:
- (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];
}
- (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];
}
и финальный штрих — обработка действий кнопок, происходит в методе соответствующего делегата:
- (void)actionSheet:(UIActionSheetDialog *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == actionSheet.numberOfButtons - 1) {
return;
}
UIAlertDialogItem *item = self.items[buttonIndex];
if (item.handler) {
item.handler(buttonIndex);
}
}
- (void)alertView:(UIAlertViewDialog *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == alertView.numberOfButtons - 1) {
return;
}
UIAlertDialogItem *item = self.items[buttonIndex];
if (item.handler) {
item.handler(buttonIndex);
}
}
Заключение
В итоге получилось простое и компактное решение, которое позволит значительно сократить время на работу с диалоговыми окнами (исходный код).
Спасибо за внимание!
Автор: devnseven