I can guide you, but you must do exactly as I say. I can guide you, but you have to do what I say. — The Matrix

In chapter 10, “Buffering,” we looked at CAMediaTimingFunction, which is something that enhances realism by controlling animation buffering to simulate physical effects such as speeding up or slowing down. What if you want to more realistically simulate physical interactions or modify animation changes based on user input in real time? In this chapter, we continue to explore a timer based animation that allows us to precisely control the display frame by frame. # Timer frame animation appears to be used to show a continuous motion, but it does not work when displaying pixels in a fixed position. In general, this display does not allow continuous movement, but merely shows a series of still images fast enough to appear to be in motion.

As we mentioned earlier, iOS refreshes the screen 60 times per second, and CAAnimation calculates the new frames that need to be displayed and draws them synchronously each time the screen is updated. The best part of CAAnimation is that it calculates interpolation and buffering for each refresh that needs to be displayed.

In Chapter 10, we figured out how to customize the buffer function and then tell an instance of CAKeyframeAnimation how to draw based on the array of frames to display. All Core Animation actually displays these frames in a sequence. Can we do this ourselves?

In fact, we did something similar in chapter 3, “Layer Geometry”, with the clock example, where we used NSTimer to animate the clock’s hands once a second, but if we set the frequency to 60 times a second, the principle is exactly the same.

Let’s try to modify the elastic ball example from Chapter 10 with NSTimer. Since we are now counting animation frames continuously after the timer starts, we need to add some additional properties to the class to store the animation’s fromValue, toValue, Duration, and current timeOffset (see Listing 11.1).

