How does insertSegment's "at" parameter works?

Hi!


I'm new to Swift development and during reading an Intro to App Development with Swift I stuck with strange advice from the book (Lesson 20.2):


for choice in topChoices {
    topCaptionSegmentedControl.insertSegment(withTitle: choice.emoji, at: topChoices.count, animated: false)
}


The "at" parameter implies it's an index/number of a segment, so it should be a counter (each element should have own position). So the code should be like this:


var i = 0
for choice in topChoices {
    topCaptionSegmentedControl.insertSegment(withTitle: choice.emoji, at: i, animated: false)
    i += 1
}


But the author of the book uses topChoices.count which is a constant value. In my case it's always equal to 3. So all three iterations of the loop we are inserting segments into the same position (3).


Can anybody explain how it works? I couldn't find a detailed explanation in the specs.

Replies

What you are missing is that count will increase during the loop. So, it will be inserted each time at the end.


Consider a similar example in playground, with simple array, to see how it works:


var table = [1, 2, 3]
for i in 0...2 {
    print(table.count)
    table.insert(i + 100, at: table.count)
    print(table.count, table)
}

You get the following:

3

4 [1, 2, 3, 100]

4

5 [1, 2, 3, 100, 101]

5

6 [1, 2, 3, 100, 101, 102]

Hi! Why does the count increase during the loop? The original topChoices array is not touched. I guess topChoices.count should always return a constant value (number of elements in the array).


P.S. Your code example is clear.

Oops! My fault! I didn't pay enough attention to the clarification in the text:


«The index can be the count of the topChoices array. (If you supply a number that’s greater than the highest segment index, it just gets added to the end.)»


However it explains everything, as for me it's an unobvious shady behavior of "at". Moreover, it's strange to see such "hacks" in the book for beginners.

Consider this second example:


table = [1, 2, 3]
for i in 0...table.count {
    print("before insert", table.count)
    table.insert(i + 100, at: table.count)
    print("after insert", table.count, table)
}


You get

before insert 3

after insert 4 [1, 2, 3, 100]

before insert 4

after insert 5 [1, 2, 3, 100, 101]

before insert 5

after insert 6 [1, 2, 3, 100, 101, 102]

before insert 6

after insert 7 [1, 2, 3, 100, 101, 102, 103]



Explanation:

on line 2, I get an evaluation of table.count (3) and copy it in the range of the for loop ; that will not change anymore


On line 4, I recompute table.count at each loop. It is recomputed, not a copy of the initial value

And after insert, this count increases.


So, it is different from this where I get a copy of the original count, not a computed var:

table = [1, 2, 3]
let count = table.count
for i in 0...count {
    print("before insert", count)
    table.insert(i + 100, at: count)
    print("after insert", count, table)
}


You get

before insert 3

after insert 3 [1, 2, 3, 100]

before insert 3

after insert 3 [1, 2, 3, 101, 100]

before insert 3

after insert 3 [1, 2, 3, 102, 101, 100]

before insert 3

after insert 3 [1, 2, 3, 103, 102, 101, 100]


Not inserted at the same place !

«The index can be the count of the topChoices array. (If you supply a number that’s greater than the highest segment index, it just gets added to the end.)»

That's a different point, not the explanation of what surprised you.


It just says that insertSegment at is safe: if you provide a value greater that the size of the array, it does not crash (it could as you are out of range) but safely reduces the value to array.count instead.


Note: that should not work for insert in an Array.

for me it's an unobvious shady behavior of "at".

I understand your concern here. Sentinel values like this are common in C-based languages, including Objective-C, because they lack expressivity. For example, in Swift you might make this

at
parameter an
Int?
, with a default value of
nil
that’s interpreted to mean at the end. Or you might make the index not an
Int
, but a custom enum like this:
enum InsertIndex {
    case atStart
    case atIndex(Int)
    case atEnd
}

These options aren’t available to you in C-based languages (for example, in a C-based language only pointers can be optional), and thus you end up with cryptic sentinel values.

As the system frameworks evolve to take advantage of Swift’s features, we should see less of this. For example, the new Network framework has a dedicated Swift API that avoids many of these issues.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

FWIW, I'm with chimit here. This is "strange advice", aka a bug in the tutorial. The problem isn't the behavior of the API (which is documented to have the "safe" behavior), but with the poor implementation strategy that the tutorial chose to use:


    topCaptionSegmentedControl.insertSegment(withTitle: choice.emoji, at: topChoices.count, animated: false)


This happens to work, if you're adding segments to an otherwise empty UISegmentedControl, but is unlikely to be the intended behavior if (say) the segmented control already had some segments. A better choice would be something like this:


    topCaptionSegmentedControl.insertSegment(withTitle: choice.emoji, 
        at: topCaptionSegmentedControl.numberOfSegments, animated: false)