Non-animatable properties in subclasses of CALAyer

2020-04-12 10:28发布

问题:

I have defined a subclass of CALayer with an animatable property as discussed here. I would now like to add another (non-animatable) property to that layer to support its internal bookkeeping.

I set the value of the new property in drawInContext: but what I find that it is always reset to 0 when the next call is made. Is this so because Core Animation assumes that this property is also for animation, and that it "animates" its value at constant 0 lacking further instructions? In any case, how can I add truly non-animatable properties to subclasses of CALayer?

I have found a preliminary workaround, which is using a global CGFloat _property instead of @property (assign) CGFloat property but would prefer to use normal property syntax.

UPDATE 1

This is how I try to define the property in MyLayer.m:

@interface MyLayer()

@property (assign) CGFloat property;

@end

And this is how I assign a value to it at the end of drawInContext::

self.property = nonZero;

The property is e.g. read at the start of drawInContext: like so:

NSLog(@"property=%f", self.property);

UPDATE 2

Maybe this it was causes the problem (code inherited from this sample)?

- (id)actionForKey:(NSString *) aKey {
    if ([aKey isEqualToString:@"someAnimatableProperty"]) {
       CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:aKey];
       animation.fromValue = [self.presentationLayer valueForKey:aKey];
       return animation;
    }
    return [super actionForKey:aKey]; // also applies to my "property"
}

回答1:

To access your standard property from within the drawing method, during an animation, you need to make a few modifications.

Implement initializer

When CoreAnimation performs your animation, it creates shadow copies of your layer, and each copy will be rendered in a different frame. To create such copies, it calls -initWithLayer:. From Apple's documentation:

If you are implementing a custom layer subclass, you can override this method and use it to copy the values of instance variables into the new object. Subclasses should always invoke the superclass implementation.

Therefore, you need to implement -initWithLayer: and use it to copy manually the value of your property on the new instance, like this:

- (id)initWithLayer:(id)layer
{
    if ((self = [super initWithLayer:layer])) {
        // Check if it's the right class before casting
        if ([layer isKindOfClass:[MyCustomLayer class]]) {
            // Copy the value of "myProperty" over from the other layer
            self.myProperty = ((MyCustomLayer *)layer).myProperty;
        }
    }
    return self;
}

Access properties through model layer

The copy, anyway, takes place before the animation starts: you can see this by adding a NSLog call to -initWithLayer:. So as far as CoreAnimation knows, your property will always be zero. Moreover, the copies it creates are readonly, if you try to set self.myProperty from within -drawInContext:, when the method is called on one of the presentation copies, you get an exception:

*** Terminating app due to uncaught exception 'CALayerReadOnly', reason:  
    'attempting to modify read-only layer <MyLayer: 0x8e94010>' ***

Instead of setting self.myProperty, you should write

self.modelLayer.myProperty = 42.0f

as modelLayer will instead refer to the original MyCustomLayer instance, and all the presentation copies share the same model. Note that you must do this also when you read the variable, not only when you set it. For completeness, one should mention as well the property presentationLayer, that instead returns the current (copy of the) layer being displayed.