Road to circular progress pull to refresh at Beamly

Pull to refresh, this friend of ours

The Pull to refresh became one of the most popular concepts used in mobile iOS apps. Loren Brichter, the author of Tweetie for iOS introduced it for the first time in 2011 and it stood the test of time. Several implementations of the pull to refresh lie out there and the most used on iOS is for sure the SVPullToRefresh by Sam Vermette. Back in 2012, the concepts of Objective-C runtime and associated objects were still obscure to most of the iOS developers but Sam used the properly to add an extra view to the UIScrollView without the need for subclassing.

Apple built a native pull to refresh publicly available as of iOS 6, called UIRefreshControl, but customizations are hard to achieve and still, too often developers fallback to an ad hoc implementations.
The most common customization is implementing a circular progress view like the one used in the Pinterest app.
This leads to a much cooler UI rather than the well-known yet obsolete rotating arrow, and it is recognizable and intuitive to all iOS users.

The concept proposed here has two main individual transitions that are dependent about the position of the finger:

  1. App logo becomes visible (alpha/opacity property)
  2. Circle progress becomes filled

You can see the final behaviour in the gif below, but I definitely recommend downloading and running the Beamly iOS app by yourself to get the right feeling.

pullToRefresh

Yes... yes... calm down, we have it open sourced

Go crazy with it on GitHub: BMYCircularProgressPullToRefresh

ptr_1

ptr_2

ptr_3

ptr_4

The first transition is very straighforward. It is just changing the opacity of the layer. The second one is more complicated and we need two images with the full circles:

  1. Light color circle image (for progress not filled)
  2. Dark color circle image (for progress filled)

the trick is to leave the first one untouched and to mask the second one based on the progress we want to display (that is dictated by how much the user scrolls). The mask we want to use is a a pie shape that become a full filled circle when the progress is 100%. We are developers and we talk code, so, here we go:

- (void)_updatePie:(CAShapeLayer *)layer forAngle:(CGFloat)degrees {
    CGFloat angle = degToRad(-90);
    CGPoint center = CGPointMake(CGRectGetWidth(layer.frame)/2.0,
                                  CGRectGetWidth(layer.frame)/2.0);
    CGFloat radius = CGRectGetWidth(layer.frame)/2.0;
    
    UIBezierPath *piePath = [UIBezierPath bezierPath];
    [piePath moveToPoint:center];
    [piePath addLineToPoint:CGPointMake(center.x, center.y - radius)];
    [piePath addArcWithCenter:center
                       radius:radius
                   startAngle:angle
                     endAngle:degToRad(degrees - 90.0f)
                    clockwise:YES];
    [piePath addLineToPoint:center];
    [piePath closePath];
    
    layer.path = piePath.CGPath;
}

The other big part of this component is the actual pull to refresh. Well, in the end there is no big surprise about it and the main idea behind the SVPullToRefresh has been used (just this implementation is nicer, protocol-based and with consistent code style, while the SVPullToRefresh is absolutely a combination of spaghetti and messed up code. Yes, I just said that). But again... I think that having an associated view on the UIScrollView is the only way to implement it properly.

This is the first bit of code we open source at Beamly, and I'm kind of proud to be the one who pushed for it :)

A peculiarity about it is the support for custom contentInsets on the underlying scrollview (as we needed in the Beamly app). I initially struggled on it for hours failing at least twice, eventually my smart colleague Stefan Dougan-Hyde added the support for it.