Listing 11.1 uses NSTimer to animate bouncy balls

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView; 
@property (nonatomic, strong) UIImageView *ballView; 
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, assign) NSTimeInterval timeOffset; 
@property (nonatomic, strong) id fromValue;
@property (nonatomic, strong) id toValue; 
@end
@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  //add ball image view
  UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; 
  self.ballView = [[UIImageView alloc] initWithImage:ballImage]; 
  [self.containerView addSubview:self.ballView];
  //animate
  [self animate]; 
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  //replay animation on tap
  [self animate];
}
float interpolate(float from, float to, float time) {
  return (to - from) * time + from; 
}
- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
{
  if ([fromValue isKindOfClass:[NSValue class]]) {
    //get type
    const char *type = [(NSValue *)fromValue objCType]; 
    if (strcmp(type, @encode(CGPoint)) == 0)
    {
    CGPoint from = [fromValue CGPointValue];
    CGPoint to = [toValue CGPointValue];
    CGPoint result = CGPointMake(interpolate(from.x, to.x, time),
    interpolate(from.y, to.y, time)); 
    return [NSValue valueWithCGPoint:result];
    } 
  }
  //provide safe default implementation
  return (time < 0.5)? fromValue: toValue; 
}
float bounceEaseOut(float t) {
  if (t < 4/11.0) {
    return (121 * t * t)/16.0; 
  }else if (t < 8/11.0) {
    return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; 
  }else if (t < 9/10.0) {
    return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; 
  }
  return(54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; } - (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; The self. The timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timerif it's already running [self.timer invalidate]; / / start the timer self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1/60.0 target: self selector: @ the selector (step) userInfo:nil repeats:YES]; } - (void)step:(NSTimer *)step {//update time offset self.timeOffset = MIN(self.timeoffset + 1/60.0, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation
  if (self.timeOffset >= self.duration) {
    [self.timer invalidate];
    self.timer = nil; 
  }
}
@end
Copy the code

It’s nice, and it’s as much code as the keyframe-based example, but there are obviously a lot of problems if you want to animate a lot of things on the screen at once.

NSTimer is not the best solution, and to understand this, we need to know exactly how NSTimer works. Each thread on iOS manages an NSRunloop, which is literally a loop through a list of tasks. But for the main thread, these tasks include the following:

  • Handling touch events
  • Sending and receiving network packets
  • Execute code that uses GCD
  • Handling timer behavior
  • Screen redraw

When you set an NSTimer, it will be inserted into the current task list and will not be executed until the specified time has passed. But there is no upper limit on when to start the timer, and it only starts after the last task in the list has completed. This usually results in a delay of a few milliseconds, but can result in a long delay if the last task is too late.

The screen redraws at a rate of sixty times a second, but like timer behavior, it is delayed if the last one in the list has been running for a long time. These delays are random, so there is no guarantee that the timer will execute exactly 60 times a second. Sometimes this happens after the screen has been redrawn, which causes a delay in updating the screen and makes it look like the animation is stuck. Sometimes the timer will run twice during a screen update, so the animation appears to jump.

There are several ways to optimize:

  • We can useCADisplayLinkKeep the frequency of updates strictly after each screen refresh.
  • Animate based on the duration of real frames rather than the assumed update frequency.
  • Adjust the animation timerrun loopMode, so that other events do not interfere.

#CADisplayLink

CADisplayLink is another NSTimer class provided by CoreAnimation. It always starts before a screen update has been completed. Its interface is designed to be similar to NSTimer, so it is essentially a replacement for the built-in implementation. But unlike timeInterval, which is measured in seconds, CADisplayLink has a frameInterval attribute of integer type that specifies how many frames must be separated before execution. The default is 1, which means that each screen update is preceded by one. But if the animation code executes for more than 1/60th of a second, you can specify a frameInterval of 2, which means the animation executes every frame (30 frames per second) or 3, which means 20 frames per second, etc.

Using CADisplayLink instead of NSTimer ensures that the frame rate is continuous enough to make the animation look smoother, but even CADisplayLink does not guarantee that every frame will execute as planned, and discrete tasks or events (such as resource-constrained background programs) that are out of control may cause the animation to occasionally lose frames. With NSTimer, the timer starts at the first opportunity, but CADisplayLink ignores frames if it misses them and picks them up with the next update.

Whether using NSTimer or CADisplayLink, we still need to process a frame for 1/60th of a second longer than expected. Since we cannot calculate the true duration of a frame, manual measurements are required. We can use CACurrentMediaTime() at the start of each frame to record the current time and compare it to the time recorded in the previous frame.

By comparing these times, we can get the actual duration of each frame, and then replace the hard-coded 1/60th of a second. Let’s update the previous example (see Listing 11.2).

Listing 11.2 smooths the animation by measuring the duration of no frames

@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) CADisplayLink *timer; @property (nonatomic, assign) CFTimeInterval duration; @property (nonatomic, assign) CFTimeInterval timeOffset; @property (nonatomic, assign) CFTimeInterval lastStep; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue; @end @implementation ViewController ... - (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; The self. The timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timerif it's already running [self.timer invalidate]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } - (void)step:(CADisplayLink *)timer { //calculate time delta CFTimeInterval thisStep = CACurrentMediaTime(); CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep; //update time offset self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation
  if (self.timeOffset >= self.duration) {
    [self.timer invalidate];
    self.timer = nil; 
  }
}
@end
Copy the code

Note that when creating CADisplayLink, we need to specify a Run Loop and a Run Loop mode. For the Run Loop, we use the main thread Run Loop. Every task added to the Run Loop has a mode assigned priority. To keep the user interface smooth, iOS gives priority to tasks related to the user interface. And when the UI is active it does pause some other tasks.

A typical example is when using UIScrollView, redrawing the contents of the scroll view takes precedence over other tasks, so standard NSTimer and network requests don’t start. Some common run loop modes are as follows:

  • NSDefaultRunLoopMode – Standard priority
  • NSRunLoopCommonModes – High priority
  • UITrackingRunLoopMode – is used toUIScrollViewAnd other controls

In our example, we use NSDefaultRunLoopMode, but it does not guarantee smooth operation of the animation, so we can use NSRunLoopCommonModes instead. But be careful, because if the animation is running at a high frame rate, you may find some other timer like task or other iOS animation like slide will pause until the animation is finished.

We can also specify multiple run loop modes for CADisplayLink at the same time, so we can add NSDefaultRunLoopMode and UITrackingRunLoopMode at the same time to ensure that it won’t be broken by sliding or affected by other UIKit animations. Like this:

self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
Copy the code

