How to enable cancel button with UISearchBar?
Asked Answered
R

22

51

In the contacts app on the iPhone if you enter a search term, then tap the "Search" button, the keyboard is hidden, BUT the cancel button is still enabled. In my app the cancel button gets disabled when I call resignFirstResponder.

Anyone know how to hide the keyboard while maintaining the cancel button in an enabled state?

I use the following code:

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
    [searchBar resignFirstResponder];
}

The keyboard slides out of view, but the "Cancel" button to the right of the search text field is disabled, so that I cannot cancel the search. The contacts app maintains the cancel button in an enabled state.

I think maybe one solution is to dive into the searchBar object and call resignFirstResponder on the actual text field, rather than the search bar itself.

Any input appreciated.

Reprobative answered 1/4, 2012 at 21:42 Comment(1)
If someone is looking for a C# solution one can find it here.Marketplace
A
21

try this

for(id subview in [yourSearchBar subviews])
{
    if ([subview isKindOfClass:[UIButton class]]) {
        [subview setEnabled:YES];
    }
}
Allochthonous answered 1/4, 2012 at 22:33 Comment(3)
See my note to Ben below. I decided to remove the code to access the internals in order to avoid any issues. Tapping the circle "x" in the search field re-enables the cancel button, and so hopefully it will not be an issue for users.Reprobative
This solution will not work when you start scrolling the table instead of tapping the "Search" button. In that case the "Cancel" button will be disabled. See my old answer bellow that deals with this problem.Rhubarb
This won't work in iOS 10. UISearchBar view hierarchy was changed. You need to go one level deeper.Weeden
L
29

This method worked in iOS7.

- (void)enableCancelButton:(UISearchBar *)searchBar
{
    for (UIView *view in searchBar.subviews)
    {
        for (id subview in view.subviews)
        {
            if ( [subview isKindOfClass:[UIButton class]] )
            {
                [subview setEnabled:YES];
                NSLog(@"enableCancelButton");
                return;
            }
        }
    }
}

(Also be sure to call it anywhere after [_searchBar resignFirstResponder] is used.)

Lvov answered 9/8, 2013 at 15:53 Comment(2)
+1 for "Also be sure to call it anywhere after [_searchBar resignFirstResponder] is used."Ratliff
This answer worked, but the answer marked as correct in this thread didn't work in iOS 10.Superimpose
A
21

try this

for(id subview in [yourSearchBar subviews])
{
    if ([subview isKindOfClass:[UIButton class]]) {
        [subview setEnabled:YES];
    }
}
Allochthonous answered 1/4, 2012 at 22:33 Comment(3)
See my note to Ben below. I decided to remove the code to access the internals in order to avoid any issues. Tapping the circle "x" in the search field re-enables the cancel button, and so hopefully it will not be an issue for users.Reprobative
This solution will not work when you start scrolling the table instead of tapping the "Search" button. In that case the "Cancel" button will be disabled. See my old answer bellow that deals with this problem.Rhubarb
This won't work in iOS 10. UISearchBar view hierarchy was changed. You need to go one level deeper.Weeden
A
10

The accepted solution will not work when you start scrolling the table instead of tapping the "Search" button. In that case the "Cancel" button will be disabled.

This is my solution that re-enables the "Cancel" button every time it is disabled by using KVO.

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // Search for Cancel button in searchbar, enable it and add key-value observer.
    for (id subview in [self.searchBar subviews]) {
        if ([subview isKindOfClass:[UIButton class]]) {
            [subview setEnabled:YES];
            [subview addObserver:self forKeyPath:@"enabled" options:NSKeyValueObservingOptionNew context:nil];
        }
    }
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    // Remove observer for the Cancel button in searchBar.
    for (id subview in [self.searchBar subviews]) {
        if ([subview isKindOfClass:[UIButton class]])
            [subview removeObserver:self forKeyPath:@"enabled"];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // Re-enable the Cancel button in searchBar.
    if ([object isKindOfClass:[UIButton class]] && [keyPath isEqualToString:@"enabled"]) {
        UIButton *button = object;
        if (!button.enabled)
            button.enabled = YES;
    }
}
Adios answered 13/9, 2012 at 12:21 Comment(1)
Nice solution. Note that you can capture scroll events by implementing - (void)scrollViewDidScroll:(UIScrollView *)scrollView and re-enable the button there as well.Reproductive
M
9

