UISearchController disable cancel UIBarButtonItem - ios

The Problem
I am trying to use UISearchController to search for a destination on a map view. I want the UISearchBar to appear in the navigation bar, but I can't seem to make it do so without it showing a cancel button to the right of it:
This Cancel button has disappeared at times, whilst I'm playing around, but I can't get it to not appear now I have got the search table showing how I want it to:
I'm sure there must be something small I'm doing ever so slightly wrong, but I can't work out what it is...
My Code
self.resultsViewController = [UITableViewController new];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:self.resultsViewController];
self.searchController.searchResultsUpdater = self;
self.searchController.hidesNavigationBarDuringPresentation = false;
self.searchController.delegate = self;
self.searchBar = self.searchController.searchBar;
self.searchBar.placeholder = self.stage.title;
self.searchBar.searchBarStyle = UISearchBarStyleMinimal;
self.definesPresentationContext = true;
self.navigationItem.titleView = self.searchBar;
self.resultsTableView = self.resultsViewController.tableView;
self.resultsTableView.dataSource = self;
self.resultsTableView.delegate = self;

There is a way easier way...
For iOS 8, and UISearchController, use this delegate method from UISearchControllerDelegate:
func didPresentSearchController(searchController: UISearchController) {
searchController.searchBar.showsCancelButton = false
}
Don't forget to set yourself as the delegate: searchController.delegate = self

Updated in light of comments
UISearchBar has a property (see the Apple docs) which determines whether the cancel button is displayed:
self.searchBar.showsCancelButton = false;
But, as per OP comments, this does not work, because the searchController keeps switching the cancel button back on. To avoid this, create a subclass of UISearchBar, and override the setShowsCancelButton methods:
#implementation MySearchBar
-(void)setShowsCancelButton:(BOOL)showsCancelButton {
// Do nothing...
}
-(void)setShowsCancelButton:(BOOL)showsCancelButton animated:(BOOL)animated {
// Do nothing....
}
#end
To ensure this subclass is used by the searchController, we also need to subclass UISearchController, and override the searchBar method to return an instance of our subclass. We also need to ensure that the new searchBar activates the searchController - I've chosen to use the UISearchBarDelegate method textDidChange for this:
#interface MySearchController () <UISearchBarDelegate> {
UISearchBar *_searchBar;
}
#end
#implementation MySearchController
-(UISearchBar *)searchBar {
if (_searchBar == nil) {
_searchBar = [[MySearchBar alloc] initWithFrame:CGRectZero];
_searchBar.delegate = self;
}
return _searchBar;
}
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if ([searchBar.text length] > 0) {
self.active = true;
} else {
self.active = false;
}
}
#end
Finally, change your code to instantiate this subclass:
self.searchController = [[MySearchController alloc] initWithSearchResultsController:self.resultsViewController];
(You will obviously need to import the relevant header files for these subclasses).

Easy solution in Swift3 - we need to make CustomSearchBar without cancel button and then override the corresponding property in new CustomSearchController:
class CustomSearchBar: UISearchBar {
override func setShowsCancelButton(_ showsCancelButton: Bool, animated: Bool) {
super.setShowsCancelButton(false, animated: false)
}}
class CustomSearchController: UISearchController {
lazy var _searchBar: CustomSearchBar = {
[unowned self] in
let customSearchBar = CustomSearchBar(frame: CGRect.zero)
return customSearchBar
}()
override var searchBar: UISearchBar {
get {
return _searchBar
}
}}
In MyViewController I initialize and configure searchController using this new custom subclass:
var videoSearchController: UISearchController = ({
// Display search results in a separate view controller
// let storyBoard = UIStoryboard(name: "Main", bundle: Bundle.main)
// let alternateController = storyBoard.instantiateViewController(withIdentifier: "aTV") as! AlternateTableViewController
// let controller = UISearchController(searchResultsController: alternateController)
let controller = CustomSearchController(searchResultsController: nil)
controller.searchBar.placeholder = NSLocalizedString("Enter keyword (e.g. iceland)", comment: "")
controller.hidesNavigationBarDuringPresentation = false
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.searchBarStyle = .minimal
controller.searchBar.sizeToFit()
return controller
})()
And it works properly and smooth

You could do like this:
- (void)willPresentSearchController:(UISearchController *)searchController {
dispatch_async(dispatch_get_main_queue(), ^{
searchController.searchBar.showsCancelButton = NO;
}); }

#pbasdf's answer works for the most part, but checking the searchText length to determine whether the UISearchController is active can add more work to the user. The corner case would be if the user hits the clear button, or deletes the only character in the search bar. This would set active to NO, which would automatically call resignFirstResponder on the UISearchBar. The keyboard would disappear and if the user wants to change the text or enter more text, it would require tapping again on the search bar.
Instead, I only set active to NO if the search bar is not the first responder (keyboard is not active and displayed), since that is effectively a cancel command.
FJSearchBar
Marking searchController.searchBar.showsCancelButton = NO doesn't seem to work in iOS 8. I haven't tested iOS 9.
FJSearchBar.h
Empty, but placed here for completeness.
#import UIKit;
#interface FJSearchBar : UISearchBar
#end
FJSearchBar.m
#import "FJSearchBar.h"
#implementation FJSearchBar
- (void)setShowsCancelButton:(BOOL)showsCancelButton {
// do nothing
}
- (void)setShowsCancelButton:(BOOL)showsCancelButton animated:(BOOL)animated {
// do nothing
}
#end
FJSearchController
Here's where you want to make the real changes. I split the UISearchBarDelegate into its own category because, IMHO, the categories make the classes cleaner and easier to maintain. If you want to keep the delegate within the main class interface/implementation, you're more than welcome to do so.
FJSearchController.h
#import UIKit;
#interface FJSearchController : UISearchController
#end
#interface FJSearchController (UISearchBarDelegate) <UISearchBarDelegate>
#end
FJSearchController.m
#import "FJSearchController.h"
#import "FJSearchBar.h"
#implementation FJSearchController {
#private
FJSearchBar *_searchBar;
BOOL _clearedOutside;
}
- (UISearchBar *)searchBar {
if (_searchBar == nil) {
// if you're not hiding the cancel button, simply uncomment the line below and delete the FJSearchBar alloc/init
// _searchBar = [[UISearchBar alloc] init];
_searchBar = [[FJSearchBar alloc] init];
_searchBar.delegate = self;
}
return _searchBar;
}
#end
#implementation FJSearchController (UISearchBarDelegate)
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
// if we cleared from outside then we should not allow any new editing
BOOL shouldAllowEditing = !_clearedOutside;
_clearedOutside = NO;
return shouldAllowEditing;
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
// hide the keyboard since the user will no longer add any more input
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (![searchBar isFirstResponder]) {
// the user cleared the search while not in typing mode, so we should deactivate searching
self.active = NO;
_clearedOutside = YES;
return;
}
// update the search results
[self.searchResultsUpdater updateSearchResultsForSearchController:self];
}
#end
Some parts to note:
I've put the search bar and the BOOL as private variables instead of properties because
They're more lightweight than private properties.
They don't need to be seen or modified by the outside world.
We check whether the searchBar is the first responder. If it's not, then we actually deactivate the search controller because the text is empty and we're no longer searching. If you really want to be sure, you can also ensure that searchText.length == 0.
searchBar:textDidChange: is invoked before searchBarShouldBeginEditing:, which is why we handled it in this order.
I update the search results every time the text changes, but you may want to move the [self.searchResultsUpdater updateSearchResultsForSearchController:self]; to searchBarSearchButtonClicked: if you only want the search performed after the user presses the Search button.

