Use a functional technique to discover an NSIndexPath

362 views Asked by At

There has got to be a more expressive way to do this using map, filter, reduce and friends.

Background:
The categories property is an [Category]?, where each element has a videos property of type [Video].

I'm trying to find the index path for video.

    var indexPath: NSIndexPath?
    for var i = 0; i < categories?.count ; ++i {
        for var j = 0; j < categories?[i].videos.count ; ++j {
            if categories?[i].videos[j] == video {
                indexPath = NSIndexPath(forRow: j, inSection: i)
            }
        }
    }

    if let indexPath = indexPath {
        tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: UITableViewRowAnimation.Automatic)
    }
3

There are 3 answers

2
Martin R On BEST ANSWER

A possible solution (written in Swift 2):

let indexPaths = categories?.enumerate().flatMap() { (section, aCategory) in
    aCategory.videos.enumerate().filter() { (_, aVideo) in
        aVideo == video
    }.map { (row, _) in
        NSIndexPath(forRow: row, inSection: section)
    }
} ?? []

indexPaths is an array of all matching index paths (or an empty array). If you don't need/like that, use

let indexPath = categories?.enumerate().flatMap() { (section, aCategory) in
    aCategory.videos.enumerate().filter() { (_, aVideo) in
        aVideo == video
    }.map { (row, _) in
        NSIndexPath(forRow: row, inSection: section)
    }
}.first

instead, which is an optional index path.

categories?.enumerate() is a sequence of (section, aCategory) pairs. For each category, aCategory.videos.enumerate() is a sequence of (row, aVideo) pairs. This sequence is filtered according to the search term, and the filtered pairs are mapped to an index path.

So the result of the transform passed to flatMap() is an array of index paths of the matching items, one array for each category. flatMap() joins these to a single array.

In Swift 1.2 this would be

let indexPaths = flatMap(enumerate(categories ?? [])) { (section, aCategory) in
    filter(enumerate(aCategory.videos)) { (_, aVideo) in
        aVideo == video
    }.map { (row, _) in
        NSIndexPath(forRow: row, inSection: section)
    }
}
0
Michael On

If you do this rarely, make a generator that returns a tuple of index paths and videos then filter on video equaling the video you need

If you make an enumeration generator, it mainly moves the loop into it's own object. categoryIndex and videoIndex become vars in the generator, and next basically does what the 2nd/3rd clauses of the for loop statement do

Right now, you're effectively working with a dictionary of index paths to videos.

If you're doing this lookup a lot and changing the data in the collection rarely, maintaining a bidirectional dictionary (video -> IP) and (IP->video) may be faster, then it's a quick operation to find the item. The generator is a way to create that data structure, but honestly, you've gotta involve a profiler if performance becomes an issue there

0
warrenm On

This isn't the most functional, nor the most concise, but it seems pretty readable to me. Assumes the video to be found is unique.

var location: NSIndexPath? = nil
for (section, category) in enumerate(categories ?? []) {
    if let row = find(category.videos, video) {
        location = NSIndexPath(forRow: row, inSection: section)
        break
    }
}