And similar, CADisplayLink NSTimer also can use different run loop pattern configuration, by other functions, rather than + scheduledTimerWithTimeInterval: constructor

The self. The timer = [NSTimer timerWithTimeInterval: 1/60.0 target: self selector: @ the selector (step) the userInfo: nil repeats: YES]; [[NSRunLoop mainRunLoop] addTimer:self.timerforMode:NSRunLoopCommonModes];
Copy the code

Even with the use of timer based animation to replicate the behavior of the keyframes in Chapter 10, there are some essential differences: in the keyframe implementation, we counted all the frames in advance, but in the new solution, we actually counted them as needed. The point is that we can modify the animation logic in real time based on user input, or integrate it with other real-time animation systems such as physics engines. #Chipmunk Let’s create a realistic gravity simulation based on physics instead of the current buffering based elastic animation, but even simulating 2D physics is extremely complicated, so don’t try to implement it, just use the open source physics engine library.

The physics engine we’re going to use is called Chipmunk. Other 2D physics engines are also available (such as Box2D), but Chipmunk is written in pure C, not C++, which makes it easier to integrate with Objective-C projects. Chipmunk comes in many versions, including an “Indie” version bundled with Objective-C. The C version is free, so we’ll just use it. At the time of writing 6.1.4 is the latest version; You can download it from http://chipmunk-physics.net.

Chipmunk’s complete physics engine is quite large and complex, but we will only use the following classes:

  • CpSpace – This is the container for all physical structures. It has a magnitude and an optional gravity vector
  • CpBody – it is a solid, unelastic rigid body. It has a coordinate, as well as other physical properties, such as mass, motion and coefficient of friction, etc.
  • CpShape – This is an abstract geometry used to detect collisions. You can add a polygon to a structure, and cpShape has various subclasses to represent the types of different shapes.

In our example, let’s model a wooden box and then fall under the influence of gravity. Let’s create a Crate class that contains the on-screen visual effects (a UIImageView) and a physical model (a cpBody and a cpPolyShape, and a cpShape polygon subclass to represent a rectangular Crate).

Using the C version of Chipmunk presents some challenges, as it currently does not support the Objective-C reference-counting model, so we need to create and release objects exactly. To simplify things, we bind the cpShape and cpBody lifetimes to Crate, create them in the Crate’s -init method, and release them in -dealloc. The configuration of the wooden box’s physical properties is complex, so it makes sense to read the Chipmunk documentation.

The view controller is used to manage the cpSpace and has the same timer logic as before. In each step, we update the cpSpace (for physical calculations and rearrangement of all structures), iterate over the object, and then update the position of our wooden box view to match the wooden box model (in this case, there is really only one structure, but we will add more later).

Chipmunk uses a coordinate system that is reversed from UIKit (y-up is positive). To make synchronization between the physical models and the views easier, we need to flip the collection coordinates of the container view (described in Chapter 3) using the geometryFlipped property, so that the model and view share the same coordinate system.

The code is shown in Listing 11.3. Notice that we are not releasing cpSpace objects anywhere. In this example, the memory space will remain for the entire life of the app, so this is fine. But in a real world scenario, we need to manage our Spaces like we create wooden box structures and shapes, encapsulate them in standard Cocoa objects, and then manage the life cycle of Chipmunk objects. Figure 11.1 shows the fallen wooden case.

Listing 11.3 uses physics to model the dropped wooden box