As of iOS 6, the button appears to be a UINavigationButton (private class) instead of a UIButton.

I have tweaked the above example to look like this.

for (UIView *v in searchBar.subviews) {
    if ([v isKindOfClass:[UIControl class]]) {
        ((UIControl *)v).enabled = YES;
    }
}

However, this is obviously brittle, since we're mucking around with the internals. It also can enable more than the button, but it works for me until a better solution is found.

We should ask Apple to expose this.

Mcclain answered 26/10, 2012 at 0:3 Comment(2)
Thanks Ben! I ran some tests with my existing code and found that if I tap the circle "x" within the search field, the cancel button is re-enabled once again. This is on iOS 5.1. I have not yet converted to iOS 6. Therefore, I decided to remove the code to re-enable the cancel button as I want to avoid issues with accessing internals. As you say, it would be nice if the API allowed us direct access to this button. I plan to play safe for now :-) Thank you for posting.Reprobative
I did a mixture of this approach with the additional delay – otherwise the setEnabled would not be recognized or overwritten internally... see https://mcmap.net/q/354775/-uisearchbar-cancel-buttonPantaloon
K
8

This seemed to work for me (in viewDidLoad):

__unused UISearchDisplayController* searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:self.searchBar contentsController:self];

I realize I should probably be using the UISearchDisplayController properly, but this was an easy fix for my current implementation.

