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.

0

13 Answers 13

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) 
Sign up to request clarification or add additional context in comments.

6 Comments

String has this method. See my comment.
@TomášLinhart Where does String declare this method? I can't find it in the Swift module.
@NikolaiRuhe I don't know. But see my post and run in it in the playground and you will see it works.
At least as of Beta 5 this is no longer the correct answer.
Use "textField.text as NSString" cast instead.
|
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 } } 

8 Comments

Definitely the Swift-most answer in there. But you can avoid the selfs :)
@DavideDeFranceschi I added the selfs for clarity. But you are correct, they are not needed
That's a nice approach, but it breaks when you use unicode characters, such as emoji. Give it a try
In Swift 2, you'd use advancedBy: let startIndex = string.startIndex.advancedBy(location); let endIndex = startIndex.advancedBy(length)
shouldn't the end index be advanced by location + length ?
|
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" 

Comments

7
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string) 

bridgeToObjectiveC can be removed in coming updates

1 Comment

Short and sweet. As of swift2, textField.text needs force unwrapped: let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
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; } 

Comments

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. } 

1 Comment

Change let to var to make revisedText mutable.
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" 

Comments

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; } 

Comments

1

Creating String.Index is cumbersome.

let string = "hello" let range = string.startIndex .. string.startIndex.succ().succ() let result = string.stringByReplacingCharactersInRange(range, withString: "si") 

3 Comments

I would post this as a bug. There should be a better way to create ranges of strings. Or maybe I just can't find it.
Feel free to report it. I also didn't find a better way.
I agree, my gut feeling was that this is a bug. For example stringByAppendingString works as expected.
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 } 

Comments

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 } 

Comments

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 } } 

Comments

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 } } 

2 Comments

When I delete one char I obtain a Fatal error: Can't advance past endIndex
@Maximelc The suggested approach is correct. Provide your code.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.