Делаем красивый Progress Bar в iOS приложении

Добрый день, дорогой Хабрахабр!

В этой статье я хочу описать способ, при помощи которого мы сделали такой красивый кастомный progress bar — на иллюстрации — в одном из последних проектов.

Задача была поставлена следующим образом:

  • Картинки были нарисованны дизайнером.
  • Progress bar должен перекрывать и блокировать весь UI.
  • Элемент должен вызываться нотификацией в NSNotificationCenter.
  • Должна быть возможность досрочно завершить действие элемента.
  • Progress bar должен быть один, вне зависимости от количества посланных нотификаций.

Заинтересовавшихся в реализации прошу под кат.

Первым делом создаем синглтон BSBeautifulProgressBarManager и прописываем в BSBeautifulProgressBarManager.h:

Жми меня!
#import <Foundation/Foundation.h>

#define kShouldShowBeautifulProgressBar @"kShouldShowBeautifulProgressBar"
#define kShouldHideBeautifulProgressBar @"kShouldHideBeautifulProgressBar"

#define beautifulProgressBarManager [BSBeautifulProgressBarManager sharedManager]

@interface BSBeautifulProgressBarManager : NSObject

+ (BSBeautifulProgressBarManager *)sharedManager;

@end

На работе мы часто используем трюк с define, чтобы избавиться от постоянного обращения к синглтонам по названию класса. Здесь, вместо [BSBeautifulProgressBarManager sharedManager], мы можем писать просто BSBeautifulProgressBarManager. Заголовчный файл мы трогать больше не будем.

Дефайны нотификаций мы обычно выносим в какой-нибудь Config.h — тогда не нужно импортировать хедер менеджера для отправки нотификаций. Однако в данном примере мы добавим трюк с избавлением от магических констант прямо в хедер синглтона.

Обратимся к реализации синглтона в файле BSBeautifulProgressBarManager.m:

Жми меня!
+ (BSBeautifulProgressBarManager *)sharedManager 
{
    static BSBeautifulProgressBarManager *sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManager = [self new];
        
        [[NSNotificationCenter defaultCenter] addObserver:sharedManager selector:@selector(showProgressBar) name:kShouldShowBeautifulProgressBar object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:sharedManager selector:@selector(hideProgressBar) name:kShouldHideBeautifulProgressBar object:nil];
    });
    return sharedManager;
}

Ничего особенного пока что не произошло — обычная реализация thread-safe синглтона. Единственное примечательное в этом методе — это подписка на нотификации показа/скрытия progress bar'a у NSNotificationCenter.

Перейдем к реализации методов showProgressBar и hideProgressBar:

Жми меня!
- (void)showProgressBar
{
    if (isShown) return;
    isShown = YES;
}

- (void)hideProgressBar
{
    if (!isShown) return;
    isShown = NO;
}

Пока что они ничего не делают. Однако два условия работы элемента мы уже выполнили: progress bar будет только один, и вызываться он будет через NSNotificationCenter. Соответственно, добавим в имплементацию приватную переменную:

Жми меня!
@implementation BSBeautifulProgressBarManager
{
    BOOL isShown;
}

Идея наша будет такой:

  • Первым слоем будет полупрозрачный черный UIView, который мы наложим на UIWindow. Таким образом мы перекроем весь UI, который есть. Оставим ссылку на этот UIView, чтобы потом убрать его по необходимости.
  • Сверху на него добавим белый UIView, который будет выполнять роль основной верхней полосы. Тоже сохраним ссылку — нам нужно будет красиво убрать этот UIView по необходимости.
  • Добавим статичную картинку серой молнии на белый UIView.
  • Добавим картинку оранжевой молнии на белый UIView. Ширину выставим равной 0, при помощи блока анимации расширим эту картинку до натуральных размеров. Таким образом мы добьемся красивого визуального эффекта.

Начнем реализацию! Первым делом сменим метод showProgressBar:

Жми меня!
- (void)showProgressBar
{
    if (isShown) return;
    isShown = YES;
    
    [self addGreyView];
    [self addWhiteView];
    [self addGreyZip];
    [self addOrangeZip];
}

