UIAccessibilityContainer in a table view - iphone

I'm trying to put a custom view inside a UITableViewCell which of course lives within a UITableView. I want to make this custom view accessible so I need to make it a UIAccessibilityContainer (since it contains several visual elements that aren't implemented as their own UIViews).
When I do this, the location of the elements get all messed up whenever the table scrolls. While paging through the elements using VoiceOver, it will automatically scroll the table to attempt to center the selected element on screen, but then the outline of where VoiceOver thinks the element is no longer lines up with where it is visually.
Note in the screenshot that the inspector says "Row 4, element 2" but the highlighted area is some random place in Row 7 since that happens to be where Row 4 was before it auto-scrolled the table.
My thought is that I might have to use UIAccessibilityPostNotification() to post a layout change when the table view scrolls, but I don't have to do that when I don't use a UIAccessibilityContainer and it feels like I shouldn't have to do it and that the system should be handling this for me - but the fact that UIAccessibilityElement needs to have it's accessibilityFrame set in screen coordinates does seem to throw a wrinkle into things. (Bonus question: Why the heck is the API designed that way? Why not define the frame relative to the element's container or something like that? Arg.)
Here's the custom view's implementation just in case there's something in here which is causing the problem. For the full project (Xcode 4), click here.
#implementation CellView
#synthesize row=_row;
- (void)dealloc
{
[_accessibleElements release];
[super dealloc];
}
- (void)setRow:(NSInteger)newRow
{
_row = newRow;
[_accessibleElements release];
_accessibleElements = [[NSMutableArray arrayWithCapacity:0] retain];
for (NSInteger i=0; i<=_row; i++) {
UIAccessibilityElement *element = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
element.accessibilityValue = [NSString stringWithFormat:#"Row %d, element %d", _row, i];
[_accessibleElements addObject:element];
[element release];
}
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[[UIColor lightGrayColor] setFill];
UIRectFill(self.bounds);
[[UIColor blackColor] setFill];
NSString *info = [NSString stringWithFormat:#"Row: %d", _row];
[info drawAtPoint:CGPointZero withFont:[UIFont systemFontOfSize:12]];
[[[UIColor whiteColor] colorWithAlphaComponent:0.5] setFill];
NSInteger x=0, y=0;
for (NSInteger i=0; i<=_row; i++) {
CGRect rect = CGRectMake(12+x, 22+y, 30, 30);
UIAccessibilityElement *element = [_accessibleElements objectAtIndex:i];
element.accessibilityFrame = [self.window convertRect:[self convertRect:rect toView:self.window] toWindow:nil];
UIRectFill(rect);
x += 44;
if (x >= 300) {
x = 0;
y += 37;
}
}
}
- (BOOL)isAccessibilityElement
{
return NO;
}
- (NSInteger)accessibilityElementCount
{
return [_accessibleElements count];
}
- (id)accessibilityElementAtIndex:(NSInteger)index
{
return [_accessibleElements objectAtIndex:index];
}
- (NSInteger)indexOfAccessibilityElement:(id)element
{
return [_accessibleElements indexOfObject:element];
}
#end
Edit: I should note that I've tried variations that update the element's accessibilityFrame in -indexOfAccessibilityElement: and -accessibilityElementAtIndex: with the idea that VoiceOver will request the element somehow whenever it needs it and that'd be a nice time to update things. However that doesn't seem to work, either. I was kind of hoping maybe VoiceOver would automatically request things to redraw, but that also doesn't seem to work. (The idea of putting the location setting code in -drawRect: comes from something I remember seeing at WWDC about this, but it was unclear to me if that was "best practice" or just happened to be convenient.)

I've solved the problem you described by adding some side effects to the accessibility methods and with collaboration from the table scroll delegates. Inside the drawRect method I calculate the local coordinates of the rectangle, so I don't need to convert the coordinates there, simply calculate them with regards to the cell's top left corner.
Then, I modified the accessor to update the frame with a side effect like this (note the y resetting):
- (id)accessibilityElementAtIndex:(NSInteger)index
{
UIAccessibilityElement *element = [accesible_items_ get:index];
CGRect rect = element.accessibilityFrame;
rect.origin.y = 0;
element.accessibilityFrame = [self.window
convertRect:rect fromView:self];
return element;
}
While this works fine for the initial view, you still get the displaced frames when the user scrolls, so in your table view controller implement the following scrolling delegate:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// This loop has a side effects, see the cell accesor code.
for (id cell in self.tableView.visibleCells)
for (int f = 0; [cell accessibilityElementAtIndex:f]; f++);
UIAccessibilityPostNotification(
UIAccessibilityLayoutChangedNotification, nil);
NSLog(#"Layout changed after scrollViewDidScroll");
}
Depending on the contents of your table not all cells may respond to the accessibility method, so you could first query each cell with respondsToSelector to avoid sending unexpected messages.
I would also post UIAccessibilityLayoutChangedNotification at the end of the cell setter creating the UIAccessibilityElement objects, or you will get log messages saying that your elements disappeared or could not be found.
These changes make the scroll work when iterating the elements one through one with the rotor, but you may get still odd results if the user scrolls with a triple finger gesture. That's because by default tableViews scroll a screen page at a time, which may not happen to have the same element boundaries as your cells, and the rotor selects a cell half visible. Depending on the scrolling direction and other UI elements, the half visible cell could overlap controls the rotor gets confused with. You need to implement paged scrolling to control avoid this behavior.

Is this problem affecting the usability of the app in VoiceOver mode? When I've played with VoiceOver on both Mac OS and iOS, the highlight boxes (especially in Web views) frequently become unmatched with their onscreen objects. If the app is still usable in VoiceOver, I'd call this a known bug and fix it if somebody complains.
After all, most of the blind people I know aren't looking at the highlight boxes.

Related

iOS : How to do proper paging in UIScrollView?

I've a UIScrollView of size (320,160). I'm adding some UIImageView into it, which are of size (213,160). The first UImageView starting from 54 (x) and so on, I've added a space of 5.0 in between each UIImageView. I've also enabled pagingEnable in IB & in coding. What my problem is its not properly working as per its property! When I scroll it should show me UIImageViews in each single page instead it showing me something like see screenshot I want output something like this see output screenshot
Where I'm doing wrong? I also having function of< (previous) & > (next) there to show images. I've asked one question yesterday which was I accepted however my requirement is little change and it won't become my solution. see my question.
Is there any special property that I've to set, or some logic I should implement? All examples I've checked and tried but I find that my requirement is some special. Point me! Thanks.
EDITED:
- (void) setImages
{
CGFloat contentOffset = 0.0f;
for (int i=0; i<[arrImgUrls count]; i++)
{
CGRect imageViewFrame = CGRectMake(contentOffset, 0.0f, 213, scrollImages.frame.size.height);
AsyncImageView *asyncImageView = [[AsyncImageView alloc] initWithFrame:imageViewFrame];
[asyncImageView.layer setMasksToBounds:YES];
NSString *urlImage = [arrImgUrls objectAtIndex:i];
NSURL *url = [NSURL URLWithString:urlImage];
[asyncImageView loadImageFromURL:url];
[scrollImages addSubview:asyncImageView];
contentOffset += asyncImageView.frame.size.width+increment;
[asyncImageView release];
}
scrollImages.contentSize = CGSizeMake([arrImgUrls count] * scrollImages.frame.size.width, scrollImages.frame.size.height);
scrollImages.pagingEnabled = YES;
scrollImages.clipsToBounds = NO;
}
-(IBAction)prevImage:(id)sender
{
_currentImage--;
[btnNext setEnabled:YES];
if (_currentImage==0)
{
[btnPrev setEnabled:NO];
[scrollImages setContentOffset:CGPointMake((_currentImage*imageWidth), 0) animated:YES];
return;
}
NSLog(#"previous:mult %d inc %d current %d",_currentImage*imageWidth,increment*_currentImage,_currentImage);
int nextImage=_currentImage+2;
[scrollImages setContentOffset:CGPointMake((((_currentImage*imageWidth)-(increment*_currentImage)))+(nextImage*increment), 0) animated:YES];
}
-(IBAction)nextImage:(id)sender
{
_currentImage++;
NSLog(#"next:mult %d inc %d current %d",_currentImage*imageWidth,increment*_currentImage,_currentImage);
[scrollImages setContentOffset:CGPointMake((_currentImage*imageWidth)+(increment*_currentImage), 0) animated:YES];
[btnPrev setEnabled:YES];
if (_imageCount-1 == _currentImage)
{
[btnNext setEnabled:NO];
}
}
Paging scroll views alway page multiples of their frame size. So in your example paging is always +320.
This behavior is good if you have content portions matching the frame of the scroll view.
What you have to do, is giving your scroll view a width of 213 and set its clipsToBounds property to NO.
After that your scroll view pages exactly how you want and you see what's left and right outside the frame.
Additionally you have to do a trick to make this left and right area delegate touches to the scroll view.
It's greatly explained in this answer.
You are forgetting to set scrollview content size.
make sure that you have set the content size to fit N number of images.
if you want to scrollview to scroll for 10 images with an image on each page
set scrollView contentSize as
[scrollView setContentSize:CGSizeMake(320 * 10,200)];

Relativelayout or LinearLayout in ios iphone development?

I want to add a subview in the top of my view, I have to recalculate the origin y value for all of other views and re-position them to leave space for the new added view.
It is very boring, as I know android have relativelayout or linearlayout can help automatically do that.
How to solve this problem easily in ios development?
I've created a library to solve just this problem: CSLinearLayoutView
You use it like this:
// create the linear layout view
CSLinearLayoutView *linearLayoutView = [[[CSLinearLayoutView alloc] initWithFrame:self.view.bounds] autorelease];
linearLayoutView.orientation = CSLinearLayoutViewOrientationVertical;
[self.view addSubview:linearLayoutView];
// create a layout item for the view you want to display and add it to the layout view
CSLinearLayoutItem *item = [CSLinearLayoutItem layoutItemForView:someView];
item.padding = CSLinearLayoutMakePadding(5.0, 10.0, 5.0, 10.0);
item.horizontalAlignment = CSLinearLayoutItemHorizontalAlignmentCenter;
item.fillMode = CSLinearLayoutItemFillModeNormal;
[linearLayoutView addItem:item];
// add more items
I've been trying to do a relative (linear) layout for a while and finally decided to just subclass UIScrollView to get it done.
I started out just replacing layoutSubviews with a simple loop through the subviews that reset the origins while keeping a running Y. But, some unexpected things are added to the scrollview, including UIInlineAutoCorrect views from textfields/views, which means these things were being mangled by the layout. So I added a little bit of logic that uses the tag property of a UIView to determine if I should lay it out:
-(void) layoutSubviews{
CGFloat runningY = 0.0f;
CGFloat widestWidth = 0.0f;
for (UIView *view in self.subviews) {
if (view.tag != 1999) {
continue;
}
view.origin = CGPointMake(view.origin.x, runningY);
runningY += view.height;
if ([view autoresizingMask] == UIViewAutoresizingFlexibleWidth) {
view.width = self.width;
}
if (view.width > widestWidth) {
widestWidth = view.width;
}
}
[self setContentSize:CGSizeMake(widestWidth, runningY)];
}
If you would still like to use unique tags for your views, you should just specify a range of tags that will be included in the layout instead of a single value.
It's not much work to subclass UIView to make sense of methods like -(void)addView:toRightOfView: etc. You could do this as you go, porting only the methods you need. You could then call these in your override of layoutSubviews as Benjamin indicates.
Views can be built using IB or they can be written programmatically; Android scores well here in making layouts readable and you can bring that benefit to iOS views created programmatically. That there are few iOS devices means beyond readability there are not (yet?) many practical benefits to this pattern.
NB. A "XIB" file is an XML file. Open it up in your favourite text editor and take a look.
** EDIT.
Here's a quick example I knocked up. It has not been tested but some thing like this will work in your subclass of UIView (call it UIRelativeView perhaps).
- (void) addSubview:(UIView *) viewOne
toRightOfSubview:(UIView *) viewTwo
{
if (viewTwo == nil ||
[self.subviews contains:viewTwo] == NO)
{
[self addSubview:viewOne];
}
else
{
CGRect frameTwo = viewTwo.frame;
CGPoint originOne = CGPointMake(frameTwo.origin.x + frameTwo.size.width,
frameTwo.origin.y);
CGRect frameOne = CGRectZero;
frameOne.origin = originOne;
frameOne.size = viewOne.frame.size;
[viewOne setFrame:frameOne];
[self addSubview:viewOne];
}
}
- (void) moveSubview:(UIView *) viewOne
toRightOfSubview:(UIView *) viewTwo
{
if (viewTwo == nil ||
[self.subviews contains:viewTwo] == NO)
{
[self addSubview:viewOne];
}
else if ([self.subviews contains:viewOne] == NO)
{
[self addSubview:viewOne toRightOfSubview:viewTwo];
}
else
{
CGRect frameTwo = viewTwo.frame;
CGPoint originOne = CGPointMake(frameTwo.origin.x + frameTwo.size.width,
frameTwo.origin.y);
CGRect frameOne = CGRectZero;
frameOne.origin = originOne;
frameOne.size = viewOne.frame.size;
[viewOne setFrame:frameOne];
}
}
You've got no luck here. iOS doesn't have provisions for positioning the views in different layouts like Android. You need to reposition all the other subviews to make the way for the new view.
There are some view resizing methods like sizeToFit and autoResizingMask but they won't help you in your case here.
iOS is much more focused on pixel accuracy than Android it is, which uses relative layouts as it has to deal with multiple screen sizes. However, in iOS, the Interface Builder is an incredibly good tool included in XCode, which you can use.
Also, if you are just adding subviews in a repetitive manner, you could override the layoutSubviews method and use that to handle to manual labour for you. You mention having to "recalculate the origin y value for all of other views and re-position them to leave space for the new added view" ... You could code that into your layoutSubviews so you don't have to do it yourself each time.
Unfortunately, though, the SDK doesn't have any of this included by default. autoresizingMask's are great but you can't use that for initial layout; it's for automatic really it when rotating only.
As of iOS 9 you can use UIStackView, which works very similarly to LinearLayout: you add views and the stack view arranges them as needed based on your sizing preferences:
Fill will leave three of them their natural size, and make the fourth one take up the most space. It uses Auto Layout's content hugging priority to decide which one to stretch.
Fill Equally will make each subview the same size so they fill all the space available to the stack view.
Fill Proportionally uses the intrinsic content size of each subview to resize them by an equal amount. So view 1 was designed to have twice as much height as views 2, 3 and 4, that ratio will remain when they are resized – all the subviews get proportionally bigger or smaller.
Equal Spacing does not resize the subviews, and instead resizes the spacing between the subviews to fill the space.
Equal Centering is the most complicated, but for many people also the most aesthetically pleasing. It attempts to ensure the centers of each subview are equally spaced.
You can also set spacing between views in the stack view, adding some padding.
WARNING: When adding stack view child views in code you should always use addArrangedSubview() like this:
stackView.addArrangedSubview(someView)
If you try to use plain old addSubview() it won't work correctly, because the stack view won't know to arrange it.
As for removing, you need to be careful to use stackView.removeArrangedSubview(someView) and someView.removeFromSuperview() otherwise the view won't be removed correctly.
You might find my UIStackView tutorial useful.

iphone - Is slow for UILabel to setText?

My code is simple. I have a UIScrollView and below it there are five UILabel. What I set in the UIScrollView delegate scrollViewDidScroll is that Every 150 pixel that the scrollView moved, I update the five UILabel with NSStrings in an NSArray.
Here is the codes:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
int offsetY = scrollView.contentOffset.Y;
BOOL canUpdate = (offsetY % 150 == 0)?YES:NO;
if (canUpdate) {
int index = offsetY / 150;
for (int i = 0;i < 5; i++) {
UILabel *label = [labelArray objectAtIndex:i];
label.text = [stringArray objectAtIndex:index];
}
}
}
It is quite slow. I mean, if I scroll quite quickly (let's say it may need update the labels several times before it stops), I can see the scroll lags, just like something is blocking its moving.
Is UILabel setText really that slow??
thanks
No, its not slow. You should try setting your label in scrollViewDidScroll delegate method.
UPDATE:
So you mean it still doesnt work !? As far as i can guess, you are trying to check if your scroll view has scrolled 150px and then update some label.
In your code, you are basically checking for exact 150 px value, as you have used modulo (%) operation.
It will not be always that you have scrolled and the scroll view offset is exactly at 150, 300, 450, etc.
No, absolutely. If it was slow, you wouldn't be able to efficiently use the contact list on the iPhone.
If your code is slow, you should look for other pieces of code that are slowing the scroll process. Switching to the scrollViewDidScroll: method, as suggested by #pratikshabhisikar, could also be of help.
Try printing a log in your scrollViewDidEndDecelerating: to see how many times it's been called.
pratikshabhisikar has it right.
You are only updating one of your labels when your contentOffset is exactly a multiple of 150 (as an integer).
(offsetY % 150)
returns true only if offsetY is divisible by 150 and leaves no remainder. So if you are scrolling too fast, that offset could skip the 'sweet spot' (multiples of 150) and you won't get an update.
try:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
int index = floor(scrollView.contentOffset.y / 150.0f);
for (int i = 0;i < 5; i++) {
UILabel *label = [labelArray objectAtIndex:i];
label.text = [stringArray objectAtIndex:index];
}
}
The 'floor' function ensures you will get the rounded down version as an int after the division - so anywhere from 0 - 150 will return 0 etc.
You might like to cache the last index loaded too (since you will be reloading the text for any amount of scrolling at present (i.e only enter the for loop if index != mCurrentIndex.

UIScrollView. Any thoughts on implementing "infinite" scroll/zoom?

So, UITableView supports essentially "infinite" scrolling. There' may be a limit but that sucker can scroll for a looonnnggg time. I would like to mimic this behavior with a UIScrollView but there are two fundamental impediments:
1) scrollView.contentSize is fixed at creation time.
2) zooming can blow any lazy-loading scheme all to hell since it can cause infinte data explosion.
Have others out there pondered this idea? Yah, I know, we are essentially talking about re-creating Google Maps here. Any insights would be much appreciated.
Cheers,
Doug
I've just finished implementing the infitine scroll for me.
In my Implementation I have UITableViewCell with a scrollView and Navigationbuttons. The scrollView contains x views all with the same width. views are alined horizontally and paging is enabled.
scrollView.clipsToBounds = YES;
scrollView.scrollEnabled = YES;
scrollView.pagingEnabled = YES;
scrollView.showsHorizontalScrollIndicator = NO;
My codelogic is like the following:
In my initialization function I
create all the views (for the scrollview) and
put them into an array and
add them to the scrollView
Then I call a function that calculates in a loop the positions for each view (each time you detect a scroll this function will need to be called too). It always takes the first element of the array and sets the frame to (0,0,...,...), the second with (i*width,0,....,....) and so on. The function beeing called looks like this:
- (void)updateOffsetsOfViews{
int xpos = 0;
for (int i=0; i<[views count]; i++) {
UIImageView *_view = [views objectAtIndex:i];
CGRect aFrame = _view.frame;
aFrame.origin.x = xpos;
aFrame.origin.y = 0.0;
_view.frame = aFrame;
xpos += viewWidth;
}
float center = 0;
if(fmod([views count],2) == 1){
center = viewWidth * ([views count]-1)/2;
}else {
center = viewWidth * [views count]/2;
}
[scrollView setContentOffset:CGPointMake(center, 0)];
lastOffset = center;
}
Then (still in the initialization process) I add an observer
[scrollView addObserver:self forKeyPath:#"contentOffset" options:0 context:nil];
so each time something in the scrollView changes I get the (observeValueForKeyPath)-function called, which looks like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
UIImageView *_viewFirst = (UIImageView *)[views objectAtIndex:0];
if ( fmod([scrollView contentOffset].x,viewWidth) == 0.0) {
if ([scrollView contentOffset].x > lastOffset) {
[views removeObjectAtIndex:0];
[views addObject:_viewFirst];
[self updateOffsetsOfViews];
}else if ([scrollView contentOffset].x < lastOffset) {
UIImageView *_viewLast = (UIImageView *)[views lastObject];
[views removeLastObject];
[views insertObject:_viewLast atIndex:0];
[self updateOffsetsOfViews];
}
}
}
And in dealloc or viewDidUnload (depends on how you implement it) don't forget to remove the observer.
[scrollView removeObserver:self forKeyPath:#"contentOffset"];
Hope this helps, you might notice some overhead, but in my implementation I also support like scrolling 5 pages (well... unlimited) at once and autoanimated scrolling etc. so you might see something that could be thrown away.
While it's impossible to have a truly infinite UIScrollView, there are some simple tricks you can use to emulate that behavior.
Handling the fixed contentSize: have some fixed-size view handled by your scroll view, and at launch or instantiation, set the content offset so that you're seeing the middle of the handled view. Then just watch the content offset (using KVO or some other method), and if you near any edge, update the content of the view with a new set of content (offset appropriately) and reset the scroll view's contentOffset property to be back in the middle.
Handling zooming: do something similar, only this time watch the zoom factor on the scroll view. Whenever it gets to a certain point, do some manipulation to whatever data you're presenting so that it appears zoomed, then reset the zoom factor to 1.0. For example, if you're scrolling an image and it gets zoomed to appear twice as large, programmatically apply some kind of transform to make the image twice as large, then reset the scroll view's zoom factor to 1.0. The image will still appear zoomed in, but the scroll view will be able to continue zooming in further as necessary. (Google Maps takes this one step further where it lazy-loads more detailed views as the user zooms - you may or may not choose to implement this.)
The StreetScroller sample project from Apple demonstrates how to perform infinite scrolling in a UIScrollView.
Bear in mind that when the scroll is animated, contentOffset changes many times, not just page by page, but with each step in the animation.
Perhaps setting contentSize to some gigantic value and then moving a limited number of underlying views around to track the view position as in the Tiling sample will do the trick.
To mitigate the possibility of eventually reaching an edge and having to abruptly recenter the view (which cancels any scrolling currently in motion), the view can be recentered when it is stationary, from time to time.
Anyway, that's what I'm about to try.

Changing the size of the UISearchBar TextField?

I have a UITableView with an Index on the side; I want to add a UISearchBar to it, but the index overlaps with the "x" to clear the search. I've noticed in the Contacts application, the textfield within the UISearchBar is resized to accommodate this, but I can't work out how to do this in my own app.
I have tried the following in my viewDidLoad, but it does not seem to work.
UITextField * textField = (UITextField *)[[self.search subviews] objectAtIndex:0];
CGRect r = textField.frame;
[textField setFrame:CGRectMake(r.origin.x, r.origin.y, r.size.height, r.size.width-30)];
Any ideas?
it's much easier than all these suggestions. In interface builder, instead of putting the Search Bar as the header of your Table View, you can put a View instead. Then, put a Navigation Bar inside this View. Grab the left resizing handle of the Navigation Bar and pull it to the right until the N B is only 25 pixels wide. Clear out the Title in the N B (double click to select it, then delete). Then, add a Search Bar into the same View. Move its right resizing handle to the left, adjust so that it abuts the N B. That's it.
You can enable a cancel button if you want too and it also won't overlap the index (remains within the search bar).
Apparently a Table View can only have 1 subview in its header, that's why you need to put the View first, then the N B and Search Bar inside it.
UPDATE: see Beginning iPhone Development from Apress, p. 241 of SDK 3 edition. You just disable the index while searching.
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (isSearching) {
return nil;
}
return keys;
}
Also they talk about adding a magnifying glass to the top of the index.
Great book all around.
Why not just make the actual UISearchBar smaller horizontally, and place an (empty) UINavigationBar to the right of it? They will render the exact same background.
Better than hacking the internals of Apple's objects that could change.
Also, when animating the UISearchBar's width, you'll notice that the inner text field is not animated along with it. You can fix this by calling UISearchBar's "layoutSubviews" within your animation block after changing its frame. (that's where it determines the size of the inner text field)
Ok, I've come up with a solution.
Create a subclass of UISearchBar
Include this code in the drawRect: method.
UITextView * textField = [self.subviews objectAtIndex:0];
textField.frame = CGRectMake(5, 6, (310 - kRightSideMargin), 31);
[super drawRect:rect];
Note: kRightSideMargin is a constant I set in my header file; I have it set to 25.
Thanks for the suggestions from everyone else.
As Padraig pointed out all you have to do is subclass out the searchBar. Create your UISearchBar subclass, and add the following code into the layoutSubviews method:
- (void)layoutSubviews
{
UITextField *searchField;
for(int i = 0; i < [self.subviews count]; i++)
{
if([[self.subviews objectAtIndex:i] isKindOfClass:[UITextField class]])
{
searchField = [self.subviews objectAtIndex:i];
}
}
if(!(searchField == nil))
{
searchField.frame = CGRectMake(4, 5, 285, 30);
}
}
This loops through all the subviews and checks them against type UITextField. That way if it ever moves in its line up of subviews this will still grab it. I found 285 to just wide enough not to overlap with the index of my tableView.
As of iOS 6, the navigation bar solution didn't work well for me because of slightly different looks now between the UISearchBar and UINavigationBar. So, I switched to something similar to Padraig's approach by subclassing the UISearchBar.
#interface SearchBarWithPad : UISearchBar
#end
#implementation SearchBarWithPad
- (void) layoutSubviews {
[super layoutSubviews];
NSInteger pad = 50;
for (UIView *view in self.subviews) {
if ([view isKindOfClass: [UITextField class]])
view.frame = CGRectMake (view.frame.origin.x, view.frame.origin.y, view.frame.size.width - pad, view.frame.size.height);
}
}
#end
Edit: Ah, I haven't tried it, but I think you might be able to set a navigation bar's clipToBounds = YES to turn off it's new shadow, thereby creating a consistent look again between the two controls.
I am using ViewDeck and want to show a UISearchbar inside the leftController.
Now the problem is if I open the left side which contains the navigation, the right bit overlaps my search field.
I got rid of this by over writing UISearchBar, the textfield will always have the same width, but in one case there is the ViewDeck overlapping and in the other case I hide the ViewDeck-bit and then the cancel button will take up the space:
Subclassing UISearchBar
#import "ViewDeckSearchBar.h"
#define kViewDeckPadding 55
#interface ViewDeckSearchBar()
#property (readonly) UITextField *textField;
#end
#implementation ViewDeckSearchBar
static CGRect initialTextFieldFrame;
- (void) layoutSubviews {
[super layoutSubviews];
// Store the initial frame for the the text field
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initialTextFieldFrame = self.textField.frame;
});
[self updateTextFieldFrame];
}
-(void)updateTextFieldFrame{
int width = initialTextFieldFrame.size.width - (kViewDeckPadding + 6);
CGRect newFrame = CGRectMake (self.textField.frame.origin.x,
self.textField.frame.origin.y,
width,
self.textField.frame.size.height);
self.textField.frame = newFrame;
}
-(UITextField *)textField{
for (UIView *view in self.subviews) {
if ([view isKindOfClass: [UITextField class]]){
return (UITextField *)view;
}
}
return nil;
}
#end
ViewController class
In my Navigation class I need to overwrite these two UISearchbarDelegate methods in order to go to fullscreen with the search results:
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar{
[self.viewDeckController setLeftSize:0];
// I am also using scopes, which works fine (they fade out when not searching)
self.searchBar.scopeButtonTitles = #[#"Food",
#"Beverages",
#"Misc"];
}
-(void)searchBarTextDidEndEditing:(UISearchBar *)searchBar{
self.viewDeckController.leftSize = 55;
}
Result
ViewDeck showing to the right:
(source: minus.com)
Search in Fullscreen (The button and the scope buttons are animated in).
(source: minus.com)
searchBar.layoutMargins = UIEdgeInsetsMake(0, 0, 0, rightPad);
My old solution of changing the UITextField frame stopped working in iOS 13. Putting a UINavigationBar to the right of the UISearchBar never worked well for me as they had different looks at top and bottom.
Sorry to drag this all up again.
I wanted the UISearchBar to be shorter, and I'm using a UISearchBarController, but without actually wanting the index. This is because I have an overlay to the right:
To do this, I fake a sectionIndex with one blank item, then hide it. Here's how I do that:
- (void)hideTableIndex {
for (UIView *view in [tableView subviews]) {
if ([view isKindOfClass:NSClassFromString(#"UITableViewIndex")]) {
view.hidden = YES;
}
}
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)aTableView {
if (aTableView == self.searchDisplayController.searchResultsTableView) {
return nil;
} else {
[self performSelector:#selector(hideTableIndex) withObject:nil afterDelay:0];
return [NSArray arrayWithObjects:#"", nil];
}
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
return 0;
}
This shortens the the UISearchBar and hides the index so it can't be tapped (a small section would otherwise hand to the left of the overlay that when tapped would scroll the UITableView to the top). Like this:
Best of all, when you use the search, you still get the full width bar:
Just put a UIView and put the search bar inside that UIView. UIView must be of same size as UISearchBar.
this worked for me.
The text field used in UISearchBar is a subclass of UITextField called UISearchBarTextField.
AFAIK, there's no way to resize a UISearchBarTextField using the public API, and the private API doesn't reveal much either.
Maybe you can take a look at UISearchBarTextField's subviews, if it has any.
UPDATE: It doesn't.
UPDATE 2: I think you should take a look at UITextField's rightView property. The below code, although it doesn't work, seems like a good starting point:
UIView *emptyView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 25, 25)];
[textField setRightView:emptyView];
[textField setRightViewMode:UITextFieldViewModeAlways];
[emptyView release];
Sorry for Necroposting, but I found another way to make a little space on the right of the textfield.
I was having the problem, that I had an indexed tableview with a searchbar as the first row. Now the index and the searchbar (made in IB, btw.) were overlapping. It tried almost everything with no success. It seems that the width and height properties of the textifield don't respond... So I came up with this:
searchBar.showsCancelButton = YES;
UIView *cButton = [searchBar.subviews objectAtIndex:2];
cButton.hidden = YES;
I still can't adjust the size of the space, but this does it for now... although... pretty weird solution...
Everyone has provided ways to modify the UI. I have discovered how to obtain identical results. You must provide the following two implementations:
Use UISearchDisplayController
More importantly, make sure you initialize it with:
- (id)initWithSearchBar:(UISearchBar *)searchBar contentsController:(UIViewController *)viewController
Failure to set a valid UISearchBar (or passing nil) will prevent the adjustment of the UITextField for the index.
You must return a valid array of titles by implementing:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView;
If you return nil, the index will not be displayed, and the UITextField will not be properly adjusted.
I've submitted a bug report to Apple, suggesting that it seems logical that only #2 should be required, not #1. I have found nothing in the Human Interface Guideline (iPhone HIG) requiring use of the UISearchDisplayController.
The key is to use the "Search Bar and Search Display Controller" and not the "Search Bar" when using Interface Builder.
It kind of looks as though Apple resize the view (note that the index is animated to the right, off screen), making it bigger than the screen.
I would imagine that you'd need to implement the searchBarTextDidBeginEditing: method of the UISearchBarDelegate to trigger this at the appropriate point. This does, however, feel a bit hacky do maybe there's a better way of doing it.
Another appraoch (though tedious) would be to resize the search bar and fill the 'gap' with a navigation bar. Works for me.
What I've come up with isn't too much better. Basically, I make an empty view with the frame that I want to use for the search bar. Then I create a UIToolbar to go behind the search bar. Be sure to set its frame to the same frame as the UIView, except that the Y value has to be -1; otherwise, you'll get two borders drawn at the top. Next create your UISearchBar, but set the frame's width to 30 (or whatever makes sense for your app) less than the UIView. Add them as subviews and set your UIView as the tableHeaderView.
I followed Mike's advice by making a UIView, then putting a Navigation Bar and UISearch Bar inside it. Only problem is first time the search bar is shown its background is the same as a Navigation Bar normally?
Interestingly, if I activate the search, then click cancel the background of this 'fixed'!?
I'm using SDK 3.0, so I removed the UISearchBar item made when I dragged a UISearchDisplayController in to my NIB, then made the view as described above and wired it up to the file owner and the searchBar outlet in the search display controller.
It work fine!!!
[searchBar setContentInset:UIEdgeInsetsMake(5, 0, 5, 35)];