Skip to Content
Course content

172: IndexPath and Range Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time working with UITableView or UICollectionView, you've encountered IndexPath. On the surface, it seems like a simple wrapper around an integer—essentially just a fancy way of saying "item 5 in section 0." But as your data structures get more complex, especially when you start performing batch updates or slicing data, the way you handle these paths and the ranges they imply can either make your code elegant or turn it into a debugging nightmare.

The temptation to manual-map indices

Let's say you're building a messaging app. You have a list of messages in a single section, and you want to implement a "Delete Selected" feature. The naive approach is to take the array of selected IndexPath objects and loop through them to remove the messages from your data source. It looks something like this:

var messages = ["Hello!", "How are you?", "I'm great!", "Swift is fun!", "Bye!"]
let selectedPaths = [IndexPath(row: 1, section: 0), IndexPath(row: 2, section: 0)]

for path in selectedPaths {
    messages.remove(at: path.row)
}

I've seen this countless times in code reviews. At first glance, it seems logical. But there's a massive trap here: index shifting. The moment you remove the item at index 1, "I'm great!" (which was at index 2) shifts up to index 1. When the loop moves to the next IndexPath and tries to remove index 2, it's actually removing "Swift is fun!" instead. If your selection list is long enough, you'll eventually hit an Index out of range crash.

Bridging IndexPath to Range types

The professional way to handle this isn't to loop and delete, but to define a Range. In Swift, ranges aren't just for for-in loops; they are first-class citizens used for slicing collections. When you're dealing with a contiguous block of items, you want to move away from individual IndexPath objects and toward Range or ClosedRange.

If you know you're deleting everything from index 1 to 2, you should use a half-open range (..<) or a closed range (...). For a data source, removeSubrange(_:) is your best friend because it handles the memory shift in one go, rather than forcing the array to re-index itself multiple times.

let start = selectedPaths.first!.row
let end = selectedPaths.last!.row

// We use a ClosedRange here because 'end' is inclusive
let rangeToRemove = start...end 
messages.removeSubrange(rangeToRemove)

This is significantly more performant. Instead of $O(n^2)$ complexity where you shift the remaining elements for every single deletion, you're doing a single memory move. I generally prefer Range (half-open) for most Swift API work because it aligns with how Array.count works, but ClosedRange is often more intuitive when you're translating a "start" and "end" IndexPath from a UI selection.

When the structure gets deeper

Now, the real power of IndexPath comes when you have multiple sections. A Range only works on a linear sequence. If you have messages grouped by date (Section 0: Monday, Section 1: Tuesday), you can't just create one Range to delete messages across both days. This is where you have to be careful.

You'll need to group your IndexPaths by section first, then create a Range for each section. If you try to flatten these into a single range, you'll end up deleting data from the wrong sections or crashing the app. Remember: IndexPath is the coordinate, but Range is the span. You use the coordinates to determine the boundaries, and the span to execute the operation.




📋 Practical Task

Implementing a Batch Message Archive

You are working on a chat application. You have a data source consisting of an array of strings called chatHistory. You need to implement a function that archives a contiguous block of messages based on a starting IndexPath and an ending IndexPath.

Requirements:

  • Create a function archiveMessages(from start: IndexPath, to end: IndexPath).
  • The function should use a ClosedRange to identify the items to be removed.
  • Use removeSubrange(_:) to remove these messages from the chatHistory array in a single operation.
  • Add a print statement that shows the remaining messages to verify that the correct range was deleted.
var chatHistory = ["Msg 1", "Msg 2", "Msg 3", "Msg 4", "Msg 5", "Msg 6"]

func archiveMessages(from start: IndexPath, to end: IndexPath) {
    // Your code here
}

// Test case: Archive from Msg 2 to Msg 4 (indices 1 through 3)
archiveMessages(from: IndexPath(row: 1, section: 0), to: IndexPath(row: 3, section: 0))
Rating
0 0

There are no comments for now.

to be the first to leave a comment.