23

I am trying to implement autocompletion, but can't find an example that works in Swift. Below, I'm tring to convert Ray Wenderlich's autocompletion tutorial and example code from 2010. Finally, the code compiles, but the table containing possible completions does not appear, and I don't have the experience to see why it is not unhidden by shouldChangeCharactersInRange.

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {

@IBOutlet weak var textField: UITextField!
let autocompleteTableView = UITableView(frame: CGRectMake(0,80,320,120), style: UITableViewStyle.Plain)

var pastUrls = ["Men", "Women", "Cats", "Dogs", "Children"]
var autocompleteUrls = [String]()

override func viewDidLoad() {
    super.viewDidLoad()

    autocompleteTableView.delegate = self
    autocompleteTableView.dataSource = self
    autocompleteTableView.scrollEnabled = true
    autocompleteTableView.hidden = true
}

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool
{
    autocompleteTableView.hidden = false
    var substring = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)

    searchAutocompleteEntriesWithSubstring(substring)
    return true     // not sure about this - could be false
}

func searchAutocompleteEntriesWithSubstring(substring: String)
{
    autocompleteUrls.removeAll(keepCapacity: false)
    var indexOfPastUrls = 0

    for curString in pastUrls
    {
        let substringRange = curString.rangeOfString(curString)

        if (indexOfPastUrls  == 0)
        {
            autocompleteUrls.append(curString)
        }
        indexOfPastUrls = indexOfPastUrls + 1
    }
    autocompleteTableView.reloadData()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return autocompleteUrls.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let autoCompleteRowIdentifier = "AutoCompleteRowIdentifier"
    var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier, forIndexPath: indexPath) as UITableViewCell
    let index = indexPath.row as Int

    cell.textLabel.text = autocompleteUrls[index]
    return cell
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
    textField.text = selectedCell.textLabel.text        
}
}
DrWhat
  • 2,360
  • 5
  • 19
  • 33
  • substringRange type is NSRange instead of let ,and after that you have to check substringRange.location==0 than autocompleteUrls.append(curString) – Tamnna Nov 12 '14 at 11:32
  • Yes - I introduced indexOfPastUrls instead of substringRange.location==0 because I couldn't work out how to make substringRange type NSRange. I've tried specifying let substringRange: NSRange and casting the other side of the equation. – DrWhat Nov 12 '14 at 13:01
  • @DrWhat..how and when do i call "func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool"" method? – dhaval shah Jun 05 '15 at 11:33
  • @dhaval shah - it's a delegate method, so my understanding is that it's called automatically when typing occurs, just before the characters are displayed. – DrWhat Jun 08 '15 at 15:45
  • yes, thanks for the reply – dhaval shah Jun 11 '15 at 13:40
  • I have used your code. But tablview did not display in swift 3. I know that table view need cell identified. But did not use tableview connection from storyboard to you view controller. Please tell me story of let autoCompleteRowIdentifier = "AutoCompleteRowIdentifier" – Enamul Haque Jul 30 '17 at 12:09
  • in swift 3 not working autocomplete. can you help me..? – Enamul Haque Aug 01 '17 at 11:46

12 Answers12

12

Replace your searchAutocompleteEntriesWithSubstring function content with the one below. I hope it would help you.

func searchAutocompleteEntriesWithSubstring(substring: String)
{
    autocompleteUrls.removeAll(keepCapacity: false)

    for curString in pastUrls
    {
        var myString:NSString! = curString as NSString

        var substringRange :NSRange! = myString.rangeOfString(substring)

        if (substringRange.location  == 0)
        {
            autocompleteUrls.append(curString)
        }
    }

    autocompleteTableView.reloadData()
}
ollieread
  • 6,018
  • 1
  • 20
  • 36
Tamnna
  • 250
  • 2
  • 10
  • 1
    Yes! This looks much better. So, in Swift myString.rangeOfString(substring) works with myString: NSString, and substring: String. I don't know how you figured that out - but THANK YOU!!! If I ever get enough reputation, I will upvote this. Unfortunately, my table of autocompletions is still not appearing, so I hope you do not mind me leaving the question open. – DrWhat Nov 12 '14 at 15:42
  • I ended up answering the question myself, so I give you the credit for doing the hard part. Thanks again. – DrWhat Nov 17 '14 at 14:37
  • Thanks for simple and wonderful Solution...! it works like a charm.. How can I achieve this when tableview have multiple sections and each section has multiple rows.......? – Abirami Bala Jun 07 '17 at 07:50
