291

How can I convert NSRange to Range<String.Index> in Swift?

I want to use the following UITextFieldDelegate method:

    func textField(textField: UITextField!,
        shouldChangeCharactersInRange range: NSRange,
        replacementString string: String!) -> Bool {

textField.text.stringByReplacingCharactersInRange(???, withString: string)

enter image description here

János
  • 32,867
  • 38
  • 193
  • 353
  • 87
    Thank you, Apple, for giving us the new Range class, but then not updating of any of the string utility classes, such as NSRegularExpression, to use them! – puzzl Dec 04 '14 at 19:29
  • https://stackoverflow.com/a/43233619/2303865 – Leo Dabus Mar 24 '19 at 21:38

16 Answers16

412

As of Swift 4 (Xcode 9), the Swift standard library provides methods to convert between Swift string ranges (Range<String.Index>) and NSString ranges (NSRange). Example:

let str = "abc"
let r1 = str.range(of: "")!

// String range to NSRange:
let n1 = NSRange(r1, in: str)
print((str as NSString).substring(with: n1)) // 

// NSRange back to String range:
let r2 = Range(n1, in: str)!
print(str[r2]) // 

Therefore the text replacement in the text field delegate method can now be done as

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

    if let oldString = textField.text {
        let newString = oldString.replacingCharacters(in: Range(range, in: oldString)!,
                                                      with: string)
        // ...
    }
    // ...
}

(Older answers for Swift 3 and earlier:)

As of Swift 1.2, String.Index has an initializer

init?(_ utf16Index: UTF16Index, within characters: String)

which can be used to convert NSRange to Range<String.Index> correctly (including all cases of Emojis, Regional Indicators or other extended grapheme clusters) without intermediate conversion to an NSString:

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = advance(utf16.startIndex, nsRange.location, utf16.endIndex)
        let to16 = advance(from16, nsRange.length, utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

This method returns an optional string range because not all NSRanges are valid for a given Swift string.

The UITextFieldDelegate delegate method can then be written as

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

    if let swRange = textField.text.rangeFromNSRange(range) {
        let newString = textField.text.stringByReplacingCharactersInRange(swRange, withString: string)
        // ...
    }
    return true
}

The inverse conversion is

extension String {
    func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.startIndex, within: utf16view) 
        let to = String.UTF16View.Index(range.endIndex, within: utf16view)
        return NSMakeRange(from - utf16view.startIndex, to - from)
    }
}

A simple test:

let str = "abc"
let r1 = str.rangeOfString("")!

// String range to NSRange:
let n1 = str.NSRangeFromRange(r1)
println((str as NSString).substringWithRange(n1)) // 

// NSRange back to String range:
let r2 = str.rangeFromNSRange(n1)!
println(str.substringWithRange(r2)) // 

Update for Swift 2:

The Swift 2 version of rangeFromNSRange() was already given by Serhii Yakovenko in this answer, I am including it here for completeness:

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

The Swift 2 version of NSRangeFromRange() is

extension String {
    func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.startIndex, within: utf16view)
        let to = String.UTF16View.Index(range.endIndex, within: utf16view)
        return NSMakeRange(utf16view.startIndex.distanceTo(from), from.distanceTo(to))
    }
}

Update for Swift 3 (Xcode 8):

extension String {
    func nsRange(from range: Range<String.Index>) -> NSRange {
        let from = range.lowerBound.samePosition(in: utf16)
        let to = range.upperBound.samePosition(in: utf16)
        return NSRange(location: utf16.distance(from: utf16.startIndex, to: from),
                       length: utf16.distance(from: from, to: to))
    }
}

extension String {
    func range(from nsRange: NSRange) -> Range<String.Index>? {
        guard
            let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex),
            let from = from16.samePosition(in: self),
            let to = to16.samePosition(in: self)
            else { return nil }
        return from ..< to
    }
}

Example:

