The UTF‐16 one you pretty much never want, unless you are parsing a document format that was specified that way. (Its treatment of surrogates would be surprising to any user.)
But despite all the places you see grapheme clusters being described as “what a user perceives as characters”, that is false more often than it is true.
Grapheme clusters would better be described as the linear unit of a text’s 1‐dimensional “chain”, between which order is meaningful, but within which order is not meaningful in the same way or not at all. The user of the text does not perceive the “e” as coming before or after the acute accent, but at the same time. However, to represent it in a binary stream, we need to agree on one or the other order for the stream’s sake. The agreed order is a matter of encoding, not of the text, and the user does not care. Concerning the finger stream, some keyboards type the accent first (mechanically advantageous), others type it after (algorithmically advantageous), even when both keyboards target the same language. But the user still (usually) perceives them as distinct graphemes. To the French speaker, “le café” contains two “e”s; try to tell him there is only one and he will roll his eyes at technology’s ineptness. The Greek speaker who enters “◌̀” to find‐and‐replace it with “◌́” expects “ψυχὴ” to turn into “ψυχή”; if it doesn’t, he will be miffed at having to do roughly 80 separate find‐and‐replace operations to accommodate all the combinations the text engine foisted on him. When in doubt, do any text processing of this sort in scalars, and maintain NF(K)D throughout.
Now, there certainly is a time and place for operating in grapheme clusters. Present either of the above users with a crossword puzzle five squares wide and he will never think to solve the puzzle with “cafe◌́” or “ψυχη◌́”. Show him that “solution” and he will say your puzzle is defective. That is an example of a time to think in clusters. But only when you know the end use case can you choose the best string unit for an operation. Often the best option is to provide both and let the user choose, in the same vein as a case sensitivity option.
Swift’s unique decision to provide grapheme cluster processing out‐of‐the‐box was very wise and helpful. However, renaming them “characters” and elevating them to the default string unit probably went to far. The ensuing obsession the community acquired with switching all logic to them is making many aspects of text processing more complicated, less reliable, less intuitive to users and generally worse not better. Do yourself a favour and refrain from “updating” an implementation to use Character
—especially when porting from another programming language—unless you know exactly what you are doing and why you are doing it.
@ShikiSuen, you are getting closer. Your choice to convert to an array effectively evades the pitfalls the rest of us have been talking about. There is still a lot of indirection that seems unnecessary to me, and it isn’t the fastest way to go about it, but nothing jumps out at me as logically unsound anymore.
Swift manages its own memory; you should not have to worry about that. If it really is leaking (which I doubt), it is Swift’s fault, not yours.
As you continue to improve your method, if you want a good test to verify your handling of offsets, try this:
assert("Ἄιδης".swapping("ι", with: "\u{345}") == "ᾌδης")
The replacement will be assimilated into the first character, so unintuitively 5 − 1 + 1 = 4. The code you originally posted would have reached out of bounds and trapped.