iOS Recipes - Matt Drance [31]
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint location = [touch locationInView:self];
NSTimeInterval time = [touch timestamp];
CGFloat locationDiff = self.beganLocation - location.x;
self.flick = (self.previousXPosition-location.x)/(time-self.prevousTimeStamp);
self.previousXPosition = location.x;
self.prevousTimeStamp = time;
self.newAngle = self.currentAngle - locationDiff/300*160;
if (self.newAngle >= CIRCLE) self.newAngle -= CIRCLE;
else if (self.newAngle < 0) self.newAngle += CIRCLE;
[CATransaction setDisableActions:YES];
self.transformed.sublayerTransform =
CATransform3DMakeRotation(RADIANS(newAngle), 0, 1, 0);
return YES;
}
The endTrackingWithTouch:withEvent: method uses the flick value to calculate which of the layers is predicted to be in front at the final position of the control after an appropriate amount of momentum. The real “trick” to making it appear as if the control has momentum is to animate the rotation of the ring of layers to the predicted number, over a fixed duration. If the flick value is high, then the change in number is greater, and the result is a larger rotation over the fixed time, showing the apparent speed. The animation uses the default easeout timing function, which adds a natural deceleration effect to the rotation before it stops at the predicted number.
NumberSpinControl/NumberSpinControl/SpinNumbers.m
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint location = [touch locationInView:self];
CGFloat halfWidth = self.bounds.size.width/2;
int newNum = 0;
if (self.flick == 0)
{
if (location.x > halfWidth + self.cubeSize/2) newNum = -1;
if (location.x < halfWidth - self.cubeSize/2) newNum = 1;
} else {
newNum = self.flick / ACCELERATIONFACTOR;
if (newNum > 150) newNum = 150;
if (newNum < -150) newNum = -150;
}
self.newAngle = self.newAngle-newNum;
if (self.newAngle < 0) self.newAngle = CIRCLE+self.newAngle;
int tileNum = self.rotAngle/2;
tileNum += self.newAngle;
tileNum = tileNum%CIRCLE;
tileNum = tileNum/self.rotAngle;
tileNum = abs(tileNum-NUM)%NUM;
[self moveToNewNumber:tileNum];
}
The moveToNewNumber method is called after the final touch or by the controller code to animate the control to a new value. We set up the rotation of the circle of layers and call sendActionsForControlEvents: with the UIControlEventValueChanged event to trigger any associated actions for that event.
NumberSpinControl/NumberSpinControl/SpinNumbers.m
-(void)moveToNewNumber:(int)newNumber
{
self.newAngle = CIRCLE-newNumber*self.rotAngle;
[CATransaction setValue:[NSNumber numberWithFloat:.5]
forKey:kCATransactionAnimationDuration];
self.transformed.sublayerTransform =
CATransform3DMakeRotation(RADIANS(self.newAngle), 0, 1, 0);
self.currentTileNum = newNumber;
self.currentAngle = self.newAngle;
[self sendActionsForControlEvents: UIControlEventValueChanged];
}
Accessing the currentNumber property triggers the calculation of the real value of the control based on the relative position of the front-facing layer and the STARTNUM.
NumberSpinControl/NumberSpinControl/SpinNumbers.m
- (int)currentNumber
{
return self.currentTileNum+STARTNUM;
}
Now that the SpinNumbers class is complete, let’s look at how we would use it. We can add an instance directly to the NumberSpinControlViewController XIB in Interface Builder by adding a base UIView, setting its size and position as required, and then specifying the SpinNumbers class as the custom class in the Identity inspector. By linking the view to an IBOutlet in the NumberSpinControlViewController.m, we can set up the target/action mechanism to call our preferred method, numberChanged, when UIControlEventValueChanged has been detected.
NumberSpinControl/NumberSpinControl/NumberSpinControlViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
[numbers addTarget:self action:@selector(numberChanged)
forControlEvents:UIControlEventValueChanged];
[numbers moveToNewNumber:2];
}
We know that the UIControlEventValueChanged