let str = "abc"
let r1 = str.range(of: "")!

// String range to NSRange:
let n1 = str.nsRange(from: r1)
print((str as NSString).substring(with: n1)) // 

// NSRange back to String range:
let r2 = str.range(from: n1)!
print(str.substring(with: r2)) // 
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Why not just do http://stackoverflow.com/a/26749314/242933? It seems like less code and still correct. – ma11hew28 Jun 19 '15 at 04:15
  • 7
    That code does not work correctly for characters consisting of more than one UTF-16 code point, e.g. Emojis. – As an example, if the text field contains the text "" and you delete that character, then `range` is `(0,2)` because `NSRange` refers to the UTF-16 characters in an `NSString`. But it counts as *one* Unicode character in a Swift string. – `let end = advance(start, range.length)` from the referenced answer does crash in this case with the error message "fatal error: can not increment endIndex". – Martin R Jun 19 '15 at 05:38
  • Hmm... OK. Thanks. I think I'll just [convert String to NSString](http://stackoverflow.com/a/26517690/242933) then as that seems to work and is less code (simpler). – ma11hew28 Jun 19 '15 at 14:32
  • 8
    @MattDiPasquale: Sure. My intention was to answer the verbatim question *"How can I convert NSRange to Range in Swift"* in a Unicode-safe way (hoping that someone might find it useful, which is not the case until now :( – Martin R Jun 19 '15 at 14:36
  • @user1687195: Thanks for fixing my stupid error in the Swift 2 code! I should have tested it. – Martin R Sep 21 '15 at 19:48
  • @MartinR hello, i've added your solution into another swift file and when calling textField.text.stringByReplacingCharactersInRange in get the error String? has no member 'rangeFromNSRange', please help. thanks – DrPatience Oct 01 '15 at 15:13
  • @DrPatience: That probably means that `textField.text` is now an *optional* and has to be unwrapped ... – Martin R Oct 01 '15 at 15:19
  • In sample. For let n1 I need to put let n1:NSrange to work. Thanks anyway. – khunshan Feb 22 '16 at 12:54
  • This doesn't seem to work anymore. Range does not have members upperBound or lowerBound anymore and utf16.index does not exist either. Or am I missing something? – ClockWise Aug 29 '16 at 10:26
  • @ClockWise: Which Xcode version? Did you see the "Update for Swift 3" at the end of the answer? – Martin R Aug 29 '16 at 10:27
  • @MartinR I have 7.3.1, not sure if my version applies to the Update for Swift 3-section you wrote. But the 2.0 seems to work so I'll use that for now. Thanks for this brilliant answer. – ClockWise Aug 29 '16 at 12:05
  • @ClockWise: Yes, Xcode 7.3.1 is Swift 2.2. Xcode 8 is Swift 3. – Martin R Aug 29 '16 at 12:07
  • 5
    Thank you, once more, for doing Apple's work for them. Without people like you, and Stack Overflow, there's no way I'd be doing iOS/OS X development. Life's too short. – Womble Sep 15 '16 at 00:00
  • where did you find the documentation? – jiminybob99 Sep 23 '16 at 13:26
  • 1
    @jiminybob99: Command-click on `String` to jump to the API reference, read all methods and comments, then try different things until it works :) – Martin R Sep 23 '16 at 13:34
  • 1
    For ``let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex)``, I think you actually want to limit it by ``utf16.endIndex - 1``. Otherwise, you can start off the end of the string. – Michael Tsai Mar 16 '17 at 15:48
  • @MichaelTsai: That is interesting. I would have expected that `utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex)` returns `nil` if `from16 == utf16.endIndex`, but it doesn't, and that causes the crash. I have changed to code so that it should work correctly now. Please let me know if you still find any issues, and thanks for the feedback! – Martin R Mar 16 '17 at 19:37
  • 1
    In Swift 4, there are now dedicated initialisers for this :) You can use `Range(nsRange, in: str)` to convert an `NSRange` to `Range?` and `NSRange(range, in: str)` to perform the reverse (and actually the `NSRange` initialiser can take an arbitrary `RangeExpression` with a `String.Index` bound, so for example can also deal with partial ranges). – Hamish Jun 08 '17 at 11:04
  • @Hamish: OK, thanks for the notice! It seems that I have to update some answers (which I will do as soon as I have the time). Do you know if that change went through a proposal (because I did not see one). Or is it part of the recent `String` changes? – Martin R Jun 08 '17 at 11:09
  • 1
    @MartinR No I don't recall seeing it in an evolution proposal – I only found out about it through watching the "What's new in Foundation" WWDC session. Unfortunately, they don't *appear* to be documented yet, so I don't know if you should hold off updating until they are – but [their location in the source is here](https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/NSRange.swift#L116) (and PR [here](https://github.com/apple/swift/pull/9709)). – Hamish Jun 08 '17 at 11:17
  • @ViniApp: Thank you for the edit suggestion. However, there is already a Swift 4 version, using the now available built-in methods. Please let me know if that does not work for you. – Martin R Sep 21 '17 at 20:44
  • For those that came here for index calculations: In large strings, index offset calculation can become expensive. Using `from16` as a starting point, you can speed this up as: `let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex - 1)` – ctietze Dec 29 '17 at 07:50
  • This also does not work if I try to replace such ( ͡° ͜ʖ ͡°) character set – Alexander Khitev Jun 07 '18 at 11:03
  • @Alexander: I replaced with ( ͡° ͜ʖ ͡°) in my above example, and it worked as expected. – Martin R Jun 07 '18 at 11:13
  • @MartinR yes, of course. Please have a look at https://github.com/alexanderkhitev/TestTextView I made a demo project and made a string which is a UIViewController property (in the project it is needed for later use) if I delete characters for example from (つ ͡ ° ͜ʖ ͡ °) つ, then I get the app crash, of course you can make guard, but then we do not we can remove this character set. Also there are problems if in the middle of a set of characters (like I wrote above) insert emoji and start deleting, then you can get such a result ͜͡ and it can not be deleted any more. – Alexander Khitev Jun 07 '18 at 11:27
