Working with IndexPaths
IndexPath’s provide an abstraction over the indexing of our structured data. They are used by common controllers and views, such as UICollectionView
, UITableView
and NSFetchedResultsController
.
IndexPath
is basically just an array of integers that defines a ‘path’ that can be used to represent your data. Lets take a look at a simple, but common example.
I’m going to start off my showing simple examples so if you want to jump straight into the more interesting stuff, scroll past the next section 🙂
Quick Example
Just to ensure we’re all on the same page, lets look at an example where we have a contacts application, with each contact grouped by their company name.
Apple:
- Steve Jobs
- Alan Kay
- Andy Hertzfeld
Microsoft:
- Bill Gates
- Paul Allen
Google:
- Larry Page
- Sergey Brin
If I wanted to reference Paul Allen I could create an IndexPath
as such:
IndexPath(item: 1, section: 1) // ::info:: Paul Allen
So as we can see defining IndexPath’s is actually quiet simple. What about using an IndexPath
to find Alan Key?
dataSource
.companies[indexPath.section]
.employees[indexPath.item]
Easy peasy!
Advanced Indexing
So as we’ve seen, using a single IndexPath
in isolation is really simple. What if I wanted to use my current IndexPath
to find the employee in a list, before the this one?
Well, on its own we can’t really do that because IndexPath
‘s don’t have a reference to other IndexPath
‘s. Which makes sense because they can be used in any contexts.
More importantly the fact that our grouping is 1 level deep and specific to the company name, is specific to the implementation in a specific part of an application.
So when would we ever want to do this in the first place?
CollectionView Layout
Have you ever implemented a UICollectionViewLayout (or FlowLayout) and realised how complicated it becomes to keep track of all of your IndexPath
‘s? It gets even more complicated when you have to deal with headers, footers and perhaps even more supplementary and decoration views.
Recently I began implementing a custom layout for the 5th or 6th time and decided I really wanted to make dealing with IndexPath
‘s a lot simpler than it had been in the past. Specifically I wanted the following features:
- To be able to iterate over all of my dataSource’s
IndexPath
‘s - To be able to query my dataSource for specific indexPaths
- To be able to easily differentiate the various kinds of
IndexPath
‘s I use (i.e. headers, footers and items)
Table Layout
For simplicity lets think about how we would build a simple layout that looks like a UITableView, with section headers and footers as well as the items for each section.
So we discussed above how IndexPath
‘s don’t know about other IndexPath
‘s so how do we plan on solving that? Well luckily enough our DataSource does know about all of our IndexPath
‘s, so we can easily write an extension on Set
to fetch all of our IndexPath
‘s for us.
Before we do that lets also add an extension to IndexPath
to support a property called kind
which will allow us to determine whether we’re dealing with a header
, footer
or item
.
internal extension IndexPath {
enum Kind: Int {
case header
case footer
case item
}
init(kind: Kind, item: Int, section: Int) {
switch kind {
case .header:
self = [section, -1, 0]
case .footer:
self = [section, item, 1]
case .item:
self = IndexPath(item: item, section: section)
}
}
}
You may notice we’re specifying 3 index values for our custom IndexPath
‘s. The first index represents the section
, the second represents our item
and the last value represents whether this is a header
or a footer
.
For all other IndexPath
‘s we use the standard convenience method.
Data Source
Now we have a better way to represent our IndexPath
‘s, we can add an extension to Set
that will collect all of the IndexPath
‘s from our DataSource, in this case our UICollectionView
.
public extension Set where Element == IndexPath {
init(from view: UICollectionView) {
var indexPaths: Set<IndexPath> = []
let sectionCount = view.dataSource?.numberOfSections?(in: view) ?? 0
for section in 0..<sectionCount {
let headerIndexPath = IndexPath(
kind: .header,
item: -1,
section: section
)
indexPaths.insert(headerIndexPath)
let itemCount = view.dataSource?
.collectionView(view, numberOfItemsInSection: section) ?? 0
for item in 0..<itemCount {
let itemIndexPath = IndexPath(
item: item,
section: section
)
indexPaths.insert(itemIndexPath)
}
let footerIndexPath = IndexPath(
kind: .footer,
item: itemCount,
section: section
)
indexPaths.insert(footerIndexPath)
}
self.init(indexPaths)
}
}
If we were to manually configure our IndexPath
‘s we would end up with something like this.
// Apple
IndexPath(kind: .header, item: -1, section: 0),
IndexPath(item: 0, section: 0),
IndexPath(item: 1, section: 0),
IndexPath(item: 2, section: 0),
IndexPath(kind: .footer, item: 3, section: 0),
// Microsoft
IndexPath(kind: .header, item: -1, section: 1),
IndexPath(item: 0, section: 1),
IndexPath(item: 1, section: 1),
IndexPath(kind: .footer, item: 2, section: 1),
// Google
IndexPath(kind: .header, item: -1, section: 2),
IndexPath(item: 0, section: 2),
IndexPath(item: 1, section: 2),
IndexPath(kind: .footer, item: 2, section: 1),
Querying
So now we have an array of IndexPath
things get a lot simple. First of all, we can easily sort our array and because our custom IndexPath
initializer ensures all paths will order correctly.
Secondly we can easily query our array for the IndexPath
‘s we care about now. So lets take a look at an extension that will allow us to query our IndexPath
‘s.
func filter(include kinds: IndexPath.KindMask, inSections sections: [Int])
-> [SubSequence.Element] {
return filter { sections.contains($0.section) && kinds.contains($0.kind) }
}
With this one simple function, we can query as such:
// fetch all IndexPath's for headers and items only
// in the first 2 sections
indexPaths
.sorted()
.filter(include: [.header, .item], inSections: [0, 1])
Now that we have a simple flat array of IndexPath
‘s we can also easily iterate over it invarious ways:
// Use an iterator
var iterator = indexPaths.makeIterator()
iterator.next()
Since we’re dealing with a simple array now, we can even index into the array to get the next/previous IndexPath
without having to consider what section we’re in and whether or not we’re the first
/last
item in that section, etc…
Summary
IndexPath
‘s are a great way to represent your data but can sometimes become unwieldly as your structures get more complex.
Hopefully this has given you some interesting ideas to help simplify your approaches and implementations as well.
Checkout the Gist to see it in action.