Decorating file downloader in iOS

Szymon Mrozek
May 28, 2018 · 4 min read
Image for post
Image for post
Decorating food, originally here

At some point downloading files is needed in most of mobile applications. Imagine an application without displaying images or a browser app without downloading files feature. Is it always as simple as calling function like SomeMagicLibrary.download(url-to-download)?

In short: no, it’s not. So what if you need some more advanced stuff? Let’s add 3 nice features to the file downloader.


Serial and parallel downloading. Sometimes you’ll need to handle downloading files one by one without concurrency, possibly because of priority or maybe the logic requires showing images in a specific order. In opposite to that sometimes you may need to download them parallel, but maybe with limited concurrency? How to achieve it? The most simple and efficient way to do that on iOS is to use a special mechanism called OperationQueue. It’s very useful for limiting number of operations or adding dependencies to other operations. Let’s define some simple setup:

let queue: OperationQueue
let limit: Int // pre-defined
queue.maxConcurrentOperationCount = limit

What we need to do is to lock this queue with particular operation. Let’s define the download operation:

let operation = BlockOperation(block: { [weak downloadItem] in
guard let item = downloadItem else { return }
let semaphore = DispatchSemaphore(value: 0)
item.completionBlock = { [weak semaphore] in
semaphore?.signal()
}
item.resume()
semaphore.wait()
})
queue.addOperation(operation)

In this simple BlockOperation I’m using object called downloadItem which is a simple wrapper around URLSessionDownloadTask. Its completion block is fired after finishing download (or cancelling it) and of course it contains resume method as each URLSessionTask do.

For locking the queue I’m using DispatchSemaphore. It locks the queue on calling wait() function and releases it when signal() is fired. With this mechanism there might be multiple operations (downloads) scheduled but only maxConcurrentOperationCount number of operations are running at the same time.


Create download queue and … delegate. With the functionality described earlier it might be very useful and easy to create the download queue for example for creating delegate method that is invoked when all operations from the queue are finished. Let’s use some magical object and call it Cedric for now. As long as Cedric is responsible for scheduling new downloads in the queue and removing them, this operation is as easy as calling delegate when queue becomes empty

func cedric(_ cedric: Cedric, didFinishWithMostRecentError error: Error?)

But wait. What about the delegate? The interesting part is that file downloader is probably the object that should notify multiple subscribers about finishing some download (or progress reporting). It’s possible by using MulticastDelegate pattern. There are a lot of articles about it so just a quick note: it’s generally weak objects array that notifies each of them one by one on each event. Let’s define:

var delegates: MulticastDelegate<CedricDelegate>

Now we can just call them:

delegates.invoke({ 
$0.cedric(self, didFinishWithMostRecentError: self.lastError)
})

Every object inside the array of delegates will be notified that cedric queue did finish downloading all items (maybe with some error). It’s sometimes very helpful to have multiple delegates and for sure very helpful when talking about files downloading.


Reusing or creating new files. Assume that you are creating a browser like application. What should happen when the user taps to download a file? Expected behavior is to download the file every single time that the user wants to do it.

But what about downloading in music application with playlists? What should happen when the user schedules a download for 2 different playlists with the same song? It should certainly reuse the same shared file.

Those two are completely different behaviours. How to handle it in file downloader? Let’s define simple enum:

public enum DownloadMode {
case newFile
case notDownloadIfExists
}

Once we have that we have to handle only 2 operations. Scheduling new downloads with particular mode and finishing download. Schedule part:

switch resource.mode { 
case .newFile:
items.append(item) // always schedule no matter if file exists
case .notDownloadIfExists:
if let existing = existingFile(for: resource.destination) {
delegates.invoke({ ... }) // notify about finish
return
} else {
// check is operation not already scheduled
guard !items.contains(resource) else { return }
items.append(item)
}
}

If newFile mode is used Cedric is trying to append some unique path component for example for existing file music.mp3 the music(1).mp3 will be created by calling uniquePath(forName:) function. After finishing download Cedric saves file under resultUrl:

switch resource.mode {
case .newFile:
resultUrl = try fileManager.createUrl(forName: destination, unique: true)
case .notDownloadIfExists:
resultUrl = try fileManager.createUrl(forName: destination, unique: false)
}
func createUrl(forName name: String, unique: Bool) throws -> URL {
let downloads = try downloadsDirectory(create: true)
try createDownloadsDirectoryIfNeeded()

if unique {
return try uniquePath(forName: name)
} else {
return downloads.appendingPathComponent(name)
}
}

Without unnecessary looking at the code inside uniquePath function, it seems to be pretty easy, right? But what a nice feature to have!


Final notes

  1. As you’ve probably already guessed I’ve written a library called Cedric, it’s written in pure Swift on top of URLSession, feel free to use it and suggest new features / fixes.
  2. This article is only an example, real implementation is a little bit more complicated but for making it more readable I’ve removed some unnecessary parts.
  3. Don’t forget to clap and follow if you like it 😎
  4. A huge thanks to my brother Mateusz for decorating my articles with correct grammar and general fixing of my English language mistakes.

Library: https://github.com/appunite/cedric

AppUnite collection

We teach how to write apps

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store