Creating smooth frame-by-frame custom animations on iOS is easier than you think. At first glance, it appears there isn’t a convenient inbuilt method to get the live frame time interval communicated to your UIView. However, the Quartz Core Framework provides the CADisplayLink class that sends a message to it’s target every time the screen is about to refresh. With the help of a small wrapper class, we can create very smooth custom animations based on the time passed since the last frame was displayed.
First, let’s define a protocol so that any object can receive messages about the frame updates. Create a single C header file (.h) called "PWDisplayLinkerDelegate.h". We only need one required protocol method that will inform the delegate that the display will be updated with a given time interval.
@protocol PWDisplayLinkerDelegate <NSObject> @required - (void)displayWillUpdateWithDeltaTime:(CFTimeInterval)deltaTime; @optional @end
Next, create the objective-c wrapper class called “PWDisplayLinker” and open the header file.
@interface PWDisplayLinker : NSObject { __weak id<PWDisplayLinkerDelegate> _delegate; //(1) CADisplayLink * _displayLink; //(2) BOOL _nextDeltaTimeZero; //(3) CFTimeInterval _previousTimestamp; //(4) }
We need four instance variables:
- A weak reference to the delegate object - can be of any class but must conform to the PWDisplayLinkerDelegate protocol.
- The CADisplayLink object associated with our wrapper class.
- Boolean to indicate if the next delta time should be zero. Used when we expect an irregular frame rate interval, for example, on initialisation or when the application becomes active.
- The previous timestamp of the CADisplayLink, so we can calculate the time difference.
Our custom init and dealloc methods will initialise our instance variables and ensure the CADisplayLink is added/removed from the run loop.
- (id)initWithDelegate:(id<PWDisplayLinkerDelegate>)delegate { self = [super init]; if(self) { _delegate = delegate; _displayLink = nil; _nextDeltaTimeZero = YES; _previousTimestamp = 0.0; [self ensureDisplayLinkIsOnRunLoop]; } return self; } - (void)dealloc { [self ensureDisplayLinkIsRemovedFromRunLoop]; } - (void)ensureDisplayLinkIsOnRunLoop { if(_displayLink == nil) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkUpdated)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; _nextDeltaTimeZero = YES; } } - (void)ensureDisplayLinkIsRemovedFromRunLoop { if(_displayLink != nil) { [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; _displayLink = nil; _nextDeltaTimeZero = YES; } }
Notice how we only create the display link and add it to the run loop if it is already nil? This ensures that the display link is only added to (and removed from) the main run loop once. If we accidentally call either method twice, or we want to make the methods public in the future , we can be sure they will work correctly.
Finally we need to create the selector that we told the CADisplayLink to call.
- (void)displayLinkUpdated { CFTimeInterval currentTime = [_displayLink timestamp]; // calculate delta time CFTimeInterval deltaTime; if(_nextDeltaTimeZero) { _nextDeltaTimeZero = NO; deltaTime = 0.0; } else { deltaTime = currentTime - _previousTimestamp; } // store the timestamp _previousTimestamp = currentTime; // inform the delegate [_delegate displayWillUpdateWithDeltaTime:deltaTime]; }
We get the current time from the timestamp provided by the display link. We calculate the frame time difference by subtracting the current time from the previous time. We also need to put in a check to make sure that the delta time is set to zero if the instance variable we created earlier is set to YES. All that remains is to store the current timestamp and inform our delegate object.
It is unnecessary for more than one instance of this class to be active at a time - typically the delegate will be the current active UIViewController who can pass the protocol message on if required. The interface for such a view controller would look something like this:
#import <UIKit/UIKit.h> #import "PWDisplayLinkerDelegate.h" @class PWDisplayLinker; @interface SomeViewController : UIViewController <PWDisplayLinkerDelegate> { // ... PWDisplayLinker * _displayLinker; UIView * _someView; CGPoint _viewPosition; } // ... @end
In this example we will create a view and also a variable that we can update each frame and use to set the view's position.
The view controller stores a strong reference to the display linker object that needs to be instantiated somewhere in the implementation like the viewDidLoad method.
- (void)viewDidLoad { [super viewDidLoad]; //... _displayLinker = [[PWDisplayLinker alloc] initWithDelegate:self]; _someView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 20.0, 20.0)]; [_someView setBackgroundColor:[UIColor blueColor]]; [self.view addSubview:_someView]; _viewPosition = CGPointMake(0.0, 0.0); //... }
At the default 60 frames per second, the period between each frame should be 1 ÷ 60 = 1.666666... We can check that our class is working properly by logging the deltaTime parameter.
delta time: 0.016667 delta time: 0.016666 delta time: 0.016624 delta time: 0.016712 delta time: 0.016667 etc...
You can now create custom frame-rate-independent animations on your views! The most simple example is moving our view across the screen at 50 points per second and down the screen at 100 points per second.
- (void)displayWillUpdateWithDeltaTime:(CFTimeInterval)deltaTime { _viewPosition.x += 50.0*deltaTime; _viewPosition.y += 100.0*deltaTime; [_someView setCenter:_viewPosition]; }