2

I'm writing a validator for a UITextField, I'm given an existing string, replacement string and an NSRange where the replacement takes place. I have two versions of my code to get the new candidate string:

a. Explicitly use NSString

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let existingString: NSString = textField.text ?? ""
    let candidateString =  existingString.stringByReplacingCharactersInRange(range, withString: string)
    //do validation on candidateString here
    return true
}

b. Generate a Range<String.Int> and use it on Swift String's stringByReplacingCharactersInRange

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let existingString = textField.text ?? ""
    let swiftRange = advance(existingString.startIndex, range.length)..<advance(existingString.startIndex, range.length) // I hate this.
    let candidateString = existingString.stringByReplacingCharactersInRange(swiftRange, withString: string)

So putting aside the fact that the code to generate the Swift range is unreadable... do these two approaches differ in execution. Specifically does Swift have a better (probably more unicode savvy) version of stringByReplacingCharactersInRange which I get by avoiding the conversion to NSString?

Rog
  • 17,070
  • 9
  • 50
  • 73
  • In your first method you probably meant `let existingString: NSString = ...`. – Your second method does *not* work correctly for all strings, compare http://stackoverflow.com/a/30404532/1187415 and the following comments. (So doing it in *pure Swift* is possible but even uglier than you thought :-) – Apart from that, I don't think there is a difference in the results. – Martin R Jul 29 '15 at 09:27
  • Not valid because NSRange references the NSString and might not be valid for the String? This would back up my feeling that the Swift conversion of the UITextfield delegate methods is only half a job. – Rog Jul 29 '15 at 12:17
  • *"NSRange references the NSString and might not be valid for the String"* Yes. – *"the Swift conversion of the UITextfield delegate methods is only half a job"* Agreed. – Martin R Jul 29 '15 at 16:52

2 Answers2

2

NSRange of NSString is equivalent to Range of String.UTF16.Index (aka String.UTF16Index).

The correct way (without NSString) is:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let existingString = textField.text ?? ""

    // convert `NSRange` to `Range<String.Index>`
    let start = String.UTF16Index(range.location)
    let end = start.advancedBy(range.length)
    let swiftRange = start.samePositionIn(string)! ..< end.samePositionIn(string)!

    let candidateString = existingString.stringByReplacingCharactersInRange(swiftRange, withString: string)

    // Do validation...

    return true
}

You can extend NSRange:

extension NSRange {
    func toStringRangeIn(string: String) -> Range<String.Index>? {
        let start = String.UTF16Index(self.location)
        let end = start.advancedBy(self.length)
        if let start = start.samePositionIn(string), end = end.samePositionIn(string) {
            return start ..< end
        }
        return nil
    }
}

var str  = "Hello "
let range = NSMakeRange(8, 4)
str.stringByReplacingCharactersInRange(range.toStringRangeIn(str)!, withString: "FOO") // -> "Hello FOO"
(str as NSString).stringByReplacingCharactersInRange(range, withString: "FOO")  // -> "Hello FOO"

with this, you can:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let existingString = textField.text ?? ""
    let candidateString = existingString.stringByReplacingCharactersInRange(
        range.toStringRangeIn(existingString)!,
        withString: string
    )

    // Do validation...

    return true
}
rintaro
  • 51,423
  • 14
  • 131
  • 139
  • Another conversion between NSRange and Swift range is suggested here: http://stackoverflow.com/a/30404532/1187415. I think it is essentially the same idea (using UTF16Index). – Martin R Jul 29 '15 at 16:34
  • A subsidiary question, what is the difference between String.UTF16Index(self.location) and advance(str.startIndex, range.location) – Rog Jul 30 '15 at 09:17
  • `String.UTF16Index(loc)` is equivalent to `advance(str.utf16.startIndex, loc)`. it's a position in UTF16 code points. On the other hand, `advance(str.startIndex, loc)` represents a position in `Character` ([*extended grapheme cluster*s](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html#//apple_ref/doc/uid/TP40014097-CH7-ID293)) sequence. – rintaro Jul 30 '15 at 09:25
0

Yes. There is a difference between the two approaches and the second one is more correct. It also handles multi-code characters correctly:

var str  = "Hello , playground"
let range = NSMakeRange(0, 8)
let swiftRange = advance(str.startIndex, range.location)..<advance(str.startIndex, range.length) // I hate this.
var newString = str.stringByReplacingCharactersInRange(swiftRange, withString: "goodbye")
// => goodbye playground
newString = (str as NSString).stringByReplacingCharactersInRange(range, withString: "goodbye")
// => "goodbye, playground"

I'm assuming that this is because the range is calculated better, not because there are different implementation of the replacement methods. swiftRange is 0..<15

Rog
  • 17,070
  • 9
  • 50
  • 73
  • Your argument seems wrong to me. The second result is wrong because `NSRange` and `Range` count characters differently (UTF-16 code points vs Unicode "characters" aka "grapheme clusters"), and your conversion between both is not correct. – Martin R Jul 29 '15 at 16:31
  • Where is the bug? in `let swiftRange = advance(str.startIndex, range.location).. – Rog Oct 01 '15 at 14:53