24

I'm trying to use UITextFieldDelegate in Swift/Xcode6 and I'm struggling with the way I'm supposed to use stringByReplacingCharactersInRange. The compiler error is 'Cannot convert the expression's type 'String' to type '$T8'.

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool
{
    let s = textField.text.stringByReplacingCharactersInRange(range:range, withString:string)
    if countElements(s) > 0 {

    } else {

    }
    return true
}

Update for Xcode 6 Beta 5: The thing is shouldChangeCharactersInRange gives an NSRange object and we'd need a Swift Range object for stringByReplacingCharactersInRange. Can this still be considered a bug as I don't see why we should still be dealing with NS* objects? The String argument of the delegate method is anyway of a Swift type.

Seppo
  • 565
  • 1
  • 5
  • 10

13 Answers13

34

Here's how to calculate the resulting string in various Swift versions.

Note that all methods use -[NSString stringByReplacingOccurrencesOfString:withString:] in exactly the same way, just differing in syntax.

This is the preferred way to calculate the resulting string. Converting to a Swift Range and use that on a Swift String is error prone. Johan's answer for example is incorrect in a couple of ways when operating on non-ASCII strings.

Swift 3:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let result = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) ?? string
    // ... do something with `result`
}

Swift 2.1:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let result = (textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
    // ... do something with `result`
}

Swift 1 (only left here for reference):

let result = textField.text.bridgeToObjectiveC().stringByReplacingCharactersInRange(range, withString:string)
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
17

I created an extension to NSRange the converted to Range<String.Index>

extension NSRange {
    func toRange(string: String) -> Range<String.Index> {
        let startIndex = advance(string.startIndex, location)
        let endIndex = advance(startIndex, length)
        return startIndex..<endIndex
    }
}

So I can create the String like this

let text = textField.text
let newText = text.stringByReplacingCharactersInRange(range.toRange(text), withString: string)

in Swift 2.1 the extension looks like:

extension NSRange {
    func toRange(string: String) -> Range<String.Index> {
        let startIndex = string.startIndex.advancedBy(location)
        let endIndex = startIndex.advancedBy(length)
        return startIndex..<endIndex
    }
}
Johan Kool
  • 15,637
  • 8
  • 64
  • 81
Johan
  • 1,951
  • 1
  • 18
  • 22
  • 1
    Definitely the Swift-most answer in there. But you can avoid the `self`s :) – DeFrenZ Feb 27 '15 at 17:01
  • 1
    @DavideDeFranceschi I added the `self`s for clarity. But you are correct, they are not needed – Johan Mar 02 '15 at 14:17
  • 1
    That's a nice approach, but it breaks when you use unicode characters, such as emoji. Give it a try – Gui Moura Jun 30 '15 at 18:17
  • 4
    In Swift 2, you'd use `advancedBy`: `let startIndex = string.startIndex.advancedBy(location); let endIndex = startIndex.advancedBy(length)` – Rob Sep 27 '15 at 16:49
  • shouldn't the end index be advanced by `location + length` ? – Alnitak Mar 02 '16 at 14:17
  • There seems to be an error in the Swift 2.1 example of this solution. I think`let endIndex = string.startIndex.advancedBy(length)` should be `let endIndex = startIndex.advancedBy(length)` – Roel Spruit May 26 '16 at 08:38
  • @Joham correct let endIIndex = startIndex.advancedBy(length) – john07 Jul 02 '16 at 03:32
  • The approach in this answer is incorrect. You are using an NSString's character index for an indices on a Swift string's chracter view. These are not interchangeable. – Nikolai Ruhe Jan 18 '17 at 10:57
15

The simplest solution I have found is using as NSString - that enables us to use NSRange.

var textField : UITextField = UITextField()
textField.text = "this is a test"

let nsRange : NSRange = NSRange(location: 0, length: 4)

let replaced = (textField.text as NSString)
               .stringByReplacingCharactersInRange(nsRange, withString: "that");

NSLog("Replaced: %@", replaced); //prints "that is a test"
Sulthan
  • 128,090
  • 22
  • 218
  • 270
7
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)

bridgeToObjectiveC can be removed in coming updates

