10
votes

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;
6
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?Christian

6 Answers

20
votes

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

13
votes

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

13
votes

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

1
votes

You could do like this:

- (void)willPresentSearchController:(UISearchController *)searchController {
dispatch_async(dispatch_get_main_queue(), ^{
    searchController.searchBar.showsCancelButton = NO;
}); }
0
votes

@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.
0
votes

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
    }
}