The general rule, as Xiaodi described, is that Index
values aren't exchangeable between distinct collection values, even if the collections compare equal.
In the specific case of String
, indices are (effectively) offsets into the string's storage, and these offset values depend on the string's encoding -- a string value that is encoded in UTF-8 will have very different indices to a string value that is encoded in UTF-16. (UTF-16 String
values can occur when NSString instances get bridged from Objective-C.)
The thing that causes your problem here is that label.stylableText
isn't at all an identical copy of the original string -- NSAttributedString
is (intentionally!) not preserving the identity of the string passed to it, so what you get back from NSAttributedString.string
is a brand new string value. In this case, this new value is backed by a bridged Cocoa NSString instance. As such, its storage is encoded in UTF-16, and indices in your original strings are no longer valid in it.
(Note: The precise details of exactly what [NS]AttributedString
is doing to the original string value are subject to change in any OS release. Do not assume anything other than that the result string has the same contents.)
Prior to iOS 16, the Swift Stdlib did not keep track of the encoding associated with the offset in String.Index
. So when code applied an (invalid) UTF-8 index to a UTF-16 string, String
could not precisely diagnose this violation. UTF-8 offsets tend to be higher than the equivalent UTF-16 offsets, so as you are seeing, such errors tend to lead to out of bounds accesses, nonsensical results, or (in some cases) even full-blown undefined behavior.
In Swift 5.7 (that shipped in iOS 16), the Stdlib started marking each String.Index value with its expected encoding, to better diagnose such problems. Unfortunately, this uncovered a large volume of existing code that includes these issues. So instead of trapping, String
is now bending over backwards to allow these to continue working, this time without nonsensical behavior: in iOS 16+, it transparently converts the offset to the right encoding when necessary. This can be extraordinarily expensive, as this requires transcoding the string every single time such an index is used.
Note: this change in Swift 5.7 did not make such indices valid; the use of such indices continue to be programmer errors. (We just haven't figured out how to properly report the issue.)