Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iOS

iOS Tricks: UIView with Additional sublayers - Applying the Same Implicit Animation Rules that the Main Layer Uses

4.47/5 (9 votes)
21 May 2014CPOL2 min read 24K   86  
When you add additional layers to a UIView, those new layers don't animate in the same way that the UIView's Backing Layer does. I have a trick that fixes this gotcha good and proper.

Introduction

When you add additional layers to a UIView, those new layers don't animate in the same way that the UIView's Backing Layer does. I have a trick that fixes this gotcha good and proper.

Background

An implicit animation is an animation which happens automatically when changing a property on a view or layer.

Every UIView has a main/backing CALayer. When a property of a CALayer is changed, it queries its delegate (the UIView) for an animation to use for that change (actionForLayer:forKey:) and since a view is the delegate for its layer, the layer queries its view for any custom animations actions that would be required. The view manages all its layers' implicit actions.

This all happens automatically with a normal UIView -> CALayer relationship. However if you add additional layers to an existing view, the new layers don't have any delegate set.

Now why not just set the new layer's delegate to the owning view, I hear you ask?

That unfortunately doesn't won't work because a layer also controls its owning view via the delegate and if there's more than one layer controlling the view, weird things start to happen.

Using the owning view is the correct idea though;

Actually, if you only send the new layer's actionForLayer:forKey: messages to the view, then the layer applies implicit animations correctly, and the view is not effected by the additional layers.

To do this, I've created a class to use as the layer's delegate.

The Class

LGLayerActionsForwarder.h
Objective-C
#import <Foundation/Foundation.h>

@interface LGLayerActionsForwarder : UIView

- (instancetype) initWithView: (UIView *) view;

@property (nonatomic, readonly) UIView *view;

@end
LGLayerActionsForwarder.m
Objective-C
#import "LGLayerActionsForwarder.h"

@implementation LGLayerActionsForwarder
{
    __weak UIView *_view;
}

- (instancetype) initWithView: (UIView *) view
{
    self = [super init];
    if (!self) return nil;

    _view = view;

    return self;
}

- (id <CAAction>) actionForLayer: (CALayer *) layer forKey: (NSString *) event
{
    return [_view actionForLayer: layer forKey: event];
}

@end

How to Use the Class

Create an instance of the LGLayerActionsForwarder class and assign it to all the layers you create.

Objective-C
_actionsForwarder = [[LGLayerActionsForwarder alloc] initWithView: self];

_yellowLayer = [CALayer layer];
_yellowLayer.delegate = _actionsForwarder;

In the CustomViewLayer example _yellowLayer is aligned to the view's frame on layoutSubviews, but would normally animate when the view is re-layed out because of a size change.

But, by assigning a delegate to the layer...

Objective-C
_yellowLayer.delegate = _actionsForwarder;

...the layer stop's animating on every change and behaves the same as the view's backing layer, which only animates within animation block. ([UIView animateWithDuration:animations:])

Objective-C
@implementation CustomViewWithLayer
{
    CALayer *_yellowLayer;
    LGLayerActionsForwarder *_actionsForwarder;
}

- (id) initWithCoder: (NSCoder *) coder 
{
    self = [super initWithCoder: coder];
    if (!self) return nil;

    // create a forwarder instance and link it to the view
    // NOTE: If you comment out just this line, then the yellow layer would function as a normal layer (animating).
    _actionsForwarder = [[LGLayerActionsForwarder alloc] initWithView: self];

    _yellowLayer = [CALayer layer];
    _yellowLayer.borderColor = [UIColor blueColor].CGColor;
    _yellowLayer.borderWidth = 1;
    _yellowLayer.cornerRadius = 20;
    _yellowLayer.delegate = _actionsForwarder;
    _yellowLayer.backgroundColor = [UIColor yellowColor].CGColor;

    [self.layer addSublayer: _yellowLayer];

    return self;
}

- (void) layoutSubviews
{
    [super layoutSubviews];

    _yellowLayer.frame = self.bounds;
}

@end

History

  • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)