#import "ViewController.h" 
#import <QuartzCore/QuartzCore.h> 
#import "chipmunk.h"
@interface Crate : UIImageView
@property (nonatomic, assign) cpBody *body;
@property (nonatomic, assign) cpShape *shape; 
@end
@implementation Crate
#define MASS 100
- (id)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])) {
    //set image
    self.image = [UIImage imageNamed:@"Crate.png"]; 
    self.contentMode = UIViewContentModeScaleAspectFill;
    //create the body
    self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height));
    //create the shape
    cpVect corners[] = { 
      cpv(0, 0), cpv(0, frame.size.height), cpv(frame.size.width, frame.size.height), cpv(frame.size.width, 0),
    };
  self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2));
  //setShape friction & Elasticity cpShapeSetFriction(self. Shape, 0.5); CpShapeSetElasticity (self. Shape, 0.8); //link the crate to the shape //so we can refer to crate from callback later on self.shape->data = (__bridge void *)self; //set the body position to match view
  cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2));
  }
  return self; 
}
- (void)dealloc {
  //release shape and body
  cpShapeFree(_shape);
  cpBodyFree(_body); 
}
@end
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView; 
@property (nonatomic, assign) cpSpace *space;
@property (nonatomic, strong) CADisplayLink *timer; 
@property (nonatomic, assign) CFTimeInterval lastStep;
@end
@implementation ViewController 
#define GRAVITY 1000
- (void)viewDidLoad {
  //invert view coordinate system to match physics
  self.containerView.layer.geometryFlipped = YES;
  //set up physics space
  self.space = cpSpaceNew(); 
  cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
  //add a crate
  Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)]; [self.containerView addSubview:crate];
  cpSpaceAddBody(self.space, crate.body);
  cpSpaceAddShape(self.space, crate.shape);
  //start the timer
  self.lastStep = CACurrentMediaTime();
  self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; 
  [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
void updateShape(cpShape *shape, void *unused) {
  //get the crate object associated with the shape
  Crate *crate = (__bridge Crate *)shape->data;
  //update crate view position and angle to match physics shape
  cpBody *body = shape->body;
  crate.center = cpBodyGetPos(body);
  crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
}
- (void)step:(CADisplayLink *)timer {
  //calculate step duration
  CFTimeInterval thisStep = CACurrentMediaTime(); 
  CFTimeInterval stepDuration = thisStep - self.lastStep; 
  self.lastStep = thisStep;
  //update physics
  cpSpaceStep(self.space, stepDuration);
  //update all the shapes
  cpSpaceEachShape(self.space, &updateShape, NULL); 
}
@end
Copy the code

The next step is to add an invisible wall around the view so that the crate doesn’t fall off the screen. Maybe you’ll do it with another rectangular cpPolyShape like you did with the wooden box, but we need to detect when the wooden box leaves the view, not when it hits, so we need a hollow rectangle instead of a solid one.

We can do this by adding four cpSegmentShape objects to our cpSpace (cpSegmentShape represents a straight line, so four of them together make a rectangle). We then assign the staticBody attribute to the space (a structure that is not affected by gravity) rather than a new cpBody instance like a wooden box, because we don’t want the bounding rectangle to slide off the screen or disappear when hit by a falling wooden box.

You can also add wooden boxes for some interaction. Finally, add an accelerator so that the gravity vector can be adjusted by tilting the phone (you need to run the program on a real device for testing, since the emulator doesn’t support accelerator events, even if the screen is rotated). Listing 11.4 shows the updated code, which runs as shown in Figure 11.2.

Since the example only supports landscape mode, swap the X and Y values of the accelerometer vectors. If you run the program in portrait, please switch them back, otherwise the gravity direction will be wrong. If you try, the crate will move laterally.

Listing 11.4 has updated code that uses walls and multiple wooden boxes

- (void)addCrateWithFrame:(CGRect)frame { Crate *crate = [[Crate alloc] initWithFrame:frame]; [self.containerView addSubview:crate]; cpSpaceAddBody(self.space, crate.body); cpSpaceAddShape(self.space, crate.shape); } - (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end { cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1); cpShapeSetCollisionType(wall, 2); CpShapeSetFriction (wall, 0.5); CpShapeSetElasticity (wall, 0.8); cpSpaceAddStaticShape(self.space, wall); } - (void)viewDidLoad { //invert view coordinate system to match physics self.containerView.layer.geometryFlipped = YES;  //set up physics space
  self.space = cpSpaceNew(); 
  cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
  //add wall around edge of view
  [self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)]; 
  [self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)]; 
  [self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)]; 
  [self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];
  //add a crates
  [self addCrateWithFrame:CGRectMake(0, 0, 32, 32)]; 
  [self addCrateWithFrame:CGRectMake(32, 0, 32, 32)]; 
  [self addCrateWithFrame:CGRectMake(64, 0, 64, 64)]; 
  [self addCrateWithFrame:CGRectMake(128, 0, 32, 32)]; 
  [self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];
  //start the timer
  self.lastStep = CACurrentMediaTime();
  self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; 
  [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; //update gravity using accelerometer [UIAccelerometer sharedAccelerometer].delegate = self; [UIAccelerometer sharedAccelerometer] updateInterval = 1/60.0; } - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { //update gravity  cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, ! [clings screen snapshot 2017-02-08 PM 8.45.04_918463.png..] -acceleration. X * GRAVITY)); }Copy the code

  • If the time step is not a fixed, precise value, the simulation of the physical effect is therefore uncertain. This means that even if the same input value is passed in, it may have different effects in different situations. Sometimes it doesn’t matter much, but in a physics-based game, the player can get confused when the same action leads to different results. It also makes testing difficult.

  • Loss of frames due to performance problems or interruptions such as incoming phone calls can result in incorrect results. Consider an object that moves as fast as a bullet, and each frame update requires moving the bullet to detect collisions. If the time between the frames had been longer, the bullet would have traveled farther in that step, passed through a wall or other obstacle, and lost its impact.

The ideal effect is to calculate the physics with a fixed time step, but still be able to update the view synchronously when the screen is redrawn (possibly causing unpredictable effects due to being out of our control).

Fortunately, since our model (in this case, cpBody in Chipmunk’s cpSpace) is separated by views (UIView objects on the screen that represent wooden boxes), it’s easy. We just need to track the time step based on the screen refresh time, and then calculate one or more simulated effects per frame.

We can do this through a simple loop. Notify the screen that it is about to refresh with each CADisplayLink startup, and then record the current CACurrentMediaTime(). We need to repeat the physics simulation in advance (in this case, 120th of a second) in a small increment until we catch up with the displayed time. We then update our view to match the display position of the current physical structure when the screen refreshes.

Listing 11.5 shows the code for the fixed time-step version

Listing 11.5 Wooden case simulation with a fixed time step

# define SIMULATION_STEP (1/120.0)
- (void)step:(CADisplayLink *)timer {
  //calculate frame step duration
  CFTimeInterval frameTime = CACurrentMediaTime();
  //update simulation
  while (self.lastStep < frameTime) {
    cpSpaceStep(self.space, SIMULATION_STEP);
    self.lastStep += SIMULATION_STEP; 
  }
  //update all the shapes
  cpSpaceEachShape(self.space, &updateShape, NULL); 
}
Copy the code

When using a fixed simulation time step, one thing to note is that the real world time used to calculate the physics does not accelerate the simulation time step. In our example, we randomly chose 120th of a second to simulate the physics. Chipmunk is fast and our example is simple, so cpSpaceStep() will do just fine without delaying frame updates.

But if the scene is complex, such as hundreds of objects interacting with each other, the physics calculations can be complicated, and cpSpaceStep() may take longer than 1/120 of a second. We didn’t measure the physical step length because we assumed it wasn’t important relative to the frame refresh, but if the simulated step length was longer, it would delay the frame rate.

It would be worse if the frame refresh time was delayed, and our simulation would need to perform more times to synchronize the real time. These additional steps continue to delay frame updates, and so on. This is known as a death spiral, because the end result is that the frame rate gets slower and slower until the application freezes.

We could calculate real-world times for physical steps by adding some code on the device, and then automatically adjust the fixed time step, but it’s not really possible. Just make sure you leave enough margin for fault tolerance, and then test on the slowest device you want to support. If the physics calculations take more than 50% of the simulation time, you need to consider increasing the simulation time step (or simplifying the scenario). If the simulation time step increases to more than 1/60 of a second (a full screen update time), you will need to reduce the animation frame rate to 30 frames per second or increase CADisplayLink’s frameInterval to ensure that no random frames are lost, or your animation will look uneven.

In this chapter, you learned how to use a timer to create an animation on a frame-by-frame basis through various animation techniques, including slowing down, physics simulation, and user input (via accelerometer).

In Part 3, we’ll look at how animation performance is constrained by hardware, and learn how to tune our code to get the best frame rate possible.