Все идет по плану — мы инкапсулировали функционал добавления элементов в отдельные методы. Пройдемся по этим методам, комментарии дам чуть ниже:

Жми меня!
@implementation BSBeautifulProgressBarManager
{
    BOOL isShown;

    // 1
    UIView *mainView;
    UIView *whiteView;
}

<...>

- (void)addGreyView
{
    // 2
    UIWindow *window = [[[UIApplication sharedApplication] windows] lastObject];
    mainView = [[UIView alloc] initWithFrame:window.bounds];
    mainView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5];
    mainView.alpha = 0.0;
    [window addSubview:mainView];

    // 3
    [UIView animateWithDuration:0.3 animations:^{
        mainView.alpha = 1.0;
    }];
}

- (void)addWhiteView
{
    // 4
    whiteView = [[UIView alloc] initWithFrame:CGRectMake(0, -64, 320, 64)];
    whiteView.backgroundColor = [UIColor whiteColor];
    [mainView addSubview:whiteView];
  
    // 5
    [UIView animateWithDuration:0.3 animations:^{
        CGRect frame = whiteView.frame;
        frame.origin.y = 0;
        whiteView.frame = frame;
    }];
}

- (void)addGreyZip
{
    // 6
    CGRect frame = CGRectMake(0, 39, 320, 14.5);
    UIImageView *greyImageView = [[UIImageView alloc] initWithFrame:frame];
    greyImageView.image = [UIImage imageNamed:@"grey"];
    [whiteView addSubview:greyImageView];
}

- (void)addOrangeZip
{
    // 7
    CGRect frame = CGRectMake(0, 0, 320, 14.5);
    CGRect frameSmaller = CGRectMake(1, 39, 0, 14.5);
    
    // 8
    UIView *container = [[UIView alloc] initWithFrame:frameSmaller];
    container.clipsToBounds = YES;
    
    // 9
    UIImageView *redImageView = [[UIImageView alloc] initWithFrame:frame];
    redImageView.image = [UIImage imageNamed:@"red"];
    [container addSubview:redImageView];
    
    // 10
    [whiteView addSubview:container];
    
    // 11
    [UIView animateWithDuration:15. animations:^{
        CGRect frame = container.frame;
        frame.size.width = 320;
        container.frame = frame;
    }];
}

Пойдем по-порядку:

  1. Добавляем ссылки на фон (mainView) и на белую вьюху (whiteView). Фон нам нужно будет показать и скрыть (fade-in и fade-out), а белую вьюху сначала красиво опустить, а потом красиво поднять.
  2. Создаем наш фон, который будет главным superview элемента, делаем его черным, получпрозрачным, и кладем на окно приложения, дабы перекрыть весь UI.
  3. Делаем fade-in для нашего фона, а вместе с ним и для всех его потомков — остальных частей элемента.
  4. Создаем нашу белую вьюху-фон для остальных элементов, кладем ее на главную вьюху, прячем ее за верхними краями предка.
  5. Красиво, анимированно спускаем белую вьюху со всеми потомками вниз. Потомками, кстати, будут молнии.
  6. Добавляем статичную картинку серой молнии на белую вьюху — ничего сложного.
  7. С оранжевой молнией все сложнее. Мы создаем две вьюхи — UIView-контейнер и UIImageView-молнию. Идея такая: добавим UIImageView внутрь вьюхи с шириной равной 0 и анимированно расширим контейнер до нужных размеров. Получим некий эффект «растущей» вправо оранжевой молнии. Для этого подготовим два фрейма: для картинки (frame) и контейнера (frameSmaller).
  8. Создаем контейнер и делаем так, чтобы его потомки не выходили за рамки.
  9. Добавляем картинку оранжевой молнии в контейнер — ничего сверхъестественного.
  10. Добавляем наш контейнер на белую вьюху.
  11. Анимируем расширение контейнера.

Замечательно! Теперь мы можем показать прогресс бар, когда нужно. Перейдем к реализации методов прятания прогресс-бара:

