In my app, I need to create a Search Suggestions interface -- very similar to Google search (it starts displaying suggestions as you type in the search field).
I did it with a UISearchController with the search bar in navigation bar, set it up like this:
// setup search controller
searchController = UISearchController(searchResultsController: searchSuggestionsController)
self.searchController.searchResultsUpdater = searchSuggestionsController
self.searchController.hidesNavigationBarDuringPresentation = false
self.navigationItem.titleView = self.searchController.searchBar
// ISSUE!! definesPresentationContext needs to be false or I can't push this
// controller multiple times on the navigation stack
self.definesPresentationContext = true
while it works fine when search controller is pushed to the navigation stack the first time, it doesn't let the search bar get the focus when it is pushed the second time, as shown below
but if I set it to false: as soon as I start typing into the search bar, the navigation bar (along with the search bar) disappears. This is expected behavior since (because of definesPresentationContext = false) UISearchController is now trying to display its view on top of UINavigationController's view, as shown below:
Is there a way to achieve this through UISearchController? If not, any pointers on how should I create a custom control for this? (Code for the minimal app shown in the animations can be downloaded here)
It's not possible to use UISearchController like this. UISearchBar and UINavigationBar are known to not play well together. What I decided to do was, every time user taps the Search button, I check childViewControllers array of my navigation controller and if I find an instance of SearchViewController in there, I pop back to it. Otherwise I push it.
// This function lives inside a UINavigationController subclass and is called whenever I need to display the search controller
func search() {
if let _ = self.topViewController as? SearchViewController {
return
}
var existingSearchController: SearchViewController? = nil
for childController in self.childViewControllers {
if let searchController = childController as? SearchViewController {
existingSearchController = searchController
}
}
if let searchController = existingSearchController {
self.popToViewController(searchController, animated: true)
return
}
self.performSegueWithIdentifier(StoryboardConstants.SegueShowSearchController, sender: nil)
}
The proper fix would have been, of course, a custom control but we did not have the time to write something custom at this stage.
Related
Strange things seem to happen when using the new iOS 11 navigationItem.searchController method on a detail view of a UISplitViewController.
The searchBar partly appears as a blank space on the first presentation, then appears in the wrong UITableViewController, but corrects itself after a few push and pops of UITableViewController.
I used to put the searchBar in the tableHeaderView, but I changed the code according to the WWDC recommendation:
if (#available(iOS 11.0, *)) {
self.navigationItem.searchController = self.searchController;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
} else {
self.tableView.tableHeaderView = self.searchController.searchBar;
}
This example is using standard sample code (default project for UISplitViewController and the Apple demo of UISearchController updated for iOS 11 (using a single UITableViewController)).
The initial view containing the searchController looks like this:
And clicking a UITableView item yields this:
However after clicking on a UITableView item and returning twice - it looks as it should:
and:
I was trying to determine why the Apple example for UISearchController worked and my code didn't. The main difference was it was embedded in UISplitViewController in the Detail View. Which means if shown in Compact mode has an extra UINavigationController in the stack. I found if my seque avoided the extra UINavigationController - it works correctly (but breaks device rotation). Similarly change the segue to modal allows it to work.
I note this is similar to this old question: UISplitViewController with new UISearchController issue with UISearchBar
I have created a sample project that demonstrates the problem (sample code: searchControllerDemo)
I'm stumped as to what is going on. So any help would be very much appreciated.
It's been a while since this erupted but thought to leave a note here for whoever will face the same issue...
On compact width devices, upon segueing from master to detail, the detail navigation controller is on top of the master view controller, unlike regular width where the two navigation controllers have their own separate root view controllers.
So, the UINavigationController of the detail view controller needs to be removed upon segue in combact width devices using UISplitViewControllerDelegate method: splitViewController(_:showDetail:sender:)
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if splitViewController.isCollapsed, let navController = vc as? UINavigationController {
if let detailVC = navController.topViewController {
splitViewController.showDetailViewController(detailVC, sender: sender)
return true
}
}
return false
}
The searchController is declared in viewDidLoad() of a TableViewController as searchController = UISearchController(searchResultsController: nil). When user selects a row in current table view, it navigates out to previous viewcontroller using self.navigationController?.popViewControllerAnimated(true). Whenever this happens i get a warning -
"Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior"
To avoid this, i am removing search controller from superview before calling popViewControllerAnimated(true) on navigation controller.
I do not get the same warning if the search is active and a row is selected. May be because i am dismissing the search controller first then popping out to previous controller, so it works fine in this case. But if i dismiss search controller in all cases whether search is active or not, it doesn't help.
Here is the code.
//Dismiss searchController if active & Navigate to previous view controller on row selection
if self.searchController.active {
self.searchController.dismissViewControllerAnimated(true, completion: {
self.navigationController?.popViewControllerAnimated(true)
})
}
else {
//remove UISearchController from superView
self.searchController.view.superview?.removeFromSuperview()
self.navigationController?.popViewControllerAnimated(true)
}
I want to understand if this is the right approach as above in else block or should i do the same within :-
viewWillDisappear/viewDidDisappear
deinit{
if let superView = searchController.view.superview
{
superView.removeFromSuperview()
}
}
Which one is the right thing to do ?
I think first one is better option that check for active before removing it from its superview.
A little background on the setting of our views:
Inside a NavigationController, we have a UITabBarController (with 3 tabs) with a UIViewController that has a UISearchController.
There is an error that if we leave the UISearchController active and switch to another view, when we return to the search view the entire screen is black.
However, when the UISearchController is not active and we switch views this does not happen.
We have tried to set the controller to not be active when segueing between views; however, when the UISearchController is active none of the segueing events get called (no log prints appear from viewWillDissapear, viewWillAppear, etc.)
Looking on other threads, we tried setting self.definesPresentationContext = true
but that does not work.
Has anyone else had this problem or know how to fix it?
Try to set the searchbarController active like this
self.resultSeachController.active = false
before you move on the next View
I faced the same problem and solved it as follows:
I extended UITabBarController and created a custom class TabBarController
class TabBarController: UITabBarController {
In that class I implemented its didSelectItem method, and in that method I called a method of the view controller that closes the search controller
// UITabBarDelegate
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
let vc = viewControllers![selectedIndex] as! CommonViewController
if vc.searchController.active {
vc.searchBarCancelButtonClicked_NoReload()
}
}
viewControllers is an array in UITabBarController that keeps all the view controllers belonging to the UITabBarController, and 'selectedIndex' is the index of the Tab (and the view controller) which was displayed, and thus one can get to the viewController that has the searchController active.
In my app all the view controllers are subclasses of a root class named CommonViewController where I put all the vars and methods that are common to all view controllers, such as all the search functionality. Therefore I simply check if the search controller is active and if it is I call a method that makes it inactive and does some other related cleanup.
I've got an app with a mapViewController embedded in a navController. In the mapVC ive got a single bar button item which when clicked I want to conditionally "push" segue to one of a number of different view controllers. To achieve that ive set up an ibaction on the button and have the conditional "performSegueWithIdentifier" code in the relevant buttons ibaction method ie
#IBAction func addButtonClicked(sender: UIBarButtonItem) {
let lastAdd = "addItem"
if lastAdd == "addItem"{
self.performSegueWithIdentifier("addItem", sender: self)
} else {
self.performSegueWithIdentifier("addEvent", sender: self)
}
}
this will take me to either the addItemVC or the addEventVC. in each of those viewControllers (ie the addItemVC and the addEventVC) I want to have a segmented control in the navigation bar which, when clicked, will take me to the alternative VC ie if addItemVC is currently displayed, and the addEvent section of the segmented control is clicked, I want to display the addEventVC. Im following Red Artisans page on how to do this but in his example he is instantiating all view controller options upfront in the app delegate and so can easily get reference to each view controller and link it to the clicked segement of the segmented control within his rootVC
Where im confused is .. seeing Im using conditional code before performing each segue, i assume that im only instantiating one viewController at a time when the bar button item is pressed. So how can i get an array of view controllers to pass to the VC im segueing to so that i can create the required segmented control in that VC. I assume i could manually create the destination VC array in my mapViewController and pass these across but wouldnt that mean im instatiating a different instance to the ones automatically created by the segue process?
Yes, you are right: if you manually create the two VCs in your mapViewController, they will be different instances from those created by a segue. So if you want to stick with Red Artisan's solution, present the VCs using code rather than segues. You can still design the two VCs in your storyboard, give them each a unique identifier and then use the instantiateViewControllerWithIdentifier function of self.storyboard to create the instances.
You can use most of Red Artisan's app delegate code in your mapViewController, but with a few tweaks: eg. to use the existing navigation controller (in which your mapViewController is embedded), and the [window ...] lines are superfluous. The thing to watch out for will be the indexDidChangeForSegmentedControl function, which assumes that the VCs you are switching between are the rootViewControllers for the navigation controller (ie. that they are the only item in the navigation controller's viewControllers array). In your case you have mapViewController as (I assume) the rootViewController, so you will have to amend the indexDidChangeForSegmentedControl function to create an array with the mapViewController at index 0 and the relevant (addItem or addEvent) VC at index 1. I don't know how well this method will animate, nor whether back buttons etc will be properly set.
If you want to stick with segues, there are a couple of solutions: one would be to use a UITabBarController (and hide the tabBar). You would have the addItem and addEvent VCs as separate tabs, and when you segue to the tabBarController, you could set which tab is selected. But my preferred solution would be to segue to a UIPageViewController. You would could either create the VCs in mapViewController and pass them as part of the segue, or just pass an indicator as to which was selected, and have the pageViewController instantiate them and present the relevant one. You could then use the UISegmentedControl to trigger switching between VCs. See this answer for something similar.
thanks pbasdf for your detailed instructions. its taken me quite a while but i seem to be close to getting it working. i followed most of your instructions and you were spot on with what you said.
first from the mapVC on press of the + bar button item i create the addItem and addEvent VCs using instantiateViewControllerWithIdentifier and create the segmented control.
#IBAction func addButtonClicked(sender: UIBarButtonItem) {
//at this stage just manually set default target VC
let lastAdd = "addItem"
//get an array of the target viewcontrollers
var viewControllers = segmentViewControllers()
//initz the segmentscontoller with the current navcontroller if doesnt already exist
if segmentsController == nil {
segmentsController = SegmentsController(navController: self.navigationController!, viewControllers: viewControllers)
segmentedControl.addTarget(segmentsController, action: "indexDidChangeForSegmentControl:", forControlEvents: UIControlEvents.ValueChanged)
//add the segmented control to the VC by setting first user experience which calls indexdidchangeforsegmentedcontrol
firstUserExperience()
}
segmentsController?.indexDidChangeForSegmentControl(segmentedControl)
}
//create an array of the target view controller. called from addbutton clicked
func segmentViewControllers() -> [UIViewController] {
//create an instance of the viewcontrollers
let addItemVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("AddItemVC") as ViewController
let addEventVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("AddEventVC") as ViewController
var viewControllers = [addItemVC, addEventVC]
return viewControllers
}
in the segmentsController i created 2 arrays, 1 to hold the different VCs and one to hold the navigation stack. i also set the passed the segmented control to the incoming VCs
class SegmentsController: NSObject {
var navController: UINavigationController?
var viewControllersOptionsArray: [UIViewController] = []
var viewControllersNavArray: [UIViewController] = []
//MARK: - INITIALIZER
init(navController: UINavigationController, viewControllers: [UIViewController]) {
self.navController = navController
self.viewControllersOptionsArray = viewControllers
}
//MARK: SEGMENT INDEX METHOD
func indexDidChangeForSegmentControl(segmentedControl: UISegmentedControl) {
var index = segmentedControl.selectedSegmentIndex
var incomingViewController = viewControllersOptionsArray[index]
//set the viewControllersNavArray
if let mapVC = navController?.viewControllers[0] as? MapViewController {
viewControllersNavArray = [mapVC, incomingViewController]
}
//set the navcontroller with a new array of viewcontrollers
navController?.setViewControllers(viewControllersNavArray, animated: true)
//set the title of the incoming view controller
incomingViewController.navigationItem.titleView = segmentedControl
//set the seg control variable of the incoming VCs
if let iVC = incomingViewController as? AddItemViewController {
iVC.segmentedControl = segmentedControl
} else if let iVC = incomingViewController as? AddEventViewController {
iVC.segmentedControl = segmentedControl
}
}
}
In the addItem and addEvent VCs i figured i needed to pass the current selectedSegmentIndex back to the mapVC if the user presses the back button - wasnt sure how to do this and ended up using an extension i downloaded called UIViewController+BackButtonHandler to handle it.
Im sure my code could be much better written but the only thing that im still having trouble with is that i want the VC transitions to be animated. the navigation seems to work fine if i set animated to false in the navController?.setViewControllers(viewControllersNavArray, animated: true) line but if i set it to true, the segmentedControl briefly flashes an appearance on the nav bar of the incoming VC but then disappears. Its still there so i can still navigate but you cant see it. i figure that it has something to do with setting it before the view has properly loaded but even if i put the code to set it ie
incomingViewController.navigationItem.titleView = segmentedControl
in the new top VC in viewDidLoad or viewDidAppear i still get the same problem. i also thought i might be able to fix it if i could put the incomingViewController.navigationItem.titleView = segmentedControl code in a completion block but the setViewControllers method doesnt appear to have a completion handler.
Any suggestions?
I'm looking to create similar functionality to Apple's maps application in Swift. Is there anyway to integrate a UISearchController in to a regular view (i.e.: not a UITableView). Dropping one in through Storyboard results in a crash after clicking inside the connected searchbar. Or is there some way I can achieve this outcome with a UITableView?
If you want to use UISearchController with a non UITableView, here is how I did it.
Since the UISearchController is not (yet!) supported by IB, you do not need to add anything in it, like a UISearchBar.
#interface UIViewControllerSubclass () <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchControllerDelegate, UISearchResultsUpdating>
#property (strong, nonatomic) UISearchController *searchController;
#end
#implementation UIViewControllerSubclass
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any custom init from here...
// Create a UITableViewController to present search results since the actual view controller is not a subclass of UITableViewController in this case
UITableViewController *searchResultsController = [[UITableViewController alloc] init];
// Init UISearchController with the search results controller
self.searchController = [[UISearchController alloc] initWithSearchResultsController:searchResultsController];
// Link the search controller
self.searchController.searchResultsUpdater = self;
// This is obviously needed because the search bar will be contained in the navigation bar
self.searchController.hidesNavigationBarDuringPresentation = NO;
// Required (?) to set place a search bar in a navigation bar
self.searchController.searchBar.searchBarStyle = UISearchBarStyleMinimal;
// This is where you set the search bar in the navigation bar, instead of using table view's header ...
self.navigationItem.titleView = self.searchController.searchBar;
// To ensure search results controller is presented in the current view controller
self.definesPresentationContext = YES;
// Setting delegates and other stuff
searchResultsController.tableView.dataSource = self;
searchResultsController.tableView.delegate = self;
self.searchController.delegate = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.searchBar.delegate = self;
}
#end
I hope it is enough to work :-)
Then of course you need at least to implement UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdater methods.
Enjoy!
Trying to figure out UISearchController myself. Setting it to the titleView is convenient, but on one of my pages, I had to put the searchBar near the top of the UIViewController:
// Add a normal View into the Storyboard.
// Set constraints:
// - height: 44
// - leading and trailing so that it spans the width of the page
// - vertical position can be anywhere based on your requirements, like near the top
#IBOutlet weak var searchContainerView: UIView!
var searchResultsController = UISearchController()
override func viewDidLoad() {
// TODO: set the searchResultsController to something
let controller = UISearchController(searchResultsController: nil)
// have the search bar span the width of the screen
controller.searchBar.sizeToFit()
// add search bar to empty View
searchContainerView.addSubview(controller.searchBar)
searchResultsController = controller
}
UPDATE:
After implementing UISearchController in a project or two, I found myself gravitating toward #adauguet's approach of embedding the search bar into the Navigation Bar.
Here's the code in Swift. One difference though is that it doesn't set the searchBar delegate, since searchResultsUpdater already listens for text changes.
override func viewDidLoad() {
super.viewDidLoad()
// locationManager.delegate = self
// locationManager.desiredAccuracy = kCLLocationAccuracyBest
// locationManager.requestWhenInUseAuthorization()
// locationManager.requestLocation()
let locationSearchTable = storyboard!.instantiateViewControllerWithIdentifier("LocationSearchTable") as! LocationSearchTable
resultSearchController = UISearchController(searchResultsController: locationSearchTable)
resultSearchController?.searchResultsUpdater = locationSearchTable
let searchBar = resultSearchController!.searchBar
searchBar.sizeToFit()
searchBar.placeholder = "Search for places"
navigationItem.titleView = resultSearchController?.searchBar
resultSearchController?.hidesNavigationBarDuringPresentation = false
resultSearchController?.dimsBackgroundDuringPresentation = true
definesPresentationContext = true
}
Also, I wrote a blog post that creates a project from scratch that uses UISearchController to display map search results. It also does other things that you might want in a map project, like get the user location, drop pins, parse placemarks into a one-line address, and create callout buttons that take you to Apple Maps for driving directions.
http://www.thorntech.com/2016/01/how-to-search-for-location-using-apples-mapkit/
The blog post is quite long, so here's the associated git repo if you just want to skip to the code:
https://github.com/ThornTechPublic/MapKitTutorial
I added a Search Bar and Search Display Controller in my View Controller in the storyboard. The view controller contains only the search bar and search display controller and does not have it's own TableView. When you add the search bar in your view controller, it sets your view controller as it's delegate automatically.
Now the Search Bar and Search Display Controller has a table view of itself which it uses to display the search results when you click inside the box and start typing. This table view expects your view controller to provide the implementations of the numberOfRowsInSection and cellForRowAtIndexPath functions for it to display the data properly.
When you run your project without these and tap inside the search bar, you will get the following error:-
tableView:numberOfRowsInSection:]: unrecognized selector sent to instance 0x7fbf63449660
*** Terminating app due to uncaught exception 'NSInvalidArgumentException'
If you see, the error is at the numberOfRowsInSection method.
Change your view controller definition from
class ViewController: UIViewController
to
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource
and implement the required methods which are:-
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return UITableViewCell()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
I have just added default return values in the above methods.
Now if you filter out your data source in your searchviewdelegate methods and set up your number of rows and cell info in the above two methods properly, it should work.
Hope this helps!
I had some trouble converting from SearchDisplayController in UIViewController to SearchController in ViewController because SearchController implementation isn't so intuitive. But you can just add in searcher from SearchController itself into any view. You cannot set constraint though because the search bar would move up when you focus/select it (if you know how to set the constraint to seachController.searchbar after adding it to any view, LET ME KNOW!). Below I am sharing a checklist that I found very important/valuable when implementing SearchController in ViewController.
//you can just add searcher to any view. It would automatically move up to show the tableView like magic. But for this reason, you cannot set constraint to the search bar to the placeholder view that you are adding it to.
[self.searchBarPlaceHolderView addSubview:self.searchController.searchBar];
//you need this to prevent search bar to drop down when you focus/select it. You would want to set this to NO if you are adding searchBar to the navigation bar's titleview.
self.searchController.hidesNavigationBarDuringPresentation = YES;
//make sure you set this in your viewController
self.extendedLayoutIncludesOpaqueBars = true;
self.definesPresentationContext = YES;
// you also need to give the search controller a tableViewController that can be displayed. You can also do just self.searchResultsController = [[UITableView alloc] init] for a generic one.
self.searchResultsController = (UITableViewController *)[ [UIStoryboard storyboardWithName:#"Main" bundle:nil] instantiateViewControllerWithIdentifier:#"searchResultsTableViewController"];
self.searchResultsController.tableView.dataSource = self;
self.searchResultsController.tableView.delegate = self;
self.searchResultsController.definesPresentationContext = NO;
self.searchController = [[UISearchController alloc] initWithSearchResultsController:self.searchResultsController];