8

The table was not appearing because the UITextField delegate was not to self in viewDidLoad. There was another final issue with the table not showing the autocompletion results, but this is also fixed. Ray Wenderlich's basic Objective-C autocompletion tutorial converted to Swift:

class ViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate,   UITableViewDataSource {

@IBOutlet weak var textField: UITextField!
@IBOutlet var autocompleteTableView: UITableView!

// @IBOutlet weak var autocompleteTableView = UITableView(frame: CGRectMake(0,80,320,120), style: UITableViewStyle.Plain)

var pastUrls = ["Men", "Women", "Cats", "Dogs", "Children"]
var autocompleteUrls = [String]()

override func viewDidLoad() {
    super.viewDidLoad()

    textField.delegate = self

    autocompleteTableView!.delegate = self
    autocompleteTableView!.dataSource = self
    autocompleteTableView!.scrollEnabled = true
    autocompleteTableView!.hidden = true
}

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool
{
    println("banana")
    autocompleteTableView!.hidden = false
    var substring = (self.textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)

    searchAutocompleteEntriesWithSubstring(substring)
    return true
}

func searchAutocompleteEntriesWithSubstring(substring: String)
{
    autocompleteUrls.removeAll(keepCapacity: false)
    println(substring)

    for curString in pastUrls
    {
        println(curString)
        var myString: NSString! = curString as NSString
        var substringRange: NSRange! = myString.rangeOfString(substring)
        if (substringRange.location == 0)
        {
            autocompleteUrls.append(curString)
        }
    }

    autocompleteTableView!.reloadData()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return autocompleteUrls.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
    let autoCompleteRowIdentifier = "AutoCompleteRowIdentifier"
    var cell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier) as? UITableViewCell

    if let tempo1 = cell
    {
        let index = indexPath.row as Int
        cell!.textLabel.text = autocompleteUrls[index]
    } else
    {
        cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: autoCompleteRowIdentifier)
    }
    return cell!
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
    textField.text = selectedCell.textLabel.text
}

}

DrWhat
  • 2,360
  • 5
  • 19
  • 33
  • I'm getting an error: found nil while unwrapping `autocompleteTableView!.delegate = self` . Did you make your `autocompleteTableView` with the Interface Builder? – Renato Parreira Sep 21 '15 at 17:09
6

I put together a tutorial that is full of pictures on how to recreate this now 6 year old tutorial

matthewhsingleton.com/coding-with-a-rubber-ducky/2016/5/26/… – RubberDucky4444

RubberDucky4444
  • 2,330
  • 5
  • 38
  • 70
4

Fixed for iOS 9.0 and Swift 2:

import UIKit

class UIAutoCompleteTextField: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    @IBOutlet weak
    var textField: UITextField!
        let autocompleteTableView = UITableView(frame: CGRectMake(0, 80, 320, 120), style: UITableViewStyle.Plain)

    var pastUrls = ["Men", "Women", "Cats", "Dogs", "Children"]
    var autocompleteUrls = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        autocompleteTableView.delegate = self
        autocompleteTableView.dataSource = self
        autocompleteTableView.scrollEnabled = true
        autocompleteTableView.hidden = true
    }

    func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) - > Bool {
        autocompleteTableView.hidden = false
        let substring = (textField.text!as NSString).stringByReplacingCharactersInRange(range, withString: string)

        searchAutocompleteEntriesWithSubstring(substring)
        return true // not sure about this - could be false
    }

    func searchAutocompleteEntriesWithSubstring(substring: String) {
        autocompleteUrls.removeAll(keepCapacity: false)

        for curString in pastUrls {
            let myString: NSString! = curString as NSString

            let substringRange: NSRange! = myString.rangeOfString(substring)

            if (substringRange.location == 0) {
                autocompleteUrls.append(curString)
            }
        }

        autocompleteTableView.reloadData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) - > Int {
        return autocompleteUrls.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) - > UITableViewCell {

        let autoCompleteRowIdentifier = "AutoCompleteRowIdentifier"
        let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier, forIndexPath: indexPath) as UITableViewCell
        let index = indexPath.row as Int

        cell.textLabel!.text = autocompleteUrls[index]
        return cell
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let selectedCell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath) !
            textField.text = selectedCell.textLabel!.text
    }
}
OhhhThatVarun
  • 3,981
  • 2
  • 26
  • 49
