It is a data structure problem, with algorithmic trade-offs. A similar effect could be seen in Java (in java.util.collections) where there are multiple types of lists, multiple types of maps (dictionaries), and multiple kinds of sets. One set may have constant-time insertion, one might sort the entries, one might preserve insertion ordering, and one might be designed for concurrent usage.
For many languages, characters started off a being a byte and expanded later to support multiple single byte character sets, short and word width characters, variable-length codepoints, and now (with Swift) extended grapheme clusters. These all have their own trade-offs, with one being that once you have variable length codepoints or extended graphemes, you can't take a counter, add it to the starting pointer, and know you are pointing to the nth character (or even that you are pointing to the start of a code point vs the middle of one).
The default shipping String in Swift is a sequence extended grapheme cluster, which basically means it represents a single printable character, whitespace, or control code. An individual printable character in unicode has no fixed binary size, indeed a single character has no maximum binary length (outside practical limits which may be enforced to prevent abuse).
As others have mentioned, you can have string-like forms other than the String type:
- A byte string in some byte-based encoding, such as latin1
- An array of characters (where each character may be a pointer to a sequence if it is an extended grapheme cluster that exceeds a certain length)
- A UTF-8 binary sequence that does not enforce correctness
There are benefits and trade-offs to each of these. All are relatively easily possible in Swift, but none are the String class - String has a design focused on handling characters for all users, whether they prefer English or Tagalog.
As per why Swift doesn't detect that you want to do offsetting and automatically switch - it is because a poor guess could have strong negative consequences. Better to give a function-rich default, and allow people to choose another option if they need it.
NSString (for what it is worth) is a cluster class, and may very well give different implementations based on how it is initialized. This can negatively affect performance, both in that string operations are harder to optimize and that the string operation may be a lot more expensive (such as performing encoding translation and/or making a full copy) than would be expected by the developer.
The reality is that nobody has come up with an ingenious way to do this yet such that it would be implementable in Swift as a default. You can either take a huge space hit, big performance hits on lookup, big performance hits in other parts of the algorithm like string mutations, limit your text to something fixed width (so not full Unicode), or make it based on some unsafe concept like codepoints or bytes rather than characters.
FWIW, my personal experience has been that when people strongly need numeric indexing into characters, they most often are working with characters as data rather than as text. For instance, HTML headers are all text but are limited to US-ASCII, which does have a strict one-byte-per-character limitation that means you can do integer offsets. There are proposals to make this sort of work easier, likely by being able to easily convert US-ASCII text to [UInt8] or Data types.