views:

139

answers:

5

Hi,

I'm running into problems when dealing with a large amount of UIButtons in my interface. I was wondering if anyone had first hand experience with this and how they did it?

When dealing with 30-80 buttons most simple, a couple of complex do you just use UIButton or do something different like drawRect, respond to touch events and get the coordinates of the touch event?

Best example is a calendar, similar to that of Apples Calendar App. Would you just draw most of the days using drawRect and then when you click a button replace it with an image or just use UIButtons? It's not so much the memory footprint or creating the buttons, just strange things are happening with them sometimes (previous question about it) and having performance issues animating them.

Thanks for any help.

+1  A: 

Instead of using 30-80 UIButtons I will prefer using images (if possible, a single image or as small number as possible) and compare the touch location.

And if I must create buttons, then obviously will not create 30-80 variables for them. I will set and get view tag to determine which one is tapped.

taskinoor
Hmmm, not the way I would of guessed. Ever animated them? Performance? I kinda like the sound of your last part. So make one button, set a view tag and other necessaries like title and just reuse it?
Rudiger
what i mean by tag is not about reusing one button. say i need 30 button. then instead of declaring 30 variables for them, i will save them in an array with a tag. and in the button handler i will use sender's tag to carry out proper tasks. thats way code will be less messed up. trying to animate many layers is not a very good idea. u really should think if u have any alternative. say i require a 10 x 10 grid. so i will require 100 buttons. instead of creating 100 buttons i will use one single image as the grid background and touch location to find out which cell is tapped.
taskinoor
and in case i need different animation for all cells simultaneously and can not be done by single image, then obviously more careful design will be required. i have used path animation on about 50 images simultaneously with no problem, even on a 2g device. but careful coding and design compromise is required to achieve good performance.
taskinoor
Path animations? Any chance of what you used? I've probably used it somewhere but wasn't aware of what it was called
Rudiger
check CAKeyframeAnimation. there is a path property where u can set a CGMutablePathRef.
taskinoor
Thanks, I'll look into it
Rudiger
+1  A: 

If this is all stuff you are animating then you could create a bunch of CALayers with their contents set to a CGImage. You would have to compare the touch location to identify the layer. CALayers have a useful style property that is an NSDictionary you can store meta-data in.

Ben
I thought dealing with layers might be one way. So UIView with a bunch of added CALayers and then compare the touch to locate the button?
Rudiger
Yeah it might be a pain to locate the layer in question but animation performance would be optimized.
Ben
+1  A: 

I just use the UIButtons unless there happens to be a specific performance issue that crops up. If they have similar functionality, however, such as a keyboard, I map them all to one IBAction and differentiate the behavior based on the sender.

What specific performance and animation issues are you running into?

Peter DeWeese
Animating and sometimes not showing up properly. Im just creating them in code, not in nibs
Rudiger
Show us the code! It could be a retention issue.
Peter DeWeese
+2  A: 

If "strange things are happening" with your buttons, you need to get to the bottom of why. Switching architectures just to avoid a problem that you don't understand (and might crop up again) doesn't sound like a good idea.

-drawRect: works by drawing to a bitmap-backed context. This happens when -displayIfNeeded is called after -setNeedsDisplay (or doing something else that implicitly sets the needsDisplay flag, like resizing a view with contentMode = UIContentModeRedraw). The bitmap-backed context is then composited to screen.

Buttons work by putting the different components (background image, foreground image, text) in different layers. The text is drawn when it changes and composited to the screen; the images are just composited directly to the screen.

The "best" way to do things is usually a combination of the two. For example, you might draw text and a background image in -drawRect: so the different layers didn't need to be composited at render time (you get an additional speedup if your view is "opaque"). You probably want to avoid full-screen animations via drawRect: (and it won't integrate so well with CoreAnimation), since drawing tends to be more expensive than compositing.

But first, I'd find out what's going wrong with UIButton. There's little point worrying about how you could make things faster until you actually find out what the slow bits are. Write code so that it is easy to maintain. UIButton is not that expensive and -drawRect: is not that bad (presumably it's even better if you use -setNeedsDisplayInRect: for a smallish rect, but then you need to calculate the rect...), but if you want a button, use UIButton.

tc.
Hmmm, it made me test one thing that fixed it - well the "strange things are happening". Which bought up another question if you could answer. Previously I was subclassing UIView to draw some lines and stuff, then in the subclass I created and added UIButtons to self. Although it worked it did some strange things and didn't display properly on normal load. I pulled out the button code from the subclassed view and created the buttons and added it to the view in my main class and it worked. What is happening? Am I doing a big no no by adding elements to a subclass UIView within the UIView?
Rudiger
There's nothing wrong, except you should generally add subviews in -initWithBlah. Strange things might happen if you add them in -drawRect: (I think -layoutSubviews is safe, but I'm not entirely sure).
tc.
Although this isn't the answer to the question I was looking for it fixed the majority of my problems. I will look into the answers other people gave to help solve my performance issue.
Rudiger
+1  A: 

