Search code examples
swiftloopsfor-loopappkitfoundation

Swift: For Loop End Condition Evaluated Only Once?


I am attempting to understand how Swift handles for-in loops.

Overview: we are iterating over the rows of an NSOutlineView. If a condition is met, we expand the item, which obviously changes the overall row count of the outlineView.

Pre-Condition: the OutlineView has 5 "root" items. Each of those has 5 child items.


Example

final class anOutlineView: NSOutlineView
{
    override func reloadData()
    {
        super.reloadData()
        
        for i in 0 ..< self.numberOfRows
        {
            // Assume we expand the item at row 0, which increases 
            // the overall outlineView row count from 5 to 10.
        }
    }
}

In this approach, the loop stops when i == 4. I assume that's because Swift evaluates the range only once, the first time it encounters it? Is there a way to change that behavior so that the conditions are re-evaluated each time through the loop, like a traditional for loop?

Replacing the for loop with a while loop obviously works and is a fine solution. I'm simply trying to understand the nuances of Swift because this behavior is not what I expected. In Objective-C, the for loop conditions were evaluated on each iteration and it was a well-known performance optimization to refrain from calling self.property in loop conditions (unless a good reason existed, as it does in this case.)


Solution

  • 0 ..< self.numberOfRows is a Range and in particular a Sequence. Iterating over a sequence is done by creating an iterator, and then calling its next() method until the iterator is exhausted, compare IteratorProtocol:

    Whenever you use a for-in loop with an array, set, or any other collection or sequence, you’re using that type’s iterator. Swift uses a sequence’s or collection’s iterator internally to enable the for-in loop language construct.

    So

    for i in 0 ..< self.numberOfRows {
        ...
    }
    

    is equivalent to

    let range = 0 ..< self.numberOfRows
    var it = range.makeIterator()
    while let i = it.next() {
        ...
    }
    

    Modifying numberOfRows during the iteration does not mutate the range (which is a value type) or the iterator, and therefore does not affect the number of iterations.