0,0
рейтинг
10 ноября 2014 в 18:08

Разработка → Создание универсального 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);
    }
}


Заключение

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

Спасибо за внимание!
Георгий Касапиди @devnseven
карма
3,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (9)

  • 0
    Не делайте так, пожалуйста:

    The UIAlertView class is intended to be used as-is and does not support subclassing.
    • 0
      Спасибо за ваше замечание. Да, действительно наследование этого класса запрещено, но насколько я знаю, это требование было введено Apple, для того, чтобы разработчики не могли изменять внешний вид UIAlertView. В моем случае, никаких манипуляций с интерфейсом не проводится, и поэтому я позволил себе эту небольшую вольность.
      • 0
        А в один прекрасный день после очередного обновления приложение начнет падать, так как окажется, что конструктор вернет объект другого класса.

        З.Ы. Скрестим пальцы и надеемся что этого не будет, так как старые версии IOS больше никому не нужны ))
        • 0
          теперь есть UIAlertController, для Apple нет смысла менять старые классы) p.s. по крайней мере я очень надеюсь на это)
  • 0
    github.com/steipete/PSTAlertController тоже опенсорснуто на днях
  • +1
    Как известно, делегаты в классе описывают как свойства с модификатором weak. Это означает, что если strong ссылок на объект-делегат больше не существует, то при попытке вызвать методы делегата возникнет исключение EXC_BAD_ACCESS.


    Тут похоже вы описываете поведение unsafe unretained свойства, weak же просто вернет безопасный nil.

    ARC introduces several new lifetime qualifiers for objects, and weak references. A weak reference does not extend the lifetime of the object it points to, and automatically becomes nil when there are no strong references to the object.
    • 0
      век живи, век учись! недоучил теорию — и грешил на weak
  • 0
    typedef NS_ENUM(NSInteger, UIAlertDialogStyle) {
    UIAlertDialogStyleAlert = 0,
    UIAlertDialogStyleActionSheet
    };

    Для меньших проблем с совместимостью все же стоило бы сделать значения равными UIAlertControllerStyle

    Есть ли cocoapod с этим или подобным кодом?
    • 0
      К сожалению пода нет, но в ближайшее время появится. Ссылку в update добавлю. Все никак руки не дойдут)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.