We all enjoy fast and responsive apps that feel good when you use them. Therefore as iOS developers we spend a lot of time on delivering the best possible experience to the user. This becomes extra important when your application has to perform some long running operation, for example downloading a large video. Depending on the size of the download, speed of the user’s internet connection, or hardware this could take a while, which can be frustrating for the user. We can improve such an experience by giving the user a sense of the current work that is happening and how far that work has progressed by using a progress indicator.
Lately I have discovered the Progress
class from Foundation which allows us to do just that. This weeks blogpost will show you the basics of Progress
by taking you through an example of how to show the progress of downloading an image to the user.
What is Progress
?
Progress
is a class that represents the completion of some work that an application is doing for example downloading or uploading a file to the server.
Many of the api’s that we use on a daily basis report their progress with Progress
for example NSURLSession
which we get to later in this blogpost. Of course you can also use Progress
to report progress from the custom components in your application and if the work your application is doing consists of multiple sub steps you can even use composition to create a tree of progress objects.
The basics of Progress
Progress
has three important properties that you should know about:
totalUnitCount
completedUnitCount
fractionCompleted
Let’s have a look at each one of them.
totalUnitCount
The totalUnitCount
represents how many units of work a Progress
object should track, for example the amount of bytes of a download.
completedUnitCount
The completedUnitCount
reports how much work of the total amount of work that the progress tracks has completed.
fractionCompleted
fractionCompleted
tells you how much of the work has completed. It is a double value between 0 and 1 where 0 means that none of the work has completed and 1 means that all the work has completed.
Localization
Progress
also has a property called localizedDescription
and localizedAdditionalDescription
which can be used to provide the user with more information on what is happening in your application. Through the kind
property you can tell the progress object what its unit represents and to format the progress as such, for example if you set it to ProgressKind.file
the localizedAdditionalDescription
gives you a byte formatted string.
An example
In this example we are going to download an image from the internet and display the progress to the user. Download the sample project if you want to follow along with this example.
The example project has one view controller which is very simple. It contains an UIImageView
for showing the downloaded image, a UIButton
to trigger a download, two UILabels
that will contain text to explain the user what’s going. It also contains a UIProgressView
which is a view that displays the progress in a progress bar.
Our view controller also has a reference to an ImageDownloader
which has a method downloadImage(with:completion:)
that downloads the image from the passed in URL
This method returns a progress object which we will use to update our UI and has a completion handler with an optional image that gets called once the download has finished.
Inside the downloadImage(with:completion)
method we create a URLSessionDownloadTask
with the passed in url of the image. Since iOS 11 a task has a property called progress
that contains the tasks progress object which we can use to update our UI.
func downloadImage(with url: URL, completion: @escaping ((UIImage?) -> Void)) -> Progress {
let task = session.downloadTask(with: url) { (url, response, error) in
...
do {
let imageData = try Data(contentsOf: url)
let image = UIImage(data: imageData)
completion(image)
} catch {
completion(nil)
}
}
task.resume()
return task.progress
}
When a download action is triggered we call the download function and store the returned progress object in a property called downloadProgress
in our view controller. It has a property observer in which we attach the new progress object to the UI. To display the progress in our progress view we set downloadProgress
to the observedProgress
property of the view. We use KVO to update our progress label every time the localizedAdditionalDescription
changes. KVO notifications of properties on Progress
are sent from the thread on which the progress was updated and therefore we have to dispatch our update of the progress label onto the main queue to make sure our UI gets updated on the main thread.
private var downloadProgress: Progress? {
didSet {
observation?.invalidate()
progressView.observedProgress = nil
guard let progress = downloadProgress else {
return
}
progressView.observedProgress = progress
progressDescriptionLabel.text = progress.localizedDescription
observation = progress.observe(\.localizedAdditionalDescription, options: [.initial, .new]) { [weak self] _, change in
DispatchQueue.main.async {
self?.progressLabel.text = change.newValue
}
}
}
}
Conclusion
Next time when your application is performing some long running task consider using Progress
to let the user know what is happening in the application. As you can see from the example some Cocoa api’s already support progress reporting and therefore just require a few steps to make it work. But there is much more you can do with Progress
. You could for example create a tree of progress objects or use the progress object to cancel, pause or resume work. Take a look at it and have some fun!
Contact me on Twitter @kairadiagne if you have any questions, comments or feedback.