[Из песочницы] Создание универсального UIAlertController'а для различных версий iOS
Одними из самых востребованных классов в 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
— (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 — этоспециальный внутренний класс (аналог UIAlertAction) @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
В нашем случае именно это и произойдет — 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]; } аналогично для 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]; } и финальный штрих — обработка действий кнопок, происходит в методе соответствующего делегата: — (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); } } ЗаключениеВ итоге получилось простое и компактное решение, которое позволит значительно сократить время на работу с диалоговыми окнами (исходный код).
Спасибо за внимание!