New iOS developer here! I love the design of scrolling up to a hidden search bar. I want to duplicate it but a lot of the solutions seem outdated, so i am really confused.
iPhone: Hide UITableView search bar by default
Going off of this, it seems like there's 2 steps.
Add search bar to the scrollable table view
Add code so it scrolls to the second row on app load (in viewdidload?) using
yourTableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)
I can't get passed #1. I tried putting in a search bar in 3 spots...
A) If I put it here (on top of the header?) it sticks at the top as expected, but not scrollable.
B) If I put it here, it's still not scrollable and it blocks the first row
C) If I put it here, it repeats on the row, as expected.
Somehow I need to put it on the table without duplicating, so I'm confused on that.
So I think you have to do this in code. If you want to do it, I followed the tutorial here and it worked.
http://www.ioscreator.com/tutorials/add-search-table-view-tutorial-ios8-swift
Add UISearchResultsUpdating
class TableViewController: UITableViewController, UISearchResultsUpdating {
Add these properties...
let tableData = ["One","Two","Three","Twenty-One", "vasdf", "vasdfd", "3343", "ad23", "454d", "vasdf32", "va343", "vasdf3r2", "vasdfd2", "vasr52f", "awefwr32vwf"]
var filteredTableData = [String]()
var resultSearchController = UISearchController()
Add self.resultSearchController here...
override func viewDidLoad() {
super.viewDidLoad()
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
self.tableView.tableHeaderView = controller.searchBar
return controller
})()
// Reload the table
self.tableView.reloadData()
}
Take care of self.resultSearchController.active conditions in these functions...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// 2
if (self.resultSearchController.active) {
return self.filteredTableData.count
}
else {
return self.tableData.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
// 3
if (self.resultSearchController.active) {
cell.textLabel?.text = filteredTableData[indexPath.row]
return cell
}
else {
cell.textLabel?.text = tableData[indexPath.row]
return cell
}
}
Then do the search code here
func updateSearchResultsForSearchController(searchController: UISearchController)
{
filteredTableData.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text as String!)
let array = (tableData as NSArray).filteredArrayUsingPredicate(searchPredicate)
filteredTableData = array as! [String]
self.tableView.reloadData()
}
And I think that's it. Good luck!
Related
I know there's been questions like this before: here or here
But it hasn't gone into much detail, as much as I'd like anyway. I have this 'Explore' page screen sort of thing. It has a search bar at the top and I'd like to show content/posts in the middle section of it. But when the user clicks on the search bar, I want go to a tableview controller, replacing the content on the screen, to show the search results.
I had a tableviewcontroller embedded in the UIView container:
import UIKit
import Firebase
class SearchTableViewController: UITableViewController, UISearchResultsUpdating {
let searchController = UISearchController(searchResultsController: nil)
#IBOutlet var searchResultsTableView: UITableView!
var usersArray = [NSDictionary?]()
var filteredIsers = [NSDictionary?]()
var ref = Database.database().reference()
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
ref.child("users").child("public").queryOrdered(byChild: "username").observe(.childAdded, with: { (snapshot) in
if let user = snapshot.value as? [String:Any]{
let username = user["username"] as? String ?? ""
if(username == SpalshScreenViewController.UserData.username){
return
}else{
self.usersArray.append(snapshot.value as? NSDictionary)
}
}else{
return
}
print(self.usersArray)
// insert rows
self.searchResultsTableView.insertRows(at: [IndexPath(row: self.usersArray.count-1, section: 0)], with: UITableView.RowAnimation.automatic )
})
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
if searchController.isActive && searchController.searchBar.text != "" {
return filteredIsers.count
}else{
return self.usersArray.count
}
}
func updateSearchResults(for searchController: UISearchController) {
filterContent(searchText: self.searchController.searchBar.text!)
}
func filterContent(searchText: String){
self.filteredIsers = self.usersArray.filter{ user in
let username = user!["username"] as? String
return(username?.lowercased().contains(searchText.lowercased()))!
}
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let user : NSDictionary?
if searchController.isActive && searchController.searchBar.text != "" {
user = filteredIsers[indexPath.row]
}else{
user = self.usersArray[indexPath.row]
}
cell.textLabel?.text = user?["firstName"] as? String
cell.detailTextLabel?.text = user?["username"] as? String
let profilePicUrl = user?["profilePicURL"] as? String
let encodedurl = URL(string: profilePicUrl!)
print(profilePicUrl)
cell.imageView!.kf.setImage(with: encodedurl, placeholder: UIImage(named: "profile_pic"))
return cell
}
}
Would I have that code in it's separate UITableViewController and then present that view when the search bar is clicked or?
I know you'd change the let searchController = UISearchController(searchResultsController: nil) to something like let searchController = UISearchController(searchResultsController: tableViewController)
but I don't know how to tie everything in. In my searchViewController I have reference to the UISearchBar, I was thinking I'd add a tap gesture onto it and present a the tablecontrollerview when it's clicked but that would mean there would be an inconsistency in animation and search bar style?
EDIT:
So basically, I have a view controller, in that view controller I have a search bar at the top (doing nothing at the moment) and underneath that search bar will be a collection view full of different posts. When the user clicks on the search bar I'd like to replace that collection view with a table view with the results of the search result. How would I achieve that?
You should set up searchResultsController and searchResultsUpdater properties of the UISearchController class
Concerning UISearchBar object, you have to use the one which comes with UISearchController and add it programmatically to a view.
view.addSubview(searchController.searchBar)
Hey #nathan if you can please explain in more details than it would be better. Other wise on base of my current understanding. If you have search bar added in top of view and you have the table view in it also then you can update your table view in search bar delegate methods. I am sharing the search bar delegate methods for your help.
extension SearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let str = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
viewModel?.search(text: str)
tableView.reloadData()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let t = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines) {
if t.count > 0 {
viewModel?.addNewSearch(text: t)
tableView.reloadData()
}
}
searchBar.resignFirstResponder()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
viewModel?.cancel()
navigationController?.popViewController(animated: true)
} }
I have used MVVM in my code so ViewModel is basically used for data update and changes. Thanks you can ask if you have any confusion.
I am trying to get my table view to update when I add a new cell to my SQLite Table. However, as of now, when I add a new cell to the SQLite Table and call reloadData, nothing updates or happens. My UITableView is embedded into my Main View Controller so that might be part of the issue. I also feel as if this may be an issue with the tableView.dataSource or something. I am not sure. I am very new to swift.
I have tried calling the reloadData from the UITableViewController file and from the UIViewController which contains the button that is supposed to add data and then update the Table. Neither have worked. I also tried to substitute it with tableView?.beginUpdates() and tableView?.endUpdates()
My button code (part of button script):
#IBAction func addTask(_ sender: Any) {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mmZZZZZ"
let taskDateFormatted = dateFormatter.string(from: taskDate.date)
let newTask = Task(title: taskTitle.text!, description: descriptionTextView.text,
date: taskDateFormatted)
let instSQLiteData = SQLiteData()
let instTaskTableVC = TaskTableVC()
instSQLiteData.uploadData(newTask.title, newTask.description, newTask.date)
instTaskTableVC.taskTable?.reloadData()
dismiss(animated: true, completion: nil)
}
My UITableViewController script/class (part of tableview script):
class TaskTableVC : UITableViewController {
#IBOutlet var taskTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.taskTable.dataSource = self
self.taskTable.delegate = self
}
How my UITableViewController loads data (part of tableview script):
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let instSQLiteData = SQLiteData()
return instSQLiteData.getRowCount()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let instSQLiteData = SQLiteData()
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath)
cell.textLabel?.text = instSQLiteData.getTasksArray()[indexPath.row]
return cell
}
Once the button is pressed I just want the table to update but I cannot get it to work even though I have tried to follow a ton of other questions with the same topic. Much thanks!
I'm trying to combine a CollectionViewwith a TableView, so fare everything works except one problem, which I cant fix myself.
I have to load some data in the CollectionViews which are sorted with the header of the TableViewCell where the CollectionView is inside. For some reason, every time I start the app, the first three TableViewCells are identical. If I scroll a little bit vertically, they change to the right Data.
But it can also happen that while using it sometimes displays the same Data as in on TableViewCell another TableViewCell, here again the problem is solved if I scroll a little.
I think the problem are the reusableCells but I cant find the mistake myself. I tried to insert a colletionView.reloadData() and to set the cells to nil before reusing, sadly this didn`t work.
My TableViewController
import UIKit
import RealmSwift
import Alamofire
import SwiftyJSON
let myGroupLive = DispatchGroup()
let myGroupCommunity = DispatchGroup()
var channelTitle=""
class HomeVTwoTableViewController: UITableViewController {
var headers = ["LIVE","Channel1", "Channel2", "Channel3", "Channel4", "Channel5", "Channel6"]
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.navigationBar.isTranslucent = false
DataController().fetchDataLive(mode: "get")
DataController().fetchDataCommunity(mode: "get")
}
//MARK: Custom Tableview Headers
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return headers[section]
}
//MARK: DataSource Methods
override func numberOfSections(in tableView: UITableView) -> Int {
return headers.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
//Choosing the responsible PrototypCell for the Sections
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellBig", for: indexPath) as! HomeVTwoTableViewCell
print("TableViewreloadMain")
cell.collectionView.reloadData()
return cell
}
else if indexPath.section >= 1 {
// getting header Titel for reuse in cell
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
else {
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
}
}
}
My TableViewCell with `CollectionView
import UIKit
import RealmSwift
var communities: Results<Community>?
class HomeVTwoTableViewCellSmall: UITableViewCell{
//serves as a translator from ChannelName to the ChannelId
var channelOverview: [String:String] = ["Channel1": "399", "Channel2": "401", "Channel3": "360", "Channel4": "322", "Channel5": "385", "Channel6": "4"]
//Initiaize the CellChannel Container
var cellChannel: Results<Community>!
//Initialize the translated ChannelId
var channelId: String = ""
#IBOutlet weak var collectionView: UICollectionView!
}
extension HomeVTwoTableViewCellSmall: UICollectionViewDataSource,UICollectionViewDelegate {
//MARK: Datasource Methods
func numberOfSections(in collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return (cellChannel.count)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCellSmall", for: indexPath) as? HomeVTwoCollectionViewCellSmall else
{
fatalError("Cell has wrong type")
}
//removes the old image and Titel
cell.imageView.image = nil
cell.titleLbl.text = nil
//inserting the channel specific data
let url : String = (cellChannel[indexPath.row].pictureId)
let name :String = (cellChannel[indexPath.row].communityName)
cell.titleLbl.text = name
cell.imageView.downloadedFrom(link :"link")
return cell
}
//MARK: Delegate Methods
override func layoutSubviews() {
myGroupCommunity.notify(queue: DispatchQueue.main, execute: {
let realm = try! Realm()
//Getting the ChannelId from Dictionary
self.channelId = self.channelOverview[channelTitle]!
//load data from Realm into variables
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.dataSource = self
self.collectionView.delegate = self
print("collectionView layout Subviews")
self.collectionView.reloadData()
})
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCommunity = (cellChannel[indexPath.row].communityId)
let home = HomeViewController()
home.showCommunityDetail()
}
}
Thanks in advance.
tl;dr make channelTitle a variable on your cell and not a global variable. Also, clear it, and your other cell variables, on prepareForReuse
I may be mistaken here, but are you setting the channelTitle on the cells once you create them? As I see it, in your viewController you create cells based on your headers, and for each cell you set TableViewController's channelTitle to be the title at the given section.
If this is the case, then the TableViewCell actually isn't receiving any information about what it should be loading before you call reloadData().
In general, I would also recommend implementing prepareForReuse in your HomeVTwoTableViewCellSmall, since it will give you a chance to clean up any stale data. Likely you would want to do something like set cellChannel and channelId to empty strings or nil in that method, so when the cell is reused that old data is sticking around.
ALSO, I just reread the cell code you have, and it looks like you're doing some critical initial cell setup in layoutSubviews. That method is going to be potentially called a lot, but you really only need it to be called once (for the majority of what it does). Try this out:
override the init with reuse identifier on the cell
in that init, add self.collectionView.dataSource = self and self.collectionView.delegate = self
add a didSet on channelTitle
set channelTitle in the viewController
So the code would look like:
var channelTitle: String = "" {
didSet {
self.channelId = self.channelOverview[channelTitle]!
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.reloadData()
}
}
This way you're only reloading your data when the cell is updated with a new channel, rather than every layout of the cell's views.
Sorry... one more addition. I wasn't aware of how your channelTitle was actually being passed. As I see it, you're using channelTitle as a global variable rather than a local one. Don't do that! remove channelTitle from where it is currently before implementing the code above. You'll see some errors, because you're setting it in the ViewController and accessing it in the cell. What you want is to set the channelTitle on the cell from the ViewController (as I outlined above). That also explains why you were seeing the same data across all three cells. Basically you had set only ONE channelTitle and all three cells were looking to that global value to fetch their data.
Hope that helps a little!
(also, you should be able to remove your else if block in the cellForRowAtIndexPath method, since the else block that follows it covers the same code. You can also delete your viewDidLoad, since it isn't doing anything, and you should, as a rule, see if you can get rid of any !'s because they're unsafe. Use ? or guard or if let instead)
I have in my iOS app a mapView, and a UISearchBar to search locations and center the user on it. The results of my search are made by autocompletion and displayed in a tableView.
I had trouble with it, when i clicked on cancel, it displayed me all of my datas in the tableView.
So I implemented this function:
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
searchTableView.hidden = true
}
But the render of that function makes this:
I don't know (and don't know how to see) if my mapView is still under the black screen.
By the way, if I delete my cancel button function, and search something, if I click on Cancel it will display something like this:
To hide my tableView again, I have to make another search, than delete it with the little cross button, then cancel.
My code, of the autocompletion search, is like that:
func initTableView() {
searchTableView.delegate = self
searchTableView.dataSource = self
searchTableView.scrollEnabled = true
searchTableView.hidden = true
}
func searchBarSearchButtonClicked() {
searchTableView.hidden = false
searchBar.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
searchBar(searchBar, textDidChange: searchBar.text!)
}
func searchBar(_searchBar: UISearchBar, textDidChange searchText: String) {
searchTableView.reloadData()
searchAutocompleteEntriesWithSubstring(searchText)
}
func searchAutocompleteEntriesWithSubstring(substring: String) {
searchBar.filtredDatas.removeAll(keepCapacity: false)
for curString in searchBar.datas {
let myString:NSString! = curString.title! as NSString
let substringRange :NSRange! = myString.rangeOfString(substring)
if (substringRange.location == 0) {
searchBar.filtredDatas.append(curString)
}
}
}
This is from my ViewController.swift file.
My searchBar variable is a custom class that contains an NSArray of datas and another of fileterDatas.
#IBOutlet weak var mapView: UGOMapController!
#IBOutlet var searchTableView: UITableView!
#IBOutlet weak var searchBar: UGOSearchBar!
Thanks for your help.
If you need more of my code, I can edit this post.
Edit:
Here are my UITableView functions, used for searchTableView
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchBar.filtredDatas.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let autoCompleteRowIdentifier = "AutoComplete"
var cell = tableView.dequeueReusableCellWithIdentifier(autoCompleteRowIdentifier) as UITableViewCell!
if cell != nil {
let index = indexPath.row as Int
if (searchBar.filtredDatas.count > 0) {
cell!.textLabel!.text = searchBar.filtredDatas[index].title
}
}
else {
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: autoCompleteRowIdentifier)
}
return cell!
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedCell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
searchBar.text = selectedCell.textLabel!.text
}
And my hierarchy:
And my viewController inspector:
You have 3 different outlets connected to the table view.
It means that when you hide the tableView, you also hide the view, because it's connected to the same Search Table View
You have to make sure that you connect your table view just to one outlet.
Edit
To be more precise:
You have to change your view controller from UITableViewController to UIViewController.
The view outlet will be connected to viewController's view property.
The tableView outlet will be removed, because in a normal UIViewController this property doesn't exist. (It's defined in UITableViewController)
SearchTableView should be connected to the searchTableView property that you define in your view controller.
I am trying to use UISearchController however I confronted with retain issue that I can't solve. MainTableview has two sections.
Section 1
Filtered Data based on some Regex
Section 2
All Data
I added UISearchController to my tableview and attached ResultsTableController as resultsTableController. It works when user search something, ResultsTableController comes forward and because I set tableview delegate to self, selecting item from ResultsTableController calls didSelectRowAtIndexPath in my MainTableViewController. However I have allocation issue if user selects something from resultsTableController.
Following happens for different scenarios
User doesn't search anything, just selects an item from
MainTableview, I see deinit messages
User searches something, cancel the search, select item from
MainTableview, I see deinit messages
User searches something, and selects an item from
ResultsTableController, I don't get deinit in my viewcontrollers
MainTableViewController.swift
var searchController: UISearchController!
// Secondary search results table view.
var resultsTableController: ResultsTableController!
var allCompanies = ["Data1","Data2","Data3"]
override func viewDidLoad() {
super.viewDidLoad()
resultsTableController = ResultsTableController()
// We want to be the delegate for our filtered table so didSelectRowAtIndexPath(_:) is called for both tables.
resultsTableController.tableView.delegate = self
searchController = UISearchController(searchResultsController: resultsTableController)
searchController.searchResultsUpdater = self
searchController.searchBar.sizeToFit()
tableView.tableHeaderView = searchController.searchBar
searchController.delegate = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.delegate = self
definesPresentationContext = true
}
}
// MARK: UISearchBarDelegate
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
// MARK: UISearchResultsUpdating
func updateSearchResultsForSearchController(searchController: UISearchController) {
// Update the filtered array based on the search text.
let filteredResults = allCompanies.filter({ company in
(company.lowercaseString as NSString).containsString(searchController.searchBar.text.lowercaseString)
})
// Hand over the filtered results to our search results table.
let resultsController = searchController.searchResultsController as! ResultsTableController
resultsController.searchResult = filteredResults
resultsController.tableView.reloadData()
}
// usual tableview methods
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if resultsTableController.searchResult.count > 0 {
selectedCompany = resultsTableController.searchResult[index]
//do something with selected company
navigationController?.popViewControllerAnimated(true)
return
}
//
selectedCompany = allCompanies[index]
navigationController?.popViewControllerAnimated(true)
}
deinit {
println("MainTableView deinit")
}
ResultTableController.swift
class ResultsTableController:UITableViewController {
var searchResult = [String]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResult.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
let index = indexPath.row
cell.textLabel?.font = UIFont(name: "Avenir-Roman", size: 16)
cell.textLabel?.text = searchResult[index].description
return cell
}
deinit {
println("ResultTableController deinit")
}
}
Hey there I ran into the issue today
apparently I need to force the dismiss of the searchController to work around the retain issue
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
searchController?.dismissViewControllerAnimated(false, completion: nil)
}
here is my sample project
https://www.dropbox.com/s/zzs0m4n9maxd2u5/TestSearch.zip?dl=0
The solution does seem to be to call dismissViewControllerAnimated on the UISearchController at some point. Most people probably don't do that since the UISearchController is somewhat of an implementation detail related to your view controller that is hosting the UISearchController.
My solution, which seems to work no matter how you present your search UI (standard present or in a popover) is to call searchController.dismissViewControllerAnimated() from your host's viewDidDisappear, after checking to see if the view controller is no longer being presented. This catches all cases, especially the popover case where the user taps outside the popover to automatically dismiss the UI, or the case where the search UI is disappearing simply because you pushed something else onto the navigation stack. In the latter case, you don't want to dismiss the UISearchController.
override func viewDidDisappear(animated: Bool)
{
super.viewDidDisappear(animated)
if presentingViewController == nil
{
searchController.dismissViewControllerAnimated(false, completion: nil)
}
}