273

The NSString version (as opposed to Swift String) of replacingCharacters(in: NSRange, with: NSString) accepts an NSRange, so one simple solution is to convert String to NSString first. The delegate and replacement method names are slightly different in Swift 3 and 2, so depending on which Swift you're using:

Swift 3.0

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

  let nsString = textField.text as NSString?
  let newString = nsString?.replacingCharacters(in: range, with: string)
}

Swift 2.x

func textField(textField: UITextField,
               shouldChangeCharactersInRange range: NSRange,
               replacementString string: String) -> Bool {

    let nsString = textField.text as NSString?
    let newString = nsString?.stringByReplacingCharactersInRange(range, withString: string)
}
juancazalla
  • 1,050
  • 11
  • 17
Alex Pretzlav
  • 15,505
  • 9
  • 57
  • 55
  • 17
    This will not work if `textField` contains multi-code unit unicode characters such as emoji. Since it is an entry field a user may well enter emoji (), other page 1 characters of multi code unit characters such as flags (). – zaph Dec 25 '14 at 15:53
  • 2
    @Zaph: since the range comes from the UITextField, which is written in Obj-C against NSString, I suspect that only valid ranges based on the unicode characters in the string would be provided by the delegate callback, and so this is safe to use, but I haven't personally tested it. – Alex Pretzlav Jan 06 '15 at 21:33
  • Unicode, all of it, is supported by `NSString` and given a `UITextField` a user may well enter multi UTF-16 bit unicode character. `NSString` is UTF-16 based and works with UTF-16 units and will return the number of UTF-16 units, not characters. Thus a `NSRange` may start or end inside a unicode character such as an emoji. There are also unicode surrogate pairs in plane 0 as well. – zaph Jan 07 '15 at 00:01
  • 2
    @Zaph, that's true, my point is that `UITextFieldDelegate`'s `textField:shouldChangeCharactersInRange:replacementString:` method won't provide a range which starts or ends inside a unicode character, since the callback is based on a user entering characters from the keyboard, and it's not possible to type only part of an emoji unicode character. Note that if the delegate were written in Obj-C, it would have this same problem. – Alex Pretzlav Jan 07 '15 at 16:28
  • 4
    Not the best answer. Easiest to read code but @martin-r has the correct answer. – Rog Jul 29 '15 at 12:20
  • 3
    Actually you guys should do this `(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)` – Wanbok Choi Mar 16 '16 at 08:04
  • @harishmistry: I've added a Swift 3.0 version, hopefully that works for you! – Alex Pretzlav Sep 25 '16 at 19:32
  • 2
    Shorter Swift 3 version if an empty string is OK when textField.text is nil: `((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)` – Matthew Quiros Oct 04 '16 at 15:33
