I am presenting a UISearchController programmatically, without adding it to the navigationItem. The Calendar app does something similar.
Without a presentation context, the search bar appears correctly, but persists after pushing another view controller.
This is expected, so we need to set definesPresentationContext on the list view controller... But that causes the search bar to render incorrectly.
Here's the code for context:
private lazy var searchController: UISearchController! = {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
// If this is set to true, the search bar animates correctly, but that's
// not the effect I'm after. See the next video.
searchController.hidesNavigationBarDuringPresentation = false
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
definesPresentationContext = true
searchButton.rx.tap.subscribe(onNext: { [unowned self] in
present(searchController, animated: true)
}).disposed(by: disposeBag)
}
Setting hidesNavigationBarDuringPresentation kind of fixes it, but we lose the tab bar, and the whole thing just looks bad.
I tried this solution (Unable to present a UISearchController), but it didn't help.
Any suggestions?
UPDATE: The issue is, more specifically, that the search bar appears behind the translucent navigation bar. Making the nav bar solid ( navigationController?.navigationBar.isTranslucent = false) makes the search bar appear under the nav bar.
I have the same problem not been able to solve this either. It seems like the problem is that either
a) the searchcontroller is presented at the very top of the viewcontroller stack, even above the navigation controller, so that it stays active into the next viewcontroller push. or,
b) the searchcontroller is presented underneath the navigationcontroller so that it remains covered by the navigation bar
One idea: don't embed the viewcontroller which is presenting the searchcontroller in a navigation controller. instead, just create a UIView which looks like a navigation bar a the top. would this be an inappropriate solution?
I haven't found a solution to the original problem, but I found a workaround: intercept navigation events, and manually dismiss the search controller.
override func viewDidLoad() {
...
// This makes the search bar appear behind the nav bar
// definesPresentationContext = true
navigationController?.delegate = self
}
extension JobListViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController, animated: Bool) {
// `animated` is false because, for some reason, the dismissal animation doesn't start
// until the transition has completed, when we've already arrived at the new controller
searchController.dismiss(animated: false, completion: nil)
}
}
I found that presenting the search controller over the navigation bar can be achieved by calling present(_:animated:completion:) on the navigation controller itself rather than the navigation controller's child.
So in your view controller you can do
navigationController?.present(searchController, animated: true)
And this will behave like the search button in the Apple's Calendar app.
Update
Regarding dismissing the search controller before pushing a new controller to the navigation stack, you can do this manually depending on how the push is done.
All the bellow will animate dismissing the search controller before the push happens. Note that I disable user interaction until the dismiss animation completes to prevent pushing the same view controller multiple times.
UIStoryboardSegue
Add this override to your view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if navigationController?.presentedViewController == searchController {
view.isUserInteractionEnabled = false
searchController.dismiss(animated: true) {
self.view.isUserInteractionEnabled = true
}
}
}
IBAction (Programmatically)
Just dismiss the search controller before pushing view controllers to the navigation stack:
#IBAction func showSecondTapped(_ sender: UIButton) {
// Dismiss the search controller first.
view.isUserInteractionEnabled = false
searchController.dismiss(animated: true) {
self.view.isUserInteractionEnabled = true
}
// Build and push the detail view controller.
if let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") {
navigationController?.pushViewController(secondViewController, animated: true)
}
}
Handling pop gesture
If the view controller that is presenting the search controller is not the root of your navigation controller, the user might be able to use the interactive pop gesture, which will also keep the search controller presented after the pop. You can handle this by making your view controller the delegate for the search controller, conform to UISearchControllerDelegate and adding the following code:
extension ViewController: UISearchControllerDelegate {
func willPresentSearchController(_ searchController: UISearchController) {
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
func willDismissSearchController(_ searchController: UISearchController) {
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
}
Related
I'm trying to present a search controller in response to a button, by setting its isActive property, but it doesn't appear.
lazy var searchController: UISeachController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.delegate = self
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
definesPresentationContext = true
}
// Called when a button on the navbar is tapped
#obcj private func searchTapped() {
// The doc says you can force the search controller to appear by setting isActive,
// but nothing happens
searchController.isActive = true
// Calling present does show it, but the search bar appears behind the navbar.
// And the delegate methods are still not called
//present(searchController, animated: true)
}
func willPresentSearchController(_ searchController: UISearchController) {
// Not called, even when calling 'present'
}
It seems isActive only works if the search bar is in the view hierarchy.
// This will place the search bar in the title view, allowing you to
// have other buttons on the right of the search bar.
navigationItem.titleView = searchController.searchBar
Or
// This takes up a whole other row under title row.
navigationItem.searchController = searchController
But if you go this route, you don't need to set isActive yourself - the search controller automatically comes up when the user interacts with the search bar. I still don't understand how isActive is supposed to be used, but it does answer the original question.
In my use case, I want to hide the bottom tabbar when navigating away from UITabbarController.
I was using
let vc = storyboard?.instantiateViewController(withIdentifier: tableData[indexPath.row]["vcIdentifier"]!)
self.hidesBottomBarWhenPushed = true
self.show(vc!, sender: self)
It sorta works, because the pushed view controller doesn't have tabbar at bottom. However, as soon as I click on navigate, the bottom tabbar of the "sender" view controller vanishes and leaves black area.
Please let me know if you need to have more information about anything. Thanks a lot in advance!
If the pushed view controller doesn't have a tab bar at the bottom, you can add this lifecycle of view controller codes.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
tabBarController?.tabBar.isHidden = false
}
you can use segue if you want to hide tabbar while going to the next screen. it will automatically hide it.
I'm trying to present a view controller below another presented view controller (like WhatsApp when you open camera and press gallery).
I tried many things but none worked ..
Use child view controller and set view of that added child view controller at the top of hierarchy. It will be top most element, so actual background of this view will be obscured, but that's the way to go.
//code inside UIViewController class
func addViewControllerAtBottom() {
let newVC = NewVCType() //just instantiate it
addChildViewController(newVC)
view.insertSubview(newVC.view, at: 0) //at 0 means it's first rendered, all others will be on top of it
}
You can reproduce this behavior by doing the following :
First, create a NavigationController with a root ViewController :
let navController = UINavigationController(rootViewController: firstController)
Then, present this navigationController with animated: false and in the completion of the present method, push your second ViewController, still with animated: false (to avoid weird animations) :
present(navController, animated: false) {
navController.pushViewController(secondController, animated: false)
}
Here you go, you got a new navigation with 2 UIViewController, like WhatsApp.
Full code, wrapped into a button's action :
#IBAction func buttonTapped(_ sender: Any) {
let navController = UINavigationController(rootViewController: firstController)
present(navController, animated: false) {
navController.pushViewController(secondController, animated: false)
}
}
i have a UICollectionViewController where i am showing list of task which is working fine, recently i tried to implement a UISearchBar for my TaskController after implementing that, when i try to launch any new viewcontroller by clicking on row inside my TaskController the newly launched view controller does not have UINavigationBar so i cant move back to my task list again. see following TaskController with task list:
Image
in above screen shot there is a star icon when user click on that, I launch following view controller which have a navigation bar(note: I have click directly without filtering records thats why i can see the navigation bar here.). UIViewController with UINavigationBar
Image
this is what i get when i click on star icon after filtering data with search bar.
navigation bar gone missing here
so i can not go back to task list controller also when i change a tab from below and come back the view controller got destroyed and i get a black screen with tab bar.
following code i have used to implement search bar which have the problem please help me to figure it out.
let taskSearchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
//set taskSearchController
taskSearchController.searchResultsUpdater = self
taskSearchController.dimsBackgroundDuringPresentation = false
navigationItem.searchController = taskSearchController
getTaskList(){
}
}
following method gives the filtered data from tasklist
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text, !searchText.isEmpty else{
self.taskList = self.originalTaskist
collectionView?.reloadData()
return
}
taskList = originalTaskist.filter({ task -> Bool in
return task.name!.lowercased().contains(searchText.lowercased())
})
collectionView?.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// This prevents the search bar to make trouble on pushed view controllers
definesPresentationContext = true
//...
}
Put definesPresentationContext = true inside of your View Controller that shows the search bar (the UICollectionViewController in your case.
Unfortunately, the documentation doesn't explain very well why this is working. This blog post explains it a little better.
So, I am using the Elastic Transition pod (Cocoapods), and when I transition my app crashes because the UITransitionContextFromViewControllerKey key is nil. I am really confused as to why that value would be nil. What are some probable causes and solutions to resolving this error?
So here is some of my code for when I am transitioning into the next view controller:
func handleTap(sender: AnyObject) {
transition.sticky = true
transition.transformType = .TranslateMid
transition.showShadow = true
transition.edge = .Left
transition.startingPoint = sender.center
performSegueWithIdentifier("mySegue", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
segue.destinationViewController.transitioningDelegate = transition
segue.destinationViewController.modalPresentationStyle = .Custom
}
Then, when it is trying to transition my app crashes complaining that this line of code (in Elastic Transition) is returning nil:
transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
According to the ElasticTransition Github page, if a view controller is presented modally with elastic transition, then the destination view controller's transitioningDelegate needs to be set to ElasticTransition and modalPresentationStyle to .Custom. However, if the view controller is pushed onto a navigation controller stack using the elastic transition, then only the navigationController?.delegate needs to be set to ElasticTransition.
After some chatting, #Harish told me that he uses a push segue. However, the code in prepareForSegue is the setup code for when one presents a view controller modally. That is likely the reason why UITransitionContextFromViewControllerKey returns nil. So I believe the solution is to set the navigationController's delegate to ElasticTransition somewhere, and remove the code in prepareForSegue.