iOS

Implement Auto Complete Form Input for iOS with Eureka

Sometimes when implementing form we may encounter an auto complete input field on ios just like on web platform, for example on w3shools:

Auto Complete Exmaple On Web Platform
Auto Complete Exmaple On Web Platform

It may be not very hard to implement if this field appear only once, but what if there are many? Definitely not very easy or you’ll have to duplicate code from one place to another. In this post you’ll be guided to implement auto complete form input and pack it into reusable module by using Eureka. Here is what you’ll achieve at the end of this tutorial:

Auto Complete Input Finish
Auto Complete Input Finish

Contents

Auto Complete Input’s Components

An auto complete input has 3 main components, which we will implement through this tutorial. These components are specified in the following image:

Each component by order is:

  1. Text input row: The view of the row display on the form. This component displays current value of the input and validation errors. Also it has a text input for user to type and search.
  2. Suggestion table container: The container for suggestion list, usually updated each time the text of Text input row changes.
  3. Suggestion table row content: Content of each row in the suggestion list.

These components are also the main tasks to do when implementing auto complete input. But before get started, you’ll need to install some base source code.

Install base source code

First, download project source code here. Open project in start folder and add the following code in to podfile:

  pod 'SuggestionRow', :git => 'https://github.com/bsv-hienpham1991/SuggestionRow.git'
  pod 'SnapKit', '~> 5.0.0'

SuggestionRow is a library for implementing auto complete form input when using Eureka. If you are wondering in the above line we are pointing the library toward another git repo, that’s because this library lacks some features for practical uses, so I have to fork and modify it. SnapKit is a library to make Auto Layout easy on both iOS and OS X.

Run command pod install to install the library.

Next, we’ll some base source code for deeper customization of auto complete input. Open xcode project in finish folder, copy folder SuggestionInputCell and paste into your currently practicing project:

Copy folder SuggestionInputCell

Note: You do not need to modify any of these, so I will not explain details about these files.

Implement text input row

We’ll start by defining skeleton for text input row. Open file ScientistSuggestionCell/ScientistSuggestionCell and add the following code:

// 1
final class ScientistSuggestionCell<T, TableViewCell: UITableViewCell>: SuggestionCellCustom<T, TableViewCell> where TableViewCell: EurekaSuggestionTableViewCell, TableViewCell.S == T {
}

// 2
final class ScientistSuggestionRow<T: SuggestionValue>: _SuggestionRowCustom<ScientistSuggestionCell<T, SuggestionTableViewCellCustom<T>>>, RowType {
    required public init(tag: String?) {
        super.init(tag: tag)
    }
}

From the top:

  1. Define custom cell as a subclass of SuggestionCellCustom with type generic including type T for value type of the row and type TableViewCell is for each items in suggestion list.
  2. Define custom row as a subclass of _SuggestionRowCustom which has cell: ScientistSuggestionCell and its correspond value.

Note: These code follows guides of Eureka. You can check details here in section Basic custom rows.

Next, add this row to the form by inserting the following code at the end of method viewDidLoad in file ViewController.swift:

form +++ Section("Input suggestions")
    // 1
    <<< ScientistSuggestionRow<Scientist>() {row in
         // 2
        row.title = "Scientist"
         // 3
        row.placeholder = "Please search the scientist name"
         // 4
        row.maxSuggestionRows = 4
        // 5
        row.add(rule: RuleRequired())
    }
    // 6
    .cellSetup({ (cell, row) in
        cell.height = { UITableView.automaticDimension }
    })
    // 7
    .onRowValidationChanged({ (cell, row) in
        UIView.performWithoutAnimation { [weak self] in
            self?.tableView.performBatchUpdates({
                row.updateCell()
            }, completion: nil)
        }
    })

In this code we:

  1. Create a row with title “Scientist”.
  2. Set placeholder text of the row.
  3. Set max number of item in suggestion item list to 4.
  4. Add validation rule as RuleRequire.
  5. Set table cell height to automatic.
  6. Each time row validate, relayout table view in order to recalculate row height, since validation errors are dynamic which will change row height.