23

This answer by Martin R seems to be correct because it accounts for Unicode.

However at the time of the post (Swift 1) his code doesn't compile in Swift 2.0 (Xcode 7), because they removed advance() function. Updated version is below:

Swift 2

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

Swift 3

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        if let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
            let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

Swift 4

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        return Range(nsRange, in: self)
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Serhii Yakovenko
  • 12,424
  • 2
  • 27
  • 26
19

You need to use Range<String.Index> instead of the classic NSRange. The way I do it (maybe there is a better way) is by taking the string's String.Index a moving it with advance.

I don't know what range you are trying to replace, but let's pretend you want to replace the first 2 characters.

var start = textField.text.startIndex // Start at the string's start index
var end = advance(textField.text.startIndex, 2) // Take start index and advance 2 characters forward
var range: Range<String.Index> = Range<String.Index>(start: start,end: end)

textField.text.stringByReplacingCharactersInRange(range, withString: string)
eonil
  • 83,476
  • 81
  • 317
  • 516
Emilie
  • 2,413
  • 13
  • 12
7

This is similar to Emilie's answer however since you asked specifically how to convert the NSRange to Range<String.Index> you would do something like this:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

     let start = advance(textField.text.startIndex, range.location) 
     let end = advance(start, range.length) 
     let swiftRange = Range<String.Index>(start: start, end: end) 
     ...

}
James Jones
  • 1,486
  • 1
  • 12
  • 22
  • 2
    The characters view and UTF16 view of a string may have different lengths. This function is using UTF16 indexes (that's what `NSRange` speaks, though its components are integers) against the string's characters view, which may fail when used on a string with "characters" that take more than one UTF16 unit to express. – Nate Cook Nov 02 '15 at 19:44
5

A riff on the great answer by @Emilie, not a replacement/competing answer.
(Xcode6-Beta5)

var original    = "This is a test"
var replacement = "!"

var startIndex = advance(original.startIndex, 1) // Start at the second character
var endIndex   = advance(startIndex, 2) // point ahead two characters
var range      = Range(start:startIndex, end:endIndex)
var final = original.stringByReplacingCharactersInRange(range, withString:replacement)

println("start index: \(startIndex)")
println("end index:   \(endIndex)")
println("range:       \(range)")
println("original:    \(original)")
println("final:       \(final)")

Output:

start index: 4
end index:   7
range:       4..<7
original:    This is a test
final:       !his is a test

Notice the indexes account for multiple code units. The flag (REGIONAL INDICATOR SYMBOL LETTERS ES) is 8 bytes and the (FACE WITH TEARS OF JOY) is 4 bytes. (In this particular case it turns out that the number of bytes is the same for UTF-8, UTF-16 and UTF-32 representations.)

Wrapping it in a func:

func replaceString(#string:String, #with:String, #start:Int, #length:Int) ->String {
    var startIndex = advance(original.startIndex, start) // Start at the second character
    var endIndex   = advance(startIndex, length) // point ahead two characters
    var range      = Range(start:startIndex, end:endIndex)
    var final = original.stringByReplacingCharactersInRange(range, withString: replacement)
    return final
}

var newString = replaceString(string:original, with:replacement, start:1, length:2)
println("newString:\(newString)")

Output:

newString: !his is a test
zaph
  • 111,848
  • 21
  • 189
  • 228
3
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

       let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)

 }