Adam Eberbach
  • 12,309
  • 6
  • 62
  • 114
Ankish Jain
  • 11,305
  • 5
  • 36
  • 34
  • 1
    Short and sweet. As of swift2, textField.text needs force unwrapped: let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string) – Eli Burke Dec 16 '15 at 21:43
6

Nothing worked for me except the following: (FYI I'm using Xcode7.0 GM, Swift 2.0, iOS9GM)

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let currentText = textField.text ?? ""
    let prospectiveText = (currentText as NSString).stringByReplacingCharactersInRange(range, withString: string)
    print("prospectiveText", prospectiveText)
    return true;
}
Ashok
  • 5,585
  • 5
  • 52
  • 80
3

As of Swift 4, this is a little simpler, like Alexander Volkov's answer, but without the extension.

   func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let revisedText: String
        if let text = textField.text, let swiftRange = Range(range, in: text) {
            revisedText = text.replacingCharacters(in: swiftRange, with: string)
        } else {
            revisedText = string
        }
        // Do something with the text and return boolean.
   }
dwsolberg
  • 879
  • 9
  • 8
2

This is a cross-post from this question, but without a way to make a Range<String.Index> the Swift-native String.stringByReplacingCharactersInRange() is pretty useless. So, here's a function to generate a Range<String.Index>:

func RangeMake(#start:Int, #end:Int) -> Range<String.Index> {
    assert(start <= end, "start must be less than or equal to end")
    func rep(str: String, count: Int) -> String {
        var result = ""
        for i in 0..count {
            result += str
        }
        return result
    }
    let length = end - start
    let padding = rep(" ", start)
    let dashes = rep("-", length)
    let search = padding + dashes
    return search.rangeOfString(dashes, options: nil, range: Range(start: search.startIndex, end: search.endIndex), locale: NSLocale.systemLocale())
}

let sourceString = "Call me Ishmael."
let range = RangeMake(start: 8, end: 15)    
let name = sourceString.substringWithRange(range)
// name = "Ishmael"
Community
  • 1
  • 1
Nate Cook
  • 92,417
  • 32
  • 217
  • 178
2

Working & tested

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let newString = NSString(string: textField.text!).replacingCharacters(in: range, with: string)

    print(newString)

      return true;
}
Mr.Javed Multani
  • 12,549
  • 4
  • 53
  • 52
1

Creating String.Index is cumbersome.

let string = "hello"
let range = string.startIndex .. string.startIndex.succ().succ()
let result = string.stringByReplacingCharactersInRange(range, withString: "si")
Tomáš Linhart
  • 13,509
  • 5
  • 51
  • 54
1

for iOS 8.3 use following code

 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
    if textField.isEqual(<textField whose value to be copied>)
    {
        <TextField to be updated>.text = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
    }

    return true
}
Avinash
  • 4,304
  • 1
  • 23
  • 18
1

With Swift 2.0, the answer from Durul must be changed because characters.count must be used instead of count().

The following must be done.

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let length = textField.text!.characters.count - range.length + string.characters.count
    if length > 0 {
        submitButton.enabled = true
    } else {
        submitButton.enabled = false
    }
    return true
}
sebastien
  • 2,489
  • 5
  • 26
  • 47
0
import UIKit

class LoginViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var submitButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }



    func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        let length = count(textField.text) - range.length + count(string)
        if length > 0 {
            submitButton.enabled = true
        } else {
            submitButton.enabled = false
        }
        return true
    }
}
Durul Dalkanat
  • 7,266
  • 4
  • 35
  • 36
0

Swift 4:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    var text = textField.text ?? ""
    text.replaceSubrange(range.toRange(string: text), with: string)
    ...
    return true
}

extension NSRange {

    /// Convert to Range for given string
    ///
    /// - Parameter string: the string
    /// - Returns: range
    func toRange(string: String) -> Range<String.Index> {
        let range = string.index(string.startIndex, offsetBy: self.lowerBound)..<string.index(string.startIndex, offsetBy: self.upperBound)
        return range
    }
}
Alexander Volkov
  • 7,904
  • 1
  • 47
  • 44