2

On iOS, if a view has several layers, then can the drawRect method just choose any one layer to display, and 1 second later, choose another layer to display, to achieve an animation effect?

Right now, I have several layers, but I don't think they are the view's layers (they are just individual layers which are not sublayers of parent layer), as I just created them using

CGLayerCreateWithContext(context, self.view.bounds.size, NULL);

and in drawRect, I use

CGContextDrawLayerAtPoint(context, self.bounds.origin, layer1);

to draw the layer onto the view... it works, but isn't this like drawing a layer onto a layer (drawing a layer onto the view's layer)? Isn't there a faster way, which is to tell the the view to use layer1 or layer2, kind of like

self.layer = layer1;  

but it can't because layer is read only. Can this be achieved?

Jeremy L
  • 3,770
  • 6
  • 41
  • 62
  • Are you trying to achieve an animation by having each frame in a separate layer and swapping the layers? – Greg May 27 '12 at 06:51
  • 1
    This question doesn't make much sense, because you're confusing two different things: `CGLayer`s and `CALayer`s. They have similar names but they are *entirely different*. You cannot mix and match them. `CGLayerCreateWithContext` makes a `CGLayer`, but `self.layer` is a `CALayer`. – Kurt Revis May 27 '12 at 06:56
  • @PartiallyFinite yes... 2 layers, for example, and animate using these 2 layers... – Jeremy L May 27 '12 at 07:49
  • @Kurt I read that instead of drawing into BitmapContext, it is better to draw into a Layer, since it is cached in the Graphics Card, so this Layer is a CGLayer? Is it possible to draw into a CALayer too? (hm... and then I saw PartiallyFinite's answer... pretty much drawing into a view, and then extract the layer from the view...) – Jeremy L May 27 '12 at 07:51
  • If you have to ask, you probably don't want to use CGLayers. The whole "cached on the graphics card" thing was completely overhyped, and doesn't really apply on iOS anyway. I explained what CGLayers are useful for in [this answer](http://stackoverflow.com/a/10675903/1218876) -- basically, if you are repeatedly drawing the SAME content into the SAME context, but in different places or with a different transform. Otherwise, there is no point. – Kurt Revis May 27 '12 at 07:58
  • 1
    The important part of @PartiallyFinite's answer was NOT the part where he "extracts the layer from the view" -- that was just an easy way to get a layer to show you. In practice you could just as easily create a `CALayer` and set its `backgroundColor` property. – Kurt Revis May 27 '12 at 08:00
  • My answer demonstrates how to cycle layers. The views being initialised with background colours being set is, as @KurtRevis said, just an easy way to procure `CALayer` objects to demonstrate. You are meant to get your own there. – Greg May 27 '12 at 08:05
  • @KurtRevis - CALayers are indeed cached on the GPU, and this makes a huge difference in terms of their performance (animation using compositing instead of a complete redraw, etc.). You're probably referring to CGLayers, which as you state are completely different things and are not cached in this manner. – Brad Larson May 27 '12 at 21:11
  • @BradLarson: Yes, my entire comment that said "you probably don't want to use CGLayers" was, in fact, about CGLayers. I know how CALayers work. I regret even getting involved in this question. – Kurt Revis May 27 '12 at 21:31
  • @KurtRevis - I figured as much. It's probably better to have a discussion like this here, because it might help other people with the same confusions about CGLayers and CALayers. I've seen variants on this question asked in multiple places, and you can see where people might get mixed up due to Apple's terminology. It's not always easy to puzzle out the documentation, given that some of it hasn't been updated since the pre-Core Animation days (I just filed a bug report on one passage that was outright wrong under current OS versions). – Brad Larson May 27 '12 at 21:52
  • @Brad Are you sure CGLayer is not cached on the GPU if CALayer is? According to Apple's docs and in Rob Napier's book, CGLayer is likely to be cached in the GPU as well. (Apple's doc: https://developer.apple.com/library/mac/#documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_layers/dq_layers.html ) – Jeremy L May 28 '12 at 04:32
  • @JeremyL - Even if it is (which I've heard varying reports about on iOS, despite the Mac documentation you point to), you're still going to need to do a slow CPU-bound rasterization phase to use a CGLayer within a UIView or CALayer, so it's generally preferable to split this content out into a CALayer. – Brad Larson May 28 '12 at 16:57

2 Answers2

3

You are trying to draw CALayer objects using CGLayer drawing methods. These are different things, and will not work interchangeably. However, if I understood your question correctly, you don't need to use drawRect: at all to switch between layers after a period of time. Here is my basic working code example:

#import <QuartzCore/QuartzCore.h>

@interface TTView {
    NSUInteger index;
    NSArray *layers;
    CALayer *currentLayer;
}

@end

@implementation TTView

- (void)switchView {
    // Remove the currently visible layer
    [currentLayer removeFromSuperlayer];

    // Add in the next layer
    currentLayer = [layers objectAtIndex:index];
    [self.layer addSublayer:currentLayer];

    // Increment the index for next time
    index += 1;
    if (index > [layers count] - 1) {
        // If we have reached the end of the array, go back to the start. You can exit the loop here by calling a different method followed by return;
        index = 0;
    }

    // Call this method again after 1 second
    [self performSelector:@selector(switchView) withObject:nil afterDelay:1];
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    // Basic initialisation. Move this to whatever method your view inits with.
    self = [super initWithCoder:aDecoder];
    if (self) {
        // Create some random objects with layers to display. Place your layers into the array here.
        UIView *a = [[UIView alloc] initWithFrame:self.bounds];
        a.backgroundColor = [UIColor redColor];
        UIView *b = [[UIView alloc] initWithFrame:self.bounds];
        b.backgroundColor = [UIColor greenColor];
        UIView *c = [[UIView alloc] initWithFrame:self.bounds];
        c.backgroundColor = [UIColor blueColor];

        // Add the layers to the array.
        layers = [[NSArray alloc] initWithObjects:a.layer, b.layer, c.layer, nil];
        // Call the method to start the loop.
        [self switchView];
    }
    return self;
}

Obviously you should replace my 3 plain coloured views with whatever layers you are planning to animate in this way, and possibly tidy up the instance variables. This is simply a basic code example that does what you want.

Greg
  • 9,068
  • 6
  • 49
  • 91
  • so if the first layer is to have a rectangle drawn inside, and the second layer needs a circle drawn inside, how would that be done? I tried using `FooView` instead of `UIView`... and use a `drawRect` in `FooView` to draw it... but the layer is totally black... (but then if I think again... `drawRect` is called by the run loop for the view hierarchy for the `keyWindow` (the top view) and down... in the above code sample, there is no such concept of `drawRect` being called for those views.) – Jeremy L May 27 '12 at 08:35
3

Following @PartiallyFinite's example, here's a similar way to get the same sort of effect, by setting your view's layer's contents property to a CGImage of your choosing.

This is more efficient than overriding -drawRect: and drawing there, because it avoids an additional draw operation and upload to the video hardware.

#import <QuartzCore/QuartzCore.h>

@interface TTView {
    NSUInteger index;
    NSMutableArray *images;
}

@end

@implementation TTView

- (id)initWithCoder:(NSCoder *)aDecoder {
    // Basic initialisation. Move this to whatever method your view inits with.
    self = [super initWithCoder:aDecoder];
    if (self) {
        // Create some random images to display. Place your images into the array here.
        CGSize imageSize = self.bounds.size;        
        images = [[NSMutableArray alloc] init];
        [images addObject:[self imageWithSize:imageSize color:[UIColor redColor]]];
        [images addObject:[self imageWithSize:imageSize color:[UIColor greenColor]]];
        [images addObject:[self imageWithSize:imageSize color:[UIColor blueColor]]];

        [self switchView];
    }
    return self;
}

- (UIImage*)imageWithSize:(CGSize)imageSize color:(UIColor*)color {
    UIGraphicsBeginImageContext(imageSize);

    // Draw whatever you like here. 
    // As an example, we just fill the whole image with the color.
    [color set];
    UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));

    UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

- (void)switchView {
    UIImage* image = [images objectAtIndex:index];
    self.layer.contents = (id)(image.CGImage);

    index = (index + 1) % images.count;

    [self performSelector:@selector(switchView) withObject:nil afterDelay:1];
}

// DO NOT override -drawRect:, and DO NOT call -setNeedsDisplay.
// You're already providing the view's contents.

@end
Kurt Revis
  • 27,695
  • 5
  • 68
  • 74
  • hm... `self.layer.contents = [images objectAtIndex:index].CGImage;` right now will say `CGImage` is a property not found... I tried to cast the first part to `(UIImage*)` but still error – Jeremy L May 27 '12 at 08:53
  • so that line works if it is `self.layer.contents = (id) ((UIImage*)[images objectAtIndex:index]).CGImage;` – Jeremy L May 27 '12 at 16:49
  • Yes, sorry, I missed adding the cast to `(id)` there. Fixed. – Kurt Revis May 27 '12 at 17:42