Replicating the App Store download button
With the release of iOS 11, Apple introduced a completely redesigned App Store app with greatly improved UI.
Among many features and improvements, one thing that I found interesting was the redesigned download/purchase/update button.
I’ve been working in my spare time on implementing the download button. After a bit of experimenting, I implemented a working version. I only needed a couple more days to clean up the code and package it into a CocoaPod. You can take a look at the source code on the AHDownloadButton GitHub repo. The final result can be seen below:
The README section describes how to use the library. In this blog post, I will provide some details on how I implemented the download button as well as discuss some interesting points about the implementation.
Download state implementation
The download button has 4 different states:
startDownload
pending
downloading
downloaded
Each of these states is implemented as a separate view class.
The startDownload
and downloaded
state use the same class - HighlightableRoundedButton
. It is a simple subclass of UIButton
that has rounded corners and highlightable background and title. To implement highlighting I’ve overridden the isHighlighted
property of UIButton
:
The pending
state uses a CircleView
. It is a UIView
subclass that has startAngleRadians
and endAngleRadians
properties that define the start and end angle of the circle. The drawing is implemented by adding a CAShapeLayer
with a circular path. The path is defined by overriding layoutSubviews
method:
The downloading
state is presented using the ProgressButton
class. It is a UIControl
subclass that uses a ProgressCircleView
to show download progress. Whenever the user updates the progress property of the ProgressButton
, the ProgressCircleView
animates the progress change. The progress property is implemented in the following way:
Whenever the progress is set to a value lower than 1, the isAnimating
property is checked to make sure that the last change is not currently animating. If it’s not, it will animate the progress change to the newest value. If the progress is set to 1 and the last change is still animating then the running animation is overridden and progress is animated from the current presentation value to 1.
This is the code that animates the progress change:
State transition animations
Transition from one state to another is animated whenever the state
property of the download button is changed. The initial implementation was very simple:
After a bit of testing, I noticed an edge case that I missed here. What would happen if the state is changed during an animation of the previous state change? The animation for the latest state update would override the previous animation and it wouldn’t look nice. I needed to make sure that, whenever the state is changed, the animation for previous change completes before I animate the latest change.
To do that, I used a background DispatchQueue
in combination with a DispatchGroup
:
There are a few things to go over here:
-
I used a background queue to chain one transition animation after another. I am doing this because I do not want to block the main thread while waiting for the previous animation to finish. Also, note that I’ve captured the value of the
state
property inside a capture list. I have to do that because I want the closure to capture thestate
value at the time of creation, not at the time of execution. -
I used a
DispatchGroup
for synchronization. I callanimationDispatchGroup.enter()
to indicate that I am going to animate a transition and that other animations need to wait for the current one to finish. -
If the transition happens from
downloading
todownloaded
state with a complete progress, then a delay has to be introduced to let the final progress animation finish before the transition animation starts. -
Since animations need to be done on the main thread I have to dispatch the animation on the main queue.
-
In the last step, I call
animationDispatchGroup.wait()
. This will cause the background queue to be blocked until theanimateTransition(from: oldValue, to: currentState)
finishes execution and callsanimationDispatchGroup.leave()
.
Using this approach I have synchronized the animations and I didn’t block the main thread while doing that. To see how animateTransition(from: oldValue, to: currentState)
works I suggest that you take a look at the source code.
Conclusion
This was an interesting and fun project to make. In this post, I’ve described the more interesting parts of the implementation. If you want to know how I implemented it in more detail, how I separated and organized the classes and how I used them, head over to my GitHub repo. You can also find the documentation that describes how you can use and customize the download button.