1

How do I convert Range<String.Index> to NSRange in Swift (< 4) without first converting the String to an NSString?

The reason I want to do this is that I want to set a UITextView's selectedText property using a Range<String.Index> value.

Alternative solution: How do I set UITextInput.selectedTextRange with a Range<String.Index> value?

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • 1
    Dupe? [NSRange from Swift Range?](https://stackoverflow.com/q/27040924/2415822). Any reason you don't want to bridge to NSString? Also, there are some non-NSString solutions in the answers to that question. – JAL Aug 03 '17 at 20:34
  • Bridging to `NSString` doesn't seem pure, intuitive, understandable, self-documenting, readable, simple, beautiful, or efficient. None of the non-`NSString` solutions to that question work for strings that contain emojis. – ma11hew28 Aug 04 '17 at 01:30

1 Answers1

10

Use String.Index's samePosition(in:) method with the string's UTF16 view. NSString uses UTF16, so the UTF16 indexes should be identical to the indexes NSString expects for the NSRange.

EDIT:

public extension NSRange {
    private init(string: String, lowerBound: String.Index, upperBound: String.Index) {
        let utf16 = string.utf16

        let lowerBound = lowerBound.samePosition(in: utf16)
        let location = utf16.distance(from: utf16.startIndex, to: lowerBound)
        let length = utf16.distance(from: lowerBound, to: upperBound.samePosition(in: utf16))

        self.init(location: location, length: length)
    }

    public init(range: Range<String.Index>, in string: String) {
        self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound)
    }

    public init(range: ClosedRange<String.Index>, in string: String) {
        self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound)
    }
}
Charles Srstka
  • 16,665
  • 3
  • 34
  • 60
  • 2
    It wasn't marked as a duplicate when I was writing the answer. Looks like the duplicate mark squeaked in right before I submitted it. – Charles Srstka Aug 03 '17 at 21:46
  • 1
    Well, I prefer your answer to any of the others. Could you provide the code for a method that I could call like: `NSRange(range, in: string)`? – ma11hew28 Aug 04 '17 at 00:38
  • I used to have just such a method, actually. I deleted it when I upgraded all my code to Swift 4, but I could probably dig it out of the git repository. Unfortunately, it'll have to wait a few days because my development machine is in the shop taking way too long to get a swollen battery replaced. Remember when we used to be able to just pop the old one out and the new one in? Nowadays you're stuck without your laptop for a week, it seems. – Charles Srstka Aug 04 '17 at 03:37
  • Finally got it back. Adding it as an edit. – Charles Srstka Aug 04 '17 at 18:59
  • It seems like a straightforward and robust solution. But after testing it, I got a mismatch, when replacing a substring using converted NSRange. In the Swift Language Guide, there is a note about it: "The count of the characters returned by the count property isn't always the same as the length property of an NSString that contains the same characters. The length of an NSString is based on the number of 16-bit code units within the string’s UTF-16 representation and not the number of Unicode extended grapheme clusters within the string." – Cable W Nov 13 '21 at 03:01