dane
  • 631
  • 6
  • 15
  • 2
    I would assign the textField.delegate to self in viewDidLoad if not done in any other place. For the benefit of others I would also point out that the above code assumes an existing cell prototype. If you want to run it with a default cell I would use the following in the cellForRowAtIndexPath method: cell = UITableViewCell(style: UITableViewCellStyle.Default , reuseIdentifier: cellIdentifier) – zevij Oct 27 '15 at 18:19
  • 2
    dane, thanks for this code, was very useful. FYI it was much easier to just add a tableview using interface builder and connect it to IBOutlet called autocompleteTableView. – LNI Dec 18 '15 at 05:49
  • 1
    As per @LNI comment, I've added a tableview under the text field directly in interface builder and connected it to an IBOutlet called autocompleteTableView. This allows me to set the reuse identifier for the prototype cell as well as the auto layout constraints for the tableview itself. This approach works for me in Xcode 7.2 and Swift 2.1. – Jervisbay Mar 12 '16 at 12:30
4

For future guys, that might get to work on autocomplete texfield with Swift 2, the code provided by @dane works well. but you have to change this line:

let cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier, forIndexPath: indexPath) as UITableViewCell

by

let cell = UITableViewCell(style: UITableViewCellStyle.Default , reuseIdentifier: cellIdentifier)

Also, you might notice that the it is case sensitive, and doesn't work if you enter lowercase string (e.g cats) by default. So to solve this issue you can replace add the option "CaseSensitiveSearch" to the substringRange declaration (in the func searchAutocompleteEntriesWithSubstring). it should look like:

let substringRange :NSRange! = myString.rangeOfString(substring,options [.CaseInsensitiveSearch])

Hope it will help you save one day!!!

Edess Elder
  • 1,461
  • 13
  • 9
  • In Swift 2.1.1 I had to adjust the syntax of the line that makes the search case-insensitive in @edess example for it compile. The new syntax is like this: let substringRange: NSRange! = myString.rangeOfString(substring, options: .CaseInsensitiveSearch) – Jervisbay Mar 12 '16 at 12:49
  • Using `let cell = UITableViewCell(style: UITableViewCellStyle.Default , reuseIdentifier: cellIdentifier)` will create a new cell every time, which would slow down the app and use up a lot of memory. It's preferred to use @dane's method but register the reuse identifier with the tableView with this method: `func registerClass(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)` – inket Apr 14 '16 at 01:33
2

Here's a way to add multiple tags based on "#" being typed in like twitter.

Variable typedSubstring is the global substring.

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

autocompleteTableView!.hidden = false
var changedText = (self.textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
var items = changedText.componentsSeparatedByString("#")

if (items.count > 0) {
  typedSubstring  = "#" + items.lastObject as NSString

  self.searchAutocompleteEntriesWithSubstring(typedSubstring)
}

return true

}

Improved on DrWhat's solution so that when you select a cell, it appends it correctly after where the user has already typed in.

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) 
let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!

let selectedText = selectedCell.textLabel?.text as String!

// Remove what has been typed already
let trimmedString = selectedText.stringByReplacingOccurrencesOfString(typedSubstring, withString: "", options: NSStringCompareOptions.LiteralSearch, range: nil)

var currentTextField = textField.text

// then append to what has been typed
textField.text = currentTextField + trimmedString
vandit
  • 411
  • 3
  • 7
2

Got it working with the below. The upper/lower case threw it off initially. I'm using it to autocomplete country names...

import UIKit

class billingAddressViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {


    @IBOutlet var countryTextField: UITextField!
    @IBOutlet var countryTableView: UITableView!

    var autocompleteCountries = [String]()

    // Get list of countries
    let countries = NSLocale.ISOCountryCodes().map { (code:String) -> String in
        let id = NSLocale.localeIdentifierFromComponents([NSLocaleCountryCode: code])
        return NSLocale(localeIdentifier: "en_US").displayNameForKey(NSLocaleIdentifier, value: id) ?? "Country not found for code: \(code)"
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        countryTextField.delegate = self

        countryTableView!.delegate = self
        countryTableView!.dataSource = self
        countryTableView!.scrollEnabled = true
        countryTableView!.hidden = true
    }

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

        print("text field has changed")
        countryTableView!.hidden = false

        let substring = (self.countryTextField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
        print(substring)
        searchAutocompleteEntriesWithSubstring(substring)
        return true
    }

