Simplifying UICollectionViewFlowLayout Delegate Method usage with Functional Programming
When writing collection view layouts, we are often subclassing UICollectionViewFlowLayout to gain access to the extra options that are provided. This is really helpful because it means we can use all of the plumbing provided by Apple for these including great support in Interface Builder for its properties.
There are six properties definied on UICollectionViewFlowLayout
that can be set by users of the layout to determine layout properties. These are:
minimumLineSpacing: CGFloat
minimumInteritemSpacing: CGFloat
itemSize: CGSize
headerReferenceSize: CGSize
footerReferenceSize: CGSize
sectionInset: UIEdgeInsets
The delegate for this layout class, UICollectionViewDelegateFlowLayout
, also has a set of methods which match these properties. They allow the user to set different values per section, with the exception of the size for an item which provides sizes for each index path.
collectionView(_:layout:minimumLineSpacingForSectionAt:)
collectionView(_:layout:minimumInteritemSpacingForSectionAt:)
collectionView(_:layout:sizeForItemAt:)
collectionView(_:layout:referenceSizeForHeaderInSection:)
collectionView(_:layout:referenceSizeForFooterInSection:)
collectionView(_:layout:insetForSectionAt:)
However, accessing these values can be a bit of a pain. For one thing there are a large number of optionals in your way:
- The layout has an optional collection view property.
- The delegate of the collection view is optional.
- That delegate may not be a
UICollectionViewDelegateFlowLayout
. - Each method itself is optional.
Add to this, if any of these fail the related property should be used instead.
Initial Aproach
An initial approach to this might take you down the route of writing an extension on UICollectionViewFlowLayout
to allow easier access methods. The snippet below shows a method which will fetch the edge insets for a given section, using the delegate method if possible otherwise falling back to the sectionInset
property.
extension UICollectionViewFlowLayout {
func inset(for section: Int) -> UIEdgeInsets {
guard
let collectionView = collectionView,
let delegate = collectionView.delegate,
let flowDelegate = delegate as? UICollectionViewDelegateFlowLayout,
let section = flowDelegate.collectionView?(collectionView, layout: self, insetForSectionAt: section)
else {
return sectionInset
}
return section
}
}
It’s easy to see how the other five methods could be implemented in exactly the same way. However, there are a few things I dislike about this. Firstly there will be a huge amount of code duplication across those six methods.
Secondly, and more problematic in my eyes, is that the delegate method is hidden in this sea of unwrapping, meaning you can’t glance at this method and make sure that the correct delegate method and fallback property are used.
Separating Out Unwrapping Code
To abstract out unwrapping the optionals and handling the fallback logic, we can write a function that takes two things:
-
A function to convert from non-optional
UICollectionViewDelegateFlowLayout
andUICollectionView
values to return an optional value. This value is generic to allow us to wrap all six of the delegate method/properties combinations. -
A fallback that will be used if the former function returns nil.
extension UICollectionViewFlowLayout {
private func retrieve<Value>(
using function: (UICollectionViewDelegateFlowLayout, UICollectionView) -> Value?,
fallback: () -> Value
) -> Value {
guard
let collectionView = collectionView,
let delegate = collectionView.delegate,
let flowDelegate = delegate as? UICollectionViewDelegateFlowLayout,
let value = function(flowDelegate, collectionView)
else {
return fallback()
}
return value
}
func inset(for section: Int) -> UIEdgeInsets {
return retrieve(using: { delegate, collectionView in
delegate.collectionView?(collectionView,
layout: self,
insetForSectionAt: section)
}, fallback: { sectionInset })
}
}
This version improves the situation of the two problems I had with the first, namely that there is less duplicated code and each individual method is easier to read and reason about.
Using @autoclosure
A little side note about why I’ve made the fallback a function is that I wanted to keep the same behaviour as the initial method and that doesn’t call the fallback property unless the call to the delegate method fails. If we had passed sectionInset
as a value, we’d be calling the property ahead of calling the delegate method.
Instead of passing a function that returns the value we can use @autoclosure
to make Swift automatically enclose an expression in a closure. By setting the fallback as @autoclosure () -> Value
we can just pass an expression and it won’t be executed until the delegate call fails. This can be seen in the code snippet below.
Passing Delegate Methods Directly
Our second attempt improved the clarity in the wrapper methods, but there is a nesting which starts to indent the code and having the retrieve
function provide a collection view only so we can pass it to the delegate method seems more excessive than we need.
In Swift we know that functions can be passed as you would any other value type, such as a struct or class. Another aspect is that Swift can reference an existing function and pass that as well.
We can also see that each of the delegate methods above has the same signature. They each take three parameters, where the first is the collection view, the second is the layout and the last is a value unique to that method, such as section
or indexPath
. I’ll refer to this last parameter as a Key
.
Given this information, it is possible to capture a reference to the delegate method and pass it straight to the retrieve
function so that it can call it rather than calling a closure we provide that then calls the delegate method.
Firstly we need to define in code the signature of each of the six delegate methods. We can take our findings from above and define a typealias to define a function over two generic types Key
and Value
that takes a collection view, layout and Key
and returns a Value
:
typealias DelegateMethod<Key, Value> =
((UICollectionView, UICollectionViewLayout, Key) -> Value)
This represents the signature for all six delegate methods of UICollectionViewDelegateFlowLayout
. We can rewrite retrieve
in terms of DelegateMethod
.
extension UICollectionViewFlowLayout {
private func retrieve<Key, Value>(
using delegateMethod: DelegateMethod<Key, Value>?,
key: Key,
fallback: @autoclosure () -> Value
) -> Value {
guard
let collectionView = collectionView,
let value = delegateMethod?(collectionView, self, key)
else {
return fallback()
}
return value
}
}
We specify that the delegate method is optional, because this will allow the calling code to be a little cleaner as all of these delegate methods are optional. retrieve
can then unwrap the collection view and call the method with it, itself as the layout and a key which is a new provided parameter. We can leave our existing fallback implementation, but we will use @autoclosure
to further improve readability at the call site.
Now we need to get a reference to the delegate method. Because we will be needing the UICollectionViewDelegateFlowLayout
for each call, we wrap this in a property.
extension UICollectionViewFlowLayout {
private var delegate: UICollectionViewDelegateFlowLayout? {
return collectionView?.delegate as? UICollectionViewDelegateFlowLayout
}
func inset(for section: Int) -> UIEdgeInsets {
return retrieve(
using: delegate?.collectionView(_:layout:insetForSectionAt:),
key: section,
fallback: sectionInset)
}
}
This allows us to ask the delegate for a reference to the function collectionView(_:layout:insetForSectionAt:)
which can be passed to retrieve
.
Compared to even the improved version this version makes every piece of each of the parameters passed neccessary. It’s very clear that the delegate is correct for a wrapping method inset(for section:)
, the key is correct and the fallback is the one that relates to the delegate method.
I have posted the final extension on UICollectionViewFlowLayout as a Gist on Github.