Kepler answered 1/10, 2013 at 21:14 Comment(2)
The cleanest solution for meZinck
'UISearchDisplayController' is deprecated: first deprecated in iOS 8.0 - UISearchDisplayController has been replaced with UISearchController :-(Dirndl
V
7

You can use the runtime API to access the cancel button.

UIButton *btnCancel = [self.searchBar valueForKey:@"_cancelButton"];
[btnCancel setEnabled:YES];
Vedetta answered 13/7, 2015 at 10:45 Comment(2)
Isn't this runtime access a private API and possibility of app rejection?Byelorussian
Nope this is not a private API but Key-Value coding. App won't be rejected.Vedetta
E
5

I expanded on what others here already posted by implementing this as a simple category on UISearchBar.

UISearchBar+alwaysEnableCancelButton.h

#import <UIKit/UIKit.h>

@interface UISearchBar (alwaysEnableCancelButton)

@end

UISearchBar+alwaysEnableCancelButton.m

#import "UISearchBar+alwaysEnableCancelButton.h"

@implementation UISearchBar (alwaysEnableCancelButton)

- (BOOL)resignFirstResponder
{
    for (UIView *v in self.subviews) {
        // Force the cancel button to stay enabled
        if ([v isKindOfClass:[UIControl class]]) {
            ((UIControl *)v).enabled = YES;
        }

        // Dismiss the keyboard
        if ([v isKindOfClass:[UITextField class]]) {
            [(UITextField *)v resignFirstResponder];
        }
    }

    return YES;
}
@end
Excrement answered 25/1, 2013 at 19:29 Comment(1)
Sounds this should work and I find it "cleaner" solution then my solution with KVO. +1Rhubarb
C
4

Here's a slightly more robust solution that works on iOS 7. It will recursively traverse all subviews of the search bar to make sure it enables all UIControls (which includes the Cancel button).

- (void)enableControlsInView:(UIView *)view
{
    for (id subview in view.subviews) {
        if ([subview isKindOfClass:[UIControl class]]) {
            [subview setEnabled:YES];
        }
        [self enableControlsInView:subview];
    }
}

Just call this method immediately after you call [self.searchBar resignFirstResponder] like this:

[self enableControlsInView:self.searchBar];

Voila! Cancel button remains enabled.

Chryselephantine answered 26/9, 2013 at 23:29 Comment(0)
M
3

Till iOS 12, you can use like this:-

if let cancelButton : UIButton = self.menuSearchBar.value(forKey: "_cancelButton") as? UIButton{
    cancelButton.isEnabled = true
}

As of iOS 13, if you use like (forKey: "_cancelButton"), so this use of private API is caught and leads to a crash, unfortunately.

For iOS 13+ & swift 5+

if let cancelButton : UIButton = self.menuSearchBar.value(forKey: "cancelButton") as? UIButton {
    cancelButton.isEnabled = true
}
Metritis answered 3/3, 2020 at 8:59 Comment(0)
D
2

I found a different approach for making it work in iOS 7.

What I'm trying is something like the Twitter iOS app. If you click on the magnifying glass in the Timelines tab, the UISearchBar appears with the Cancel button activated, the keyboard showing, and the recent searches screen. Scroll the recent searches screen and it hides the keyboard but it keeps the Cancel button activated.

This is my working code:

UIView *searchBarSubview = self.searchBar.subviews[0];
NSArray *subviewCache = [searchBarSubview valueForKeyPath:@"subviewCache"];
if ([subviewCache[2] respondsToSelector:@selector(setEnabled:)]) {
    [subviewCache[2] setValue:@YES forKeyPath:@"enabled"];
}

I arrived at this solution by setting a breakpoint at my table view's scrollViewWillBeginDragging:. I looked into my UISearchBar and bared its subviews. It always has just one, which is of type UIView (my variable searchBarSubview).

enter image description here

Then, that UIView holds an NSArray called subviewCache and I noticed that the last element, which is the third, is of type UINavigationButton, not in the public API. So I set out to use key-value coding instead. I checked if the UINavigationButton responds to setEnabled:, and luckily, it does. So I set the property to @YES. Turns out that that UINavigationButton is the Cancel button.

This is bound to break if Apple decides to change the implementation of a UISearchBar's innards, but what the hell. It works for now.

Decane answered 25/2, 2014 at 17:26 Comment(1)
Thanks Matt. I will try this when I get a chance.Reprobative
P
2

SWIFT version for David Douglas answer (tested on iOS9)

func enableSearchCancelButton(searchBar: UISearchBar){
    for view in searchBar.subviews {
        for subview in view.subviews {
            if let button = subview as? UIButton {
                button.enabled = true
            }
        }
    }
}
Paramour answered 5/11, 2015 at 6:11 Comment(0)
G
2

Most of the posted solutions are not robust, and will let the Cancel button get disabled under various circumstances.

I have attempted to implement a solution that always keeps the Cancel button enabled, even when doing more complicated things with the search bar. This is implemented as a custom UISearchView subclass in Swift 4. It uses the value(forKey:) trick to find both the cancel button and the search text field, and listens for when the search field ends editing and re-enables the cancel button. It also enables the cancel button when switching the showsCancelButton flag.

It contains a couple of assertions to warn you if the internal details of UISearchBar ever change and prevent it from working.

import UIKit

final class CancelSearchBar: UISearchBar {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    private func setup() {
        guard let searchField = value(forKey: "_searchField") as? UIControl else {
            assertionFailure("UISearchBar internal implementation has changed, this code needs updating")
            return
        }

        searchField.addTarget(self, action: #selector(enableSearchButton), for: .editingDidEnd)
    }

    override var showsCancelButton: Bool {
        didSet { enableSearchButton() }
    }

    @objc private func enableSearchButton() {
        guard showsCancelButton else { return }
        guard let cancelButton = value(forKey: "_cancelButton") as? UIControl else {
            assertionFailure("UISearchBar internal implementation has changed, this code needs updating")
            return
        }

        cancelButton.isEnabled = true
    }
}
Gnaw answered 3/12, 2017 at 11:32 Comment(1)
Nice, and works well! Note that as of iOS 11, these properties are called "searchField" and "cancelButton", no underscore.Glaudia
A
1

Building on smileyborg's answer, just place this in your searchBar delegate:

- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{   
    dispatch_async(dispatch_get_main_queue(), ^{
        __block __weak void (^weakEnsureCancelButtonRemainsEnabled)(UIView *);
        void (^ensureCancelButtonRemainsEnabled)(UIView *);
        weakEnsureCancelButtonRemainsEnabled = ensureCancelButtonRemainsEnabled = ^(UIView *view) {
            for (UIView *subview in view.subviews) {
                if ([subview isKindOfClass:[UIControl class]]) {
                [(UIControl *)subview setEnabled:YES];
                }
                weakEnsureCancelButtonRemainsEnabled(subview);
            }
        };

        ensureCancelButtonRemainsEnabled(searchBar);
    });
 }

This solution works well on iOS 7 and above.

Anticipant answered 5/9, 2014 at 7:0 Comment(0)
L
1

For iOS 10, Swift 3:

for subView in self.movieSearchBar.subviews {
    for view in subView.subviews {
        if view.isKind(of:NSClassFromString("UIButton")!) {
            let cancelButton = view as! UIButton
            cancelButton.isEnabled = true
        }
    }
}
Lest answered 9/1, 2017 at 7:47 Comment(0)
B
1

For iOS 9/10 (tested), Swift 3 (shorter):

searchBar.subviews.flatMap({$0.subviews}).forEach({ ($0 as? UIButton)?.isEnabled = true })
Bromeosin answered 5/7, 2017 at 10:43 Comment(0)
S
1

For iOS 11 and above, Swift 4-5:

extension UISearchBar {
  func alwaysShowCancelButton() {
    for subview in self.subviews {
      for ss in subview.subviews {
        if #available(iOS 13.0, *) {
          for s in ss.subviews {
            self.enableCancel(with: s)
          }
        }else {
          self.enableCancel(with: ss)
        }
      }
    }
  }
  private func enableCancel(with view:UIView) {
   if NSStringFromClass(type(of: view)).contains("UINavigationButton") {
      (view as! UIButton).isEnabled = true
    }
  }
}

UISearchBarDelegate

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    self.searchBar.resignFirstResponder()
    self.searchBar.alwaysShowCancelButton()
  }
