Haneke 1.0 is a major refactoring with API changes and improvements on all fronts. At the time of writing this the new version sits in its own pull request, awaiting pre-release feedback before being merged into master. Whereas the pull request acts as a changelog, this post gives me an opportunity to comment on two of the most important design decisions of the refactoring.
Leaner UIKit categories
The entry point to Haneke is it
UIImageView category. It allows to load images from network or disk with one line of code, leveraging the full power of image caching and automatic resizing in the process. Before 1.0, the
UIImageView category had 4 responsibilities:
- Loading images from network or disk
- Determining the image size and aspect ratio based on the view properties
- Interfacing with the cache
- Displaying images
Of those, only determining the image properties and displaying images are specific to
UIImageView. When users of the library started asking for a similar category for
UIButton (none more enthusiastic than Aleix Ventayol), it became clear that the rest of the code could be reused.
My first instinct was to create a
UIView category and move the code there. This was a step in the right direction, and such category now exists in version 1.0, mostly for helper code that interfaces with the cache. However, loading images from network or disk is a common need for any client of the cache, no matter if it’s a UIKit view or not. Shouldn’t this responsibility be independent of the presentation layer? And where should it be?
Turns out an early design decision was the answer. Haneke had the concept of cache entities since its inception, where an entity represents an image-key pair whose image will be requested only if needed. When an image is not available in the cache in the required format (size, aspect ratio and other transformations), the cache will ask the corresponding entity to provide the original image. Providing the original image is assumed to be an expensive operation (in memory and performance), so entities help to make sure this is done when absolutely needed.
Loading images from network and disk are in fact particularizations of providing the original image. The cache entity behavior is simply a protocol, but by creating concrete implementations that know how to load an image from an URL or path two reusable components were born:
HNKDiskEntity. And the
UIImageView category went from about 400 lines of code to about 150, a great deal of which are convenience methods.
A standalone disk cache
I tend to design Objective-C libraries to use as less files as possible, often grouping many classes in the same .m file. This improves library usability (nothing beats dragging a single header-implementation pair to play with something) at the expense of ease of the development (modifying longer files can be daunting). Haneke initially was one of those single file libraries, a thousand line class with helpers.
When reimagining Haneke for Swift with Luis Ascorbe, Oriol Blanc and Joan Romano we didn’t start with this constraint because Swift supports frameworks, and the number of files in a library becomes less important. Also, it would have been an early optimization to be concerned about file modularity. Naturally, each responsibility became its own class, and one of those is
DiskCache, the Swift class for the disk cache.
Going back to Haneke for Objective-C, previous to version 1.0 the
HNKCache class was responsible for both managing both the memory and disk cache. Haneke is an image cache, and there are optimizations at memory level that are specific to images. At disk level, though, there’s nothing special about how Haneke reads and writes files. This meant that a generic-purpose least-recently-used disk cache was hidden inside
HNKCache, which became evident while developing the Swift version. So was born
HNKDiskCache sets and fetches data from disk, and doesn’t have any dependencies to the rest of Haneke. Of course, it was designed to serve Haneke’s needs, so its implementation has some quirks. All of its operations are performed asynchronously in a serial queue, which is public in case you need to wait for any of them to finish (a blessing for writing unit tests). Also, it has a somewhat odd enumeration method that loads all key-data pairs by descending access date. This is used by Haneke to preload images in memory, and I’m not sure it has any other use case.
Separating the disk cache into its own component also made the code easier to test and allowed me to greatly increase test coverage without resorting to white-box tests. Haneke 1.0 now is currently at 99% test coverage, with 200 hundred unit tests. While I do miss having a single file for the cache, I opted to put
HNKDiskCache in its own file to reinforce its independency with the rest of Haneke and increase its discoverability. Here’s hoping that people who need a disk cache find it.