HiDeoo
  • 10,353
  • 8
  • 47
  • 47
Darshan Panchal
  • 277
  • 3
  • 5
2

In Swift 2.0 assuming func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {:

var oldString = textfield.text!
let newRange = oldString.startIndex.advancedBy(range.location)..<oldString.startIndex.advancedBy(range.location + range.length)
let newString = oldString.stringByReplacingCharactersInRange(newRange, withString: string)
Brenden
  • 7,708
  • 11
  • 61
  • 75
2
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    guard let current = textField.text, let r = Range(range, in: current) else {
        return false
    }
    
    let text = current.replacingCharacters(in: r, with: string)
    // ...
    return true
}
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
1

Here's my best effort. But this cannot check or detect wrong input argument.

extension String {
    /// :r: Must correctly select proper UTF-16 code-unit range. Wrong range will produce wrong result.
    public func convertRangeFromNSRange(r:NSRange) -> Range<String.Index> {
        let a   =   (self as NSString).substringToIndex(r.location)
        let b   =   (self as NSString).substringWithRange(r)

        let n1  =   distance(a.startIndex, a.endIndex)
        let n2  =   distance(b.startIndex, b.endIndex)

        let i1  =   advance(startIndex, n1)
        let i2  =   advance(i1, n2)

        return  Range<String.Index>(start: i1, end: i2)
    }
}

let s   =   ""
println(s[s.convertRangeFromNSRange(NSRange(location: 4, length: 2))])      //  Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 4))])      //  Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 2))])      //  Improper range. Produces wrong result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 1))])      //  Improper range. Produces wrong result.

Result.





Details

NSRange from NSString counts UTF-16 code-units. And Range<String.Index> from Swift String is an opaque relative type which provides only equality and navigation operations. This is intentionally hidden design.

Though the Range<String.Index> seem to be mapped to UTF-16 code-unit offset, that is just an implementation detail, and I couldn't find any mention about any guarantee. That means the implementation details can be changed at any time. Internal representation of Swift String is not pretty defined, and I cannot rely on it.

NSRange values can be directly mapped to String.UTF16View indexes. But there's no method to convert it into String.Index.

Swift String.Index is index to iterate Swift Character which is an Unicode grapheme cluster. Then, you must provide proper NSRange which selects correct grapheme clusters. If you provide wrong range like the above example, it will produce wrong result because proper grapheme cluster range couldn't be figured out.

If there's a guarantee that the String.Index is UTF-16 code-unit offset, then problem becomes simple. But it is unlikely to happen.

Inverse conversion

Anyway the inverse conversion can be done precisely.

extension String {
    /// O(1) if `self` is optimised to use UTF-16.
    /// O(n) otherwise.
    public func convertRangeToNSRange(r:Range<String.Index>) -> NSRange {
        let a   =   substringToIndex(r.startIndex)
        let b   =   substringWithRange(r)

        return  NSRange(location: a.utf16Count, length: b.utf16Count)
    }
}
println(convertRangeToNSRange(s.startIndex..<s.endIndex))
println(convertRangeToNSRange(s.startIndex.successor()..<s.endIndex))

Result.

(0,6)
(4,2)
eonil
  • 83,476
  • 81
  • 317
  • 516
  • In Swift 2, `return NSRange(location: a.utf16Count, length: b.utf16Count)` must be changed to `return NSRange(location: a.utf16.count, length: b.utf16.count)` – Elliot Sep 07 '15 at 05:32
