4

I have a grid of 30 UIButtons and potentially even more, subclassed to be rendered using layers: a base CALayer, a CAShapeLayer, a CAGradientLayer and a CATextLayer. I am trying to minimize the overall time required to render/display the buttons when the corresponding xib file is loaded. If I simply setup each button in turn in viewDidLoad, the time required for the view to appear is about 5-6 seconds, which is clearly too much.

In order to speed-up the buttons setup, I am using Grand Central Dispatch as follows. In viewDidLoad, I setup each button layers using dispatch_async on the global queue (adding to the base layer the shape and the gradient layers), so that the buttons can be rendered in a different thread. A the end of the block, the CATextLayer is added to the gradient layer.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

    CGRect base_bounds = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height - self.layer.bounds.size.height * 0.10f);
    CGPoint move_point = CGPointMake(0.0f, base_bounds.size.height * 0.10f);
    self.layer.masksToBounds = NO;
    baseLayer = [CALayer layer];

    baseLayer.cornerRadius = 10.0;
    baseLayer.shadowOffset = CGSizeMake(0.0f, 2.0f);
    baseLayer.shadowOpacity = 1.5f;
    baseLayer.shadowColor = [UIColor blackColor].CGColor;
    baseLayer.shadowRadius = 2.5f;
    baseLayer.anchorPoint = CGPointMake(0.5f, 0.5f);
    baseLayer.position    = move_point;

    CAShapeLayer *shape = [CALayer layer];
    shape.bounds = base_bounds;
    shape.cornerRadius = 10.0;
    shape.anchorPoint      = CGPointMake(0.0f, 0.0f);
    shape.position         = move_point;
    shape.backgroundColor = [UIColor darkGrayColor].CGColor;

    gradient = [CAGradientLayer layer];
    gradient.anchorPoint      = CGPointMake(0.0f, 0.0f);
    gradient.position         = CGPointMake(0.0f, 0.0f);
    gradient.bounds           = base_bounds;
    gradient.cornerRadius     = 10.0;
    gradient.borderColor      = [UIColor colorWithRed:0.72f
                                                green:0.72f
                                                 blue:0.72f
                                                alpha:1.0].CGColor;
    gradient.borderWidth      = 0.73;
    gradient.colors = [NSArray arrayWithObjects:
                       (id)[UIColor whiteColor].CGColor,
                       (id)[UIColor whiteColor].CGColor,
                       nil];


    [baseLayer addSublayer:shape];
    [baseLayer addSublayer:gradient];
    [self.layer addSublayer:baseLayer];

    [textLayer setBounds:gradient.bounds];
           [textLayer setPosition:CGPointMake(CGRectGetMidX(textLayer.bounds), CGRectGetMaxY(textLayer.bounds) - 6)];
           [textLayer setString:self.titleLabel.text]; 
           [textLayer setForegroundColor:[UIColor blackColor].CGColor];
           [gradient addSublayer:textLayer];

});

This approach reduce the overall time to about 2-3 seconds. I am wondering if anyone can suggest a faster way to render the buttons. Please note that I am not interested to any solution which discards the use of layers.

Thank you in advance.

Massimo Cafaro
  • 25,429
  • 15
  • 79
  • 93

3 Answers3

3

Perhaps I'm missing the point but wouldn't you be better off overriding the UIButton drawRect: method and doing your drawing in CoreGraphics (CG) things would be drawn A LOT faster than seconds, you can easily do gradients, text, images with the CG API. If I understand correctly you have 4 layers per button and 30+ buttons in the same view (120+ layers)? If so, I don't think you are meant to draw so many layers (rendering/blending all of them individually would explain the huge render time). Another possibility would be to have 4 big layers, for all of the buttons.

jbat100
  • 16,757
  • 4
  • 45
  • 70
  • Yes, you understood correctly: 4 layers per button, at least 30 buttons to start with. Your idea of abandoning layers in favor of Core Graphics calls surely deserve a test. I am going to override the drawRect: method and implement this. I will let you know about. – Massimo Cafaro Oct 03 '11 at 16:27
  • it took half an hour to switch from CALayers to Core Graphics calls. The iPad 2 using GCD was not able to render the layers in less than 2-3 seconds, now even a modest iPhone 3GS without using GCD is able to show the buttons instantly. Up-voted, accepted as final answer and awarded the bounty. – Massimo Cafaro Oct 04 '11 at 04:51
1

Here's my idea,

Separate your CALayers from UIButton - UIKit won't allow anything on the background thread.

When you have an opportunity in a screen preceding the button grid, use [buttonGridViewController performSelectorInBackground:renderCALayers] to render your CALayers in the background.

When you load your button grid in viewDidLoad, overlay UIButtons with type UIButtonTypeCustom, and backgroundColor = [UIColor clearColor] over the top of your 'button' CALayers (make sure to call bringSubviewToFront on the UIButtons so they get the touch events).

If you don't know how many buttons you are rendering, you might want to pre-render the maximum number you might display.

If you have any questions please comment.

EDIT:

A few related items,

What to replace a UIButton to improve frame rate?

UI design - Best way to deal with many UIButtons

How to get touch event on a CALayer?

I believe the only way you'll get quicker load from this point is to either replace or remove UIButtons altogether and intercept touch events with another approach. That way all rendering can be done in a background thread before the view is presented.

Community
  • 1
  • 1
Jacob Jennings
  • 2,796
  • 1
  • 24
  • 26
  • for various reasons I can not separate the CALAyers from the UIButton. However, as shown in mycode snippet, CALayers can be rendered in background.The snippet showed that I was rendering on the main thread the text layer only. I have just discovered that even this text layer may be rendered on a thread different from the main thread. With this modification, the view is now loaded immediately, but the buttons labels only appear 2-3 seconds later. Currently, I am using an HUD display that appears meanwhile the buttons are rendered. – Massimo Cafaro Oct 01 '11 at 08:51
0

Have you considered using NSOperation to generate your button's layers? In your code, you'd instantiate an operation queue and then iterate through each button, firing an operation (passing the respective parameters required to generate that button's layer) into the queue as each button is initiated.

Within your NSOperation, generate your layer using CoreGraphics, then pass the layer back to the button that initiated the operation (in this case, the destination button) via your completion handler (be sure to call that on the main thread). This should give you pretty good performance as your buttons layer's will be rendered off the main thread using CoreGraphics, then passed back to UIKit for presentation only after the layer has been rendered.

NSOperation sits atop GCD, has a marginal overhead but provides benefits in your case, such as the ability to set dependent prerequisites and prioritization.

Also, I'm pretty sure you shouldn't call UIColor in the GCD block you shared.

Hope this is helpful!

isaac
  • 4,867
  • 1
  • 21
  • 31
  • the approach you suggested is essentially the same I have used before actually switching to GCD. It takes almost the same amount of time. FYI: you can set UIColors in the block as shown, without any problem. – Massimo Cafaro Oct 04 '11 at 04:43
  • It's similar to what you described, but not identical. You asked for suggestions, I gave one. I certainly won't again! Touching UIKit from anywhere outside the main thread is expressly forbidden by every piece of documentation that touches on it, regardless of whether or not it works. – isaac Oct 04 '11 at 13:45