We recently released the updated and improved iOS application for Mmmm.com - a user curated visual menu for finding awesome food (and drink) in the city of Leeds, West Yorkshire.
Whilst testing the application it became apparent that the implemented image caching mechanisms were not appropriate for an inherently image heavy application.
I delved back into the source code and noted that for some reason I had attempted to reinvent the wheel - I had implemented my own caching methodology within my
UICollectionView adapters (which implement the
The logic was fine. I had opted to cache images in memory using
NSCache. The idea was that rather than re-download an image from the server when a user scrolls back to a particular cell we would load the image directly from the cache.
I had also implemented a filesystem cache which would save the last ten displayed photos to the filesystem and would save details of their locations to a SQLite database (interfacing with the SQLite database through the fantastic FMDB library). The intention of this caching layer was that upon reopening the app we could display the last ten photos almost instantaneously prior to sending off a query to the server to request more up to date photos. It was implemented to provide an optimal user experience such that a user sees beautiful pictures of food upon reopening the app as opposed to a boring loading screen.
There was in fact no problem. The caching worked as expected and it saved on unnecessary server requests. The problem was what my caching mechanism didn't do.
If a user quickly scrolled through the collection of photos cached images would be shown (where appropriate) or requests would be sent off to the server to load each image for each cell. Even the cells that had been scrolled past extremely quickly and were no longer on screen. Not only does this result in unnecessary data usage but it also means that the requests to load the images for the cells that are on screen are at the end of a queue of unnecessary requests.
Fortunately I had noticed this and had implemented the concept of a request queue within my delegate. It essentially tracked ongoing requests and cancelled unnecessary ones.
This in principle should have worked but because of the complexities associated with asynchronous requests in collection views - race conditions, reused cells etc it was resulting in some visual issues such as flickering images and incorrect images on certain cells.
There is an interesting and informative question on StackOverflow which touches on some of these problems. It is well worth a read.
It was this that spurred me to look back at the code and come up with a plan for refactoring it.
Developing a plan of attack wasn't difficult. As soon as I looked at the code I was completely bemused as to why I had not used the AlamofireImage extension (of which I was aware). I believe the reason that I hadn't was because at the time the code had been originally written AlamofireImage did not exist. As such I had not reinvented the wheel.. I had created it. Unfortunately (Fortunately?) someone had since created a much better wheel.
My logic had been flawless, and what I had implemented was good. It was simply the case that here in July 2016 we have Alamofire which implements what I had done and much more.
Alamofire is actively developed, and has an active community of top quality developers maintaining it. It is a perfect example of the awesomeness of open source. Alamofire is what I utilise for the network interaction across the Mmmm application and as such it seemed like a logical extension to utilise the AlamofireImage extension for my image needs.
AlamofireImage is well documented and extremely simple to use. It provides various methods for downloading, caching, and manipulating images (transitions, rounded corners etc). In addition to this it specifically provides
UIImageView extension methods which utilise these methods to provide simple 'helper' functionality for displaying these images.
The best bit about these
UIImageView methods is that they handle a lot of the complexity behind the scenes. One could very easily naively implement them and encounter no issues. For example if I attempt to load an image from a URL into a
UIImageView within a
UICollectionViewCell utilising the
af_setImageWithURL method it will automatically handle the cancellation of unnecessary requests when the containing cell is reused.
N.B. A well written app will take advantage of the abundance of information returned by Alamofire to act on situations like this appropriately. Alamofire will return an appropriate error code when a request is cancelled such that you can adjust the UI or execute any other behind the scenes actions as needed.
For further details on some of the benefits offered by the UIImageView extension methods, see here. It also outlines some of the things that the extension does not cover that you should be aware of.
There a few takeaways from this:
Take the time to look back through old code - there may be something from 'back in the day' that is no longer suitable or optimal given the fast paced nature of software development.
Open source is great. Alamofire has nearly 18,000 stars on GitHub. It is clear that a lot of people use it. Don't reinvent the wheel. If you want to spend time coding networking and caching mechanisms simply contribute to Alamofire :)
Be careful and considerate of your user base. I avoided any issue by catching it in advance. It is patently apparent that users like a good experience within your app, and they (generally) like money. If you are needlessly sending out network requests to load images you would be surprised at how quickly data usage can add up. Users are not going to be very happy if you drain their data caps.