You can now run the project and try typing some text into it. You’ll see there is some white space appear below the input each time text exists in the text field. Next we’ll make this white space display data.

Open file Scientist.swift and add the following code:

extension Scientist: SuggestionValue {
    // 1
    init?(string stringValue: String) {
        return nil
    }

    // 2
    var suggestionString: String {
        return "\(firstName) \(lastName)"
    }

    // 3
    static func == (lhs: Scientist, rhs: Scientist) -> Bool {
        return lhs.id == rhs.id
    }
}

In this code, you extend struct Scientist with protocolSuggestionValue :

  1. Because SuggestionValue edapts protocol InputTypeInitiable so we have to implement this function. Not sure why it has to be like this since in the original repo of SuggestionRow. Let’s just ignore and move on to the next one.
  2. Implement property suggestionString to display each item of suggestion list.
  3. Because SuggestionValue edapts protocol Equatable so we have to implement function check equal too.

Open file ViewController.swift, add the following code to class ViewController to declare data list to use for suggestion list:

    let users: [Scientist] = [Scientist(id: 1, firstName: "Albert", lastName: "Einstein"),
                         Scientist(id: 2, firstName: "Isaac", lastName: "Newton"),
                         Scientist(id: 3, firstName: "Galileo", lastName: "Galilei"),
                         Scientist(id: 4, firstName: "Marie", lastName: "Curie"),
                         Scientist(id: 5, firstName: "Louis", lastName: "Pasteur"),
                         Scientist(id: 6, firstName: "Michael", lastName: "Faraday"),
                         Scientist(id: 5, firstName: "Louis", lastName: "Pasteur"),
                         Scientist(id: 6, firstName: "Marie", lastName: "Curie"),
                         Scientist(id: 7, firstName: "Thomas", lastName: "Edison"),
                         Scientist(id: 8, firstName: "Stephen", lastName: "Hawking"),
                         Scientist(id: 9, firstName: "Alan", lastName: "Turing"),
                         Scientist(id: 10, firstName: "Leonardo da", lastName: "Vinci"),
                         Scientist(id: 11, firstName: "Nikolas", lastName: "Tesla"),
                         Scientist(id: 12, firstName: "Ada", lastName: "Lovelace"),
                         Scientist(id: 13, firstName: "Richard", lastName: "Feyman"),
                         Scientist(id: 14, firstName: "Benjamin", lastName: "Franklin"),
                         Scientist(id: 15, firstName: "James", lastName: "D. Watson")]

Next, in the block code that setting update row of the form, below line row.title = "Scientist" add the following code:

row.asyncFilterFunction = {[weak self] (text, completion) in
    guard let self = self else {
        completion([])
        return
    }
    completion(self.users.filter({ $0.firstName.lowercased().contains(text.lowercased()) }))
}

This code config property asyncFilterFunction of class _SuggestionRow, which will be called each time user enter text in order to retrieve list for display in suggestion list.

Run the project and try typing again, now you can see suggestion list has been display corresponding to the typing text:

Auto Complete Input Basic Row
Auto Complete Input Basic Row

It’s working, but not pretty right? Let’s customize UI of the row.

In Xcode Project Navigator, select folder ScientistSuggestionCell and 2 following files: ScientistSuggestionCellContentView.swift and ScientistSuggestionCellContentView.xib. These 2 files contains UI layout of the row with IBOutlets already connected. But there is one important IBOutlet you must not forget to connect. In the interface builder, select the UITextField and connect it to IBOutlet with name textField:

Connect UITextField IBOutlet
Connect UITextField IBOutlet

Important: ScientistSuggestionCellContentView is subclass of SuggestionTableViewCellContentView which is mandatory: All custom view of text input row must subclass SuggestionCellContentView so that _SuggestionRowCustom can apply custom UI.

Open file ScientistSuggestionCell.swift, in method required public init(tag: String?) add the following code to load custom UI into ScientistSuggestionCell:

