Making a beautiful Progress Bar in an iOS app

Good afternoon, dear Habrahabr!

In this article I want to describe the way in which we made such a beautiful custom progress bar - in the illustration - in one of the latest projects.

The task was set as follows:

  • Pictures were drawn by the designer.
  • The progress bar should overlap and block the entire UI.
  • The item must be called by a notification in NSNotificationCenter.
  • It should be possible to prematurely terminate the element.
  • There should be one progress bar, regardless of the number of notifications sent.

Those interested in the implementation, I ask for cat.

First of all, we create a singleton BSBeautifulProgressBarManager and write it in BSBeautifulProgressBarManager.h :

Push me!
#import 
#define kShouldShowBeautifulProgressBar @"kShouldShowBeautifulProgressBar"
#define kShouldHideBeautifulProgressBar @"kShouldHideBeautifulProgressBar"
#define beautifulProgressBarManager [BSBeautifulProgressBarManager sharedManager]
@interface BSBeautifulProgressBarManager : NSObject
+ (BSBeautifulProgressBarManager *)sharedManager;
@end

At work, we often use the define trick to get rid of the constant use of singletones by class name. Here, instead of [BSBeautifulProgressBarManager sharedManager], we can simply write BSBeautifulProgressBarManager. We will not touch the header file anymore.

We usually put out notification notifications in some Config.h - then we don’t need to import the manager header to send notifications. However, in this example, we will add a trick to get rid of magic constants directly into the singleton header.

Let's look at the singleton implementation in the BSBeautifulProgressBarManager.m file :

Push me!
+ (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;
}

Nothing special has happened so far - the usual thread-safe singleton implementation. The only remarkable thing in this method is the subscription to show / hide progress bar'a notifications at NSNotificationCenter.

Let's move on to the implementation of the showProgressBar and hideProgressBar methods:

Push me!
- (void)showProgressBar
{
    if (isShown) return;
    isShown = YES;
}
- (void)hideProgressBar
{
    if (!isShown) return;
    isShown = NO;
}

So far they are not doing anything. However, we have already fulfilled two conditions for the operation of the element: there will be only one progress bar, and it will be called through NSNotificationCenter. Accordingly, we will add a private variable to the implementation:

Push me!
@implementation BSBeautifulProgressBarManager
{
    BOOL isShown;
}

Our idea will be this:

  • The first layer will be the translucent black UIView, which we will overlay on the UIWindow. Thus, we will redraw the entire UI that is. We’ll leave a link to this UIView to later remove it as needed.
  • Add a white UIView on top of it, which will serve as the main upper strip. We’ll also save the link - we will need to beautifully remove this UIView as necessary.
  • Add a static picture of gray lightning to a white UIView.
  • Add an orange zipper picture to the white UIView. Set the width equal to 0, with the help of the animation block, we expand this picture to its natural size. Thus we will achieve a beautiful visual effect.

Let's start the implementation! First, change the showProgressBar method:

Push me!
- (void)showProgressBar
{
    if (isShown) return;
    isShown = YES;
    [self addGreyView];
    [self addWhiteView];
    [self addGreyZip];
    [self addOrangeZip];
}

Everything goes according to plan - we encapsulated the functionality of adding elements to individual methods. Let's go through these methods, I will give comments below:

Push me!
@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;
    }];
}

Let's go in order:

  1. Add links to the background (mainView) and the white view (whiteView). We will need to show and hide the background (fade-in and fade-out), and at first it’s beautiful to lower the white view and then to raise it beautifully.
  2. We create our background, which will be the main superview of the element, make it black, translucent, and put it on the application window in order to cover the entire UI.
  3. We make a fade-in for our background, and with it for all its descendants - the rest of the element.
  4. Create our white view background for the rest of the elements, put it on the main view, hide it behind the upper edges of the ancestor.
  5. Beautifully, animatedly lower the white view with all the descendants down. Descendants, by the way, will be lightning.
  6. Add a static picture of gray lightning to a white view - nothing complicated.
  7. With orange lightning, everything is more complicated. We create two views - a UIView container and a UIImageView lightning. The idea is this: add a UIImageView inside the view with a width of 0 and animate the container to the desired size. We get a certain effect of "growing" to the right orange lightning. To do this, prepare two frames: for the picture (frame) and the container (frameSmaller).
  8. Create a container and make sure that its descendants do not go beyond.
  9. Add a picture of orange lightning to the container - nothing supernatural.
  10. Add our container to the white view.
  11. We animate the container extension.

Wonderful! Now we can show the progress bar when needed. Let's move on to the implementation of methods for hiding the progress bar:

Push me!
- (void)hideProgressBar
{
    if (!isShown) return;
    isShown = NO;
    [self hideGreyView];
    [self hideWhiteView];
    [self finish];
}

The idea is simple: we hide the main view (black-translucent or, simply, gray), at this time we remove the white view with all its insides, and also bring the orange lightning instantly to the end - because if it is not expanded to the end, then you need bring to show the user, they say, the action is completed.

Let's get down to implementation, comments below:

Push me!
- (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];
}

Everything is simple:

  1. We do a fade-out with removal from the ancestor for the main view and all its entrails, respectively.
  2. We clean up the white view with all the insides
  3. The most cruel and “loophole” method - put a new gray lightning on top
  4. We put a new orange zipper on top - this will create the feeling that the lightning hidden from below has disappeared

By the way, here are the pictures you need:

Push me!



That's all! You have learned a piece of a real, working social network. In fact, they plunged into the code from production. We can call a custom, beautiful progress bar by sending the necessary notifications.

Conclusion


Thank you for reading to the end! I will be happy to answer any of your questions. If you find any typos or inaccuracies, feel free to write to my Habracenter!

Be sure to write what you want to see in the next article.

Also popular now: