I have implemented UISearchController so that it appears overtop the UINavigationBar upon tapping a search icon. After filtering the table, the user should be able to select a row which will push a new view controller. The problem is upon doing so, the search controller is still being shown overtop the navigation bar.
I have researched this and it was suggested you should set self.definedPresentationContext to true. This does not work when the search bar is presented overtop the navigation bar. When you tap the search icon the keyboard appears but the search bar does not animate in.
Another option I found is to dismiss the search controller in viewWillDisappear, by setting active to false. The problem with this solution is it animates away during the push transition. And when you navigate back, the search is obviously no longer active, therefore the user has lost the search context and all results are shown.
I would like to know how to push a view controller, keep the search active, but not show the search interface overtop of the next view controller. Ideally the search interface would be pushed away to the left as the new view controller is pushed, and return upon swiping back to the search results.
I have tweaked Apple's UIKit Catalog sample code to show the undesired behavior. To reproduce the problem, download this project, run it on iPhone, tap the back button, select Search, select Present Over Navigation Bar, tap the search icon, type in a search term, and select a result. Notice the search bar remains visible. Here is the code from SearchPresentOverNavigationBarViewController.swift:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if searchController.active {
//searchController.active = false //undesirably dismisses search, animates away during push, upon going back search is not active
}
}
#IBAction func searchButtonClicked(button: UIBarButtonItem) {
//self.definesPresentationContext = true //causes the search to not appear
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
presentViewController(searchController, animated: true, completion: nil)
}
I'm a little late to the show but I recently found myself in a similar situation and want to share my experience.
So let's say you have presented your search interface, the user has typed something into the search field and you have showed them some results. Upon selecting a result I push the new view controller to the original navigation controller, that is the navigation controller that contains the view controller which presented the search interface. When I do this, the search interface gets pushed to the left and when you tap the back button, the search interface is brought back and its state (query and results) is preserved.
I should mention that in the controller that contains the searchable content, the one that presents the search interface, I have enabled presentation context.
Also, my search bar is integrated in the header view of the table view that displays the search content, so I don't present the search interface explicitly - when the user taps inside the search field, it automatically goes up and pushes the navigation bar out of the screen. This is the only difference in our setups that I can think of. Anyways, hope this helps you. Cheers!
Related
I have a navigation controller that pushes an UITabBarController. So, all my tab view controllers will share the same navigation bar. Not a problem so far because I setup the navigation item's content every time tabBarController(_:didSelect:) is called.
Let's say we have only 2 controllers in the tab bar and every controller will have an UISearchController. Being attached to the navigation item, this search controllers will be configured every time tabBarController(_:didSelect:) is called.
Every thing works and looks as required until now.
The problem is that when I tap the search bar from the first controller the keyboard appears and the search works, but when I tap the search bar from the second controller nothing happens.
[Being in the second tab]
I know that the UISearchBarDelegate's searchBarShouldBeginEditing(_:) is called, but if I try in that method to set searchController.isActive = true, then the app will crash with the following message: Attempt to present UISearchController on <FirstTabViewController> whose view is not in the window hierarchy.
Have you ever encountered this issue?
Do you set the definesPresentationContext property to true when you access the second view controller?
How to set SearchBar to be active after navigating back from another view controller?
Type the search string and click search button on search suggestion page, then this page popped by navigation controller. The search result page which was navigated back has a search bar with search text, and refresh the result table view.
But this SearchBar's cancel button is inactive, the first click make search bar become editing mode and the second click just can make it work.
How to set SearchBar's cancel button to be active, so I only need to click cancel button once.
Search suggestion page, will be popped
Search result page, cancel button, two states: active and inactive
If you're just trying to activate the search bar when it's parent view appears, then you can just add the following to the view controller that contains it:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if searchBar.canBecomeFirstResponder {
searchBar.becomeFirstResponder()
}
}
I need to implement the navigation bar with custom back image and custom title. For example: if i am in "Login with facebook" page i need to show the "Login with facebook" besides the custom back image. For this i am following the code like this:
let yourBackImage = UIImage(named: "back_button_image")
self.navigationController?.navigationBar.backIndicatorImage = yourBackImage
self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = yourBackImage
self.navigationController?.navigationBar.backItem?.title = "Custom"
But I am getting the output like this:
Note: The title is of the current page.
How to achieve this?
If the back title is too long iOS will change it to 'Back'.
However if you add a left bar button item - this you can set to a longer length.
long back
From Apple docs: https://developer.apple.com/documentation/uikit/uinavigationcontroller
In cases where the title of a back button is too long to fit in the available space, the navigation bar may substitute the string “Back” for the actual button title. The navigation bar does this only if the back button is provided by the previous view controller. If the new top-level view controller has a custom left bar button item—an object in the
leftBarButtonItem
or
leftBarButtonItems
property of its navigation item—the navigation bar does not change the button title.
To do this go to the Storyboard, in Interface Builder, drag a Bar Button Item into your navigation bar and set its title.
*Note that you will need to hook this up to your view controller to pop it.
#IBAction func pop(_ sender: UIBarButtonItem) {
_ = navigationController?.popViewController(animated: true)
}
You'll find a lot of StackOverflow attempts to answer this (quite common) issue, and the best way that I've found is also rather complicated. You'll have to do this for every UINavigationController, UINavigationBar, and UINavigationItem in your app, and that's painful.
How Apple wants you to do it (as far as I can tell)
For UINavigationBars in a storyboard, set the Back and Back Mask values (in the Attributes inspector) to the name of your custom image.
For every view controller's UINavigationItem, set its Back attribute value to an empty string or single space.
Easy enough, right? Nope. For one thing, the empty string or single-space Back title isn't always honored. And what if you have dozens, or even hundreds, of view controllers? What if your application has multiple UINavigationControllers? What if you set configure your view controllers (or at least their navigation items) programmatically? Then it's not so easy.
How StackOverflow thinks you should do it
I'm not going to link to all of the SO answers that recommend this way of doing things. I'll just summarize it as:
Hide the navigation item's back button with UINavigationItem.setHidesBackBarItem(_:animated:).
Create a UIBarButtonItem with your custom back image and empty title, then add it to your navigation item's leftBarButtonItem.
Sit back and wait for user bug reports to flood in with complaints that your app doesn't support swiping to navigate back.
Or, if you want to be clever, attach your navigation controller's interactivePopGestureRecognizer to your custom left bar button item. This usually works, but it feels like a hack that's likely to break in future releases. If you're ok with that, then you can stop reading right here.
How I think you should do it
If you have a lot of view controllers (our app has 200+) and/or a mix of VCs configured programmatically and in storyboards, this may ultimately be an easier way.
Create a subclass of UINavigationBar and set this as the class for all navigation bars in your storyboards and code. (If you're doing it in code, note that the nav bar is a read-only property; it can be set only in the navigation controller's initializer.)
The UINavigationBar manages a stack of UINavigationItems, one for each view controller on the UINavigationController's stack, and keeps them in sync with their view controllers. In other words, when you push a view controller onto the nav controller, that view controller's nav item also gets pushed onto the nav bar, and it gets popped when the VC is popped. When the navigation bar pushes or pops an item, it uses the new topmost item's title, titleView, and/or button item properties to set configure its own title view and buttons, and uses the title of the navigation item immediately below the topmost one to configure its back button and back image. If the next-to-topmost item's title is nil, then the nav bar uses the default value of "Back". You don't want that. If you can set the Back attribute to an empty or single-space string in the storyboard, then you should be able to do the same thing in code, right? Wrong. The back text is not a public property in UINavigationItem! WTF?
Instead, you have to set the title of the next-to-topmost item to an empty string. "But wait!" you say, "Doesn't that permanently change the next-to-topmost item's title, so that when I navigate back to that view controller, its title will say "Back" instead of its original title?" And you'd be right. What can you do? In your subclass of UINavigationBar, add an array of Optional title strings, like so:
var navigationItemTitles = [String?]()
(Why Optional? Because navigation items may have a custom titleView instead of a title.)
Override the navigation bar's pushNavigationItem(_:animated:) and popNavigationItemAnimated(_:) so that they modify and restore the navigation items' titles, respectively:
override func pushNavigationItem(_ item: UINavigationItem, animated: Bool) {
super.pushNavigationItem(item, animated: animated)
// Store the new back item's title, then set its title to
// an empty string.
if let backItem = backItem {
navigationItemTitles.push(backItem.title)
backItem.title = ""
}
}
override func popNavigationItemAnimated(_ animated: Bool) {
super.popNavigationItemAnimated(animated)
// Restore the new top item's title.
if navigationItemTitles.count > 0 {
topItem?.title = navigationItemTitles.popLast
}
}
Caveats
This is the only way I've found that's foolproof and not hacky. If Apple simply made the navigation item's Back property publicly settable, then a lot of this could be avoided.
(For what its worth, not only does our app have 200+ view controllers, it also has multiple navigation controllers with different navigation bar looks and feels. That's a whole other layer of complexity.)
Update 2:
Since people are still looking at this question: know that I realized it's impossible to replicate this bug on an actual, isolated iOS device. When you present the search controller, iOS presents a keyboard as well, which covers the tab bar. To switch tabs, you must dismiss the keyboard, which will dismiss the search controller as well.
The reason I say "isolated" is because I don't know if you can replicate this with a Bluetooth keyboard and don't have one to try. The iOS keyboard might not be presented with an external keyboard connected
Update:
I added a viewWillDisappear override to View 1 and found some interesting results:
When the searchbar is not present, switching tabs does dismiss the view before loading the next view. However, when the searchbar is present, the view is NOT dismissed before loading the next view
I have a tab bar controller at the root, which has 2 tabs. Each goes to a navigation controller that takes the device to the view. Here's a crude diagram:
/-> Navigation Controller -> View 1
tab bar controller
\-> Navigation Controller -> View 2
This works fine normally. However, View 1 has a UISearchController. The bug is that if you click the search button, the search controller is presented. If you then click View 2 in the tab bar and switch back to View 1, the search controller is still there and the view is black.
My current hacky "fix" is to disable the tab bar while the search bar is apparent and re-enable it if the user selects "Cancel". This has 2 issues:
It's jank; the user should be able to switch tabs whenever they want
It only re-enables if the user hits Cancel, not if they simply tap out of the search
How can I fix this? Here are pictures of what I'm talking about
I solved this by following the answer at:
UISearchController causes black screen Swift 2.0
I implemented it like so in viewDidLoad:
self.definesPresentationContext = true
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
searchController.dimsBackgroundDuringPresentation = false
searchController.definesPresentationContext = true
This pretty much solved the problem for me. Then I encountered another problem with trying to segue away at didSelectRowAt, which had previously never been an issue. I solved this by first popping the UISearchController, then performing a segue as normal:
_ = self.navigationController?.popViewController(animated: true)
I have an app with a UITabBarController and a few tabs, each of which contains a UINavigationController. I also have a UISearchController triggered by a search button on the nav bar, which has a custom search results view controller. Search works just fine, as does tapping cancel. I implemented presentSearchController: to present the search controller as a view controller using
[self presentViewController:animated:completion:];
, which also works fine.
Where I run into trouble is if the user dismisses the keyboard (search controller active), and then switches tabs on the tab bar, comes back to the original tab with search controller active, and then taps "Cancel", the underlying UINavigationController's view stack is gone from the main window hierarchy and doesn't get reloaded until I switch tabs and come back with search mode inactive.
This is similar to this issue: "From View Controller" disappears using UIViewControllerContextTransitioning
Except I don't use any custom transitions, and the tab bar still shows after tapping cancel. Printing the view stack shows the tab bar is the only subview of the window until I switch tabs again and everything displays as normal.
What is the best way to go about solving this? I really hate to brute force re-add the navigation controller's view to the window on didDismissSearchController: as suggested. Not only does this seem like a bad idea, but I also run into issues with the z-ordering of the tab bar and the navigation controller when explicitly re-adding the nav controller to the key window.
Adding my brute-force solution:
- (void)didDismissSearchController:(UISearchController*)searchController
{
if (![self.view isDescendantOfView:[UIApplication sharedApplication].keyWindow]) {
NSUInteger currIndex = self.tabBarController.selectedIndex;
NSUInteger tempIndex = self.tabBarController.selectedIndex == 0 ? 1 : 0;
[self.tabBarController setSelectedIndex:tempIndex];
[self.tabBarController setSelectedIndex:currIndex];
}
}