2
\$\begingroup\$

My goal is to determine, of all of the currently visible cells in a collection view, which section has the most visible cells.

Start by getting the index paths for the visible cells:

let visible = collectionView.indexPathsForVisibleItems // [IndexPath]

Originally I used an NSCountedSet followed by mapping and sorting.

let counted = NSCountedSet(array: visible.map { $0.section })
let section = counted.allObjects.map { ($0, counted.count(for: $0)) }.sorted { $0.1 > $1.1 }.first?.0 as? Int

This works but I don't like NSCountedSet because it deals with Any.

I then replaced the use of NSCountedSet with reduce by reducing the visible array into a dictionary and then using a similar map and sort.

let counted = visible.reduce([Int: Int]()) { (result, path) -> [Int: Int] in
    var updated = result
    updated[path.section, default: 0] += 1
    return updated
}
let section = counted.keys.map { ($0, counted[$0]!) }.sorted { $0.1 > $1.1 }.first?.0

This works as well but I'm hoping there are ways to improve this.

Two main questions:

  1. Can the reduce closure be improved? Is there a better way to return the updated dictionary where the keys are section and the values are the count?

  2. Once I have the dictionary of sections and counts, is there a better way to find the section with the highest count? Is there something better than mapping to a tuple, sorting those tuples, and finally grabbing the first one?

BTW - If there is a tie for highest count, I don't care which of those sections is returned.

\$\endgroup\$
3
\$\begingroup\$

Re 1: You can take advantage of reduce(into:_:) which was introduced in Swift 4 precisely for this purpose:

This method is preferred over reduce(_:_:) for efficiency when the result is a copy-on-write type, for example an Array or a Dictionary.

See also SE-0171 Reduce with inout:

Motivation

The current version of reduce needs to make copies of the result type for each element in the sequence. The proposed version can eliminate the need to make copies (when the inout is optimized away).

In your case:

let counted = visible.reduce(into: [Int: Int]()) { (result, path) in
    result[path.section, default: 0] += 1
}

The compiler can also infer the type of the initial accumulator automatically if the closure consists only of a single statement:

let counted = visible.reduce(into: [:]) { (result, path) in
    result[path.section, default: 0] += 1
}

Re 2: A dictionary is a collection of key/value pairs, therefore it can be sorted directly, without mapping each key to a tuple first:

let section = counted.sorted(by: { $0.value > $1.value }).first?.key

Even better, use max(by:) to find the dictionary entry with the maximal value:

let section = counted.max(by: { $0.value < $1.value })?.key

This is shorter and eliminates the intermediate arrays and the dictionary lookup. It is more efficient than sorting an array because only a single traversal of the collection is done. And even if the forced unwrapping is safe in your case, it is nice not to have it.


One could also make this a generic method for sequences

extension Sequence {
    func mostFrequent<T: Hashable>(by map: (Element) -> T) -> T? {
        let counted = reduce(into: [:]) { (result, elem) in
            result[map(elem), default: 0] += 1
        }
        return counted.max(by: { $0.value < $1.value })?.key
    }
}

which is then used as

let section = collectionView.indexPathsForVisibleItems.mostFrequent(by: { $0.section })
\$\endgroup\$
  • \$\begingroup\$ Excellent. That reduce(into:) was just what I needed to make that part cleaner. And I had been thinking about how to use max but I forgot about being able to use it like this with a dictionary. Great info. The extension is a great touch too. \$\endgroup\$ – rmaddy May 15 at 21:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.