Створення універсального 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), використання яких має ряд недоліків:
    1. програмні модулі з хорошою підтримкою можна перерахувати по пальцях (найчастіше їх творці швидко закидають це невдячна справа);
    2. якщо в проекті використовується кілька компонентів від різних розробників, то при їх взаємодії можуть виникати проблеми (рідко, але це можливо).
  • Перевіряти версію 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 — це

спеціальний внутрішній клас (аналог 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 <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];
}

аналогічно для 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);
}
}


Висновок

У підсумку вийшло просте і компактне рішення, яке дозволить значно скоротити час на роботу з діалоговими вікнами (вихідний код).

Спасибі за увагу!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.