1

I've found the cleanest swift2 only solution is to create a category on NSRange:

extension NSRange {
    func stringRangeForText(string: String) -> Range<String.Index> {
        let start = string.startIndex.advancedBy(self.location)
        let end = start.advancedBy(self.length)
        return Range<String.Index>(start: start, end: end)
    }
}

And then call it from for text field delegate function:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let range = range.stringRangeForText(textField.text)
    let output = textField.text.stringByReplacingCharactersInRange(range, withString: string)

    // your code goes here....

    return true
}
Danny Bravo
  • 4,534
  • 1
  • 25
  • 43
  • I noticed my code was no longer working after the latest Swift2 update. I've updated my answer to work with Swift2. – Danny Bravo Nov 05 '15 at 16:01
0

In the accepted answer I find the optionals cumbersome. This works with Swift 3 and seems to have no problem with emojis.

func textField(_ textField: UITextField, 
      shouldChangeCharactersIn range: NSRange, 
      replacementString string: String) -> Bool {

  guard let value = textField.text else {return false} // there may be a reason for returning true in this case but I can't think of it
  // now value is a String, not an optional String

  let valueAfterChange = (value as NSString).replacingCharacters(in: range, with: string)
  // valueAfterChange is a String, not an optional String

  // now do whatever processing is required

  return true  // or false, as required
}
Murray Sagal
  • 8,454
  • 4
  • 47
  • 48
0
extension StringProtocol where Index == String.Index {

    func nsRange(of string: String) -> NSRange? {
        guard let range = self.range(of: string) else {  return nil }
        return NSRange(range, in: self)
    }
}
zheng
  • 468
  • 3
  • 8
0

Because NSRange, when used in NSString operations, represents positions of the UTF-16 units. Then the shortest way to convert to String.Index is to initialise via String.Index(utf16Offset: Int, in: StringProtocol) initialiser.

let string = "...."
let nsRange = NSRange(....) // This NSRange belongs to `string` variable.
let range = String.Index(utf16Offset: nsRange.lowerBound, in: string)
        ..< String.Index(utf16Offset: nsRange.upperBound, in: string)

Example:

let string = "a-\u{1112}\u{1161}\u{11AB}--\u{E9}\u{20DD}-‼--(العلاجية)-f"
let rangeOfLeftParenthesis = (string as NSString).range(of: "(")
let rangeOfRightParenthesis = (string as NSString).range(of: ")")
print("string: \(string)")
let lowerBound = String.Index.init(utf16Offset: rangeOfLeftParenthesis.upperBound, in: string)
let upperBound = String.Index.init(utf16Offset: rangeOfRightParenthesis.lowerBound, in: string)
let arabicSentenceRange = lowerBound ..< upperBound // Instance of `Range<String.Index>`
print("arabicSentenceRange: \(string[arabicSentenceRange])")

Output:

string: a-한--é⃝-‼--(العلاجية)-f
arabicSentenceRange: العلاجية
Vlad
  • 6,402
  • 1
  • 60
  • 74
-1

The Swift 3.0 beta official documentation has provided its standard solution for this situation under the title String.UTF16View in section UTF16View Elements Match NSString Characters title

  • [Here's the link to Apple Docs > String.UTF16View > UTF16View Elements Match NSString Characters](https://developer.apple.com/documentation/swift/string/utf16view#2848381) – ryanholden8 Jan 28 '21 at 20:53
-2

Swift 5 Solution

Short answer with main extension

extension NSRange {

    public init(range: Range<String.Index>, 
                originalText: String) {

        self.init(location: range.lowerBound.utf16Offset(in: originalText),
                  length: range.upperBound.utf16Offset(in: originalText) - range.lowerBound.utf16Offset(in: originalText))
    }
}

For detailed answer check here

Dhaval H. Nena
  • 3,992
  • 1
  • 37
  • 50