-
-
Save cliss/d235173ff74df1f14424d349185a5787 to your computer and use it in GitHub Desktop.
UISearchController replacement. Relies on a couple of internal categories, helpers, etc.
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
// | |
// SearchController.swift | |
// Prefer | |
// | |
// Created by Bryan Irace on 5/31/17. | |
// Copyright © 2017 Prefer. All rights reserved. | |
// | |
import RxSwift | |
import RxCocoa | |
import SharedUI | |
protocol SearchResultsUpdater: class { | |
func updateSearchResults(for query: String?) | |
func returnedFromSearch(with query: String?) | |
} | |
/** | |
An alternative to `UISearchController`. `UISearchController` is inflexible and seems to have lots of bugs. So, reinvent | |
the wheel we shall. | |
*/ | |
final class SearchController { | |
// MARK: - Subviews | |
private lazy var dimmedView: UIView = DimmedView().usingAutoLayout() | |
// MARK: - Mutable | |
/** | |
Not passed into the initializer since in many cases, this will be the object that owns the search controller, | |
meaning the search controller will be lazily-instantiated. | |
*/ | |
weak var searchResultsUpdater: SearchResultsUpdater? | |
// MARK: - Inputs | |
private let searchResultsViewController: UIViewController | |
private unowned let textField: UITextField | |
private unowned let parentViewController: UIViewController | |
private unowned let containingScrollView: UIScrollView | |
// MARK: - Other Instance Variables | |
private let disposeBag = DisposeBag() | |
// MARK: - Initialization | |
/// Create a new instance | |
/// | |
/// - Parameters: | |
/// - searchResultsViewController: View controller to display results in | |
/// - textField: Text field used to drive searches | |
/// - parentViewController: View controller to be the parent of the search results view controller | |
/// - containingScrollView: Scroll view that the text field is contained by (e.g. a table that the tet field it is | |
/// the header of). Scrolling will be disabled when the results are being shown. | |
init( | |
searchResultsViewController: UIViewController, | |
textField: UITextField, | |
parentViewController: UIViewController, | |
containingScrollView: UIScrollView | |
) { | |
self.searchResultsViewController = searchResultsViewController | |
self.textField = textField | |
self.parentViewController = parentViewController | |
self.containingScrollView = containingScrollView | |
setUpObservers() | |
} | |
// MARK: - Private | |
private func setUpObservers() { | |
textField | |
.rx | |
.text | |
// Do you really need the closure here? I don't *think* so. -CML | |
.distinctUntilChanged({ (lhs, rhs) -> Bool in | |
lhs == rhs | |
}) | |
.subscribe(onNext: { [weak self] query in | |
self?.searchResultsUpdater?.updateSearchResults(for: query) | |
}) | |
.disposed(by: self.disposeBag) | |
Observable | |
// I'll allow it this time. But as usual, I'd advocate command/scan here. -CML | |
.combineLatest( | |
textField.rx.hasText, | |
textField.rx.isEditing | |
) | |
.map({ (hasQuery, isEditing) in | |
hasQuery || isEditing | |
}) | |
.distinctUntilChanged() | |
.subscribe(onNext: { [weak self] showResults in | |
if showResults { | |
self?.showSearchResultsViewController() | |
} | |
else { | |
self?.hideSearchResultsViewController() | |
} | |
}) | |
.disposed(by: self.disposeBag) | |
textField | |
.rx | |
.controlEvent(.editingDidEndOnExit) | |
.subscribe(onNext: { [weak self] in | |
self?.searchResultsUpdater?.returnedFromSearch(with: self?.textField.text) | |
}) | |
.disposed(by: self.disposeBag) | |
} | |
private func showSearchResultsViewController() { | |
containingScrollView.isScrollEnabled = false | |
parentViewController.view.addSubview(dimmedView) | |
pinToParent(dimmedView) | |
dimmedView.hideAndFadeIn() | |
parentViewController.addChildViewController(searchResultsViewController) | |
parentViewController.view.addSubview(searchResultsViewController.view) | |
searchResultsViewController.didMove(toParentViewController: parentViewController) | |
searchResultsViewController.view.translatesAutoresizingMaskIntoConstraints = false | |
pinToParent(searchResultsViewController.view) | |
} | |
private func pinToParent(_ view: UIView) { | |
NSLayoutConstraint.activate([ | |
view.topAnchor.constraint(equalTo: textField.bottomAnchor), | |
view.leftAnchor.constraint(equalTo: parentViewController.view.leftAnchor), | |
view.bottomAnchor.constraint(equalTo: parentViewController.bottomLayoutGuide.topAnchor), | |
view.rightAnchor.constraint(equalTo: parentViewController.view.rightAnchor), | |
]) | |
} | |
private func hideSearchResultsViewController() { | |
dimmedView.removeFromSuperview() | |
searchResultsViewController.willMove(toParentViewController: nil) | |
searchResultsViewController.view.removeFromSuperview() | |
searchResultsViewController.removeFromParentViewController() | |
containingScrollView.isScrollEnabled = true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment