(Edit: as many responses have pointed out, this approach isn’t going to work in every single case. But then, you weren’t just going to copy and paste this code without thinking about what you actually need, were you? Were you?)
Me: Hey, The Internet. I’d like to load some images in my iOS app.
The Internet: Loading images, huh? Have you tried the asset catalogue?
Me: No no no, I’ve explained this wrong. I need to load some images from URLs.
The Internet: Have you looked at On Demand Resources? Maybe you could host the images on the App Store and load them through the asset catalogue?
Me: What is your thing with the asset catalogue today? No, I need to load some images from URLs that won’t be known until runtime.
The Internet: Oh, use a third party dependency. Duh.
This is an (exaggerated) conversation I’ve had countless times with the internet, and it always ends the same way. Use a third party dependency. This made sense in the past: networking on Apple platforms used to be kind of a pain. However, since iOS 7 we’ve had access to
(NS)URLSession, a much improved set of APIs. This means that there’s no good reason to go straight for a third party dependency when writing networking code.
In this post we’re going to look at how we can go about implementing image loading without using third party dependencies.
The Wrong Way
Me: So about that image loading then.
The Internet: Oh, you still care about that huh? Fine.
Me: Yup. So how do I do it?
The Internet: Without dependencies?
Me: Without dependencies.
The Internet: Oh it’s super easy. Four lines of code.
Let’s get this out of the way early. There is a wrong way to do this. When you look this sort of thing up online you’ll no doubt come across something like this Stack Overflow answer.
This… works. But that’s about the best thing you can say about it. So what’s the issue?
Data(contentsOf:) is the issue.
Take a closer look at the code. Notice something odd? Loading data from the server is an asynchronous operation. We make the request, and then some time later the server responds. But that code doesn’t look asynchronous. And it isn’t. In fact,
Data(contentsOf:) is synchronous. Which means that if you run this code on the main thread (which you have to, because it’s updating the UI) you’re going to lock your app for as long as it takes for the request to complete.
In fact, the documentation for this method says the following:
Do not use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
(N.B. yes I’ve done this… oops).
The Right Way
Me: Well that was bullshit.
The Internet: Look, I just find stuff. Actually reading it is your job.
The Internet: Anyhow, those Apple docs seem awfully useful. Have you looked at
Me: Go on…
Let’s do this the right way then. We’ll be using the
dataTask(with:completionHandler:). This method takes in the
URL we wish to make the request to as its first argument, and a completion handler with type
@escaping (Data?, URLResponse?, Error?) -> Void as the second argument. We call it like this (ignoring the possibility of errors):
This already looks better. We’re doing the loading off the main thread, and then coming back onto the main thread to update the UI.
At this point, there’s only really one difference between our code and that of many third party libraries: implementing this as an extension of
UIImageView. But that’s super easy to do as well:
Here we’ve even gone so far as to add in a placeholder image which can optionally be shown while the real image is being loaded. This was a really simple addition once we had the networking part sorted.
Me: So in 13 lines of code I managed to implement a feature that I’d normally have brought in a whole library for.
The Internet: So why are you telling me? Go write a damn blog about it or something.