Thank you for the reproduction sample! I do have the same crash reports for my app, but was unable to reproduce and thus debug it. With your sample I've set with the following workaround:
import UIKit
extension NSTextStorage {
// Newline characters must not be followed by variation selector.
// This invalid sequence sometimes appears in UITextView when adding newline
// in certain combinations of characters.
// For instance, if you insert this string:
// [[string from https://shorturl.at/uISV7, blocked by Apple for some reason]]
// then place cursor before first ❤️ and press return,
// you get a string in which there's a substring "\nu{FE0F}"
// for which UITextView by a series of calls tries to get NSTextLocation
// which leads to objc exception being thrown.
// This method fixed this by moving newline to where it belongs:
// back to the first location that isn't variation selector
// (presumably it's the symbol to which variation selector actually belongs).
@objc fileprivate func fixInvalidVariationSelectorsAndProcessEditing() {
struct InvalidNewline {
var location: Int
var desiredLocation: Int
}
var invalidLocations: [InvalidNewline] = []
let utf16 = Array(string.utf16)
let newline: UInt16 = 0x000A
for index in utf16.indices.dropLast() {
if utf16[index] == newline, utf16[index + 1].isVariationSelector {
var properIndex = index - 1
while properIndex > 0, utf16[properIndex].isVariationSelector == true {
properIndex -= 1
}
invalidLocations.append(.init(location: index, desiredLocation: properIndex))
}
}
invalidLocations.forEach {
mutableString.deleteCharacters(in: NSRange(location: $0.location, length: 1))
mutableString.insert("\n", at: $0.desiredLocation)
}
// Call original method which is swizzed by this
fixInvalidVariationSelectorsAndProcessEditing()
}
static func fixInvalidVariationSelectors() {
method_exchangeImplementations(
class_getInstanceMethod(Self.self, #selector(fixInvalidVariationSelectorsAndProcessEditing))!,
class_getInstanceMethod(Self.self, #selector(processEditing))!
)
}
}
extension UInt16 {
fileprivate var isVariationSelector: Bool {
Unicode.Scalar(self)?.properties.isVariationSelector == true
}
}
I've tried my best to describe the origin of the crash and how it's fixed in the comments above the method.
There's still a minor issue though: after adding a newline in the buggy spot, trying to delete it using backspace leads to ❤️ being removed instead of newline (while the cursor is visually located at the left side of ❤️). I assume that there should be similar way to work around this bug too, but I didn't bother.