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) // 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!

Adavanced 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?

Introducing UICollectionViewLayout

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:

  1. To be able to iterate over all of my dataSource's IndexPath's
  2. To be able to query my dataSource for specific indexPaths
  3. 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.

Easy peasy!

Adavanced 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?

Introducing UICollectionViewLayout

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:

  1. To be able to iterate over all of my dataSource's IndexPath's
  2. To be able to query my dataSource for specific indexPaths
  3. 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.

You can download the playground to give it a try, or you can checkout the GIST here.

If you liked this post or want to discuss more, leave a comment or follow me on Twitter


If you liked this post or want to discuss more, leave a comment below or find me on Twitter