UISearchController disable cancel UIBarButtonItem
Asked Answered
J

6

10

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:

enter image description here

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:

enter image description here

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;
Joli answered 8/2, 2015 at 16:19 Comment(1)
Hey, I can't answer your question, but currently I am trying to get the same functionallity to work as you just did. I wonder how it is possible to use the UiSearchBar withou a uitableView to create a suggestion list for addresses like you did. Could you provide a few more code examples?Goffer
F
13

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).

Fanestil answered 8/2, 2015 at 17:24 Comment(7)
Doesn't work unfortunately, I believe that pertains to the cancel button within the search bar itself. This search button seems to be controlled by the UISearchControllerJoli
@Joli I've updated my answer with a solution involving subclassing the searchBar.Fanestil
Thanks, this was what I was considering, but was hoping there would be an easier solution. I'll probably get a chance to implement this evening, then I'll mark as correct :)Joli
updateSearchResultsForSearchController stopped working when I used this method. Any tips?Siamese
@Siamese Sorry, I had that problem before I implemented the textDidChange method to activate the search controller. But that fixed it for me.Fanestil
@Fanestil I was able to get it working like so: gist.github.com/vinnybad/b455e2b6d49f045a73b1Siamese
Thanks for this good anwser, I think no need to override -(void)setShowsCancelButton:(BOOL)showsCancelButton because it call only the second one, and no need to implement the delegate of the search barSaprogenic
O
20

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

Orran answered 9/7, 2015 at 16:28 Comment(3)
This kind of works, but the Cancel button briefly shows before disappearing.Covington
@Covington Interesting... what version of iOS? It doesn't show at all for me :(Orran
I'm finding that by doing as suggested above subsequent calls to showsCancelButton work as expected, at least in my case.Pointdevice
F
13

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).

Fanestil answered 8/2, 2015 at 17:24 Comment(7)
Doesn't work unfortunately, I believe that pertains to the cancel button within the search bar itself. This search button seems to be controlled by the UISearchControllerJoli
@Joli I've updated my answer with a solution involving subclassing the searchBar.Fanestil
Thanks, this was what I was considering, but was hoping there would be an easier solution. I'll probably get a chance to implement this evening, then I'll mark as correct :)Joli
updateSearchResultsForSearchController stopped working when I used this method. Any tips?Siamese
@Siamese Sorry, I had that problem before I implemented the textDidChange method to activate the search controller. But that fixed it for me.Fanestil
@Fanestil I was able to get it working like so: gist.github.com/vinnybad/b455e2b6d49f045a73b1Siamese
Thanks for this good anwser, I think no need to override -(void)setShowsCancelButton:(BOOL)showsCancelButton because it call only the second one, and no need to implement the delegate of the search barSaprogenic
R
13

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

Recommendation answered 3/2, 2017 at 17:29 Comment(1)
Works perfect, also with Swift 4.2 and iOS 11/12.Zahara
E
1

You could do like this:

- (void)willPresentSearchController:(UISearchController *)searchController {
dispatch_async(dispatch_get_main_queue(), ^{
    searchController.searchBar.showsCancelButton = NO;
}); }
Eolande answered 24/9, 2016 at 16:57 Comment(0)
U
0

@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:

  1. 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.
  2. 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.
  3. searchBar:textDidChange: is invoked before searchBarShouldBeginEditing:, which is why we handled it in this order.
  4. 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.
Uppsala answered 6/3, 2016 at 6:4 Comment(0)
J
0

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
    }
}
Jerome answered 18/5, 2017 at 12:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.