Styling the cancel button in a UISearchBar - ios

I have a UISearchBar that has a cancel button (it's displayed using -(void)setShowsCancelButton:animated). I've changed the tintColor of the search bar like this in an attempt to get a grayish searchbar:
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 40)];
searchBar.tintColor = [UIColor colorWithWhite:0.8 alpha:1.0];
This is what it looks like now - notice how the cancel button is also gray: http://twitpic.com/c0hte
Is there a way to set the color of the cancel button separately so it looks more like this: http://twitpic.com/c0i6q

You can use UIAppearance to style the cancel button without iterating subviews of the UISearchBar, but the UIButton header does not currently have any methods annotated with UI_APPEARANCE_SELECTOR.
EDIT: Drill down the subviews till you get that cancel button
But this usually returns nil until
searchBar.setShowsCancelButton(true, animated: true) is called.
extension UISearchBar {
var cancelButton : UIButton? {
if let view = self.subviews.first {
for subView in view.subviews {
if let cancelButton = subView as? UIButton {
return cancelButton
}
}
}
return nil
}
}

In iOS 5.0+, you can use the appearnce proxy.
Before the search bar is showed.:
UIBarButtonItem *searchBarButton = [UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil];
[searchBarButton setBackgroundImage:myCancelButtonImageNormal forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[searchBarButton setBackgroundImage:myCancelButtonImageHighlighted forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
[searchBarButton setTitleTextAttributes:barButtonTitleTextAttributesNormal forState:UIControlStateNormal];
[searchBarButton setTitleTextAttributes:barButtonTitleTextAttributesHighlighted forState:UIControlStateHighlighted];
If you use [UIButton appearanceWhenContainedIn:[UISearchBar class], nil], it will affect other buttons (e.g. clear button). So, you'd better not use UIButton's appearnce. Try UIBarButtonItem.

Change the title of 'Cancel' button:
[[UIButton appearanceWhenContainedIn:[UISearchBar class], nil] setTitle:#"newTitle" forState:UIControlStateNormal];
Swift equivalent:
let cancelButton = UIButton.appearance(whenContainedInInstancesOf: [UISearchBar.self])
cancelButton?.setTitle("cancel".localized, for: .normal)

Though this might not be exactly relevant to the original question, the solution is still applicable in the larger sense of trying to customize the Cancel button in the UISearchBar. Thought this will help others who are stuck in such a scenario.
My situation was to change the cancel button's title, but with a twist, wherein I did not want to show the cancel button by default but only wanted it to show up, when the user enters the search mode (by clicking inside the search text field). At this instant, I wanted the cancel button to carry the caption "Done" ("Cancel" was giving a different meaning to my screen, hence the customization).
Nevertheless, here's what I did (a combination of caelavel's and Arenim's solutions):
Subclassed UISearchBar as MyUISearchBar with these two methods:
-(void) setCloseButtonTitle: (NSString *) title forState: (UIControlState)state
{
[self setTitle: title forState: state forView:self];
}
-(void) setTitle: (NSString *) title forState: (UIControlState)state forView: (UIView *)view
{
UIButton *cancelButton = nil;
for(UIView *subView in view.subviews){
if([subView isKindOfClass:UIButton.class])
{
cancelButton = (UIButton*)subView;
}
else
{
[self setTitle:title forState:state forView:subView];
}
}
if (cancelButton)
[cancelButton setTitle:title forState:state];
}
And in the viewcontroller which uses this Searchbar, the following piece of code takes care of showing the cancel button and customizing its title:
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
MyUISearchBar *sBar = (MyUISearchBar *)searchBar;
[sBar setShowsCancelButton:YES];
[sBar setCloseButtonTitle:#"Done" forState:UIControlStateNormal];
}
Strangely enough, I did not have to do anything to hide the cancel button, as it is hidden by default, when the search mode is exited.

What you want to do is pretty tough. There is no built-in hook to get at the cancel button.
However, there are a couple of options if you are willing to jimmy open the hood.
First off, UISearchBar is a UIView, and the Cancel button is also a view, which is added into the search bar as a subview, just as you would expect.
I have experimented a little, and can tell you that when the button is onscreen it has a size of 48,30.
So in viewWillAppear, you can do something like this:
Find the cancel button view in [searchBar subviews] by looking for one with size 48,30. (There only seems to be one -- this could change...) You could be doubly careful and look for one that is in approximately the correct position (differs in landscape and portrait).
Add a subview to the cancel button.
The subview should be a UIControl (so that you can set enabled = NO, in order to make sure touch events get to the actual cancel button)
It needs to have the right color and rounded corners; you will need to fudge the size for reasons I don't yet understand (55,30 seems to work)
This will work if searchBar.showsCancelButton is always YES; if you want it to disappear when not editing the search string, you will need to find a hook to add the overlay each time the cancel button appears.
As you can see, this is some ugly tinkering. Do it with eyes wide open.

You can find the cancel button by looping through the subviews of the search bar and checking for the class type (instead of the size):
UIButton *cancelButton = nil;
for(UIView *subView in yourSearchBar.subviews){
if([subView isKindOfClass:UIButton.class]){
cancelButton = (UIButton*)subView;
}
}
And then change the tint color:
[cancelButton setTintColor:[UIColor colorWithRed:145.0/255.0 green:159.0/255.0 blue:179.0/255.0 alpha:1.0]];

If you want to configure your cancel button on UISearchBar you should get the UIButton object from your UISearchBar object. Example below
UISearchBar *s_bar = [[UISearchBar alloc] initWithFrame:CGRectMake(50,20,300,30)];
s_bar.delegate = self;
s_bar.barStyle = UIBarStyleDefault;
s_bar.showsCancelButton = YES;
UIButton *cancelButton;
for (id button in s_bar.subviews)
{
if ([button isKindOfClass:[UIButton class]])
{
cancelButton=(UIButton*)button;
break;
}
}

Custom UISearchBar and override method -addSubview:
- (void) addSubview:(UIView *)view {
[super addSubview:view];
if ([view isKindOfClass:UIButton.class]) {
UIButton *cancelButton = (UIButton *)view;
[cancelButton setBackgroundImage:[UIImage imageNamed:#"xxxx.png"] forState:UIControlStateNormal];
[cancelButton setBackgroundImage:[UIImage imageNamed:#"yyyy.png"] forState:UIControlStateHighlighted];
}
}

I'll give a detailed answered regarding the UIAppearance technique. First, you need to understand that the cancel button is a private UINavigationButton:UIButton. After some inspection, it appears that UINavigationButton will respond to those UIAppearance selectors:
// inherited from UINavigationButton
#selector(setTintColor:)
#selector(setBackgroundImage:forState:style:barMetrics:)
#selector(setBackgroundImage:forState:barMetrics:)
#selector(setTitleTextAttributes:forState:)
#selector(setBackgroundVerticalPositionAdjustment:forBarMetrics:)
#selector(setTitlePositionAdjustment:forBarMetrics:)
#selector(setBackButtonBackgroundImage:forState:barMetrics:)
#selector(setBackButtonTitlePositionAdjustment:forBarMetrics:)
#selector(setBackButtonBackgroundVerticalPositionAdjustment:forBarMetrics:)
// inherited from UIButton
#selector(setTitle:forState:)
Coincidentally, those selectors match those of a UIBarButtonItem. Meaning the trick is to use two separate UIAppearance to handle the private class UINavigationButton.
/* dual appearance technique by Cœur to customize a UINavigationButton */
Class barClass = [UISearchBar self];
UIBarButtonItem<UIAppearance> *barButtonItemAppearanceInBar = [UIBarButtonItem appearanceWhenContainedIn:barClass, nil];
[barButtonItemAppearanceInBar setTintColor:...];
[barButtonItemAppearanceInBar setBackgroundImage:... forState:... style:... barMetrics:...];
[barButtonItemAppearanceInBar setBackgroundImage:... forState:... barMetrics:...];
[barButtonItemAppearanceInBar setTitleTextAttributes:... forState:...];
[barButtonItemAppearanceInBar setBackgroundVerticalPositionAdjustment:... forBarMetrics:...];
[barButtonItemAppearanceInBar setTitlePositionAdjustment:... forBarMetrics:...];
// only for a backButton in an UINavigationBar, not for a cancelButton in an UISearchBar
//[barButtonItemAppearanceInBar setBackButtonBackgroundImage:... forState:... barMetrics:...];
//[barButtonItemAppearanceInBar setBackButtonTitlePositionAdjustment:... forBarMetrics:...];
//[barButtonItemAppearanceInBar setBackButtonBackgroundVerticalPositionAdjustment:... forBarMetrics:...];
UIButton<UIAppearance> *buttonAppearanceInBar = [UIButton appearanceWhenContainedIn:barClass, nil];
// warning: doesn't work for iOS7+
[buttonAppearanceInBar setTitle:... forState:...];
This will let you customize your Cancel button as much as you want.

After you've initialized your UISearchBar, you can probe into it's subviews and customize each of them. Example:
for (UIView *view in searchBar.subviews) {
//if subview is the button
if ([[view.class description] isEqualToString:#"UINavigationButton"]) {
//change the button images and text for different states
[((UIButton *)view) setEnabled:YES];
[((UIButton *)view) setTitle:nil forState:UIControlStateNormal];
[((UIButton *)view) setImage:[UIImage imageNamed:#"button image"] forState:UIControlStateNormal];
[((UIButton *)view) setBackgroundImage:[UIImage imageNamed:#"button"] forState:UIControlStateNormal];
[((UIButton *)view) setBackgroundImage:[UIImage imageNamed:#"button_pressed"] forState:UIControlStateSelected];
[((UIButton *)view) setBackgroundImage:[UIImage imageNamed:#"button_pressed"] forState:UIControlStateHighlighted];
//if the subview is the background
}else if([[view.class description] isEqualToString:#"UISearchBarBackground"]) {
//put a custom gradient overtop the background
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = view.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)[[some uicolor] CGColor], (id)[[another uicolor] CGColor], nil];
[view.layer insertSublayer:gradient atIndex:0];
//if the subview is the textfield
}else if([[view.class description] isEqualToString:#"UISearchBarTextField"]){
//change the text field if you wish
}
}
Worked out great for me! Especially the gradient :)

Swift 2.1.1:
There's no simple way to hook in and style the search bar, you need to grab the subview manually from the search bar and then apply your changes.
var cancelButton: UIButton
let topView: UIView = self.customSearchController.customSearchBar.subviews[0] as UIView
for subView in topView.subviews {
if subView.isKindOfClass(NSClassFromString("UINavigationButton")!) {
cancelButton = subView as! UIButton
cancelButton.enabled = true
cancelButton.setTitle("TestTitle", forState: UIControlState.Normal) // Change to set the title
cancelButton.setBackgroundImage(UIImage(named: "ImageName"), forState: .Normal) // Change this to set a custom cancel button image, set the title to "" to remove 'Cancel' text
}
}

First of all I'd like to thank #Eliott from this https://stackoverflow.com/a/37381821/1473144
I had to make a few adjustments for his answer to work in my specs that go below.
Please, I ask the OP to update the accepted answer as it's VERY outdated.
Swift 3, iOS 10 & Xcode 8.2.1
searchBar.showsCancelButton = true
var cancelButton: UIButton
let topView: UIView = self.searchBar.subviews[0] as UIView
for subView in topView.subviews {
if let pvtClass = NSClassFromString("UINavigationButton") {
if subView.isKind(of: pvtClass) {
cancelButton = subView as! UIButton
cancelButton.setTitle("", for: .normal)
cancelButton.tintColor = UIColor.black
cancelButton.setImage(#imageLiteral(resourceName: "searchX"), for: .normal)
}
}
}

Well, here is function, which can change Cancel's button label. Modify it, if you want.
Usage is:
nStaticReplaceStringInView(mySearchBar, #"Cancel", #"NewCancelButtonLabel");
void nStaticReplaceStringInView(UIView * view, NSString * haystack, NSString * needle)
{
for(int i=0; i<[view.subviews count]; i++)
{
nStaticReplaceStringInView([view.subviews objectAtIndex:i], haystack,needle);
}
if([view respondsToSelector:#selector(titleForState:)])
{
//NSLog(#"%# || %#",[view titleForState:UIControlStateNormal], haystack);
if(NSStrEq([view titleForState:UIControlStateNormal] , haystack))
{
[view setTitle: needle forState: UIControlStateNormal];
}
}
}

- (void) searchBarTextDidBeginEditing:(UISearchBar *)theSearchBar
{
NSArray *arr = [theSearchBar subviews];
UIButton *cancelButton = [arr objectAtIndex:3];
[cancelButton setTitle:#"yourtitle" forState:UIControlStateNormal];
}
Just take a log of arr amd see at which index control lies. In the same way u can set UITextField properties:
NSArray *arr = [searchbar subviews];
UITextField *searchfield = [arr objectAtIndex:2];
[searchfield setTextAlignment:UITextAlignmentRight];

I have many UISearchBar items throughout my app, so I wrote this category to add a property so you can access mySearchBar.cancelButton. (If you're new to categories, read more about extending objects with Categories here.)
Keep in mind you should only access this when the Cancel button is visible because UISearchBar seems to create a new button object every time it shows. Don't save the pointer to the cancelButton, just get it when needed:
#interface UISearchBar (cancelButton)
#property (readonly) UIButton* cancelButton;
- (UIButton *) cancelButton;
#end
#implementation UISearchBar (cancelButton)
- (UIButton *) cancelButton {
for (UIView *subView in self.subviews) {
//Find the button
if([subView isKindOfClass:[UIButton class]])
{
return (UIButton *)subView;
}
}
NSLog(#"Error: no cancel button found on %#", self);
return nil;
}
#end

stupid way
for(id cc in [SearchBar subviews])
{
if([cc isKindOfClass:[UIButton class]])
{
UIButton *btn = (UIButton *)cc;
......
Do whatever you want
.......
}
}

extension UISearchBar {
var cancelButton : UIButton? {
let topView: UIView = self.subviews[0] as UIView
if let pvtClass = NSClassFromString("UINavigationButton") {
for v in topView.subviews {
if v.isKind(of: pvtClass) {
return v as? UIButton
}
}
}
return nil
}
}

UISearchBar *searchBar;
[searchBar setShowsCancelButton:YES animated:YES];
UIButton *cancelButton =
YES == [searchBar respondsToSelector:NSSelectorFromString(#"cancelButton")] ?
[searchBar valueForKeyPath:#"_cancelButton"] : nil;
cancelButton.titleEdgeInsets = UIEdgeInsetsMake(0, -10, 0, 10);
[cancelButton setTitle:#"New :)" forState:UIControlStateNormal];

For iOS 11 and Swift 4.
Create a subclass of UISearchController.
Override method:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("layout")
if let btn = searchBar.subviews[0].subviews[2] as? UIButton {
btn.frame = CGRect(x: 306, y: 20, width: 53, height: 30)
}
}

For iOS 10 & above, use following method
[[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:#[[UISearchBar class]]] setTintColor:[UIColor blackColor]];

The best way to style the cancelButton is without using UIAppearance is like this it is for Swift5 iOS13 and it works best with UISearchResultController.searchBar too
extension UISearchBar {
func changeSearchBarAppearance(appearance: MyAppearance) {
self.barTintColor = appearance.searchbar.barTintColor
self.tintColor = appearance.searchbar.tintColor
if let textField = self.subviews.first?.subviews.last?.subviews.first {
textField.tintColor = .black
}
}
}
setting serachBar tintColor will set the tintColor of all items including the cancelButton but with this the blinker in the searchField will also be set with the same tintColor so find the textfield and set its tintColor will solve the blinker issue

Related

Wrong barButton item tint for play/pause buttons built programatically

I have a UIToolbar with audio controls. Sometime around iOS 7 upgrade the color of the bar changed and iOS started shading my buttons differently. That introduced a bug where the play and pause buttons which I change programatically don't match the new look of the toolbar:
Toolbar starts out looking fine:
Now I pressed play so code inserted pause button but wrong color:
Now I pressed pause and code inserted play button, again with wrong color:
I want the play and pause buttons to have that same dark look as the other buttons. I assume I have to do something differently when building the replacement buttons. The raw icon images are all that lighter color, but iOS toolbar seems to be automatically recoloring to darker color from the storyboard, as seen in first screenshot.
Here's the code I use to build the buttons:
- (UIBarButtonItem *) makeBarButton: (NSString *) imageName action:(SEL)targetAction
{
UIImage* image = [UIImage imageNamed:imageName];
CGRect frame = CGRectMake(0 - 20, 0 - 20, image.size.width + 20, image.size.height + 20);
UIButton *button = [[UIButton alloc] initWithFrame:frame];
[button addTarget:self action:targetAction forControlEvents:UIControlEventTouchUpInside];
[button setImage:image forState:UIControlStateNormal];
[button setShowsTouchWhenHighlighted:YES];
return [[UIBarButtonItem alloc] initWithCustomView:button];
}
- (void) setAsPlaying:(BOOL)isPlaying
{
self.rootViewController.playing = isPlaying;
// we need to change which of play/pause buttons are showing, if the one to
// reverse current action isn't showing
if ((isPlaying && !self.pauseButton) || (!isPlaying && !self.playButton))
{
UIBarButtonItem *buttonToRemove = nil;
UIBarButtonItem *buttonToAdd = nil;
if (isPlaying)
{
buttonToRemove = self.playButton;
self.playButton = nil;
self.pauseButton = [self makeBarButton:#"icon_pause.png" action:#selector(pauseAudio:)];
buttonToAdd = self.pauseButton;
}
else
{
buttonToRemove = self.pauseButton;
self.pauseButton = nil;
self.playButton = [self makeBarButton:#"icon_play.png" action:#selector(playAudio:)];
buttonToAdd = self.playButton;
}
// Get the reference to the current toolbar buttons
NSMutableArray *toolbarButtons = [[self.toolbar items] mutableCopy];
// Remove a button from the toolbar and add the other one
if (buttonToRemove)
[toolbarButtons removeObject:buttonToRemove];
if (![toolbarButtons containsObject:buttonToAdd])
[toolbarButtons insertObject:buttonToAdd atIndex:4];
[self.toolbar setItems:toolbarButtons];
}
}
Appreciate your help with this.
Check the following:
If the images are stored inside your image assets, make sure they are template images.
Then you can either A) set their tint colour in Interface Builder to the grey colour you prefer or B) do it programmatically in your makeBarButton method as such;
button.tintColor = [UIColor grayColor];

How to change the position or hide magnifier icon in UISearchBar in IOS 7?

I am working on IOS 7 application.By default its appearing like Pic(1).But I need to change it as Pic(2).I googled and found few answers for the requirement,but it has not changed.Or else I need to hide.So that I can manage with background image.This is first image
I used below code to modify it.But didnt succeed.
In .h file
#property(nonatomic,strong) IBOutlet UISearchBar *findSearchBar;
In .m file
#synthesize findSearchBar;
- (void)viewDidLoad
{
[super viewDidLoad];
[self setSearchIconToFavicon];
}
- (void)setSearchIconToFavicon
{
// The text within a UISearchView is a UITextField that is a subview of that UISearchView.
UITextField *searchField;
for (UIView *subview in self.findSearchBar.subviews)
{
if ([subview isKindOfClass:[UITextField class]]) {
searchField = (UITextField *)subview;
break;
}
}
if (searchField)
{
UIView *searchIcon = searchField.leftView;
if ([searchIcon isKindOfClass:[UIImageView class]])
{
NSLog(#"aye");
}
searchField.rightView = nil;
searchField.leftView = nil;
searchField.leftViewMode = UITextFieldViewModeNever;
searchField.rightViewMode = UITextFieldViewModeAlways;
}
}
I am not getting how to make the center of the view's image to nil.Its really killing my time.Please help me.where I had gone wrong.
UITextField *txfSearchField = [looksearchbar valueForKey:#"_searchField"];
[txfSearchField setBackgroundColor:[UIColor whiteColor]];
[txfSearchField setLeftViewMode:UITextFieldViewModeNever];
[txfSearchField setRightViewMode:UITextFieldViewModeNever];
[txfSearchField setBackground:[UIImage imageNamed:#"searchbar_bgImg.png"]];
[txfSearchField setBorderStyle:UITextBorderStyleNone];
//txfSearchField.layer.borderWidth = 8.0f;
//txfSearchField.layer.cornerRadius = 10.0f;
txfSearchField.layer.borderColor = [UIColor clearColor].CGColor;
txfSearchField.clearButtonMode=UITextFieldViewModeNever;
Try this may be it will help u........
I'm not sure how to left-align the placeholder, but as of iOS 5.0 there's a simple, supported way to modify the search bar's text field properties, e.g.:
[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setLeftViewMode:UITextFieldViewModeNever];
which will hide the magnifying glass icon.
You could try:
searchBar.setImage(UIImage(named: "yourimage")!, forSearchBarIcon: UISearchBarIcon.Clear, state: UIControlState.Normal)

EXC_BAD_ACCESS with UIBarButtonItem appearanceWhenContainedIn on iOS 7

//Set all cancel buttons in search bars to "Done"
id searchBarButton = [UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil];
if([[[UIDevice currentDevice] systemVersion] floatValue] < 7) {
[searchBarButton setTitle:#"Done"];
} else {
//Can't do anything here or i get EXC_BAD_ACCESS
}
This is giving a EXC_BAD_ACCESS when called in viewDidLoad only on iOS 7 Gold Master and newer. iOS 7 beta 6 and older is fine.
Is there a different way to do this in iOS 7?
NSLog("%#", searchBarButton) results in this on iOS7:
2013-10-01 16:14:25.972 MP Staging[12293:a0b] <_UIBarItemAppearance:0x1aaf72d0> <Customizable class: UIBarButtonItem> when contained in (
UISearchBar
) with invocations (null)>
and this on iOS 6
<_UIBarItemAppearance: 0x1c671aa0>
setTitle will fail in iOS7.
Try below code from this blog:
-(void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller{
self.searchDisplayController.searchBar.showsCancelButton = YES;
UIButton *cancelButton;
UIView *topView = self.searchDisplayController.searchBar.subviews[0];
for (UIView *subView in topView.subviews) {
if ([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
cancelButton = (UIButton*)subView;
}
}
if (cancelButton) {
//Set the new title of the cancel button
[cancelButton setTitle:#"Annuller" forState:UIControlStateNormal];
}
}
I'm using this without any problems in 7.1, however, it does seem to crash on 7.0.x (device and sim) - hopefully this means they've brought the property back in 7.1, but it also means that we have to use one of the above subview traversing hacks for the interim versions.
id barButtonAppearanceInSearchBar = [UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil];
[barButtonAppearanceInSearchBar setTitleTextAttributes:#{NSFontAttributeName:[UIFont fontWithName:#"HelveticaNeue" size:15],
NSForegroundColorAttributeName : [UIColor blackColor]
} forState:UIControlStateNormal];
[barButtonAppearanceInSearchBar setTitle:#"Done"];
UIBarButtonItem's title property is not available through the UIAppearance proxy.
I don't know why it was working in iOS 6, but it's definitely not supposed to.
The only alternative you seem to have is to "hack" the UISearchBar by crawling its subviews looking for the button and setting the title, but:
it's very fragile, as any implementation change to the subviews structure will break your code
it's not global you will have to do this on any UISearchBar instance
According to this answer you can perform this "hack" in the searchDisplayControllerWillBeginSearch: method of UISearchDisplayDelegate like follows:
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
[theSearchBar setShowsCancelButton:YES animated:NO];
UIButton *cancelButton;
UIView *topView = theSearchBar.subviews[0];
for (UIView *subView in topView.subviews) {
if ([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
cancelButton = (UIButton*)subView;
}
}
if (cancelButton) {
[cancelButton setTitle:#"YourTitle" forState:UIControlStateNormal];
}
}

UIKeyboardTypeDecimalPad with negative numbers

I'm working on an iOS app that requires the user to enter numbers into a UITextField using the keyboard type UIKeyboardTypeDecimalPad. However I just realized that there is no support for entering negative numbers, which is a requirement of the application.
Any ideas or thoughts on how I can accomplish this?
You can use a UIToolbar as an input accessory view for your UITextField and put a button with a "+/-" (plus/minus sign in it).
UIToolbar *toolbar = [[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 44)];
UIBarButtonItem *plusMinusBbi = [[UIBarButtonItem alloc]initWithTitle:#"+/-" style:UIBarButtonItemStylePlain target:self action:#selector(togglePositiveNegative:)];
toolbar.items = #[plusMinusBbi];
self.textField.inputAccessoryView = toolbar;
I don't believe that something like that is possible right now (since Apple hasn't implemented it yet). The only option would be to design your own keyboard or use a complete ASCII one.
func addToolbar() {
let toolbar = UIToolbar()
toolbar.sizeToFit()
let plusMinusButton = UIBarButtonItem(title: "+/-", style: .done, target: self, action: #selector(plusMinusAction))
plusMinusButton.tintColor = .black
plusMinusButton.width = UIScreen.main.bounds.width / 3
toolbar.items = [plusMinusButton]
toolbar.barTintColor = #colorLiteral(red: 0.7812563181, green: 0.8036255836, blue: 0.8297665119, alpha: 1)
toolbar.isTranslucent = false
myField.inputAccessoryView = toolbar
}
#objc func plusMinusAction() {
let text = myField.text ?? ""
if text.hasPrefix("-") {
myField.text = String(text.suffix(text.count - 1))
} else {
myField.text = "-\(text)"
}
}
From what I've found apple has yet to implement such a standard keyboard. However it is possible to add UIButtons to any keyboard window. this link should help, or similar tutorial this link should also help
Basically you register a NSNotificationListener to listen for the keyboard coming up. Grab the keyboards frame and add a UIButton to its view. The above link isn't quite what we want but its the right idea.
In appDelegate,
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
- (void)keyboardDidShow:(NSNotification *)note
{
// Get the Very Top Window on the Display. That's where the Keyboard is.
NSInteger topWindow = [[[UIApplication sharedApplication] windows] count] - 1;
UIWindow *keyboard = [[[UIApplication sharedApplication] windows] objectAtIndex:topWindow];
// If the dot has not been created (first time the keyboard has been displayed) create it.
if (self.dot == nil)
{
self.dot = [UIButton buttonWithType:UIButtonTypeCustom];
// Make the dot a subview of the view containing the keyboard.
[keyboard addSubview:self.dot];
// Place the dot in the correct location on the keyboard.
[self.dot setFrame:CGRectMake(0, 427, 106, 53)];
// Set the overlay graphics. (Use TransDecimalDown.png and TransDecimalUp.png for the Alert Style Keyboard.
[self.dot setImage:[UIImage imageNamed:#"DecimalUp.png"] forState:UIControlStateNormal];
[self.dot setImage:[UIImage imageNamed:#"DecimalDown.png"] forState:UIControlStateHighlighted];
// Give the dot something to do when pressed.
[self.dot addTarget:self action:#selector(sendDecimal:) forControlEvents:UIControlEventTouchUpInside];
}
// Bring the dot to the front of the keyboard.
[keyboard bringSubviewToFront:self.dot];
}
- (void)sendDecimal:(id)sender {
// Post a notification that the dot has been pressed. Observing view controllers are then responsible for adding the actual decimal.
[[NSNotificationCenter defaultCenter] postNotificationName:#"DecimalPressed" object:nil];
// Play the Keyboard Click. If the user has these sound effects turned off, the decimal will still click. Sorry. :( (Also, doesn't seem to work on the simulator, no keyboard clicks seem to.)
AudioServicesPlaySystemSound(0x450);
}
sorry for the horrible code format but you get the idea :)
This clearly isn't my best answer.. but i'm unable to delete it since it is accepted.
may this code will help you:
if you want a negative number just use "-"
NSString *fieldString = [NSString stringWithFormat:#"%#",Textfield.text];
NSLog(#"%#",fString);
int fieldValue;
value = [fString intValue];
NSLog(#"%d",fieldValue);
this will work for decimal numbers
double fieldValue;
value = [fString doubleValue];
NSLog(#"%f",fieldValue);
To accomplish the task I came using '.inputAccessoryView' with the textfield
On viewDidLoad
self.textField.inputAccessoryView = [self accessoryViewForTextField:self.textField];
then
- (UIView *)accessoryViewForTextField:(UITextField *)textField{
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)];
view.backgroundColor = [UIColor lightGrayColor];
UIButton *minusButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIButton *doneButton = [UIButton buttonWithType:UIButtonTypeCustom];
[minusButton setTitle:#"-" forState:UIControlStateNormal];
[doneButton setTitle:NSLocalizedString(#"Done", #"Done") forState:UIControlStateNormal];
minusButton.backgroundColor = [UIColor magentaColor];
doneButton.backgroundColor = [UIColor blueColor];
CGFloat buttonWidth = view.frame.size.width/3;
minusButton.frame = CGRectMake(0, 0, buttonWidth, 44);
doneButton.frame = CGRectMake(view.frame.size.width - buttonWidth, 0, buttonWidth, 44);
[minusButton addTarget:self action:#selector(minusTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[doneButton addTarget:self action:#selector(doneTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[view addSubview:minusButton];
[view addSubview:doneButton];
return view;
}
this will add a custom view just above the keyboard as a part of it
finally to get the 'minus'
#pragma mark - IBActions
- (IBAction)minusTouchUpInside:(id)sender
{
NSString *value = self.textField.text;
if (value.length > 0) {
NSString *firstCharacter = [value substringToIndex:1];
if ([firstCharacter isEqualToString:#"-"]){
self.textField.text = [value substringFromIndex:1];
}else{
self.textField.text = [NSString stringWithFormat:#"-%#", value];
}
}
}
- (IBAction)doneTouchUpInside:(id)sender
{
[self.textField resignFirstResponder];
}

How to change the default text of Cancel Button which appears in the UISearchBar +iPhone

I am developing an Application where I wanted to change the text of Search String in the SearchBar. I wanted to change the text of Cancel Button Also which appears next to the SearchBar. Before entering any string in the search bar we wil get the Search String as the default string. I wanted to change the text of that string and when we click on that searchbar we get a cancel button next to searchbar and I wanted to change the text of that cancel button.
Use the appearance proxy:
id barButtonAppearanceInSearchBar = [UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil];
[barButtonAppearanceInSearchBar setBackgroundImage:grayBackgroundImage forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[barButtonAppearanceInSearchBar setTitleTextAttributes:#{
NSFontAttributeName : [UIFont fontWithName:#"HelveticaNeue-CondensedBold" size:20],
NSForegroundColorAttributeName : [UIColor blackColor]
} forState:UIControlStateNormal];
[barButtonAppearanceInSearchBar setTitle:#"X"];
You also need to have the "searchBar setShowsCancelButton" before the procedure.
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
[theSearchBar setShowsCancelButton:YES animated:NO];
for (UIView *subView in theSearchBar.subviews){
if([subView isKindOfClass:[UIButton class]]){
[(UIButton*)subView setTitle:#"Done" forState:UIControlStateNormal];
}
}
}
Note also: use UIButton to avoid problems with Apple!
Solution for iOS 7. All credits for this go to Mr. Jesper Nielsen - he wrote the code.
-(void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller {
UIButton *cancelButton;
UIView *topView = theSearchBar.subviews[0];
for (UIView *subView in topView.subviews) {
if ([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
cancelButton = (UIButton*)subView;
}
}
if (cancelButton) {
[cancelButton setTitle:#"YourTitle" forState:UIControlStateNormal];
}
}
If by "Search String", you mean the placeholder, then this should do it:
[searchBar setPlaceholder:#"Whatever you want"];
As for changing the text of the cancel button, that may be a bit more difficult. Apple does not use a standard UIBarButtonItem for this, or even a non-standard UIButton. Instead they use a UINavigationButton for the cancel button in the search bar. Since this is not a documented public class, attempting to modify it could very well get your app rejected from the App Store. If you do want to risk rejection, then you could search through the subviews of searchBar:
for(UIView *view in [searchBar subviews]) {
if([view isKindOfClass:[NSClassFromString(#"UINavigationButton") class]]) {
[(UIBarItem *)view setTitle:#"Whatever you want"];
}
}
Note that the cancel button is loaded lazily, so you will have to do this modification when the search bar is activated by the user.
In iOS 7 if you are using UISearchBar just write this code in searchBarTextDidBeginEditing: method
searchBar.showsCancelButton = YES;UIView* view=searchBar.subviews[0];
for (UIView *subView in view.subviews) {
if ([subView isKindOfClass:[UIButton class]]) {
UIButton *cancelButton = (UIButton*)subView;
[cancelButton setTitle:#"إلغاء" forState:UIControlStateNormal];
}
}
I would like to fix the UIAppearance technique, as yar1vn code won't work with Xcode 5. With the following you will have code that works perfectly for both iOS 6 and iOS 7.
First, you need to understand that the cancel button is a private UINavigationButton:UIButton. Hence, it is not an UIBarButtonItem. After some inspection, it appears that UINavigationButton will respond to those UIAppearance selectors:
// inherited from UINavigationButton
#selector(setTintColor:)
#selector(setBackgroundImage:forState:style:barMetrics:)
#selector(setBackgroundImage:forState:barMetrics:)
#selector(setTitleTextAttributes:forState:)
#selector(setBackgroundVerticalPositionAdjustment:forBarMetrics:)
#selector(setTitlePositionAdjustment:forBarMetrics:)
#selector(setBackButtonBackgroundImage:forState:barMetrics:)
#selector(setBackButtonTitlePositionAdjustment:forBarMetrics:)
#selector(setBackButtonBackgroundVerticalPositionAdjustment:forBarMetrics:)
// inherited from UIButton
#selector(setTitle:forState:)
Coincidentally, those selectors match those of a UIBarButtonItem. Meaning the trick is to use two separate UIAppearance to handle the private class UINavigationButton.
/* dual appearance technique by Cœur to customize a UINavigationButton */
Class barClass = [UISearchBar self];
UIBarButtonItem<UIAppearance> *barButtonItemAppearanceInBar = [UIBarButtonItem appearanceWhenContainedIn:barClass, nil];
[barButtonItemAppearanceInBar setTintColor:...];
[barButtonItemAppearanceInBar setBackgroundImage:... forState:... style:... barMetrics:...];
[barButtonItemAppearanceInBar setBackgroundImage:... forState:... barMetrics:...];
[barButtonItemAppearanceInBar setTitleTextAttributes:... forState:...];
[barButtonItemAppearanceInBar setBackgroundVerticalPositionAdjustment:... forBarMetrics:...];
[barButtonItemAppearanceInBar setTitlePositionAdjustment:... forBarMetrics:...];
[barButtonItemAppearanceInBar setBackButtonBackgroundImage:... forState:... barMetrics:...];
[barButtonItemAppearanceInBar setBackButtonTitlePositionAdjustment:... forBarMetrics:...];
[barButtonItemAppearanceInBar setBackButtonBackgroundVerticalPositionAdjustment:... forBarMetrics:...];
UIButton<UIAppearance> *buttonAppearanceInBar = [UIButton appearanceWhenContainedIn:barClass, nil];
[buttonAppearanceInBar setTitle:... forState:...];
Now, this technique works for the Cancel button, but it also works for the Back button if you change the barClass to [UINavigationBar self].
This solution work for me - iOs7 and iOs8:
#interface ... : ...
#property (strong, nonatomic) IBOutlet UISearchBar *search;
#end
and
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
[searchBar setShowsCancelButton:YES animated:YES];
NSArray *searchBarSubViews = [[self.search.subviews objectAtIndex:0] subviews];
UIButton *cancelButton;
for (UIView *subView in searchBarSubViews) {
if ([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
cancelButton = (UIButton*)subView;
break;
}
}
if (cancelButton) {
[cancelButton setTitle:#"New cancel" forState:UIControlStateNormal];
}
//insert this two lines below if you have a button appearance like this "Ne...cel"
[searchBar setShowsCancelButton:NO animated:YES];
[searchBar setShowsCancelButton:YES animated:YES];
}
On iOS 7, if you've set displaysSearchBarInNavigationBar = YES on UISearchDisplayController, replacing the cancel button title via subview recursion or the appearance proxy will not work.
Instead, use your own bar button in viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;
UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(#"A Custom Title", nil)
style:UIBarButtonItemStyleBordered
target:self
action:#selector(cancelButtonTapped:)];
// NB: Order is important here.
// Only do this *after* setting displaysSearchBarInNavigationBar to YES
// as that's when UISearchDisplayController creates it's navigationItem
self.searchDisplayController.navigationItem.rightBarButtonItem = barItem;
}
Jeremytripp 's working Code in Swift
I couldn't find the same code in Swift so I "translated" it myself:
func searchDisplayControllerWillBeginSearch(controller: UISearchDisplayController) {
self.searchDisplayController?.searchBar.showsCancelButton = true
var cancelButton: UIButton
var topView: UIView = self.searchDisplayController?.searchBar.subviews[0] as UIView
for subView in topView.subviews {
if subView.isKindOfClass(NSClassFromString("UINavigationButton")) {
cancelButton = subView as UIButton
cancelButton.setTitle("My Custom Title", forState: UIControlState.Normal)
}
}
}
If you just want to localized the default "Cancel" title for cancel button, I prefer to change the value of CFBundleDevelopmentRegion key from en to your localized region in Info.plist file in project.
Here is my change,
<key>CFBundleDevelopmentRegion</key>
<string>zh_CN</string>
after that, the default "Cancel" title will show as Chinese "取消". This change will also affect all the default region values, for example, the pasteboard operations' action titles on UITextField/UITextView will be localized, "Select" -> "选择", "Paste" -> "粘贴"...
By the way, the Info.plist file could be localized perfectly.
Enjoy!
Instead of referencing the non-public UINavigationButton class, I did the following. I'm hoping that it will make it through App Store review!
for (id subview in searchBar.subviews) {
if ([subview respondsToSelector:#selector(setTitle:)]) {
[subview setTitle:#"Map"];
}
}
If you're still having trouble with changing the Cancel button in iOS7, this is currently working for me:
-(void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller{
self.searchDisplayController.searchBar.showsCancelButton = YES;
UIButton *cancelButton;
UIView *topView = self.searchDisplayController.searchBar.subviews[0];
for (UIView *subView in topView.subviews) {
if ([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
cancelButton = (UIButton*)subView;
}
}
if (cancelButton) {
//Set the new title of the cancel button
[cancelButton setTitle:#"Hi" forState:UIControlStateNormal];
}
}
if the SearchBar is in the navigationBar, the code will be different than the usual answer; You need to search for NavigationBar's subviews instead.
-(void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller{
UINavigationBar * navigationBar = self.navigationController.navigationBar;
for (UIView *subView in navigationBar.subviews){
if([subView isKindOfClass:NSClassFromString(#"UINavigationButton")]){
[(UIButton*)subView setTitle:#"İptal" forState:UIControlStateNormal];
}
}}
and This work in iOS7+ , if you still can't set the title you should learn view debugging - This is how I solved this problem of mine.
This brief tutorial outlines the key points of View-Debugging very well:
http://www.raywenderlich.com/98356/view-debugging-in-xcode-6
if #available(iOS 13.0, *) {
controller.searchBar.setValue("Done", forKey:"cancelButtonText")
} else {
controller.searchBar.setValue("Done", forKey:"_cancelButtonText")
}
🤦‍♂️
Actually controller.searchBar.setValue("Done", forKey:"cancelButtonText") works for all iOS versions
Working short code in Swift 2.1 (iOS7-9 tested)
#IBOutlet weak var searchBar: UISearchBar!
func enableSearchBarCancelButton(enable: Bool, title: String? = nil) {
searchBar?.showsCancelButton = enable
if enable {
if let _cancelButton = searchBar?.valueForKey("_cancelButton"),
let cancelButton = _cancelButton as? UIButton {
cancelButton.enabled = enable //comment out if you want this button disabled when keyboard is not visible
if title != nil {
cancelButton.setTitle(title, forState: UIControlState.Normal)
}
}
}
}

Resources