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

в 16:06, , рубрики: iOS, iphone, objective-c, кастомные элементы, разработка под iOS

Делаем красивый 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 в iOS приложении
Делаем красивый Progress Bar в iOS приложении

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

Заключение

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

Обязательно напишите, что же вы хотите увидеть в следующей статье.

Автор: backmeupplz

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js