Shaikh answered 26/1, 2020 at 16:9 Comment(0)
R
0
for (UIView *firstView in searchBar.subviews) {
    for(UIView* view in firstView.subviews) {
        if([view isKindOfClass:[UIButton class]]) {
             UIButton* button = (UIButton*) view;
             [button setEnabled:YES];
        }
    }
}
Rumanian answered 4/3, 2014 at 7:14 Comment(1)
Some explanation would be niceAcme
O
0

You can create your CustomSearchBar inheriting from UISearchBar and implement this method:

- (void)layoutSubviews {

    [super layoutSubviews];

    @try {
        UIView *baseView = self.subviews[0];

        for (UIView *possibleButton in baseView.subviews)
        {
            if ([possibleButton respondsToSelector:@selector(setEnabled:)]) {
                [(UIControl *)possibleButton setEnabled:YES];
            }
        }
    }
    @catch (NSException *exception) {
        NSLog(@"ERROR%@",exception);
    }
}
Ornithopter answered 3/3, 2015 at 20:55 Comment(0)
L
0

A better solution is

[UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil].enabled = YES;
Lysin answered 20/10, 2016 at 10:34 Comment(0)
R
0

Better & Easy method:

[(UIButton *)[self.searchBar valueForKey:@"_cancelButton"] setEnabled:YES];
Revocation answered 22/6, 2017 at 5:29 Comment(0)
R
0

Swift 5 & iOS 14

if let cancelButton : UIButton = self.menuSearchBar.value(forKey: "cancelButton") as? UIButton {
    cancelButton.isEnabled = true
}
Remissible answered 13/3, 2021 at 5:13 Comment(0)
H
0

One alternative that should be slightly more robust against UIKit changes, and doesn't reference anything private by name is to use the appearance proxy to set the tag of the cancel button. i.e., somewhere in setup:

let cancelButtonAppearance = UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self])
cancelButtonAppearance.isEnabled = true
cancelButtonAppearance.tag = -4321

Then we can use the tag, here the magic number -4321 to find the tag:

extension UISearchBar {
    var cancelButton: UIControl? {
         func recursivelyFindButton(in subviews: [UIView]) -> UIControl? {
             for subview in subviews.reversed() {
                 if let control = subview as? UIControl, control.tag == -4321 {
                     return control
                 }
                 if let button = recursivelyFindButton(in: subview.subviews) {
                     return button
                 }
             }
             return nil
         }
         return recursivelyFindButton(in: subviews)
     }
}

And finally use searchBar.cancelButton?.isEnabled = true whenever the search bar loses focus, such as in the delegate. (Or if you use the custom subclass and call setShowsCancelButton from the delegate, you can override that function to also enable the button whenever it is shown.)

Hales answered 9/4, 2021 at 9:49 Comment(1)
(Reversing the order of subviews cuts down the number of steps from 17 to 3 on iOS 14.4, but is of course optional and subject to change.)Hales

© 2022 - 2024 — McMap. All rights reserved.