contentViewProvider = ViewProvider<SuggestionCellContentView>(nibName: "ScientistSuggestionCellContentView", bundle: Bundle.main)

The ViewProvider struct init with 2 params: nibName and bundle, similiar to UIViewController thus help you customize UI easily for text input row.

Run the project and here is what the screen displays if you follow correctly:

Auto Complete Input Row Custom UI

As you can see the custom UI has been displayed on screen, but the alignment and font size, font color of the textfield is not correct. It turns out Eureka always change these properties of textfield to default each time func update() of table cell is called. We’ll fix this by adding the following code to class ScientistSuggestionCell like this:

final class ScientistSuggestionCell<T, TableViewCell: UITableViewCell>: SuggestionCellCustom<T, TableViewCell> where TableViewCell: EurekaSuggestionTableViewCell, TableViewCell.S == T {
    // 1
    var originClearButtonMode: UITextField.ViewMode?
    var originTextAlignment: NSTextAlignment?
    var originFont: UIFont?
    var originTextColor: UIColor?
    
    override func setup() {
        super.setup()
        
        // 2
        originClearButtonMode = textField.clearButtonMode
        originTextAlignment = textField.textAlignment
        originFont = textField.font
        originTextColor = textField.textColor
    }
    
    override func update() {
        super.update()
        
        // 3
        if let unwrapped = originClearButtonMode {
            textField.clearButtonMode = unwrapped
        }
        if let unwrapped = originTextAlignment {
            textField.textAlignment = unwrapped
        }
        textField.font = originFont
        textField.textColor = originTextColor
    }
}

In this code, you:

  1. Declare store properties for UITextField.
  2. Store properties of UITextField in xib after loaded.
  3. Restore properties of UITextField when update view.

Run the project, you’ll see the text field has been displayed correctly.

OK now do you see the red label which is error label displays at the bottom of the row? Let’s add logic display error label by adding the following code at the end of func update():

if let unwrapped = bsContentView as? ScientistSuggestionCellContentView {
    // 1
    unwrapped.errorContainer.isHidden = row.isValid
    
    // 2
    let errorMessages =
        row.validationErrors.map { error in
            return error.msg
        }
        .joined(separator: "\n")
    
    // 3
    unwrapped.errorLabel.text = errorMessages
}

Here‘s what‘s happening with this code:

  1. Display error label only when the row has validation errors.
  2. Get error message from validation errors and concatenating into multiline string.
  3. Assign the error message to error label.

Run the project and check. You’ll see the error label has been hidden.

Before finishing implementing text input row, add the following code in method setup of ScientistSuggestionCell to draw round border aroung textfield:

if let contentView = bsContentView as? ScientistSuggestionCellContentView {
    contentView.roundedView.layer.borderColor = UIColor(red: 233.0/255.0, green: 234.0/255.0, blue: 242.0/255.0, alpha: 1).cgColor
}

Run the project again. Here is what you’ll see if the code is correct:

Auto Complete Input Text Field Round Border
Auto Complete Input Text Field Round Border

At this time you’ve done with implement text input row. Now it’s time to move on to implement suggestion table container.

Implement suggestion table container

In Xcode Project Navigator, select folder ScientistSuggestionCell and 2 following files: ScientistSuggestionTableContainer.swift and ScientistSuggestionTableContainer.xib. These 2 files also contains UI layout for the table view container with IBOutlets already connected.

Open ScientistSuggestionTableContainer.swift and you’ll see the class is a subclass of SuggestionTableContainer, which is required by _SuggestionRowCustom to make it works.

In the interface builder, select the UITableView and connect it to IBOutlet with name tableView:

Connect UITableView IBOutlet

Run the project and here is a screenshot:

Custom Table Container

Focus on the red oval mark. The border and left right spacing of suggestion table container displays correctly, but the space with the text field is not. We need the table container to be close right to the text field, not the bottom of row. Also the border of suggestion table container must include the text field too. Let’s fix these problems.