I was able to get the UISearchBar to behave as desired without subclassing by calling setShowsCancelButton in a couple of UISearchBarDelegate methods:
I call it in textDidChange and searchBarCancelButtonClicked. Here's what my implementation looks like:
extension MyViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.characters.isEmpty == false {
searchBar.setShowsCancelButton(true, animated: true)
// whatever extra stuff you need to do
} else {
searchBar.setShowsCancelButton(false, animated: true)
// whatever extra stuff you need to do
}
// whatever extra stuff you need to do
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.setShowsCancelButton(false, animated: false)
searchBar.text = nil
searchBar.resignFirstResponder()
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
// whatever extra stuff you need to do
}
}

Related

Keyboard is appearing and immediately disappearing when UISearchBar.becomeFirstResponder() is getting called

I have UISearchController in the navigationItem.searchController and I want to make it focus when the user selects "Search" from the menu.
So shortly, when the user is tapping on the "Search" option in the menu (UITableViewCell) it's getting the view controller that have the searchController in it and calling:
guard let navigationVC = presentingViewController as? UINavigationController else { return }
guard let documentsVC = navigationVC.topViewController as? DocumentsViewController else { return }
documentsVC.searchController.searchBar.becomeFirstResponder()
Then, the UISearchBar is getting focus, the keyboard is appearing and then it's immediately disappearing, and I don't have any code that would make it disappear (like view.endEditing()).
1 GIF is worth more than 1,000 words:
So, after many tries I got some way to make it work, but I'm sure there is a much more elegant ways to do this, so if someone think that they have better way, please post it here and I may use it and mark your answer as the correct one.
Create the function focusOnSearchBar() in YourViewController:
func focusOnSearchBar() {
let searchBar = searchController.searchBar
if searchBar.canBecomeFirstResponder {
DispatchQueue.main.async {
searchBar.becomeFirstResponder()
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.focusOnSearchBar()
}
}
}
What it actually do is use itself recursively and check (every 0.1 sec) if searchBar.canBecomeFirstResponder. This is the problematic/not elegant thing.
Then, add this to viewDidAppear():
if focusOnSearch {
searchController.isActive = true
}
Don't forget to add extension to your ViewController for UISearchControllerDelegate (and of course, set searchController.delegate = self) and implement didPresentSearchController (that will be invoke by setting searchController.isActive = true):
extension YourViewController: UISearchControllerDelegate {
func didPresentSearchController(_ searchController: UISearchController) {
if focusOnSearch {
focusOnSearchBar()
}
}
}
Now all you have to do is to set focusOnSearch = true in the prepare(for segue:sender:).
*Note: if you want to focusOnSearchBar while you are in the same viewController of the searchBar, just set:
focusOnSearch = true
searchController.isActive = true
And it will work by itself.
Make your searchbar first responder in the viewDidLoad method. That will make sure everything is ready before focusing the search bar.

How to display empty tableview by default when using UISearchController in Swift? [duplicate]