Жми меня!
- (void)hideProgressBar
{
    if (!isShown) return;
    isShown = NO;
    
    [self hideGreyView];
    [self hideWhiteView];
    [self finish];
}

Идея проста: прячем основную вьюху (черную-полупрозрачную или, просто, серую), в это время убираем вверх белую вьюху со всеми ее внутренностями, да еще и оранжевую молнию доводим мгновенно до конца — ведь, если она не расширена до конца, значит нужно довести, чтобы показать юзеру, мол, действие завершено.

Приступим к реализации, комментарии ниже:

Жми меня!
- (void)hideGreyView
{
    // 1
    [UIView animateWithDuration:0.3 animations:^{
        mainView.alpha = 0.0;
    } completion:^(BOOL finished){
        [mainView removeFromSuperview];
    }];
}

- (void)hideWhiteView
{
    // 2
    [UIView animateWithDuration:0.3 animations:^{
        CGRect frame = whiteView.frame;
        frame.origin.y = -64;
        whiteView.frame = frame;
    }];
}

- (void)finish
{
    // 3
    CGRect frame = CGRectMake(0, 39, 320, 14.5);
    UIImageView *greyImageView = [[UIImageView alloc] initWithFrame:frame];
    greyImageView.backgroundColor = [UIColor whiteColor];
    greyImageView.image = [UIImage imageNamed:@"grey"];
    [whiteView addSubview:greyImageView];
    
    // 4
    frame = CGRectMake(1, 39, 320, 14.5);
    UIImageView *redImageView = [[UIImageView alloc] initWithFrame:frame];
    redImageView.image = [UIImage imageNamed:@"red"];
    [whiteView addSubview:redImageView];
}

Все просто:

  1. Делаем fade-out с удалением с предка для главной вьюхи и всех ее внутренностей, соответственно.
  2. Убираем вверх белую вьюху со всеми внутренностями
  3. Самый жестокий и «влоб» метод — кладем сверху новую серую молнию
  4. Кладем сверху новую оранжевую молнию — это создаст ощущение, что молния, спрятанная снизу, исчезла

Кстати, вот и нужные картинки:

Жми меня!



Вот и все! Вы познали кусочек реальной, работающей социальной сети. Фактически, окунулись в код из продакшна. Мы можем вызывать кастомный, красивый progress bar при помощи посылки нужных нотификаций.

Заключение


Спасибо, что дочитали до конца! Я с радостью отвечу на любые ваши вопросы. Если вы нашли какие-нибудь опечатки или неточности — смело пишите в мой Хабрацентр!