First, we’re gonna make border of table container includes the text field. Open file ScientistSuggestionTableContainer.xib and change table container’s vertical space to superview equal to -46, which is text field height:

Change table container's vertical space
Change table container’s vertical space

Navigate to file ScientistSuggestionCell.swift, in method setup of class ScientistSuggestionCell add the following code:

suggestionViewYOffset = {
    return -8
}

suggestionViewYOffset is a property of EurekaSuggestionTableViewCell which allows us to change vertical space between suggestion table container and the bottom of text input row. Since the padding bottom between text field and the row is 8 so in the closure we return by 8.

Run the project and try typing some text, this time you’ll see the the suggestion table container is positioned correctly.

Suggestion table container is positioned correctly
Suggestion table container is positioned correctly

Did you see the bottom line of the text field is rounded? This is not good, we need it to be straight. To fix this we need to hide the rounded border of text field and show the straight line instead. In method setup of ScientistSuggestionCell add the following code:

tableViewContainer?.addObserver(self, forKeyPath: "hidden", options: .init(arrayLiteral: [.old, .new]), context: nil)

Add the following code below line var originTextColor: UIColor? :

var token: NSKeyValueObservation?

Then in method setup add:

token = tableViewContainer?.observe(\UIView.isHidden, options: .new, changeHandler: { [weak self] tableViewContainer, change in
    guard let contentView = self?.bsContentView as? ScientistSuggestionCellContentView,
          let isHidden = change.newValue else { return }
    contentView.separator.isHidden = isHidden
    contentView.roundedView.isHidden = !isHidden
})

Run the project and try again, now you’ll see each time suggestion list show the rounded border of text field is hidden and the straight bottom border is shown, just as we expected. But when we turn off keyboard and edit the textfield again, the position again becomes incorrect:

Suggestion table container position incorrect

It’s because error label’s height changes and push the suggestion table container down. We’ll fix this by change suggestionViewYOffset as follow:

