NSRange to Range<String.Index>
Asked Answered
V

16

295

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

Valentino answered 5/8, 2014 at 12:1 Comment(2)
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! – Scutter
https://mcmap.net/q/23835/-how-to-convert-range-in-nsrange-duplicate – Kufic
P
275

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)
}
Panties answered 22/10, 2014 at 21:46 Comment(8)
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 (πŸ‡ͺπŸ‡Έ). – Diphthong
@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. – Panties
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. – Diphthong
@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. – Panties
Not the best answer. Easiest to read code but @martin-r has the correct answer. – Hostler
Actually you guys should do this (textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string) – Primogenial
@harishmistry: I've added a Swift 3.0 version, hopefully that works for you! – Panties
Shorter Swift 3 version if an empty string is OK when textField.text is nil: ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) – Methaemoglobin
M
414

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 = "aπŸ‘ΏbπŸ‡©πŸ‡ͺc"
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 = "aπŸ‘ΏbπŸ‡©πŸ‡ͺc"
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 = "aπŸ‘ΏbπŸ‡©πŸ‡ͺc"
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)) // πŸ‡©πŸ‡ͺ
Myca answered 22/5, 2015 at 19:29 Comment(25)
Why not just do https://mcmap.net/q/23816/-nsrange-to-range-lt-string-index-gt? It seems like less code and still correct. – Mcnully
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". – Myca
Hmm... OK. Thanks. I think I'll just convert String to NSString then as that seems to work and is less code (simpler). – Mcnully
@MattDiPasquale: Sure. My intention was to answer the verbatim question "How can I convert NSRange to Range<String.Index> in Swift" in a Unicode-safe way (hoping that someone might find it useful, which is not the case until now :( – Myca
@user1687195: Thanks for fixing my stupid error in the Swift 2 code! I should have tested it. – Myca
@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 – Exteroceptor
@DrPatience: That probably means that textField.text is now an optional and has to be unwrapped ... – Myca
In sample. For let n1 I need to put let n1:NSrange to work. Thanks anyway. – Anting
This doesn't seem to work anymore. Range<String.Index> does not have members upperBound or lowerBound anymore and utf16.index does not exist either. Or am I missing something? – Advent
@ClockWise: Which Xcode version? Did you see the "Update for Swift 3" at the end of the answer? – Myca
@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. – Advent
@ClockWise: Yes, Xcode 7.3.1 is Swift 2.2. Xcode 8 is Swift 3. – Myca
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. – Changeful
where did you find the documentation? – Duppy
@jiminybob99: Command-click on String to jump to the API reference, read all methods and comments, then try different things until it works :) – Myca
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. – Codee
@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! – Myca
In Swift 4, there are now dedicated initialisers for this :) You can use Range(nsRange, in: str) to convert an NSRange to Range<String.Index>? 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). – Gest
@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? – Myca
@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 (and PR here). – Gest
@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. – Myca
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) – Quicksilver
This also does not work if I try to replace such ( Ν‘° ΝœΚ– Ν‘°) character set – Travertine
@Alexander: I replaced πŸ‡©πŸ‡ͺ with ( Ν‘° ΝœΚ– Ν‘°) in my above example, and it worked as expected. – Myca
@MartinR yes, of course. Please have a look at 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. – Travertine
P
275

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)
}
Panties answered 22/10, 2014 at 21:46 Comment(8)
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 (πŸ‡ͺπŸ‡Έ). – Diphthong
@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. – Panties
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. – Diphthong
@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. – Panties
Not the best answer. Easiest to read code but @martin-r has the correct answer. – Hostler
Actually you guys should do this (textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string) – Primogenial
@harishmistry: I've added a Swift 3.0 version, hopefully that works for you! – Panties
Shorter Swift 3 version if an empty string is OK when textField.text is nil: ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) – Methaemoglobin
T
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)
    }
}
Tootle answered 3/9, 2015 at 15:18 Comment(0)
F
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)
Featherstitch answered 5/8, 2014 at 12:13 Comment(0)
A
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) 
     ...

}
Annulet answered 5/11, 2014 at 3:39 Comment(1)
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. – Likker
D
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
Diphthong answered 5/8, 2014 at 12:33 Comment(0)
U
3
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

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

 }
Udo answered 11/8, 2016 at 9:20 Comment(0)
B
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)
Brendon answered 9/2, 2016 at 1:40 Comment(0)
A
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
}
Aggression answered 2/6, 2021 at 11:41 Comment(0)
J
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)
Jaquith answered 25/12, 2014 at 2:48 Comment(1)
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) – Kynthia
P
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
}
Prepotency answered 28/8, 2015 at 12:17 Comment(1)
I noticed my code was no longer working after the latest Swift2 update. I've updated my answer to work with Swift2. – Prepotency
U
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
}
Ulrikaumeko answered 11/5, 2017 at 20:3 Comment(0)
H
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)
    }
}
Haya answered 8/11, 2018 at 9:15 Comment(0)
S
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: Ψ§Ω„ΨΉΩ„Ψ§Ψ¬ΩŠΨ©
Sheepherder answered 14/11, 2021 at 16:59 Comment(0)
C
-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

Coom answered 25/6, 2016 at 5:56 Comment(1)
I
-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

Intellectuality answered 21/7, 2021 at 11:25 Comment(1)
If you are using utf16Offset then worth to use Range<String.UTF16View.Index> instead of Range<String.Index>. – Sheepherder

© 2022 - 2024 β€” McMap. All rights reserved.