There is a lengthy and interesting discussion on the Vision Pro #developer Discord channel that boils down to surprising behavior removing elements of a collection. (Well, perhaps not surprising to folks here.)
children.forEach { $0.removeFromParent() }
fails to remove all children of RealityKit Entity.ChildCollection, as it uses an IndexingIterator with Int as an index type, if I understood the discussion correctly.
While the responsibility ultimately lies with the RealityKit API choice which is off-topic here, the underlying collection and iterator types causing the confusion are something the consumer of the API doesn't see and can usually ignore completely; similar APIs such as UIKit, SpriteKit, and SceneKit vend similar node hierarchies where the above code works as expected using different underlying types.
I post there here wondering if folks have anything to add to the Discord discussion on the language level. What could make such confusions either less likely in the first place, or easier to diagnose when they occur? What is the simplest idiomatic work-around to the problem?
It only works because the method internally makes a copy of the array before returning it as part of a caching optimization. Prior to macOS Sonoma, NSView.subviews didn’t make a copy, so this exact pattern would cause an AppKit app to crash. (In fact, some applications depended on AppKit returning the mutable array directly and had to be worked around!)
While it is certainly a safer API design to copy the collection before returning it, you should generally copy any collection if you’re going to iterate over it in a way that can potentially mutate it. If the framework has already copied it for you, the extra “copy” is cheap.
Though I must admit, after further thought this possibility is even more obscure in a language with value-type arrays. At least in ObjC you can reason about the array you get back being an instance of a mutable subclass.
Please clarify. The idea was to combat the potential (or real) issues that could arise during collection mutation during enumeration by removing one element and then "starting over" the enumeration (of even if the relevant iterator was invalidated during mutation it won't matter). It might not be efficient but it is at least safe. A classical example for a dictionary:
var children = [1:1, 2:2, 3:3, 4:4, 5:5, 6:6]
// let's pretend we don't have neither this
// children.removeAll()
// nor this
// children = [:]
// nor this
// for key in children.keys {
// children.removeValue(forKey: key)
//}
// nor this:
// for kv in children {
// children.removeValue(forKey: kv.key)
// }
// bad way to empty dictionary (will crash):
// children.indices.forEach { i in
// children.remove(at: i)
// }
// not ideal but will do the job:
while !children.isEmpty {
children.remove(at: children.indices.first!)
}