so when I first ran this code I expected the endIndex to return the last index in the array a.k.a 4 from the ballImages array, but it returns 5? So I had to change
Int.random(in: 0 ... ballImages.endIndex)
to
Int.random(in: 0 ..< ballImages.endIndex)
it kinda threw me off. Am I missunderstanding something here or does the endIndex really return the length and not really the index?
You can read endIndex's documentation here. As it says, it's the "past the end" position for a Collection. I think it's defined like that so it's useful for both the full range of indices (startIndex ..< endIndex) as well as what the next index should be, but I'll let someone with more expertise confirm the reason why.
Also the thing about index, is that the integer mental model ends quickly once you use something other than Array.
Array is the only instance that guarantees that first index is 0, the index after is 1, and so on. Other types has a lot of freedom to do something else. Such as Set, Dictionary which use their own struct as index. Even if some type use Int, it's not guaranteed to start with 0, and increase by 1.
P.S.
It's also nice to have an invalid index at the end, as one can easily write
while index < collection.endIndex {
...
}
which is not easy to do if you have lastIndex instead of endIndex.
array.insert("G", at: array.indices.last!)
print(array) // ["A", "B", "C", "D", "E", "G", "F"] ?!?
print(array[array.endIndex]) // Fatal error: Index out of bounds.
Conceptually “last” sort of refers to the equivalent of fence panels, whereas “end” sort of refers to the equivalent of fence posts (of which there is always one more). See Fencepost Arithmetic.