Last active
August 24, 2016 06:58
-
-
Save phlippieb/3be2b00104d8cd86304badf20b45cfbe to your computer and use it in GitHub Desktop.
A swift autocomplete solution. I needed this to subclass a specific textview class (from the Material pod, if you're interested), but it should work fine with UITextView. I also used RxSwift to capture text changes, which can probably be done with the text view's delegate methods, though it might be harder.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AutoCompleteTextField.swift | |
// | |
// Created by Phlippie Bosman on 2016/08/23. | |
// Copyright © 2016 Kalido. All rights reserved. | |
// | |
import Material | |
import RxSwift | |
@objc protocol AutoCompleteProvider { | |
// for async fetching | |
func provideSuggestionsForAutoCompleteTextField(textField: AutoCompleteTextField, forString string: String, toCallback callback: ([String]) -> Void) | |
// for dismissing async fetch | |
optional func disposeFetchForAutoCompleteTextField(textField: AutoCompleteTextField) | |
} | |
class AutoCompleteTextField: ErrorTextField, UITableViewDelegate, UITableViewDataSource { | |
// public interface | |
var autocompleteDelay: NSTimeInterval = 0.5 | |
var autoCompleteProvider: AutoCompleteProvider? | |
var autoCompleteFont: UIFont = RobotoFont.lightWithSize(12) | |
var autoCompleteTextColor: UIColor = MaterialColor.black | |
var autoCompleteBackgroundColor: UIColor = MaterialColor.white | |
var autoCompleteSeparatorsEnabled: Bool = false | |
var autoCompleteMaxHeight: CGFloat = 500 | |
// b/c my tableview got stuck behind other subviews of this textview's superview | |
// set this to the textview's superview (might work with getter property?) | |
var viewToBringAutoCompleteToFront: UIView? | |
// ui | |
private let completionsTableView = UITableView(forAutoLayout: ()) | |
private var tableViewHeightConstraint: NSLayoutConstraint? | |
// data | |
private var autoCompleteSuggestionStrings: [String] = [] | |
private let reuseIdentifier = "cell" | |
private let disposeBag = DisposeBag() | |
// call this! in like viewDidLoad or something | |
func setupAutoComplete() { | |
completionsTableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier) | |
completionsTableView.separatorStyle = autoCompleteSeparatorsEnabled ? .SingleLine : .None | |
completionsTableView.backgroundColor = autoCompleteBackgroundColor | |
completionsTableView.delegate = self | |
completionsTableView.dataSource = self | |
self.rx_text | |
.skip(1) // an event always fires at startup and it is useless | |
.doOnNext { [weak self] (text) in | |
if text == "" { | |
self?.dismissAutoCompleteSuggestions() | |
} | |
if let this = self, | |
let provider = this.autoCompleteProvider, | |
let dispose = provider.disposeFetchForAutoCompleteTextField { | |
dispose(this) | |
} | |
} | |
.throttle(autocompleteDelay, scheduler: MainScheduler.instance) | |
.subscribeNext { [weak self] (text) in | |
if text == "" { | |
self?.dismissAutoCompleteSuggestions() | |
} else if let this = self | |
where this.isFirstResponder(), | |
let provider = this.autoCompleteProvider { | |
provider.provideSuggestionsForAutoCompleteTextField(this, forString: text, toCallback: this.presentAutoCompleteSuggestions) | |
} | |
} | |
.addDisposableTo(disposeBag) | |
addSubview(completionsTableView) | |
tableViewHeightConstraint = completionsTableView.autoSetDimension(.Height, toSize: 20) | |
completionsTableView.autoMatchDimension(.Width, toDimension: .Width, ofView: self) | |
completionsTableView.autoPinEdge(.Top, toEdge: .Bottom, ofView: self, withOffset: 8) | |
completionsTableView.autoPinEdgeToSuperviewEdge(.Left, withInset: 0) | |
completionsTableView.userInteractionEnabled = true | |
adjustTableviewHeight() | |
} | |
func numberOfSectionsInTableView(tableView: UITableView) -> Int { | |
return 1 | |
} | |
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return autoCompleteSuggestionStrings.count | |
} | |
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { | |
let cell = UITableViewCell(style: .Default, reuseIdentifier: reuseIdentifier) | |
cell.backgroundColor = autoCompleteBackgroundColor | |
let suggestion = autoCompleteSuggestionStrings[indexPath.row] | |
if let label = cell.textLabel { | |
label.text = suggestion | |
label.font = autoCompleteFont | |
label.textColor = autoCompleteTextColor | |
} | |
cell.addGestureRecognizer( | |
UITapGestureRecognizer(target: self, action: #selector(didSelectSuggestion(_:)))) | |
cell.userInteractionEnabled = true | |
return cell | |
} | |
func presentAutoCompleteSuggestions(suggestions: [String]) { | |
autoCompleteSuggestionStrings = suggestions | |
completionsTableView.reloadData() | |
adjustTableviewHeight() | |
viewToBringAutoCompleteToFront?.bringSubviewToFront(self) | |
} | |
private func adjustTableviewHeight() { | |
if let heightConstraint = tableViewHeightConstraint { | |
let h = min(completionsTableView.contentSize.height, autoCompleteMaxHeight) | |
UIView.animateWithDuration(0.3) { | |
heightConstraint.constant = h | |
self.setNeedsUpdateConstraints() | |
} | |
} | |
} | |
func didSelectSuggestion(sender: AnyObject!) { | |
guard let tap = sender as? UIGestureRecognizer, | |
let cell = tap.view as? UITableViewCell, | |
let cellLabel = cell.textLabel, | |
let suggestion = cellLabel.text | |
else { return } | |
text = suggestion | |
dismissAutoCompleteSuggestions() | |
} | |
func dismissAutoCompleteSuggestions() { | |
autoCompleteSuggestionStrings = [] | |
completionsTableView.reloadData() | |
adjustTableviewHeight() | |
} | |
/* | |
This allows the suggestion cells to capture touch events | |
even though the table view is entirely outside the text view's frame | |
*/ | |
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { | |
return super.pointInside(point, withEvent: event) || | |
completionsTableView.pointInside( | |
convertPoint(point, toView: completionsTableView), | |
withEvent: event) | |
} | |
override func resignFirstResponder() -> Bool { | |
dismissAutoCompleteSuggestions() | |
return super.resignFirstResponder() | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Pods used:
Material
RxSwift