From fb8ce0a0ea3e2b2dead130cdd7edf7eba717fb4c Mon Sep 17 00:00:00 2001 From: Gray Campbell <12163070+graycampbell@users.noreply.github.com> Date: Fri, 29 Sep 2017 16:53:36 -0500 Subject: [PATCH 1/4] Add search bar --- .../GCCountryPickerViewController.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/GCCountryPicker/GCCountryPickerViewController.swift b/GCCountryPicker/GCCountryPickerViewController.swift index 5abae28..d674ce6 100644 --- a/GCCountryPicker/GCCountryPickerViewController.swift +++ b/GCCountryPicker/GCCountryPickerViewController.swift @@ -16,6 +16,7 @@ public final class GCCountryPickerViewController: UITableViewController { public var delegate: GCCountryPickerDelegate? fileprivate var countries = [GCCountry]() + fileprivate var searchController: UISearchController! // MARK: Initializers @@ -81,6 +82,15 @@ extension GCCountryPickerViewController { fileprivate func configureNavigationBar() { self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancel(barButtonItem:))) + + self.searchController = UISearchController(searchResultsController: nil) + + self.searchController.searchResultsUpdater = self + + self.navigationItem.searchController = self.searchController + self.navigationItem.hidesSearchBarWhenScrolling = false + + self.definesPresentationContext = true } // MARK: Targets @@ -91,6 +101,14 @@ extension GCCountryPickerViewController { } } +extension GCCountryPickerViewController: UISearchResultsUpdating { + + public func updateSearchResults(for searchController: UISearchController) { + + + } +} + // MARK: - Table View extension GCCountryPickerViewController { From f9e9e295e690149c51aa20a29ad800d5c47a03a4 Mon Sep 17 00:00:00 2001 From: Gray Campbell <12163070+graycampbell@users.noreply.github.com> Date: Sun, 1 Oct 2017 19:12:46 -0500 Subject: [PATCH 2/4] Add search functionality, update docs, and refactor --- GCCountryPicker.xcodeproj/project.pbxproj | 8 ++ GCCountryPicker/GCCountryPicker.h | 2 - GCCountryPicker/GCCountryPickerDelegate.swift | 4 +- .../GCCountryPickerViewController.swift | 62 +++++++----- .../GCSearchResultsController.swift | 97 +++++++++++++++++++ GCCountryPicker/GCSearchResultsDelegate.swift | 20 ++++ GCCountryPickerDemo/ViewController.swift | 5 +- 7 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 GCCountryPicker/GCSearchResultsController.swift create mode 100644 GCCountryPicker/GCSearchResultsDelegate.swift diff --git a/GCCountryPicker.xcodeproj/project.pbxproj b/GCCountryPicker.xcodeproj/project.pbxproj index a5b5874..183d354 100644 --- a/GCCountryPicker.xcodeproj/project.pbxproj +++ b/GCCountryPicker.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ E2644B401F7D92DD0034EA06 /* GCCountryPicker.h in Headers */ = {isa = PBXBuildFile; fileRef = E2644B3E1F7D92DD0034EA06 /* GCCountryPicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E27C15561F81955600DA9091 /* GCSearchResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27C15551F81955600DA9091 /* GCSearchResultsController.swift */; }; + E27C15581F81963600DA9091 /* GCSearchResultsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27C15571F81963600DA9091 /* GCSearchResultsDelegate.swift */; }; E2F9BFAE1F7D959A00131AF8 /* GCCountryPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F9BFAD1F7D959A00131AF8 /* GCCountryPickerViewController.swift */; }; E2F9BFB11F7D97D000131AF8 /* GCCountryPickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F9BFB01F7D97D000131AF8 /* GCCountryPickerDelegate.swift */; }; E2F9BFB51F7DA08900131AF8 /* GCCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F9BFB41F7DA08900131AF8 /* GCCountry.swift */; }; @@ -18,6 +20,8 @@ E2644B3B1F7D92DD0034EA06 /* GCCountryPicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GCCountryPicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E2644B3E1F7D92DD0034EA06 /* GCCountryPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GCCountryPicker.h; sourceTree = ""; }; E2644B3F1F7D92DD0034EA06 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E27C15551F81955600DA9091 /* GCSearchResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCSearchResultsController.swift; sourceTree = ""; }; + E27C15571F81963600DA9091 /* GCSearchResultsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCSearchResultsDelegate.swift; sourceTree = ""; }; E2F9BFAD1F7D959A00131AF8 /* GCCountryPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCCountryPickerViewController.swift; sourceTree = ""; }; E2F9BFB01F7D97D000131AF8 /* GCCountryPickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCCountryPickerDelegate.swift; sourceTree = ""; }; E2F9BFB41F7DA08900131AF8 /* GCCountry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCCountry.swift; sourceTree = ""; }; @@ -58,6 +62,8 @@ E2F9BFB41F7DA08900131AF8 /* GCCountry.swift */, E2F9BFB01F7D97D000131AF8 /* GCCountryPickerDelegate.swift */, E2F9BFAD1F7D959A00131AF8 /* GCCountryPickerViewController.swift */, + E27C15571F81963600DA9091 /* GCSearchResultsDelegate.swift */, + E27C15551F81955600DA9091 /* GCSearchResultsController.swift */, E2F9BFB61F7DA32500131AF8 /* CountryCodes.plist */, E2644B3F1F7D92DD0034EA06 /* Info.plist */, ); @@ -145,9 +151,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E27C15561F81955600DA9091 /* GCSearchResultsController.swift in Sources */, E2F9BFAE1F7D959A00131AF8 /* GCCountryPickerViewController.swift in Sources */, E2F9BFB11F7D97D000131AF8 /* GCCountryPickerDelegate.swift in Sources */, E2F9BFB51F7DA08900131AF8 /* GCCountry.swift in Sources */, + E27C15581F81963600DA9091 /* GCSearchResultsDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GCCountryPicker/GCCountryPicker.h b/GCCountryPicker/GCCountryPicker.h index 66ce496..fcc904c 100644 --- a/GCCountryPicker/GCCountryPicker.h +++ b/GCCountryPicker/GCCountryPicker.h @@ -14,5 +14,3 @@ FOUNDATION_EXPORT double GCCountryPickerVersionNumber; FOUNDATION_EXPORT const unsigned char GCCountryPickerVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/GCCountryPicker/GCCountryPickerDelegate.swift b/GCCountryPicker/GCCountryPickerDelegate.swift index de80332..2e8f14f 100644 --- a/GCCountryPicker/GCCountryPickerDelegate.swift +++ b/GCCountryPicker/GCCountryPickerDelegate.swift @@ -7,7 +7,7 @@ import UIKit -/// The delegate of a GCCountryPickerViewController object must adopt the GCCountryPickerDelegate protocol. The protocol's optional methods allow the delegate to handle country selection and customize the picker's appearance. +/// The delegate of a GCCountryPickerViewController object must adopt the GCCountryPickerDelegate protocol. public protocol GCCountryPickerDelegate { @@ -26,7 +26,7 @@ public protocol GCCountryPickerDelegate { /// --- /// /// Your delegate’s implementation of this method should pass the country on to any custom code that needs it, and then it should dismiss the picker view. - + /// /// - Parameter countryPicker: The controller object managing the country picker interface. /// - Parameter country: The country selected by the user. diff --git a/GCCountryPicker/GCCountryPickerViewController.swift b/GCCountryPicker/GCCountryPickerViewController.swift index d674ce6..b57ca63 100644 --- a/GCCountryPicker/GCCountryPickerViewController.swift +++ b/GCCountryPicker/GCCountryPickerViewController.swift @@ -9,38 +9,37 @@ import UIKit // MARK: Properties & Initializers +/// The GCCountryPickerViewController class defines a view controller containing a country picker interface. + public final class GCCountryPickerViewController: UITableViewController { // MARK: Properties + /// The object that acts as the delegate of the country picker view controller. + public var delegate: GCCountryPickerDelegate? fileprivate var countries = [GCCountry]() fileprivate var searchController: UISearchController! + fileprivate var searchResultsController = GCSearchResultsController() // MARK: Initializers - public required init?(coder aDecoder: NSCoder) { - - super.init(coder: aDecoder) - } - - public init(navigationTitle: String) { - - super.init(style: .plain) - - self.navigationItem.title = navigationTitle - } + /// Initializes and returns a newly allocated country picker view controller object. + /// + /// - Returns: An initialized country picker view controller object. public convenience init() { - self.init(navigationTitle: "Country") + self.init(style: .plain) + + self.navigationItem.title = "Country" } } // MARK: - View -public extension GCCountryPickerViewController { +extension GCCountryPickerViewController { public override func viewDidLoad() { @@ -83,9 +82,11 @@ extension GCCountryPickerViewController { self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancel(barButtonItem:))) - self.searchController = UISearchController(searchResultsController: nil) + self.searchController = UISearchController(searchResultsController: self.searchResultsController) self.searchController.searchResultsUpdater = self + self.searchController.searchBar.delegate = self.searchResultsController + self.searchResultsController.delegate = self self.navigationItem.searchController = self.searchController self.navigationItem.hidesSearchBarWhenScrolling = false @@ -93,19 +94,39 @@ extension GCCountryPickerViewController { self.definesPresentationContext = true } - // MARK: Targets - @objc func cancel(barButtonItem: UIBarButtonItem) { self.delegate?.countryPickerDidCancel(self) } } +// MARK: - UISearchResultsUpdating + extension GCCountryPickerViewController: UISearchResultsUpdating { public func updateSearchResults(for searchController: UISearchController) { + var searchResults = [GCCountry]() + if let searchText = searchController.searchBar.text?.localizedLowercase.replacingOccurrences(of: "(^\\s+)|(\\s+$)", with: "", options: .regularExpression, range: nil) { + + if !searchText.isEmpty { + + searchResults = self.countries.filter { $0.localizedDisplayName.localizedLowercase.hasPrefix(searchText) } + } + } + + self.searchResultsController.searchResults = searchResults + } +} + +// MARK: - GCSearchResultsDelegate + +extension GCCountryPickerViewController: GCSearchResultsDelegate { + + func searchResultsController(_ searchResultsController: GCSearchResultsController, didSelectSearchResult searchResult: GCCountry) { + + self.delegate?.countryPicker(self, didSelectCountry: searchResult) } } @@ -121,12 +142,7 @@ extension GCCountryPickerViewController { // MARK: - UITableViewDelegate -public extension GCCountryPickerViewController { - - public override func numberOfSections(in tableView: UITableView) -> Int { - - return 1 - } +extension GCCountryPickerViewController { public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -141,7 +157,7 @@ public extension GCCountryPickerViewController { // MARK: - UITableViewDataSource -public extension GCCountryPickerViewController { +extension GCCountryPickerViewController { public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { diff --git a/GCCountryPicker/GCSearchResultsController.swift b/GCCountryPicker/GCSearchResultsController.swift new file mode 100644 index 0000000..e2ff18c --- /dev/null +++ b/GCCountryPicker/GCSearchResultsController.swift @@ -0,0 +1,97 @@ +// +// GCSearchResultsController.swift +// GCCountryPicker +// +// Created by Gray Campbell on 10/1/17. +// + +import UIKit + +// MARK: Properties & Initializers + +/// The GCSearchResultsController class defines a view controller containing a search results interface. + +class GCSearchResultsController: UITableViewController { + + // MARK: Properties + + /// The object that acts as the delegate of the search results view controller. + + var delegate: GCSearchResultsDelegate? + + var searchResults = [GCCountry]() { + + didSet { + + self.tableView.reloadData() + } + } +} + +// MARK: - View + +extension GCSearchResultsController { + + override func viewDidLoad() { + + super.viewDidLoad() + + self.configureTableView() + } +} + +// MARK: - UISearchBarDelegate + +extension GCSearchResultsController: UISearchBarDelegate { + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + + if !self.searchResults.isEmpty { + + let rect = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 0.5) + + self.tableView.scrollRectToVisible(rect, animated: true) + } + } +} + +// MARK: - Table View + +extension GCSearchResultsController { + + fileprivate func configureTableView() { + + self.tableView.keyboardDismissMode = .onDrag + + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell") + } +} + +// MARK: - UITableViewDelegate + +extension GCSearchResultsController { + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + return self.searchResults.count + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + self.delegate?.searchResultsController(self, didSelectSearchResult: self.searchResults[indexPath.row]) + } +} + +// MARK: - UITableViewDataSource + +extension GCSearchResultsController { + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) + + cell.textLabel?.text = self.searchResults[indexPath.row].localizedDisplayName + + return cell + } +} diff --git a/GCCountryPicker/GCSearchResultsDelegate.swift b/GCCountryPicker/GCSearchResultsDelegate.swift new file mode 100644 index 0000000..200458d --- /dev/null +++ b/GCCountryPicker/GCSearchResultsDelegate.swift @@ -0,0 +1,20 @@ +// +// GCSearchResultsDelegate.swift +// GCCountryPicker +// +// Created by Gray Campbell on 10/1/17. +// + +import UIKit + +/// The delegate of a GCSearchResultsController object must adopt the GCSearchResultsDelegate protocol. + +protocol GCSearchResultsDelegate { + + /// Tells the delegate that the user picked a search result. + /// + /// - Parameter searchResultsController: The controller object managing the search results interface. + /// - Parameter searchResult: The search result selected by the user. + + func searchResultsController(_ searchResultsController: GCSearchResultsController, didSelectSearchResult searchResult: GCCountry) +} diff --git a/GCCountryPickerDemo/ViewController.swift b/GCCountryPickerDemo/ViewController.swift index 364f328..84cbacd 100644 --- a/GCCountryPickerDemo/ViewController.swift +++ b/GCCountryPickerDemo/ViewController.swift @@ -12,7 +12,9 @@ import GCCountryPicker class ViewController: UIViewController { + // MARK: Properties + fileprivate var country: GCCountry! } // MARK: - View @@ -37,8 +39,6 @@ extension ViewController { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Change Country", style: .plain, target: self, action: #selector(showCountryPicker(barButtonItem:))) } - // MARK: Targets - @objc func showCountryPicker(barButtonItem: UIBarButtonItem) { let countryPickerViewController = GCCountryPickerViewController() @@ -62,6 +62,7 @@ extension ViewController: GCCountryPickerDelegate { func countryPicker(_ countryPicker: GCCountryPickerViewController, didSelectCountry country: GCCountry) { + self.country = country self.navigationItem.title = country.localizedDisplayName self.dismiss(animated: true, completion: nil) From 971fb5ac422c23b38ae3b9a56dc4f4612884b48f Mon Sep 17 00:00:00 2001 From: Gray Campbell <12163070+graycampbell@users.noreply.github.com> Date: Sun, 1 Oct 2017 19:43:04 -0500 Subject: [PATCH 3/4] Refactor --- GCCountryPickerDemo/ViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GCCountryPickerDemo/ViewController.swift b/GCCountryPickerDemo/ViewController.swift index 84cbacd..ae01140 100644 --- a/GCCountryPickerDemo/ViewController.swift +++ b/GCCountryPickerDemo/ViewController.swift @@ -36,7 +36,7 @@ extension ViewController { fileprivate func configureNavigationBar() { self.navigationItem.title = Locale.current.localizedString(forRegionCode: "US") - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Change Country", style: .plain, target: self, action: #selector(showCountryPicker(barButtonItem:))) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(self.showCountryPicker(barButtonItem:))) } @objc func showCountryPicker(barButtonItem: UIBarButtonItem) { From 301b6d24501de9e17d89cf6d9808e81b34184f1e Mon Sep 17 00:00:00 2001 From: Gray Campbell <12163070+graycampbell@users.noreply.github.com> Date: Sun, 1 Oct 2017 20:20:14 -0500 Subject: [PATCH 4/4] Improve searching --- GCCountryPicker/GCCountryPickerViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GCCountryPicker/GCCountryPickerViewController.swift b/GCCountryPicker/GCCountryPickerViewController.swift index b57ca63..60e0211 100644 --- a/GCCountryPicker/GCCountryPickerViewController.swift +++ b/GCCountryPicker/GCCountryPickerViewController.swift @@ -112,7 +112,7 @@ extension GCCountryPickerViewController: UISearchResultsUpdating { if !searchText.isEmpty { - searchResults = self.countries.filter { $0.localizedDisplayName.localizedLowercase.hasPrefix(searchText) } + searchResults = self.countries.filter { $0.localizedDisplayName.localizedLowercase.range(of: "\\b\(searchText)", options: .regularExpression, range: nil, locale: .current) != nil } } }