Image fetching is a common ‘gotcha’ for new learners. I’ve used it in the classroom numerous times. It’s fun. See, first you surprise them with the realization that their JSON response doesn’t contain the image data, just a url to the image data. Then you let them figure out what to do with that.
Inevitably, they download the images inside their repeating content’s display, or worse, try to save the images to a separate collection that is never in sync with the source data collection. Either way, their code is twisted up every which way. You chuckle to yourself, remembering how the developer you learned from did this to you, savoring the moment as they once did.
Then you have to actually fix it. If there isn’t already a custom model, you make one, and you either put it in charge of the download task or have some manager object do it and assign the image back to the model when it’s done. But that’s only half the battle. The other half is to somehow notify the UI that a downloaded image is ready for display.
Then you feel bad. Not because you put them through hell to start, but because the real hell is actually in the solution.
FutureImage
Let’s do this better. Let’s just completely ignore the download task altogether.
We’ll accomplish this by subclassing UIImage. Why?
Well that sounds great, but there’s nothing here. And what’s this extension on UIImageView it’s talking about? Seems like that’s where the magic is.
We’ll have an initializer and some logic in this class eventually, but indeed, the show stealer is the extension.
What’s going on here amounts to a (leaky) delegate relationship. But since UIImageView is the only class in UIKit that we use to display images, there’s no need for us to worry our consumers about it. And so we hide it away for simplicity. What’s left is merely the FutureImage instance, and the futureImage property on UIImageView to which it is assigned.
A consumer initializes a future with a url to the remote image, then assigns that future to the image view that will display that image. That’s it. Everything else is managed behind the scenes without a care for what’s happening on the outside, including reassignment of the future to another image view, which is common in repeating content.
This is where the initializer and some of the private members of FutureImage come in.
As was indicated, there’s almost nothing here. Just the fetch task (which contains nothing of consequence) and some didSet handlers that enable the delegate mechanisms. It’s lean on logic and API, but big on code impact. The best kind of abstraction.
Ignore The Download™️
Let’s start with a typical Decodable model:
At this point, you’d either need to custom implement the initializer and immediately start the download task for the image, or wait until some later point when it’s about to be displayed, but either way provide a mechanism for broadcasting to the UI when the download is complete.
Let’s use a future instead:
By declaring the future lazy, we delay the download task until the first time it’s used. So let’s use it:
What happens next? Well, if this is the first time the user has scrolled to this item, the image view gets the placeholder and the image download starts. When the download is finished, the image view updates.
If the image was already downloaded, the image view just gets the real image immediately. You don’t care either way, and neither does the image view. If the user scrolls before the download has finished, the recycled image view would get the next future and all the same things happen.
You’re passing around an actual image object. And you can use it to behave as if you already have the final image. It’s fun to pretend, isn’t it?
Okay but now what happens when the user taps on that item and we need to display the detail page for it?
Not only did you ignore that download task again, you then went and passed an array of futures as if it was an array of the actual blamming images. How do you sleep at night?
Now, obviously, that detailImageCarousel is going to need to know to be on the lookout for FutureImage instances so that it can assign them to the futureImage properties on its image views. The only way to avoid that would be to also subclass UIImageView and override its image property. But then you’re muddling your UI, and I think it’s much nicer to keep the hidden things behind the model, and make everything explicit in your UI.
But FutureImage objects areUIImage objects, and you can treat them as such in places where you don’t want to create extra API just for handling futures. The code above really can work. I’ve done it.
Cacheing (is evil)
The nice thing about this abstraction + model combo is that the model effectively becomes the image cache, and your UI is none the wiser. As you pass the model objects around your app, the UI will get the images without knowing that they didn’t need downloaded.
But what happens if you repeat a fetch request and trash your model instances? You’d be wastefully downloading some of the same images over again potentially.
Well, at the risk of allowing this abstraction to outgrow itself, I think it’s a pretty good place to abstract longer-term caching as well. I’m not going to go into depth on it here as I think it would betray the focus of this post. But this is how the public API could end up looking:
Which would be provided by a new nested CachePolicy type in FutureImage that looks something like:
Inside FutureImage, you’d store this cache policy object and use it to retrieve a cached image if available, or to otherwise cache the image after it’s downloaded. The FutureImage wouldn’t need to be privy to the underlying details since the CachePolicy would hide them away. The FutureImage logic would stay tidy, aside from some checks for image presense in its initializer.
Nothing new…
This was super fun to work on because I wasn’t going off of any outside references. I was making it up as I went along, and kept thinking I’d hit a blocker at some point, but I didn’t. And it ended up nicer to use than I ever thought it would.
That said, I expect this pattern has been done before, and probably in a popular library even. If you’ve read the other posts here, you know dependencies are not by bag. So while I did make this myself, and I did have a blast doing it, and I therefore wanted to share it, I do not lay claim to it.
If you use it, I hope it’s as fun for you as it was for me. But maybe don’t use it. Maybe see if you can come up with something even better. That’s where the real fun is.