Обязательно напишите, что же вы хотите увидеть в следующей статье.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 23
  • +3
    Позволю себе заметить, что в такой реализации о повороте экрана можно забыть: всё поедет. Поэтому нужно добавить необходимые констрейнты, а к тому же добавлять mainView не в окно, т.е.:

    // вместо
    [window addSubview:mainView];
    // надо
    UIView *view = [[window subviews] lastObject];
    [view addSubview:mainView];
    

    Тогда события смены ориентации будут приходить в mainView исправно. Кстати, весьма удобно вынести инициализацию UI в storyboard/xib и в синглтоне держать только указатель на view controller.

    По статье в целом — неплохой туториал, много кому может пригодиться ;)
    • +2
      Спасибо! В задаче не было поддержки ориентации отличной от портретной, поэтому вот так сделали. Наш косяк, да.

      Комментарий правильный! Нужно делать именно так.
    • +4
      Нет самого главного, свойства progress.
      Если говорить про отрисовку, то вариантов масса. Я бы скорее всего отрисовал через CAShapeLayer, но если и использовать картинки, то здесь очевидно что можно использовать паттерн, который легко масштабирется и может быть любой длинны (UIColor colorWithPatternImage).
      Если говорить об анимации, то не совсем понятно зачем столько блоков анимации фейдинга, когда с одними и теми же таймингами. И нет отмены анимаци, если необходимо перезапустить анимацию с другими параметрами, это когда дело дойдёт до конкретного использования данного класса.
      И раз уж каждый раз создаются вьюшки после removeFromSuperview надо очищать переменную, которая ссылается на эту вьюшку.

      Про стиль написания кода можно многое сказать, но всё же пару заметок:
      — не надо использовать префикс k для констант и именование лучше делать по формуле ИмяКлассИмяКонстанты;
      — не надо использовать define для констант, вместо этого: extern NSString *const BSBeautifulProgressBarManagerShowProgressBarNotification;
      — приватные переменные _всегда_ реализовывать через свойства приватной категории;
      — всегда использовать фигурные скобки в if, а то знаете что бывает (return return);
      — флоат числа определять как флоат: 0.5f

      Цель статьи какая?
      • +1
        1. Цель статьи — показать, что под капотом реально существующего проекта, да и некоторые вещи объяснить новичкам.
        2. Про стиль написания кода можно очень (очень!) долго вести холивары, прошу от этого воздержаться.
        3. Можно сделать очень многое и сильно улучшить этот код :) Спасибо за комментарии! Уверен, они помогут кому-нибудь улучшить наш подход.
        • +1
          Не стоит вопринимать слово стиль буквально, это уже давно намного больше чем просто выравние кода, хотя это тоже важно.
          • +2
            Слушайте, ну как это может быть этот код реального проекта, когда это просто болванка, которая по нотификешену показывает 15 секундную анимацию?
            • 0
              А как по Вашему работает progress bar при отправке imessage или sms? Обратите внимание, логика та же самая :)
              • +3
                Логика прогресс бара как раз не в том, чтобы показывать фейк 15 секунд, в результате которого пользователь может ничего не получить, а наоборот подтверить что работа идёт и он отображает данный прогресс. Для отправки SMS/iMessage используется несколько состояний прогресс бара:
                1. Задачу менеджер отправки сообщений принял, определил в очередь и проверяет доступность сети/интернета.
                2. Если связь есть, то в зависимость от типа связи соответствующие тайминги на прогресс отправки, но это не 100% прогресс бара.
                3. Когда сообщение отправлено или связи нет включается финальная анимация завершения работы.
                • 0
                  Т.е. когда мы через плохое соединение отправляем imessage и прогресс пробегает за 5 секунд, а потом еще секунд 20 висит почти в конце, это не фейк? Да, он не доходит до конца, но он не отображает реальный процесс отправки данных.
                  • 0
                    Думаю вы сами ответили на свой вопрос. В данной статье (предположим с плохим соединением) прогресс бар через 15 секунд покажет что задача выполнена 100%, но при этом экран полностью заблокирован. Такое приложение пользователи сочтут глюкавым и тормознутым. Вообще, не хорошо блокировать экран, когда можно выпонять задачу в параллельном потоке.
                    • 0
                      Сожалею, но Вы ошибаетесь. Чтобы закрыть прогресс бар, нужно вызвать у него метод hideProgressBar, и только тогда он добежит быстро до конца (если еще не добежал) и закроется. Если мы отправляем сообщение с плохим соединением, то прогресс бар закроется по окончанию отправки или по таймауту сети. Основная задача этого прогресс бара (так же как и при отправке смс) показать пользователю, что процесс отправки пошел, ему вовсе не нужно показывать реальное количество отправленных данных.
                      Блокировать экран не хорошо, с этим не поспоришь, это нужно просто дорабатывать/переделывать.
                      • 0
                        Кратенько напишу, что делает код выше:
                        1. Подписка на 2 нотификации: показать/скрыть прогресс бар, кстати нотификации абсолютно бесполезные, если мы говорим о синглтоне, в любом месте можно дёрнуть соотвествующий метод.
                        2. Показываем прогресс бар (showProgressBar)
                        2.1. Добавление затемнения и анимация 0.3 сек
                        2.2. Добавление белой плашки прогресс бара и анимация 0.3 сек
                        2.3. Добавление серого зипа к белой плашки
                        2.4. Добавление контейнера оранжего зипа и картинки оранжевого зипа (ох сколько вьюшек, хотя надо было использовать просто backgroundColor) и сразу же (!) анимация на 15 секунд.
                        3. Скрываем, если пришла нотификация
                        3.1 Анимация скрытия вьюшек за 0.3 сек (альфа и белая плашка вертикально)
                        И никаких быстренько потом добежать до конца нет.
                        • 0
                          Имелось в виду не добегание с помощью анимации, в 4 пункте в конце статьи «Кладем сверху новую оранжевую молнию». Это происходит очень быстро и визуально выглядит как добегание до конца.
                      • +1
                        Я понял о чем Вы говорите, да, нужно сделать чтобы прогресс не добегал до 100%, а останавливался на 90% примерно и ждал hideProgressBar, сейчас и правда выглядит не до конца понятно.
                        • +1
                          Да, как минимум.
                • +1
                  Да, это — код социальной сети, написанной за 72 часа в рамках хакатона :)
              • +3
                Чем вам константы не угодили? Это общепринятый подход к их именованию в ObjC.
                • –2
                  Год-два назад сам также именовал и всё ещё с ними работаю. Я всё понимаю, исторически сложилось. Но всё же это не в стиле Objective-C. В новых фрейворках iOS и в популярных open-source проектах видно что всё больше уходят от k префикса. А в итоге по работе скажу, что стало намного удобнее, когда сразу по имени класса начинаешь ввод и поиск константы. Если часто используете Open Quickly… (⇧⌘+O) то быстро можно привыкнуть к поиску констант без k.
                  • +3
                    Это все же не повод быть таким категоричным. Популярные open-source проекты — тоже не показатель :)
                • +1
                  не надо использовать префикс k для констант и именование лучше делать по формуле ИмяКлассИмяКонстанты;

                  Я бы небыл столь категоричен в плане именования, в разных командах принято по разному и в этом ничего плохого нет.
                  не надо использовать define для констант, вместо этого: extern NSString *const BSBeautifulProgressBarManagerShowProgressBarNotification;

                  Да, вот с этим не поспоришь, так делать лучше

                  приватные переменные _всегда_ реализовывать через свойства приватной категории;

                  Здесь тоже нельзя быть категоричным, минус предложенного вами варианта — фейковая инкапсуляция, к такой проперти можно обратиться из вне. К переменной класса же(Помеченной как private) это сделать не на столько просто.

                  всегда использовать фигурные скобки в if, а то знаете что бывает (return return);

                  Это да, хорошая практика

                  флоат числа определять как флоат: 0.5f

                  Здесь тоже соглашусь
                  • +1
                    1. k префикс и константа — я не настолько категоричен к префиксу k сколько к именованию константы. Например kShouldShowBeautifulProgressBar: не хорошо называть константы, которые начинаются как ShouldShow… Во первых это похоже больше на булеву переменную. Во вторых это внешняя константа, а в этом пространстве имён огромное количество других констант и в objective-c так называемый namespacing строится от 2-х, 3-х и т.д. буквенного префикса соответствующего проекта/модуля для соответствующих классов, протоколов, констант и т.д. Константы от класса соответственно строятся от имени класса и смысловой нагрузки константы. Поэтому и получаем такую длинную константу примерно такую: BSBeautifulProgressBarManagerShowProgressBarNotification.

                    Правило для именования констант, енумов и блоков простое: чем больше область видимости, тем больше контекстной/родительской состовляющей, т.е. если константа в области видимости всего класса или даже внешняя, то добавляем имя класса как префикс. Константы внутри метода достаточно просто по имени. Почти тоже самое для имён переменных и методов. Обратный пример, если переменная живёт в области видимости 2-3-х строк, тогда её можно назвать очень кратко: i, frame, view, hidden и т.д.

                    Про k префикс это всё же рекомендация, будет проще и удобнее, поверьте. Но это по большей части рекомендация именно для objective-c.

                    2. Приватные переменные. Здесь мнительность совсем лишняя, что вообще-то можно всех обмануть и получить доступ к свойству. А важно это работа с ARC, KVO и многопоточностью. На WWDC неоднократно повторяют об использовании именно свойств через приватную категорию вместо приватных переменных.
                • +1
                  это, про название переменных, вместо isShown использовать «visible» или «hidden»
                  • +2
                    Правильный комментарий! Спасибо за правки :)

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