As I understand, the default behaviour of UISearchController is:
On tapping search bar, background is dimmed and 'cancel' button is shown. SearchResultsController is not shown till this point.
SearchResultsController is displayed only if search bar is not empty.
I want to display SearchResultsController even when search bar is empty but selected (i.e is case 1 above).
Simply put, instead of background dimming, I would like to show Search results.
Is there a way for doing this?
More Clarification:
I am not using UISearchController to filter results shown on the view on which it is shown, but some other unrelated results.
It will be like what facebook does on its 'News Feed'. Tapping on search bar shows search suggestions initially and then, when we start editing, it shows search results which might not be related to news feed.
You can simply implement the UISearchResultsUpdating protocol and set the results controller view to always show in updateSearchResultsForSearchController:
func updateSearchResultsForSearchController(searchController: UISearchController) {
// Always show the search result controller
searchController.searchResultsController?.view.hidden = false
// Update your search results data and reload data
..
}
This works because the method is called even when the search bar is activated, without any text.
If your searchBar is active but has no text, the underlying tableView results are shown. That's the built-in behavior, and the reason why searchResultsController is hidden for that state.
To change the behavior when search is active but not filtering, you're going to have to show the searchResultsController when it is normally still hidden.
There may be a good way to accomplish this via <UISearchResultsUpdating> and updateSearchResultsForSearchController:. If you can solve it via the protocol, that's the preferred way to go.
If that doesn't help, you're left with hacking the built-in behavior. I wouldn't recommend or rely on it, and it's going to be fragile, but here's an answer if you choose that option:
Make sure your tableViewController conforms to <UISearchControllerDelegate>, and add
self.searchController.delegate = self;
Implement willPresentSearchController:
- (void)willPresentSearchController:(UISearchController *)searchController
{
dispatch_async(dispatch_get_main_queue(), ^{
searchController.searchResultsController.view.hidden = NO;
});
}
This makes the searchResultsController visible after its UISearchController set it to hidden.
Implement didPresentSearchController:
- (void)didPresentSearchController:(UISearchController *)searchController
{
searchController.searchResultsController.view.hidden = NO;
}
For a better way to work around the built-in behavior, see malhal's answer.
Updated for iOS 13
From iOS13, we got system API support for this behaviour. You can set the property showsSearchResultsController = true
For iOS 12 and below
I am recently working on UISearchController. I want to show search history in searchResultsController when search bar is empty. So searchResultsController needs to show up whenever UISearchController gets presented.
Here, I use another solution to make the searchResultsController always visible by overriding the hidden property in a custom view.
for example, my searchResultsController is a UITableViewController. I create a VisibleTableView as a subclass of UITableView, and then change the UITableView custom class of searchResultsController to VisibleTableView in xib or storyboard. This way, my searchResultsController will never be hidden by UISearchController.
The good things here:
Easier to implement than KVO.
No delay to show searchResultsController. Flipping the hidden flag in "updateSearchResults" delegate method works, but there is a delay to show the searchResultsController.
It does't reset the hidden flag, so there is no UI gap/jumping between hidden and visible.
Swift 3 sample code:
class VisibleTableView: UITableView {
override var isHidden: Bool {
get {
return false
}
set {
// ignoring any settings
}
}
}
I have tried PetahChristian solution, the preload result did show up when we first focus the searchbar, but when we enter something then clear it, the preload results will not reappear.
I came up with another solution. We only need to add a delegate into SearchResultsController and call it when our searchController.searchBar.text is empty. Something like this:
SearchResultsController:
protocol SearchResultsViewControllerDelegate {
func reassureShowingList() -> Void
}
class FullSearchResultsViewController: UIViewController, UISearchResultsUpdating{
var delegate: SearchResultsViewControllerDelegate?
...
func updateSearchResultsForSearchController(searchController: UISearchController) {
let query = searchController.searchBar.text?.trim()
if query == nil || query!.isEmpty {
...
self.delegate?.reassureShowingList()
...
}
...
}
And in the controller contains the SearchController, we add our delegate:
self.searchResultsController.delegate = self
func reassureShowingList() {
searchController.searchResultsController!.view.hidden = false
}
With tricky things like this I recommend the sledge hammer approach! That is to detect when something tries to make it hidden and when it does, change it back. This can be done via KVO (Key Value Observing). This will work no matter what, without having to handle all the intricacies of the search bar. Sorry the code is complicated but KVO is an older style API but my code follows recommend practice. In your SearchResultsViewController put this:
static int kHidden;
#implementation SearchResultsViewController
-(void)viewDidLoad{
[super viewDidLoad];
[self.view addObserver:self
forKeyPath:#"hidden"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:&kHidden];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// if it was our observation
if(context == &kHidden){
// if the view is hidden then make it visible.
if([[change objectForKey:NSKeyValueChangeNewKey] boolValue]){
self.view.hidden = NO;
}
}
else{
// if necessary, pass the method up the subclass hierarchy.
if([super respondsToSelector:#selector(observeValueForKeyPath:ofObject:change:context:)]){
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
}
- (void)dealloc
{
[self.view removeObserver:self forKeyPath:#"hidden"];
}
// Here have the rest of your code for the search results table.
#end
This works in all cases including if the text is cleared.
Lastly, to prevent the table doing an ugly fade to grey then to white when the search activates, use this:
self.searchController.dimsBackgroundDuringPresentation = NO;
Swift 3 Version:
If your searchResultController is not nil and you are using a separate table view controller to show the search results, then you can make that table view controller conform to UISearchResultUpdating and in the updateSearchResults function, you can simply unhide the view.
func updateSearchResults(for searchController: UISearchController) {
view.hidden = false
}
Swift 4 Version:
func updateSearchResults(for searchController: UISearchController) {
view.isHidden = false
}
What is being hidden is the search results controller's view. Therefore it is sufficient to unhide it any time it might be hidden. Simply do as follows in the search results controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.isHidden = false
}
func updateSearchResults(for searchController: UISearchController) {
self.view.isHidden = false
// ... your other code goes here ...
}
Now the results view (i.e. the table view) is always visible, even when the search bar text is empty.
By the way, the iOS Mail app behaves like this, and I assume that's how it's implemented (unless Apple has access to some secret private UISearchController setting).
[Tested in iOS 10 and iOS 11; I didn't test on any earlier system.]
The Swift 2.3 version of #malhal's approach:
class SearchResultsViewController : UIViewController {
var context = 0
override func viewDidLoad() {
super.viewDidLoad()
// Add observer
view.addObserver(self, forKeyPath: "hidden", options: [ .New, .Old ], context: &context)
}
deinit {
view.removeObserver(self, forKeyPath: "hidden")
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &self.context {
if change?[NSKeyValueChangeNewKey] as? Bool == true {
view.hidden = false
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
}
Swift 4 version of malhals answer:
class SearchController: UISearchController {
private var viewIsHiddenObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
viewIsHiddenObserver = self.searchResultsController?.view.observe(\.hidden, changeHandler: { [weak self] (view, _) in
guard let searchController = self else {return}
if view.isHidden && searchController.searchBar.isFirstResponder {
view.isHidden = false
}
})
}
}
Please note the [weak self]. Otherwise you would introduce a retain cycle.
I think you are mistaken.
SearchResultsController only appears when there are results. This is slightly different than your interpretation.
The results are loaded manually based on the text in the search bar. So you can intercept it if the search bar is empty and return your own set of results.
If you don't want to dim the results, set the dimsBackgroundDuringPresentation property to false.
This will make sure that the underlying content is not dimmed during a search.
You will also have to make sure you return results even when the searchText is empty otherwise an empty tableview will be displayed.
I spent a lot of time with this, and ultimately the solution I went with is like #malhals's, but the amount of code is significantly reduced by using facebook's KVOController: https://github.com/facebook/KVOController . Another advantage here is that if your searchResultsController is a UINavigationController then you don't need to subclass it just to add #malhal's code.
// always show searchResultsController, even if text is empty
[self.KVOController observe:self.searchController.searchResultsController.view keyPath:#"hidden" options:NSKeyValueObservingOptionNew block:^(id observer, UIView* view, NSDictionary *change) {
if ([change[NSKeyValueChangeNewKey] boolValue] == YES) {
view.hidden = NO;
}
}];
self.searchController.dimsBackgroundDuringPresentation = NO;
The easiest way is to use ReactiveCocoa with this extension https://github.com/ColinEberhardt/ReactiveTwitterSearch/blob/master/ReactiveTwitterSearch/Util/UIKitExtensions.swift
presentViewController(sc, animated: true, completion: {
sc.searchResultsController?.view.rac_hidden.modify({ value -> Bool in
return false
})
} )
where sc is your UISearchController
I really liked Simon Wang's answer and worked with it and this is what I did and it works perfectly:
I subclass the UISearchController in my custom class:
class CustomClass: UISearchController {
override var searchResultsController: UIViewController? {
get {
let viewController = super.searchResultsController
viewController?.view.isHidden = false
return viewController
}
set {
// nothing
}
}
}
Also make sure you don't have this anywhere in your code:
self.resultsSearchController.isActive = true
resultsSearchController is my UISearchController
Simply what I was using this case
func updateSearchResults(for searchController: UISearchController) {
if let inputText = searchController.searchBar.text, !inputText.isEmpty {
self.view.isHidden = false
}
}
where self.view is a view of "searchResultsController" during initialisation of UISearchController.
var searchController = UISearchController(searchResultsController: searchResultsController)

UISearchController: show results even when search bar is empty

As I understand, the default behaviour of UISearchController is:
On tapping search bar, background is dimmed and 'cancel' button is shown. SearchResultsController is not shown till this point.
SearchResultsController is displayed only if search bar is not empty.
I want to display SearchResultsController even when search bar is empty but selected (i.e is case 1 above).
Simply put, instead of background dimming, I would like to show Search results.
Is there a way for doing this?
More Clarification:
I am not using UISearchController to filter results shown on the view on which it is shown, but some other unrelated results.
It will be like what facebook does on its 'News Feed'. Tapping on search bar shows search suggestions initially and then, when we start editing, it shows search results which might not be related to news feed.
You can simply implement the UISearchResultsUpdating protocol and set the results controller view to always show in updateSearchResultsForSearchController:
func updateSearchResultsForSearchController(searchController: UISearchController) {
// Always show the search result controller
searchController.searchResultsController?.view.hidden = false
// Update your search results data and reload data
..
}
This works because the method is called even when the search bar is activated, without any text.
If your searchBar is active but has no text, the underlying tableView results are shown. That's the built-in behavior, and the reason why searchResultsController is hidden for that state.
To change the behavior when search is active but not filtering, you're going to have to show the searchResultsController when it is normally still hidden.
There may be a good way to accomplish this via <UISearchResultsUpdating> and updateSearchResultsForSearchController:. If you can solve it via the protocol, that's the preferred way to go.
If that doesn't help, you're left with hacking the built-in behavior. I wouldn't recommend or rely on it, and it's going to be fragile, but here's an answer if you choose that option:
Make sure your tableViewController conforms to <UISearchControllerDelegate>, and add
self.searchController.delegate = self;
Implement willPresentSearchController:
- (void)willPresentSearchController:(UISearchController *)searchController
{
dispatch_async(dispatch_get_main_queue(), ^{
searchController.searchResultsController.view.hidden = NO;
});
}
This makes the searchResultsController visible after its UISearchController set it to hidden.
Implement didPresentSearchController:
- (void)didPresentSearchController:(UISearchController *)searchController
{
searchController.searchResultsController.view.hidden = NO;
}
For a better way to work around the built-in behavior, see malhal's answer.
Updated for iOS 13
From iOS13, we got system API support for this behaviour. You can set the property showsSearchResultsController = true
For iOS 12 and below
I am recently working on UISearchController. I want to show search history in searchResultsController when search bar is empty. So searchResultsController needs to show up whenever UISearchController gets presented.
Here, I use another solution to make the searchResultsController always visible by overriding the hidden property in a custom view.
for example, my searchResultsController is a UITableViewController. I create a VisibleTableView as a subclass of UITableView, and then change the UITableView custom class of searchResultsController to VisibleTableView in xib or storyboard. This way, my searchResultsController will never be hidden by UISearchController.
The good things here:
Easier to implement than KVO.
No delay to show searchResultsController. Flipping the hidden flag in "updateSearchResults" delegate method works, but there is a delay to show the searchResultsController.
It does't reset the hidden flag, so there is no UI gap/jumping between hidden and visible.
Swift 3 sample code:
class VisibleTableView: UITableView {
override var isHidden: Bool {
get {
return false
}
set {
// ignoring any settings
}
}
}
I have tried PetahChristian solution, the preload result did show up when we first focus the searchbar, but when we enter something then clear it, the preload results will not reappear.
I came up with another solution. We only need to add a delegate into SearchResultsController and call it when our searchController.searchBar.text is empty. Something like this:
SearchResultsController:
protocol SearchResultsViewControllerDelegate {
func reassureShowingList() -> Void
}
class FullSearchResultsViewController: UIViewController, UISearchResultsUpdating{
var delegate: SearchResultsViewControllerDelegate?
...
func updateSearchResultsForSearchController(searchController: UISearchController) {
let query = searchController.searchBar.text?.trim()
if query == nil || query!.isEmpty {
...
self.delegate?.reassureShowingList()
...
}
...
}
And in the controller contains the SearchController, we add our delegate:
self.searchResultsController.delegate = self
func reassureShowingList() {
searchController.searchResultsController!.view.hidden = false
}
With tricky things like this I recommend the sledge hammer approach! That is to detect when something tries to make it hidden and when it does, change it back. This can be done via KVO (Key Value Observing). This will work no matter what, without having to handle all the intricacies of the search bar. Sorry the code is complicated but KVO is an older style API but my code follows recommend practice. In your SearchResultsViewController put this:
static int kHidden;
#implementation SearchResultsViewController
-(void)viewDidLoad{
[super viewDidLoad];
[self.view addObserver:self
forKeyPath:#"hidden"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:&kHidden];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// if it was our observation
if(context == &kHidden){
// if the view is hidden then make it visible.
if([[change objectForKey:NSKeyValueChangeNewKey] boolValue]){
self.view.hidden = NO;
}
}
else{
// if necessary, pass the method up the subclass hierarchy.
if([super respondsToSelector:#selector(observeValueForKeyPath:ofObject:change:context:)]){
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
}
- (void)dealloc
{
[self.view removeObserver:self forKeyPath:#"hidden"];
}
// Here have the rest of your code for the search results table.
#end
This works in all cases including if the text is cleared.
Lastly, to prevent the table doing an ugly fade to grey then to white when the search activates, use this:
self.searchController.dimsBackgroundDuringPresentation = NO;
Swift 3 Version:
If your searchResultController is not nil and you are using a separate table view controller to show the search results, then you can make that table view controller conform to UISearchResultUpdating and in the updateSearchResults function, you can simply unhide the view.
func updateSearchResults(for searchController: UISearchController) {
view.hidden = false
}
Swift 4 Version:
func updateSearchResults(for searchController: UISearchController) {
view.isHidden = false
}
What is being hidden is the search results controller's view. Therefore it is sufficient to unhide it any time it might be hidden. Simply do as follows in the search results controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.isHidden = false
}
func updateSearchResults(for searchController: UISearchController) {
self.view.isHidden = false
// ... your other code goes here ...
}
Now the results view (i.e. the table view) is always visible, even when the search bar text is empty.
By the way, the iOS Mail app behaves like this, and I assume that's how it's implemented (unless Apple has access to some secret private UISearchController setting).
[Tested in iOS 10 and iOS 11; I didn't test on any earlier system.]
The Swift 2.3 version of #malhal's approach:
class SearchResultsViewController : UIViewController {
var context = 0
override func viewDidLoad() {
super.viewDidLoad()
// Add observer
view.addObserver(self, forKeyPath: "hidden", options: [ .New, .Old ], context: &context)
}
deinit {
view.removeObserver(self, forKeyPath: "hidden")
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &self.context {
if change?[NSKeyValueChangeNewKey] as? Bool == true {
view.hidden = false
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
}
Swift 4 version of malhals answer:
class SearchController: UISearchController {
private var viewIsHiddenObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
viewIsHiddenObserver = self.searchResultsController?.view.observe(\.hidden, changeHandler: { [weak self] (view, _) in
guard let searchController = self else {return}
if view.isHidden && searchController.searchBar.isFirstResponder {
view.isHidden = false
}
})
}
}
Please note the [weak self]. Otherwise you would introduce a retain cycle.
I think you are mistaken.
SearchResultsController only appears when there are results. This is slightly different than your interpretation.
The results are loaded manually based on the text in the search bar. So you can intercept it if the search bar is empty and return your own set of results.
If you don't want to dim the results, set the dimsBackgroundDuringPresentation property to false.
This will make sure that the underlying content is not dimmed during a search.
You will also have to make sure you return results even when the searchText is empty otherwise an empty tableview will be displayed.
I spent a lot of time with this, and ultimately the solution I went with is like #malhals's, but the amount of code is significantly reduced by using facebook's KVOController: https://github.com/facebook/KVOController . Another advantage here is that if your searchResultsController is a UINavigationController then you don't need to subclass it just to add #malhal's code.
// always show searchResultsController, even if text is empty
[self.KVOController observe:self.searchController.searchResultsController.view keyPath:#"hidden" options:NSKeyValueObservingOptionNew block:^(id observer, UIView* view, NSDictionary *change) {
if ([change[NSKeyValueChangeNewKey] boolValue] == YES) {
view.hidden = NO;
}
}];
self.searchController.dimsBackgroundDuringPresentation = NO;
The easiest way is to use ReactiveCocoa with this extension https://github.com/ColinEberhardt/ReactiveTwitterSearch/blob/master/ReactiveTwitterSearch/Util/UIKitExtensions.swift
presentViewController(sc, animated: true, completion: {
sc.searchResultsController?.view.rac_hidden.modify({ value -> Bool in
return false
})
} )
where sc is your UISearchController
I really liked Simon Wang's answer and worked with it and this is what I did and it works perfectly:
I subclass the UISearchController in my custom class:
class CustomClass: UISearchController {
override var searchResultsController: UIViewController? {
get {
let viewController = super.searchResultsController
viewController?.view.isHidden = false
return viewController
}
set {
// nothing
}
}
}
Also make sure you don't have this anywhere in your code:
self.resultsSearchController.isActive = true
resultsSearchController is my UISearchController
Simply what I was using this case
func updateSearchResults(for searchController: UISearchController) {
if let inputText = searchController.searchBar.text, !inputText.isEmpty {
self.view.isHidden = false
}
}
where self.view is a view of "searchResultsController" during initialisation of UISearchController.
var searchController = UISearchController(searchResultsController: searchResultsController)

iOS8 Cannot hide cancel button on search bar in UISearchController

My goal is to prevent the cancel button from appearing in a search bar in a UISearchController. I started with Apple's Table Search with UISearchController sample code and hid the cancel button as seen in the code snip below. However, when the user taps in the text field, the cancel button still appears. Any help?
override func viewDidLoad() {
super.viewDidLoad()
resultsTableController = ResultsTableController()
searchController = UISearchController(searchResultsController: resultsTableController)
searchController.searchResultsUpdater = self
searchController.searchBar.sizeToFit()
tableView.tableHeaderView = searchController.searchBar
searchController.searchBar.delegate = self
//Hide cancel button - added by me
searchController.searchBar.showsCancelButton = false
...
I think there are three ways of achieving that:
Override searchDisplayControllerDidBeginSearch and use the following code:
searchController.searchBar.showsCancelButton = false
Subclass UISearchBar and override the layoutSubviews to change that var when the system attempts to draw it.
Register for keyboard notification UIKeyboardWillShowNotification and apply the code in point 1.
Of course can always implement your search bar.
For iOS 8, and UISearchController, use this delegate method from UISearchControllerDelegate:
func didPresentSearchController(searchController: UISearchController) {
searchController.searchBar.showsCancelButton = false
}
Don't forget to set yourself as the delegate: searchController.delegate = self
Simply subclass UISearchController & UISearchBar.
class NoCancelButtonSearchController: UISearchController {
let noCancelButtonSearchBar = NoCancelButtonSearchBar()
override var searchBar: UISearchBar { return noCancelButtonSearchBar }
}
class NoCancelButtonSearchBar: UISearchBar {
override func setShowsCancelButton(_ showsCancelButton: Bool, animated: Bool) { /* void */ }
}
The following github project subclasses UISearchBar which is presented as solution 2:
https://github.com/mechaman/CustomSearchControllerSwift
On top of it, it also subclasses UISearchController to enable one to put the search bar in places other than the tableView header!
Hope this helps.
This was the simplest solution I could come up with in Swift.
Custom search controller:
class CustomSearchController: UISearchController {
var _searchBar: CustomSearchBar
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
self._searchBar = CustomSearchBar()
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
override init(searchResultsController: UIViewController?) {
self._searchBar = CustomSearchBar()
super.init(searchResultsController: searchResultsController)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var searchBar: UISearchBar {
return self._searchBar
}
}
Custom search bar:
class CustomSearchBar: UISearchBar {
override func setShowsCancelButton(showsCancelButton: Bool, animated: Bool) {
// do nothing
}
}
The most important piece of this was to only create the _searchBar object once in init vs. creating it inside of the stored property.
Just subclass your UISearchController and do the following:
class CustomSearchController: UISearchController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
searchBar.showsCancelButton = false
}
}
This was the easiest solution I could came up with in order to solve the flashing cancel-button issue.
TL;DR:
Subclassing UISearchBar and overriding setShowsCancelButton: and setShowsCancelButton:animated: hides the cancel button.
SOLUTION
I set active to NO if the search bar is not the first responder (keyboard is not active and displayed), since that is effectively a cancel command.
FJSearchBar
Marking searchController.searchBar.showsCancelButton = NO doesn't seem to work in iOS 8. I haven't tested iOS 9.
FJSearchBar.h
Empty, but placed here for completeness.
#import UIKit;
#interface FJSearchBar : UISearchBar
#end
FJSearchBar.m
#import "FJSearchBar.h"
#implementation FJSearchBar
- (void)setShowsCancelButton:(BOOL)showsCancelButton {
// do nothing
}
- (void)setShowsCancelButton:(BOOL)showsCancelButton animated:(BOOL)animated {
// do nothing
}
#end
FJSearchController
Here's where you want to make the real changes. I split the UISearchBarDelegate into its own category because, IMHO, the categories make the classes cleaner and easier to maintain. If you want to keep the delegate within the main class interface/implementation, you're more than welcome to do so.
FJSearchController.h
#import UIKit;
#interface FJSearchController : UISearchController
#end
#interface FJSearchController (UISearchBarDelegate) <UISearchBarDelegate>
#end
FJSearchController.m
#import "FJSearchController.h"
#import "FJSearchBar.h"
#implementation FJSearchController {
#private
FJSearchBar *_searchBar;
BOOL _clearedOutside;
}
- (UISearchBar *)searchBar {
if (_searchBar == nil) {
// if you're not hiding the cancel button, simply uncomment the line below and delete the FJSearchBar alloc/init
// _searchBar = [[UISearchBar alloc] init];
_searchBar = [[FJSearchBar alloc] init];
_searchBar.delegate = self;
}
return _searchBar;
}
#end
#implementation FJSearchController (UISearchBarDelegate)
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
// if we cleared from outside then we should not allow any new editing
BOOL shouldAllowEditing = !_clearedOutside;
_clearedOutside = NO;
return shouldAllowEditing;
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
// hide the keyboard since the user will no longer add any more input
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (![searchBar isFirstResponder]) {
// the user cleared the search while not in typing mode, so we should deactivate searching
self.active = NO;
_clearedOutside = YES;
return;
}
// update the search results
[self.searchResultsUpdater updateSearchResultsForSearchController:self];
}
#end
Some parts to note:
I've put the search bar and the BOOL as private variables instead of properties because
They're more lightweight than private properties.
They don't need to be seen or modified by the outside world.
We check whether the searchBar is the first responder. If it's not, then we actually deactivate the search controller because the text is empty and we're no longer searching. If you really want to be sure, you can also ensure that searchText.length == 0.
searchBar:textDidChange: is invoked before searchBarShouldBeginEditing:, which is why we handled it in this order.
I update the search results every time the text changes, but you may want to move the [self.searchResultsUpdater updateSearchResultsForSearchController:self]; to searchBarSearchButtonClicked: if you only want the search performed after the user presses the Search button.
Swift:
The following worked for me, added under viewDidLoad, because I never wanted that button:
let searchBarStyle = searchBar.value(forKey: "searchField") as? UITextField
searchBarStyle?.clearButtonMode = .never
Make sure to add the ID for the searchBar in the storyboard.
Use UISearchControllerDelegate.
func willPresentSearchController(_ searchController: UISearchController) {
searchController.searchBar.setValue("", forKey:"_cancelButtonText")
}

Setting action for back button in navigation controller

I'm trying to overwrite the default action of the back button in a navigation controller. I've provided a target an action on the custom button. The odd thing is when assigning it though the backbutton attribute it doesn't pay attention to them and it just pops the current view and goes back to the root:
UIBarButtonItem *backButton = [[UIBarButtonItem alloc]
initWithTitle: #"Servers"
style:UIBarButtonItemStylePlain
target:self
action:#selector(home)];
self.navigationItem.backBarButtonItem = backButton;
As soon as I set it through the leftBarButtonItem on the navigationItem it calls my action, however then the button looks like a plain round one instead of the arrowed back one:
self.navigationItem.leftBarButtonItem = backButton;
How can I get it to call my custom action before going back to the root view? Is there a way to overwrite the default back action, or is there a method that is always called when leaving a view (viewDidUnload doesn't do that)?
Try putting this into the view controller where you want to detect the press:
-(void) viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
// back button was pressed. We know this is true because self is no longer
// in the navigation stack.
}
[super viewWillDisappear:animated];
}
I've implemented UIViewController-BackButtonHandler extension. It does not need to subclass anything, just put it into your project and override navigationShouldPopOnBackButton method in UIViewController class:
-(BOOL) navigationShouldPopOnBackButton {
if(needsShowConfirmation) {
// Show confirmation alert
// ...
return NO; // Ignore 'Back' button this time
}
return YES; // Process 'Back' button click and pop view controller
}
Download sample app.
Unlike Amagrammer said, it's possible. You have to subclass your navigationController. I explained everything here (including example code).
Swift Version:
(of https://stackoverflow.com/a/19132881/826435)
In your view controller you just conform to a protocol and perform whatever action you need:
extension MyViewController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool {
performSomeActionOnThePressOfABackButton()
return false
}
}
Then create a class, say NavigationController+BackButton, and just copy-paste the code below:
protocol NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool
}
extension UINavigationController {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
// Prevents from a synchronization issue of popping too many navigation items
// and not enough view controllers or viceversa from unusual tapping
if viewControllers.count < navigationBar.items!.count {
return true
}
// Check if we have a view controller that wants to respond to being popped
var shouldPop = true
if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
shouldPop = viewController.shouldPopOnBackButtonPress()
}
if (shouldPop) {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
// Prevent the back button from staying in an disabled state
for view in navigationBar.subviews {
if view.alpha < 1.0 {
UIView.animate(withDuration: 0.25, animations: {
view.alpha = 1.0
})
}
}
}
return false
}
}
It isn't possible to do directly. There are a couple alternatives:
Create your own custom UIBarButtonItem that validates on tap and pops if the test passes
Validate the form field contents using a UITextField delegate method, such as -textFieldShouldReturn:, which is called after the Return or Done button is pressed on the keyboard
The downside of the first option is that the left-pointing-arrow style of the back button cannot be accessed from a custom bar button. So you have to use an image or go with a regular style button.
The second option is nice because you get the text field back in the delegate method, so you can target your validation logic to the specific text field sent to the delegate call-back method.
For some threading reasons, the solution mentionned by #HansPinckaers wasn't right for me, but I found a very easier way to catch a touch on the back button, and I wanna pin this down here in case this could avoid hours of deceptions for someone else.
The trick is really easy : just add a transparent UIButton as a subview to your UINavigationBar, and set your selectors for him as if it was the real button!
Here's an example using Monotouch and C#, but the translation to objective-c shouldn't be too hard to find.
public class Test : UIViewController {
public override void ViewDidLoad() {
UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
b.BackgroundColor = UIColor.Clear; //making the background invisible
b.Title = string.Empty; // and no need to write anything
b.TouchDown += delegate {
Console.WriteLine("caught!");
if (true) // check what you want here
NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
};
NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
}
}
Fun fact : for testing purposes and to find good dimensions for my fake button, I set its background color to blue... And it shows behind the back button! Anyway, it still catches any touch targetting the original button.
Overriding navigationBar(_ navigationBar:shouldPop): This is not a good idea, even if it works. for me it generated random crashes on navigating back. I advise you to just override the back button by removing the default backButton from navigationItem and creating a custom back button like below:
override func viewDidLoad(){
super.viewDidLoad()
navigationItem.leftBarButton = .init(title: "Go Back", ... , action: #selector(myCutsomBackAction)
...
}
========================================
Building on previous responses with UIAlert in Swift5 in a Asynchronous way
protocol NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress(_ completion: #escaping (Bool) -> ())
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count < navigationBar.items!.count {
return true
}
// Check if we have a view controller that wants to respond to being popped
if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
viewController.shouldPopOnBackButtonPress { shouldPop in
if (shouldPop) {
/// on confirm => pop
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
/// on cancel => do nothing
}
}
/// return false => so navigator will cancel the popBack
/// until user confirm or cancel
return false
}else{
DispatchQueue.main.async {
self.popViewController(animated: true)
}
}
return true
}
}
On your controller
extension MyController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress(_ completion: #escaping (Bool) -> ()) {
let msg = "message"
/// show UIAlert
alertAttention(msg: msg, actions: [
.init(title: "Continuer", style: .destructive, handler: { _ in
completion(true)
}),
.init(title: "Annuler", style: .cancel, handler: { _ in
completion(false)
})
])
}
}
This technique allows you to change the text of the "back" button without affecting the title of any of the view controllers or seeing the back button text change during the animation.
Add this to the init method in the calling view controller:
UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];
temporaryBarButtonItem.title = #"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
Easiest way
You can use the UINavigationController's delegate methods. The method willShowViewController is called when the back button of your VC is pressed.do whatever you want when back btn pressed
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Here's my Swift solution. In your subclass of UIViewController, override the navigationShouldPopOnBackButton method.
extension UIViewController {
func navigationShouldPopOnBackButton() -> Bool {
return true
}
}
extension UINavigationController {
func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
if let vc = self.topViewController {
if vc.navigationShouldPopOnBackButton() {
self.popViewControllerAnimated(true)
} else {
for it in navigationBar.subviews {
let view = it as! UIView
if view.alpha < 1.0 {
[UIView .animateWithDuration(0.25, animations: { () -> Void in
view.alpha = 1.0
})]
}
}
return false
}
}
return true
}
}
Found a solution which retains the back button style as well.
Add the following method to your view controller.
-(void) overrideBack{
UIButton *transparentButton = [[UIButton alloc] init];
[transparentButton setFrame:CGRectMake(0,0, 50, 40)];
[transparentButton setBackgroundColor:[UIColor clearColor]];
[transparentButton addTarget:self action:#selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
[self.navigationController.navigationBar addSubview:transparentButton];
}
Now provide a functionality as needed in the following method:
-(void)backAction:(UIBarButtonItem *)sender {
//Your functionality
}
All it does is to cover the back button with a transparent button ;)
I don't believe this is possible, easily. The only way I believe to get around this is to make your own back button arrow image to place up there. It was frustrating for me at first but I see why, for consistency's sake, it was left out.
You can get close (without the arrow) by creating a regular button and hiding the default back button:
self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:#"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
There's an easier way by just subclassing the delegate method of the UINavigationBar and override the ShouldPopItemmethod.
This approach worked for me (but the "Back" button will not have the "<" sign):
- (void)viewDidLoad
{
[super viewDidLoad];
UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:#"Back"
style:UIBarButtonItemStyleBordered
target:self
action:#selector(backButtonClicked)];
self.navigationItem.leftBarButtonItem = backNavButton;
}
-(void)backButtonClicked
{
// Do something...
AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[delegate.navController popViewControllerAnimated:YES];
}
onegray's solution is not safe.According to the official documents by Apple,https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html,
we should avoid doing that.
"If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but can cause problems when using categories to add methods to standard Cocoa or Cocoa Touch classes."
Using Swift:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}
Here is Swift 3 version of #oneway's answer for catching navigation bar back button event before it gets fired. As UINavigationBarDelegate cannot be used for UIViewController, you need to create a delegate that will be triggered when navigationBar shouldPop is called.
#objc public protocol BackButtonDelegate {
#objc optional func navigationShouldPopOnBackButton() -> Bool
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count < (navigationBar.items?.count)! {
return true
}
var shouldPop = true
let vc = self.topViewController
if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
shouldPop = vc.navigationShouldPopOnBackButton()
}
if shouldPop {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
for subView in navigationBar.subviews {
if(0 < subView.alpha && subView.alpha < 1) {
UIView.animate(withDuration: 0.25, animations: {
subView.alpha = 1
})
}
}
}
return false
}
}
And then, in your view controller add the delegate function:
class BaseVC: UIViewController, BackButtonDelegate {
func navigationShouldPopOnBackButton() -> Bool {
if ... {
return true
} else {
return false
}
}
}
I've realised that we often want to add an alert controller for users to decide whether they wanna go back. If so, you can always return false in navigationShouldPopOnBackButton() function and close your view controller by doing something like this:
func navigationShouldPopOnBackButton() -> Bool {
let alert = UIAlertController(title: "Warning",
message: "Do you want to quit?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
present(alert, animated: true, completion: nil)
return false
}
func yes() {
print("yes")
DispatchQueue.main.async {
_ = self.navigationController?.popViewController(animated: true)
}
}
func no() {
print("no")
}
Swift 4 iOS 11.3 Version:
This builds on the answer from kgaidis from https://stackoverflow.com/a/34343418/4316579
I am not sure when the extension stopped working, but at the time of this writing (Swift 4), it appears that the extension will no longer be executed unless you declare UINavigationBarDelegate conformity as described below.
Hope this helps people that are wondering why their extension no longer works.
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
}
}
By using the target and action variables that you are currently leaving 'nil', you should be able to wire your save-dialogs in so that they are called when the button is "selected". Watch out, this may get triggered at strange moments.
I agree mostly with Amagrammer, but I don't think it would be that hard to make the button with the arrow custom. I would just rename the back button, take a screen shot, photoshop the button size needed, and have that be the image on the top of your button.
You can try accessing the NavigationBars Right Button item and set its selector property...heres a reference UIBarButtonItem reference, another thing if this doenst work that will def work is, set the right button item of the nav bar to a custom UIBarButtonItem that you create and set its selector...hope this helps
For a form that requires user input like this, I would recommend invoking it as a "modal" instead of part of your navigation stack. That way they have to take care of business on the form, then you can validate it and dismiss it using a custom button. You can even design a nav bar that looks the same as the rest of your app but gives you more control.
To intercept the Back button, simply cover it with a transparent UIControl and intercept the touches.
#interface MyViewController : UIViewController
{
UIControl *backCover;
BOOL inhibitBackButtonBOOL;
}
#end
#implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Cover the back button (cannot do this in viewWillAppear -- too soon)
if ( backCover == nil ) {
backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
// show the cover for testing
backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
[backCover addTarget:self action:#selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
UINavigationBar *navBar = self.navigationController.navigationBar;
[navBar addSubview:backCover];
}
}
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[backCover removeFromSuperview];
backCover = nil;
}
- (void)backCoverAction
{
if ( inhibitBackButtonBOOL ) {
NSLog(#"Back button aborted");
// notify the user why...
} else {
[self.navigationController popViewControllerAnimated:YES]; // "Back"
}
}
#end
At least in Xcode 5, there is a simple and pretty good (not perfect) solution. In IB, drag a Bar Button Item off the Utilities pane and drop it on the left side of the Navigation Bar where the Back button would be. Set the label to "Back." You will have a functioning button that you can tie to your IBAction and close your viewController. I'm doing some work and then triggering an unwind segue and it works perfectly.
What isn't ideal is that this button does not get the < arrow and does not carry forward the previous VCs title, but I think this can be managed. For my purposes, I set the new Back button to be a "Done" button so it's purpose is clear.
You also end up with two Back buttons in the IB navigator, but it is easy enough to label it for clarity.
Swift
override func viewWillDisappear(animated: Bool) {
let viewControllers = self.navigationController?.viewControllers!
if indexOfArray(viewControllers!, searchObject: self) == nil {
// do something
}
super.viewWillDisappear(animated)
}
func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
for (index, value) in enumerate(array) {
if value as UIViewController == searchObject as UIViewController {
return index
}
}
return nil
}
Found new way to do it :
Objective-C
- (void)didMoveToParentViewController:(UIViewController *)parent{
if (parent == NULL) {
NSLog(#"Back Pressed");
}
}
Swift
override func didMoveToParentViewController(parent: UIViewController?) {
if parent == nil {
println("Back Pressed")
}
}
Swift version of #onegray's answer
protocol RequestsNavigationPopVerification {
var confirmationTitle: String { get }
var confirmationMessage: String { get }
}
extension RequestsNavigationPopVerification where Self: UIViewController {
var confirmationTitle: String {
return "Go back?"
}
var confirmationMessage: String {
return "Are you sure?"
}
}
final class NavigationController: UINavigationController {
func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
popViewControllerAnimated(true)
return true
}
let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
dispatch_async(dispatch_get_main_queue(), {
let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
UIView.animateWithDuration(0.25) {
dimmed.forEach { $0.alpha = 1 }
}
})
return
})
alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
dispatch_async(dispatch_get_main_queue(), {
self.popViewControllerAnimated(true)
})
})
presentViewController(alertController, animated: true, completion: nil)
return false
}
}
Now in any controller, just conform to RequestsNavigationPopVerification and this behaviour is adopted by default.
Use isMovingFromParentViewController
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(true)
if self.isMovingFromParentViewController {
// current viewController is removed from parent
// do some work
}
}
The answer from #William is correct however, if the user starts a swipe-to-go-back gesture the viewWillDisappear method is called and even self won't be in the navigation stack (that is, self.navigationController.viewControllers won't contain self), even if the swipe is not completed and the view controller is not actually popped. Thus, the solution would be to:
Disable the swipe-to-go-back gesture in viewDidAppear and only allow using the back button, by using:
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)])
{
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Or simply use viewDidDisappear instead, as follows:
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
if (![self.navigationController.viewControllers containsObject:self])
{
// back button was pressed or the the swipe-to-go-back gesture was
// completed. We know this is true because self is no longer
// in the navigation stack.
}
}
The solution I have found so far is not very nice, but it works for me. Taking this answer, I also check whether I'm popping programmatically or not:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ((self.isMovingFromParentViewController || self.isBeingDismissed)
&& !self.isPoppingProgrammatically) {
// Do your stuff here
}
}
You have to add that property to your controller and set it to YES before popping programmatically:
self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

Resources