    func searchAutocompleteEntriesWithSubstring(substring: String) {
        autocompleteCountries.removeAll(keepCapacity: false)
        print(substring)

        for curString in countries {
            //print(curString)
            let myString: NSString! = curString.lowercaseString as NSString
            let substringRange: NSRange! = myString.rangeOfString(substring.lowercaseString)
            if (substringRange.location == 0) {
                autocompleteCountries.append(curString)
            }
        }

        countryTableView!.reloadData()
    }

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


    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return autocompleteCountries.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let autoCompleteRowIdentifier = "AutoCompleteRowIdentifier"
        var cell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier) as UITableViewCell!

        if let tempo1 = cell {
            let index = indexPath.row as Int
            cell!.textLabel!.text = autocompleteCountries[index]
        }

        else {
            cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: autoCompleteRowIdentifier)
        }

        return cell!
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
        countryTextField.text = selectedCell.textLabel!.text
        countryTableView.hidden = true
    }

}
Tunaki
  • 132,869
  • 46
  • 340
  • 423
ramblin
  • 21
  • 1
2

table view added without storyboard

class ViewController: UIViewController  , UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {


@IBOutlet weak var textField: UITextField!

var autocompleteTableView: UITableView!
var pastUrls = ["Men", "Women", "Cats", "Dogs", "Children","aaaaaaaaa","aaaaaaaaaaaaaaaaaaa","aaaaaaaaa","a","aa","aaa"]
var autocompleteUrls = [String]()

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    autocompleteTableView = UITableView(frame: CGRectMake(self.textField.bounds.minX,self.textField.bounds.maxY,self.textField.bounds.width,self.textField.bounds.height * 4), style: UITableViewStyle.Plain)

    textField.delegate = self

    autocompleteTableView.delegate = self
    autocompleteTableView.dataSource = self
    autocompleteTableView.scrollEnabled = true
    autocompleteTableView.hidden = false

    autocompleteTableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    self.view.addSubview(autocompleteTableView)
}

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


func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool
{
    autocompleteTableView.hidden = false
    var substring = (self.textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)

    searchAutocompleteEntriesWithSubstring(substring)
    return true      // not sure about this - could be false
}

func searchAutocompleteEntriesWithSubstring(substring: String)
{
    autocompleteUrls.removeAll(keepCapacity: false)

    for curString in pastUrls
    {
        var myString:NSString! = curString as NSString

        var substringRange :NSRange! = myString.rangeOfString(substring)

        if (substringRange.location  == 0)
        {
            autocompleteUrls.append(curString)
        }
    }

    autocompleteTableView.reloadData()
    //autocompleteTableView.hidden = false
}

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return autocompleteUrls.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

            let autoCompleteRowIdentifier = "cell"
            var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier, forIndexPath: indexPath) as UITableViewCell
            let index = indexPath.row as Int

            cell.textLabel!.text = autocompleteUrls[index]



            return cell




}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
    textField.text = self.autocompleteUrls[indexPath.row]

    self.autocompleteTableView.hidden = true
}











 }
Cloy
  • 2,141
  • 23
  • 32
1

This post helped me thanks, just in case you guys are working with google places API in swift 3 and you need case-insensitive here is the updated code you just have to put:

let subStringRange : NSRange! = myString.range(of: substring, options: .caseInsensitive)
1

Replace cellForRowAtIndexPath with following function

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell = UITableViewCell()
    var data = autocompleteUrls[indexPath.row]
    cell.textLabel?.text = data as String
    return cell
}
Alexander Ciesielski
  • 10,506
  • 5
  • 45
  • 66
Parth
  • 11
  • 1
0

Here you have a nice-to-have library in order to simplify that work: https://github.com/apasccon/SearchTextField

pishke
  • 11
  • 1
  • 1
    Whilst this may theoretically answer the question, [it would be preferable](//meta.stackoverflow.com/q/8259) to include the essential parts of the answer here, and provide the link for reference. – mx0 Jul 21 '17 at 21:48
0

Add view.addSubview(autocompleteTableView) in your viewdidload. It will work.

Robert Columbia
  • 6,313
  • 15
  • 32
  • 40
  • 1
    Hi, Welcome to Stack Overflow and thank you for your first answer. To make the answer more useful to other people, it is best practice to make your answer more detailed for instance including a code example that shows where to add the code you suggest exactly. Also to annotate your answer with text that explains why it addresses the OP's original question. – Spangen Jun 07 '18 at 09:02