suggestionViewYOffset = { [weak self] in
    guard let self = self else { return -8 }
    let errorHeight: CGFloat
    if let contentView = self.bsContentView as? ScientistSuggestionCellContentView {
        if contentView.errorContainer.isHidden == true {
            errorHeight = 0
        } else {
            errorHeight = contentView.errorContainer.systemLayoutSizeFitting(CGSize(width: self.bounds.width - 64, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height
        }
    } else {
        errorHeight = 0
    }
    return -8 - errorHeight
}

Run project and try again, you’ll see the suggestion table container position is displayed correctly.

Change the code set up form in class ViewController as follow:

let section = Section("Input suggestions")
for _ in 0..<15 {
    section
    <<< ScientistSuggestionRow<Scientist>() {row in
        row.title = "Scientist"
        row.maxSuggestionRows = 4
        row.placeholder = "Please search the scientist name"
        row.add(rule: RuleRequired())
        row.asyncFilterFunction = {[weak self] (text, completion) in
            guard let self = self else {
                completion([])
                return
            }
            completion(self.users.filter({ $0.firstName.lowercased().contains(text.lowercased()) }))
        }
    }.cellSetup({ (cell, row) in
        cell.height = { UITableView.automaticDimension }
    })
    .onRowValidationChanged({ (cell, row) in
        UIView.performWithoutAnimation { [weak self] in
            self?.tableView.performBatchUpdates({
                row.updateCell()
            }, completion: nil)
        }
    })
}

form +++ section

Run the project, pick a field close to the bottom of the screen, when editing the field try scrolling the form. You’ll see the keyboard is dismissed automatically which is not good.

Cannot scroll when show suggestion list
Cannot scroll when show suggestion list

To prevent this add the following code into class ViewController:

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    let beginDragging: Bool
    if
        let cell = tableView.findFirstResponder()?.formCell(),
        cell is BaseSuggestionTableCellType {
        beginDragging = false
    } else {
        beginDragging = true
    }
    
    if beginDragging == true {
        super.scrollViewWillBeginDragging(scrollView)
    }
}

Run the project and test, now you can scroll to suggestion list when editing.

Now there is another problem. Focus on the first row then scroll to the final row. On the toolbar which is on top of the keyboard tap the next button, you’ll see that focus row does not switch to the next focus, on some older ios version it may even crash.

It is because we’re at the bottom of the table view while the second row is not render on screen, therefore the second row is not available to be focused. So we’re gonna fix this by remove next and previous button of the toolbar. Add the following code to class ViewController:

override func inputAccessoryView(for row: BaseRow) -> UIView? {
    if row.baseCell is BaseSuggestionTableCellType {
        let navigation = NavigationAccessoryView(frame: .zero)
        let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        navigation.setItems([flexibleSpace, navigation.doneButton], animated: false)
        navigation.doneClosure = { [weak self] in
            self?.view.endEditing(true)
        }
        return navigation
    }
    return super.inputAccessoryView(for: row)
}

Run the project and try editing a row, you’ll see that the next and previous button is now not available anymore.

Implement suggestion table row content

Now we’re gonna set up the final component: suggestion table row content.

In Xcode Project Navigator, select folder ScientistSuggestionCell and the following file: ScientistSuggestionTableViewCellContentView.xib.

In the interface builder, select the root view and subclass it as SuggestionTableViewCellContentView.

Subclass root view to class SuggestionTableViewCellContentView
Subclass root view to class SuggestionTableViewCellContentView

Select the UILabel and connect it to IBOutlet with name textLabel:

Connect label to IBOutlet with name textLabel
Connect label to IBOutlet with name textLabel

Open file ScientistSuggestionCell.swift, in method init of class ScientistSuggestionRow add the following line:

tableViewCellContentProvider = ViewProvider<SuggestionTableViewCellContentView>(nibName: "ScientistSuggestionTableViewCellContentView", bundle: Bundle.main)

Add the following code to the method setup of class ScientistSuggestionCell:

suggestionTableViewCellHeight = { (indexPath) in
    return 46
}

This code is used to config table view cell height of each item in suggestion list.

Run the project and you’ll see the suggestion table row content has been replaced with corresponding layout in ScientistSuggestionTableViewCellContentView.xib.

Unsolved problem

If you come this far, you’ll have finished this tutorial. But there is one unsolvable problem on ios 14 caused by ios. I note here to let you beware of it.

Bug 1: Run the project, scroll to edit the final row then tap the first item of suggestion list. The following problems happen:

  • The text input row does not show the selected value.
  • The table view autoshink its contentInset.
  • You cannot scroll to the bottom edge of suggestion list.

Bug 2: Scroll to edit the final row then tap the final item of suggestion list, the text input row show not the final item but the first item of suggestion list.

Here is the gif of the bugs recorded:

Auto Complete Input Bug
Auto Complete Input Bug

These bugs occurred only in the final rows where there is not enough space for suggestion list and table view’s contentInset has to be extended. I have debug but the call stack print all code lies in operating system function. It seems to be a bug of ios on UITableView.

There is a workaround to fix this, but not perfectly, we can fix by adding header into table view such as:

let section = Section("Input suggestions") { section in
    var header = HeaderFooterView<UIView>(.class)
    header.height = {180}
    header.onSetupView = { view, _ in
        view.backgroundColor = .clear
        view.isUserInteractionEnabled = false
    }
    section.footer = header
}

With this table view’s contentInset does not need to be extended, thus make problem fixed, but the table view will have extra space at the bottom. This is currently an inevitable cost to make it work again.

Wraps up

Great job on going through this tutorial!

In this tutorial you have learnt how to build auto complete form input for ios using Eureka and SuggestionRow. In summary, to implement auto complete form input there are 3 main components need to be done including text input row, suggestion table container and suggestion table row content. I hope this tutorial will be helpful to you. If you have anything please let me know in the comment section.

Leave a Reply

Your email address will not be published. Required fields are marked *