iphone - Is slow for UILabel to setText? - iphone

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.

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)];

UIAccessibilityContainer in a table view

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.

Using layoutsubviews() works properly when cycling through UIButtons, but not when cycling while retrieving them by tag

I'm trying to mimic the iPhone Springboard drag and drop functionality for a tile game and am currently using MatrixCells to generate UIButtons in the View initalisation and add them to the view. The MatrixCells have an order tag, which is used for their own button as well ([button setTag:[cell order]];).
When using `layoutSubviews' to cycle through the UIButtons in the view to arrange them in a grid, it works fine. The grid pops up and my dragging functionality allows me to free-drag the buttons where I will.
- (void)layoutSubviews {
int row = 0;
int col = 0;
for ( UIButton *button in [self subviews] ) {
[button setFrame:CGRectMake( col * 80 + 5, row * 80 + 25, 70, 70)];
if (col < 3) {
col++;
}
else {
row++;
col = 0;
}
}
}
However, cycling through subviews doesn't work when changing the order of the cells, since I'm pretty sure I'm not supposed to mess around in there at all. So, I wanted to cycle through my MatrixCells and use their order to get the appropriate view and then change the frame. However, the layout gets messed up at button 0 and I can't use them as buttons anymore.
- (void)layoutSubviews {
int row = 0;
int col = 0;
for (MatrixCell *cell in currentOrder) {
UIView *view = [self viewWithTag:[cell order]];
[view setFrame:CGRectMake( col * 80 + 5, row * 80 + 25, 70, 70 )];
if (col < 3) {
col++;
}
else {
row++;
col = 0;
}
}
}
Here is a picture with the results: http://i52.tinypic.com/2q903lk.png
viewWithTag seemed the be the most elegant solution for this. So I'm at a loss. Any idea why these implementations don't render the same?
It looks to me rather like you have a background UIImage to provide that grey gradient backdrop. It also looks like your button 0 might have a [cell order] of 0. I'm going to guess that you've never changed the tag of your background UIImage, which will thus default to 0.
When you call [self viewWithTag] for the first UIButton (labeled 0), it will find the first subview with a tag 0 - the background image, not the UIButton you expected - and move that. This, I suspect. is why all the other buttons line up fine, why the background moves oddly, and why your 0 button is not visible.
[self viewWithTag:0] returns self first, instead of the expected (UIButton *), since I didn't change the view's tag upon initialization. Apparantly tags default to 0 at initialization. layoutsubviews then proceeded to change the frame of self, messing up the layout and functionality. I solved this dilemma by changing the view's tag to 999 at initialization.

Scrolling speed during UITableView re-ordering mode (not performance related)

Is there a way to increase the speed that you can drag a cell up/down during a table's movable row mode of a UITableView? For instance, there seems to be a standard speed that the table will allow you to drag the cell when you are moving it around and the scroll speed seems to increase if you hold it near the top/bottom edge of the device screen. For a given cell height and a whole bunch of cells, this might take a while to re-order them all if I have to use their standard slow scrolling.
So basically, I would like to increase the speed that it scrolls up/down when you are dragging the cell so it won't take as long to get to where you want to drop the cell in place.
I understand that you can speed up the moving process by decreasing cell height to place more cells on the device screen, but I'd rather do this only if I can't increasing scrolling speed.
Any suggestions or ideas from past experiences with this? Any advice is greatly appreciated!
Here you go. A proof of concept of a speed enhancer of the scroll when you move a cell, SDK 3.1. It may not pass Apple's requirements since overriding _beginReorderingForCell and _endReorderingForCell looks a little off-spec. There are other ways to determine if a cell starts or ends reordering (e.g. subclassing UITableViewCell and finding some measure) but this is the easiest I think.
The approach is quite simple: for every movement of Y pixels down, we move 2*Y pixels down, only when reordering.
The problem is that the currently dragged cell is a subview of the table view, so it shifts with the table view if we move it. If we are to correct for that within this setContentOffset, it has no effect since the position of the cell will be set based on values calculated apart from the current contentOffset. Therefore we correct an instant later using performSelector.
I left the debugging lines in there for convenience. All you need to do is to use FastUITableView instead of UITableView (esp. in you NIB)
You may of course want to add some timing things, so that the speed only goes up after 1 second or so. That will be trivial.
#interface FastUITableView : UITableView
{
UITableViewCell *draggingCell;
CGFloat lastY;
}
#end
#implementation FastUITableView
-(void)_beginReorderingForCell:(UITableViewCell*)cell
{
printf("begin reordering for cell %x\n",cell);
draggingCell = cell;
lastY = -1.0f;
[super _beginReorderingForCell:cell];
}
-(void)_endReorderingForCell:(UITableViewCell*)cell wasCancelled:(BOOL)cancelled animated:(BOOL)animated
{
printf("end reordering for cell %x\n",cell);
draggingCell = nil;
[super _endReorderingForCell:cell wasCancelled:cancelled animated:animated];
}
-(void)setContentOffset:(CGPoint)pt
{
if ( !draggingCell )
{
[super setContentOffset:pt];
return;
}
if ( lastY < 0 ) lastY = pt.y;
CGPoint fast = pt;
float diff = pt.y - lastY;
//diff *= 0.5; /// <<--- control speed here
fast.y = pt.y + diff;
if ( fast.y > self.contentSize.height - self.superview.frame.size.height )
{
CGFloat corr = fast.y - self.contentSize.height + self.superview.frame.size.height;
printf("Correction: %f\n",corr);
fast.y -= corr;
diff -= corr;
} else if ( fast.y < 0.0f ) {
CGFloat corr = -fast.y;
printf("Correction: %f\n",corr);
diff += corr;
fast.y += corr;
}
[self performSelector:#selector(moveCell:) withObject:[NSNumber numberWithFloat:diff] afterDelay:0.01];
lastY = fast.y;
// printf("setting content offset: %f -> %f of max %f\n",pt.y, fast.y, self.contentSize.height);
[super setContentOffset:fast];
}
-(void)moveCell:(NSNumber*)diff
{
CGRect frame = draggingCell.frame;
frame.origin.y += [diff floatValue];
// printf("shifting cell %x with %f\n",draggingCell,[diff floatValue]);
draggingCell.frame = frame;
}
If the list is going to get that long, you may want to consider other reordering mechanisms. For example, my Netflix queue includes a number in each call specifying the order of the queue, 1 for the movie about to ship, 2 for the next and so on to 187 or so for the most recent additions to the queue. I can drag an entry to reorder like on the iPhone, but I can also change the order numbers in the cells to reorder the entries, which is much easer than having to drag #187 to the 10th spot in the queue. There is also a button in each entry to put that entry on the very top.
In your app, you can add extra controls in edit view to assist in reordering such as the order number or "up/down 10" buttons.
If there are common reasons your users would want to order entries, you can have a button that handles that, such as "move up to next nearest blue entry."
From my experience, tableView scroll speed depends on distance between dragged cell and content origin/end points.
So to increase scroll speed, you don't need to subclass UITableView. Instead, you can set contentInset property of a tableView.
For example, call this in viewDidLoad:
tableView.contentInset = UIEdgeInsetsMake(64, 0, 64, 0)
Value 64 assumes that there are statusBar and navBar above tableView, top/bottom constraint between tableView and superView is set to 0 and edgesForExtendedLayout is set to .all.
That tableView goes under navBar and statusBar but because of contentInset it looks like it's attached to navBar.

How would you implement an scrollable grid in your iPhone app?

I have about 50 images of 50 x 50 pixel size each. I want the user to pick one of them. So first I thought about an UITableView, but that's just not the right thing. It wastes a lot of screen space. Rather than putting all images one below the other, it would be better to show a grid of lets say 6 columns and n rows.
I would use an UIScrollView and fill it up with UIView objects which I automatically arrange so that they appear like a grid. Is that the way to go? Or any other suggestions?
I'm doing something very similar in my app- an NxN grid with an image underneath, and another subview on top to draw the "lines", all owned by a UIScrollView. I recommend having a separate view to draw the images, something like:
-(void) drawRect(CGRect rect) {
CGRect smallerRect = CGRectMake(x, y, width, height);
[yourImage drawRect: smallerRect];
// repeat as needed to draw the grid
}
Another poster mentioned that you won't be able to get touch events if your view is owned by a UIScrollView- this is simply not true. I have it working. You might need to set the following though:
[yourScrollView setUserInteractionEnabled: YES]
[yourGridView setUserInteractionEnabled: YES]
The three20 library has a class that does this.
A table view doesn't necessarily imply showing one image per row, as you suggest. Table cells can be customized, and a cell subclass with six 50x50 UIImageViews would be pretty simple. It's maybe less flexible than a scroll view but if six images per row is your goal then a table view is the quickest way to get it.
three20 is horrible,ive used it, i dont recommend it... displaying a grid is easy...
- (void) reloadGridView
{
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(5, 54, scrollViewWidth, scrollViewHeight8)];
scrollView.delegate = self;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.userInteractionEnabled = YES;
scrollView.scrollEnabled = YES;
[self.view addSubview:scrollView];
int x = 10;
int y = 10;
divisor = 1;
for (int i = 0; i < [self.photosArray count]; i++) {
int buttonTag = divisor;
UIImage *thumb = [UIImage imageWithContentsOfFile:[self thumbPathAtIndex:i]];
//********* CREATE A BUTTON HERE **********
//********* use the thumb as its backgroundImage *******
if(divisor%4==0){
y+=70;
x = 10;
}else{
x += 70;
}
divisor++;
}
[scrollView setContentSize:CGSizeMake(scrollViewWidth, ([self.photosArray count]%4 == 0) ? y : y+100)];
}
and if you want 6 images when in landscape - setup a BOOL for isLandscape
if (isLandscape) {
//change the divisor to change # 6 instead of 4
}
if you need a more advanced gridView check out AQGridView (Google it)