I recently ran across this problem myself when developing a game for the iPhone. I was using UIButtons to hold game tiles, then stylized them with transparent images, background colors and text.

It all worked well for a small number of tiles. Once we got to about 50, however, the performance dropped significantly. After scouring Google I discovered that others had experienced the same problem. It seems the iPhone struggles with lots of transparent buttons onscreen at once. Not sure if it's a bug in the UIButton code or just a limitation of the graphics hardware on the device, but either way, it's beyond your control as a programmer.

My solution was to draw the board by hand using Core Graphics. It seemed daunting at first, but in reality it was pretty easy. I just placed one big UIImageView on my ViewController in Interface Builder, made it an IBOutlet so I could alter it from Objective-C, then constructed the image with Core Graphics.

Since a UIImageView doesn't handle taps, I used the touchesBegan method of my UIViewController, and then triangulated the x/y coordinates of the touch to the precise tile on my game board.

The board now renders in less than a tenth of a second. Bingo!

If you need sample code, just let me know.

UPDATE: Here's a simplified version of the code I'm using. Should be enough for you to get the gist.

//  CoreGraphicsTestViewController.h
//  CoreGraphicsTest

#import <UIKit/UIKit.h>

@interface CoreGraphicsTestViewController : UIViewController {
    UIImageView *testImageView;

}

@property (retain, nonatomic) IBOutlet UIImageView *testImageView;


-(void) drawTile: (CGContextRef) ctx row: (int) rowNum col: (int) colNum isPressed: (BOOL) tilePressed;

@end

... and the .m file ...

//  CoreGraphicsTestViewController.m
//  CoreGraphicsTest

#import "CoreGraphicsTestViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <CoreGraphics/CoreGraphics.h>

@implementation CoreGraphicsTestViewController

@synthesize testImageView;

int iTileSize;
int iBoardSize;

- (void)viewDidLoad {
    int iRow;
    int iCol;

    iTileSize = 75;
    iBoardSize = 3;

    [testImageView setBounds: CGRectMake(0, 0, iBoardSize * iTileSize, iBoardSize * iTileSize)];


    CGRect rect = CGRectMake(0.0f, 0.0f, testImageView.bounds.size.width, testImageView.bounds.size.height);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();

    for (iRow = 0; iRow < iBoardSize; iRow++) {
        for (iCol = 0; iCol < iBoardSize; iCol++) {
            [self drawTile: context row: iRow col: iCol color: isPressed: NO];
        }
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    [testImageView setImage: image];

    UIGraphicsEndImageContext();

    [super viewDidLoad];
}


- (void)dealloc {
    [testImageView release];
    [super dealloc];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView: testImageView];

    if ((location.x >= 0) && (location.y >= 0) && (location.x <= testImageView.bounds.size.width) && (location.y <= testImageView.bounds.size.height)) {

        UIImage *theIMG = testImageView.image;

        CGRect rect = CGRectMake(0.0f, 0.0f, testImageView.bounds.size.width, testImageView.bounds.size.height);
        UIGraphicsBeginImageContext(rect.size);
        CGContextRef context = UIGraphicsGetCurrentContext();

        [theIMG drawInRect: rect];

        iRow = location.y / iTileSize;
        iCol = location.x / iTileSize;

        [self drawTile: context row: iRow col: iCol color: isPressed: YES];


        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

        [testImageView setImage: image];

        UIGraphicsEndImageContext();
    }
}


-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UIImage *theIMG = testImageView.image;

    CGRect rect = CGRectMake(0.0f, 0.0f, testImageView.bounds.size.width, testImageView.bounds.size.height);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();

    [theIMG drawInRect: rect];

    [self drawTile: context row: iRow col: iCol isPressed: NO];


    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    [testImageView setImage: image];

    UIGraphicsEndImageContext();
}

-(void) drawTile: (CGContextRef) ctx row: (int) rowNum col: (int) colNum isPressed: (BOOL) tilePressed {

    CGRect rrect = CGRectMake((colNum * iTileSize), (rowNum * iTileSize), iTileSize, iTileSize); 
    CGContextClearRect(ctx, rrect);

    if (tilePressed) {
        CGContextSetFillColorWithColor(ctx, [[UIColor redColor] CGColor]);
    } else {
        CGContextSetFillColorWithColor(ctx, [[UIColor greenColor] CGColor]);
    }

UIImage *theImage = [UIImage imageNamed:@"tile.png"];
[theImage drawInRect: rrect];
}
Axeva
Some code how you constructed it with Core Graphics would be good. Also did you respond by changing the button image when a user tapped on it?
Rudiger
Okay, I've added some sample code. It's a watered-down version of what I'm doing in my App, but it's enough for you to understand the concept. It handles touches and highlights the tile pressed as well. Enjoy!
Axeva