We make Folio, a pretty kick-ass iPad app that we give away to our partners to showcase their inventory at art fairs. Whilst making it we tried to ensure that all of the application fits in with the Artsy website aesthetic, and recently the last natively styled control fell to our mighty code hammers. That was the UISearchBar.
When displaying only search results in a table it makes a lot of sense to use Apple's UISearchDisplayController as it handles a lot of edge cases for you. However the downside is that you lose some control over how the views interact.
The search bar was the only native control that actually made it into the version 1 release. This was mainly due to it requiring a bit of black magic in order to get it to work the way we wanted. So lets go through the code and rip it to pieces.
First up, you're going to want to make yourself a subclass of the UISearchBar, I'm going to be calling ours ARSearchBar. Here's our public header.
123456
@interfaceARSearchBar : UISearchBar// Called from The SearchDisplayController Delegate-(void)showCancelButton:(BOOL)show;-(void)cancelSearchField;@end
Inside the implementation file we declare private instance variables for keeping track of the textfield and the Cancel button. This is so we can avoid finding them in the view hierarchy when we want to change the frame it during resizing.
So, to look at setting the size we've found it easiest to deal with that in an overrode setFrame and setting the height of the new frame before it goes to the super class. As the search bar doesn't change its height between state changes like text insertion it shouldn't pose a problem to have it hardcoded.
What does pose a problem though is making sure that the subviews inside the search bar are positioned correctly with respect to the new height, this is amended in layoutSubviews. In our case the textfield should take up almost all of the search bar.
Next up is that we can't access our foundSearchField because it's not been found yet! Personally, I'm a big fan of using nibs for everything ( and pretty pumped about Storyboards too ) so we do our searching in awakeFromNib .
1234567891011
-(void)awakeFromNib{[superawakeFromNib];// find textfield in subviewsfor(inti=[self.subviewscount]-1;i>=0;i--){UIView*subview=[self.subviewsobjectAtIndex:i];if([subview.classisSubclassOfClass:[UITextFieldclass]]){foundSearchTextField=(UITextField*)subview;}}}
This gives us a textfield, next up we want to stylize it. The perfect place for this is just after finding the textfield that you use to search in.
123456789101112131415161718192021
-(void)stylizeSearchTextField{// Sets the background to a static black by removing the gradient viewfor(inti=[self.subviewscount]-1;i>=0;i--){UIView*subview=[self.subviewsobjectAtIndex:i];// This is the gradient behind the textfieldif([subview.descriptionhasPrefix:@"<UISearchBarBackground"]){[subviewremoveFromSuperview];}}// now change the search textfield itselffoundSearchTextField.borderStyle=UITextBorderStyleNone;foundSearchTextField.backgroundColor=[UIColorwhiteColor];foundSearchTextField.background=nil;foundSearchTextField.text=@"";foundSearchTextField.clearButtonMode=UITextFieldViewModeNever;foundSearchTextField.leftView=[[UIViewalloc]initWithFrame:CGRectMake(0,0,TextfieldLeftMargin,0)];foundSearchTextField.placeholder=@"";foundSearchTextField.font=[UIFontserifFontWithSize:ARFontSansLarge];}
You might be wondering why we removed the placeholder text? We needed more control over the style and positioning of the placeholder text and the search icon. These are easily controlled by the UISearchDisplayController subclass rather than inside the custom search bar. This is also the place that we can deal with having our custom Cancel button.
The original Cancel button is something that we choose to keep around, rather than removing it form the view hierarchy, that's so we can have our overlay Cancel button call its method instead of trying to replicate the cancel functionality ourselves.
To keep track of the Cancel button we need to know when its meant to appear, and when its meant to disappear. Because the Cancel button is created at runtime every time a search is started we need to
know when thats happening so we can hide it, we can do that by registering for UITextFieldTextDidBeginEditingNotification on the textfield once it's been found. We do this in awakeFromNib.
12345678910111213141516
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(removeOriginalCancel)name:UITextFieldTextDidBeginEditingNotificationobject:foundSearchTextField];-(void)removeOriginalCancel{// remove the original buttonfor(inti=[self.subviewscount]-1;i>=0;i--){UIView*subview=[self.subviewsobjectAtIndex:i];if([subview.classisSubclassOfClass:[UIButtonclass]]){// This is called every time a search is began,// so make sure to get the right button!if(subview.frame.size.height!=ViewHeight){subview.hidden=YES;}}}}
Finally we have the styling of the button. I've summed it up here as a lot of it is very application specific.
-(void)createButton{ARFlatButton*cancelButton=[ARFlatButtonbuttonWithType:UIButtonTypeCustom];[[cancelButtontitleLabel]setFont:[UIFontsansSerifFontWithSize:ARFontSansSmall]];NSString*title=[@"Cancel"uppercaseString];[cancelButtonsetTitle:titleforState:UIControlStateNormal];[cancelButtonsetTitle:titleforState:UIControlStateHighlighted];CGRectbuttonFrame=cancelButton.frame;buttonFrame.origin.y=ViewMargin;buttonFrame.size.height=ViewHeight;buttonFrame.size.width=66;buttonFrame.origin.x=self.frame.size.width-buttonFrame.size.width-ViewMargin+CancelAnimationDistance;cancelButton.frame=buttonFrame;[cancelButtonaddTarget:selfaction:@selector(cancelSearchField)forControlEvents:UIControlEventTouchUpInside];overlayCancelButton=cancelButton;[selfaddSubview:overlayCancelButton];[selfbringSubviewToFront:overlayCancelButton];}-(void)cancelSearchField{// tap the original button!for(inti=[self.subviewscount]-1;i>=0;i--){UIView*subview=[self.subviewsobjectAtIndex:i];if([subview.classisSubclassOfClass:[UIButtonclass]]){if(subview.frame.size.height!=ViewHeight){UIButton*realCancel=(UIButton*)subview;[realCancelsendActionsForControlEvents:UIControlEventTouchUpInside];}}}}
The complete code is available as a gist under the MIT license.
Comments