Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Switch to layer-based animation technique using CATransformLayer. Als…

…o added shadows to the folding panels.
  • Loading branch information...
commit 23a54f8f6419f88bee9fe07761afd24070990f7c 1 parent f094f11
authored May 29, 2012
5  EnterTheMatrix/FoldViewController.h
@@ -17,12 +17,9 @@
17 17
 @property (weak, nonatomic) IBOutlet UISegmentedControl *skewSegment;
18 18
 @property (weak, nonatomic) IBOutlet UIView *controlFrame;
19 19
 
20  
-@property (strong, nonatomic) UIImageView *foldTop;
21  
-@property (strong, nonatomic) UIImageView *foldBottom;
22  
-
23 20
 @property (readonly) CGFloat skew;
24  
-@property (readonly) CGFloat skewAngle;
25 21
 
26 22
 - (void)handlePinch:(UIGestureRecognizer *)gestureRecognizer;
  23
+- (IBAction)skewValueChanged:(UISegmentedControl *)sender;
27 24
 
28 25
 @end
499  EnterTheMatrix/FoldViewController.m
@@ -12,7 +12,8 @@
12 12
 
13 13
 #define FOLD_HEIGHT	120.
14 14
 #define DEFAULT_DURATION 0.3
15  
-#define DEFAULT_SKEW	-(1. / 280)
  15
+#define FOLD_SHADOW_OPACITY 0.5
  16
+#define FOLD_SHADOW_ADJUSTMENT_FACTOR 0.5
16 17
 
17 18
 @interface FoldViewController ()
18 19
 
@@ -21,6 +22,18 @@ @interface FoldViewController ()
21 22
 @property (assign, nonatomic) CGFloat pinchStartGap;
22 23
 @property (assign, nonatomic) CGFloat lastProgress;
23 24
 
  25
+@property (strong, nonatomic) UIView *animationView;
  26
+@property (strong, nonatomic) CALayer *perspectiveLayer;
  27
+@property (strong, nonatomic) CALayer *topSleeve;
  28
+@property (strong, nonatomic) CALayer *bottomSleeve;
  29
+@property (strong, nonatomic) CALayer *upperFoldShadow;
  30
+@property (strong, nonatomic) CALayer *lowerFoldShadow;
  31
+@property (strong, nonatomic) CALayer *firstJointLayer;
  32
+@property (strong, nonatomic) CALayer *secondJointLayer;
  33
+@property (assign, nonatomic) CGPoint animationCenter;
  34
+@property (readonly, nonatomic) SkewMode skewMode;
  35
+@property (readonly, nonatomic) BOOL isInverse;
  36
+
24 37
 @end
25 38
 
26 39
 @implementation FoldViewController
@@ -29,14 +42,23 @@ @implementation FoldViewController
29 42
 @synthesize folding;
30 43
 @synthesize pinchStartGap;
31 44
 @synthesize lastProgress;
  45
+
  46
+@synthesize animationView = _animationView;
  47
+@synthesize perspectiveLayer = _perspectiveLayer;
  48
+@synthesize topSleeve = _topSleeve;
  49
+@synthesize bottomSleeve = _bottomSleeve;
  50
+@synthesize upperFoldShadow = _upperFoldShadow;
  51
+@synthesize lowerFoldShadow = _lowerFoldShadow;
  52
+@synthesize firstJointLayer = _firstJointLayer;
  53
+@synthesize secondJointLayer = _secondJointLayer;
  54
+@synthesize animationCenter = _animationCenter;
  55
+
32 56
 @synthesize contentView;
33 57
 @synthesize topBar;
34 58
 @synthesize centerBar;
35 59
 @synthesize bottomBar;
36 60
 @synthesize skewSegment;
37 61
 @synthesize controlFrame;
38  
-@synthesize foldBottom;
39  
-@synthesize foldTop;
40 62
 
41 63
 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
42 64
 {
@@ -72,36 +94,6 @@ - (void)viewDidLoad
72 94
 	UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
73 95
 	pinchGesture.delegate = self;
74 96
 	[self.view addGestureRecognizer:pinchGesture];
75  
-	
76  
-	// We want to split the center bar in 2 and create 2 images from it- these will be our folding halves
77  
-	
78  
-	// Calculate the 2 rects
79  
-	CGRect barRect = self.centerBar.bounds;
80  
-	CGRect topRect = barRect;
81  
-	topRect.size.height = barRect.size.height / 2;
82  
-	CGRect bottomRect = topRect;
83  
-	bottomRect.origin.y = topRect.size.height;
84  
-	
85  
-	// paint the images from the view
86  
-	UIEdgeInsets insets = UIEdgeInsetsMake(0, 1, 0, 1);
87  
-	UIImage *topImage = [MPAnimation renderImageForAntialiasing:[MPAnimation renderImageFromView:self.centerBar withRect:topRect] withInsets:insets];
88  
-	UIImage *bottomImage = [MPAnimation renderImageForAntialiasing:[MPAnimation renderImageFromView:self.centerBar withRect:bottomRect] withInsets:insets];
89  
-	
90  
-	// account for 1-pixel clear margin we introduced for anti-aliasing
91  
-	topRect = CGRectInset(topRect, -insets.left, -insets.top);
92  
-	bottomRect = CGRectInset(bottomRect, -insets.left, -insets.top);
93  
-
94  
-	// create UIImageView's to hold the images
95  
-	[self setFoldTop: [[UIImageView alloc] initWithImage:topImage]];
96  
-	self.foldTop.frame = topRect;
97  
-	self.foldTop.layer.anchorPoint = CGPointMake(0.5, 0); // anchor at top
98  
-	[self setFoldBottom:[[UIImageView alloc] initWithImage:bottomImage]];
99  
-	self.foldBottom.frame = bottomRect;
100  
-	self.foldBottom.layer.anchorPoint = CGPointMake(0.5, 1); // anchor at bottom
101  
-	[self setDropShadow:self.foldTop];
102  
-	[self setDropShadow:self.foldBottom];
103  
-	[[self.foldTop layer] setShadowPath:[[UIBezierPath bezierPathWithRect:CGRectInset([self.foldTop bounds], insets.left, insets.top)] CGPath]];	
104  
-	[[self.foldBottom layer] setShadowPath:[[UIBezierPath bezierPathWithRect:CGRectInset([self.foldBottom bounds], insets.left, insets.top)] CGPath]];
105 97
 }
106 98
 
107 99
 - (void)viewDidUnload
@@ -110,8 +102,6 @@ - (void)viewDidUnload
110 102
 	[self setTopBar:nil];
111 103
 	[self setCenterBar:nil];
112 104
 	[self setBottomBar:nil];
113  
-	[self setFoldTop:nil];
114  
-	[self setFoldBottom:nil];
115 105
     [self setSkewSegment:nil];
116 106
     [self setControlFrame:nil];
117 107
     [super viewDidUnload];
@@ -125,9 +115,14 @@ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interface
125 115
 
126 116
 #pragma mark - Properties
127 117
 
  118
+- (SkewMode)skewMode
  119
+{
  120
+	return (SkewMode)[self.skewSegment selectedSegmentIndex];
  121
+}
  122
+
128 123
 - (CGFloat)skew
129 124
 {
130  
-	switch ((SkewMode)[self.skewSegment selectedSegmentIndex])
  125
+	switch ([self skewMode])
131 126
 	{
132 127
 		case SkewModeInverse:
133 128
 			return 1 / ((FOLD_HEIGHT / 2) *  4.666666667);
@@ -146,34 +141,22 @@ - (CGFloat)skew
146 141
 	}
147 142
 }
148 143
 
149  
-- (CGFloat)skewAngle
  144
+- (BOOL)isInverse
150 145
 {
151  
-	switch ((SkewMode)[self.skewSegment selectedSegmentIndex])
152  
-	{
153  
-		case SkewModeInverse:
154  
-			return 90 + degrees(atan(1/4.666666667));
155  
-			
156  
-		case SkewModeNone:
157  
-			return 90;
158  
-			
159  
-		case SkewModeLow:
160  
-			return degrees(atan(12));
161  
-			
162  
-		case SkewModeNormal:
163  
-			return degrees(atan(4.666666667));
164  
-			
165  
-		case SkewModeHigh:
166  
-			return degrees(atan(1.5));
167  
-			
168  
-	}
  146
+	return [self skewMode] == SkewModeInverse;
169 147
 }
170  
-
  148
+			
171 149
 #pragma mark - methods
172 150
 
173 151
 - (void)setDropShadow:(UIView *)view
174 152
 {
175  
-	view.layer.shadowOpacity = 0.5;
176  
-	view.layer.shadowOffset = CGSizeMake(0, 1);
  153
+	[self setDropShadowForLayer:[view layer]];
  154
+}
  155
+
  156
+- (void)setDropShadowForLayer:(CALayer *)layer
  157
+{
  158
+	layer.shadowOpacity = 0.5;
  159
+	layer.shadowOffset = CGSizeMake(0, 1);
177 160
 }
178 161
 
179 162
 #pragma mark - Gesture handlers
@@ -231,6 +214,12 @@ - (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer {
231 214
 	}
232 215
 }
233 216
 
  217
+- (IBAction)skewValueChanged:(UISegmentedControl *)sender {
  218
+	CATransform3D transform = CATransform3DIdentity;	
  219
+	transform.m34 = [self skew];
  220
+	[[self perspectiveLayer] setSublayerTransform:transform];
  221
+}
  222
+
234 223
 #pragma mark - UIGestureRecognizerDelegate
235 224
 
236 225
 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
@@ -242,70 +231,65 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
242 231
 
243 232
 - (void)startFold
244 233
 {
245  
-	[self setFolding:YES];
246  
-	// replace the center bar with the 2 image halves
247  
-	CGRect barRect =  self.centerBar.frame;//[self.contentView convertRect:self.centerBar.frame fromView:self.centerBar];
248  
-	CGRect topRect = barRect;
249  
-	topRect.size.height = barRect.size.height / 2;
250  
-	CGRect bottomRect = topRect;
251  
-	bottomRect.origin.y += topRect.size.height;
252  
-	
253  
-	// account for 1-pixel clear margin we introduced for anti-aliasing
254  
-	UIEdgeInsets insets = UIEdgeInsetsMake(0, 1, 0, 1);
255  
-	topRect = CGRectInset(topRect, -insets.left, -insets.top);
256  
-	bottomRect = CGRectInset(bottomRect, -insets.left, -insets.top);
257  
-
258  
-	self.foldTop.frame = topRect;
259  
-	self.foldBottom.frame = bottomRect;
  234
+	[CATransaction begin];
  235
+	[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
260 236
 
261  
-	[self.contentView insertSubview:foldTop aboveSubview:self.centerBar];
262  
-	[self.contentView insertSubview:foldBottom aboveSubview:self.foldTop];
  237
+	[self setFolding:YES];
  238
+	[self buildLayers];
  239
+	[self doFold:[self isFolded]? 1 : 0];
263 240
 
264  
-	[self.centerBar setHidden:YES];
  241
+	[CATransaction commit];
265 242
 }
266 243
 
267 244
 - (void)doFold:(CGFloat)difference
268 245
 {
269 246
 	CGFloat progress = fabsf(difference) / FOLD_HEIGHT;
  247
+	if ([self isFolded])
  248
+		progress = 1 - progress;
  249
+	
  250
+	if (progress < 0)
  251
+		progress = 0;
  252
+	else if (progress > 1)
  253
+		progress = 1;
  254
+	
270 255
 	if (progress == [self lastProgress])
271 256
 		return;
272 257
 	[self setLastProgress:progress];
273 258
 	
274  
-	CATransform3D tUpperFold;
275  
-	CATransform3D tLowerFold;
  259
+	double angle = radians(90 * progress);
  260
+	double cosine = cos(angle);
  261
+	double foldHeight = cosine * FOLD_HEIGHT;
  262
+	CGFloat scale = [[UIScreen mainScreen] scale];
  263
+
  264
+	// to prevent flickering on non-retina devices (due to 1 point white border at edge of top and bottom panels),
  265
+	// keep height in pixels (not points) an integer, and back out correct angle from there
  266
+	foldHeight = round(foldHeight * scale) / scale;
  267
+	angle = acos(foldHeight / FOLD_HEIGHT);
  268
+	if (((int)(foldHeight * scale)) % 2 == 1)
  269
+	{
  270
+		// If height is an odd-# of pixels, shift position down half a pixel to keep top and bottom sleeves on pixel boundaries
  271
+		[self.animationView setCenter:CGPointMake(self.animationCenter.x, self.animationCenter.y + (0.5/scale))];
  272
+	}
  273
+	else 
  274
+	{
  275
+		[self.animationView setCenter:[self animationCenter]];
  276
+	}
  277
+
  278
+	[CATransaction begin];
  279
+	[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
276 280
 	
277  
-	CGFloat verticalOffset = [self calculateFold:progress upperFold:&tUpperFold lowerFold:&tLowerFold];
  281
+	self.firstJointLayer.transform = CATransform3DMakeRotation(-1*angle, 1, 0, 0);
  282
+	self.secondJointLayer.transform = CATransform3DMakeRotation(2*angle, 1, 0, 0);
  283
+	self.topSleeve.transform = CATransform3DMakeRotation(1*angle, 1, 0, 0);
  284
+	self.bottomSleeve.transform = CATransform3DMakeRotation(-1*angle, 1, 0, 0);
278 285
 	
279  
-	self.topBar.layer.transform = CATransform3DMakeTranslation(0, verticalOffset, 0);
280  
-	self.bottomBar.layer.transform = CATransform3DMakeTranslation(0, -verticalOffset, 0);
281  
-	self.foldTop.layer.transform = tUpperFold;
282  
-	self.foldBottom.layer.transform = tLowerFold;
283  
-}
284  
-
285  
-- (CGFloat)calculateFold:(CGFloat)progress upperFold:(CATransform3D*)upperFoldTransform lowerFold:(CATransform3D*)lowerFoldTransform{
286  
-	if ([self isFolded])
287  
-		progress = 1 - progress;
  286
+	BOOL inverse = [self isInverse];
  287
+	self.upperFoldShadow.opacity = inverse? 0 : FOLD_SHADOW_OPACITY * (1- cosine);
  288
+	self.lowerFoldShadow.opacity = ((inverse? 1 : FOLD_SHADOW_ADJUSTMENT_FACTOR) * FOLD_SHADOW_OPACITY) * (1 - cosine);
288 289
 	
289  
-	// We need to move the folding flaps towards the center based on the cosine of the angle of our fold
290  
-	// Basically what this does is keep the bottom of the top fold (and top of the bottom fold) anchored to the midpoint.
291  
-	CGFloat cosine = cosf(radians(90 * progress));
292  
-	CGFloat verticalOffset = (FOLD_HEIGHT / 2) * (1- cosine); // how much to offset each panel by
293  
-	
294  
-	// fold the top and bottom halves of the center panel away from us
295  
-	CATransform3D tTop = CATransform3DIdentity;
296  
-	tTop.m34 = [self skew] * progress;
297  
-	tTop = CATransform3DTranslate(tTop, 0, verticalOffset, 0); // shift panel towards center
298  
-	tTop = CATransform3DRotate(tTop, radians([self skewAngle] * progress), -1, 0, 0); // rotate away from viewer
299  
-	*upperFoldTransform = tTop;
300  
-	
301  
-	CATransform3D tBottom = CATransform3DIdentity;
302  
-	tBottom.m34 = [self skew] * progress;
303  
-	tBottom = CATransform3DTranslate(tBottom, 0, -verticalOffset, 0); // shift panel towards center
304  
-	tBottom = CATransform3DRotate(tBottom, radians(-[self skewAngle] * progress), -1, 0, 0); // rotate away from viewer
305  
-	self.foldBottom.layer.transform = tBottom;
306  
-	*lowerFoldTransform = tBottom;
307  
-	
308  
-	return verticalOffset;
  290
+	self.perspectiveLayer.bounds = (CGRect){CGPointZero, CGSizeMake(self.perspectiveLayer.bounds.size.width, foldHeight)};
  291
+
  292
+	[CATransaction commit];
309 293
 }
310 294
 
311 295
 - (void)endFold
@@ -313,7 +297,7 @@ - (void)endFold
313 297
 	BOOL finish = NO;
314 298
 	if ([self isFolded])
315 299
 	{
316  
-		finish = 1 - cosf(radians(90 * (1-[self lastProgress]))) <= 0.5;		
  300
+		finish = 1 - cosf(radians(90 * [self lastProgress])) <= 0.5;
317 301
 	}
318 302
 	else
319 303
 	{
@@ -335,16 +319,30 @@ - (void)postFold:(BOOL)finish
335 319
 	if (finish)
336 320
 		[self setFolded:![self isFolded]];
337 321
 	
338  
-	// remove the 2 image halves and restore the center bar
339  
-	[foldTop removeFromSuperview];
340  
-	[foldBottom removeFromSuperview];
  322
+	// remove the animation view and restore the center bar
  323
+	[self.animationView removeFromSuperview];
  324
+	self.animationView = nil;
  325
+	self.perspectiveLayer = nil;
  326
+	self.topSleeve = nil;
  327
+	self.bottomSleeve = nil;
  328
+	self.upperFoldShadow = nil;
  329
+	self.lowerFoldShadow = nil;
  330
+	self.firstJointLayer = nil;
  331
+	self.secondJointLayer = nil;
341 332
 	
342  
-	if (![self isFolded])
  333
+	if ([self isFolded])
  334
+	{
  335
+		self.topBar.transform = CGAffineTransformMakeTranslation(0, FOLD_HEIGHT/2);
  336
+		self.bottomBar.transform = CGAffineTransformMakeTranslation(0, -FOLD_HEIGHT/2);
  337
+		[self.centerBar setHidden:YES];
  338
+	}
  339
+	else 
343 340
 	{
344 341
 		self.topBar.transform = CGAffineTransformIdentity;
345 342
 		self.bottomBar.transform = CGAffineTransformIdentity;
346 343
 		[self.centerBar setHidden:NO];
347 344
 	}
  345
+	[self.contentView setHidden:NO];	
348 346
 }
349 347
 
350 348
 - (void)animateFold:(BOOL)finish
@@ -354,26 +352,7 @@ - (void)animateFold:(BOOL)finish
354 352
 	// Figure out how many frames we want
355 353
 	CGFloat duration = DEFAULT_DURATION;
356 354
 	NSUInteger frameCount = ceilf(duration * 60); // we want 60 FPS
357  
-	
358  
-	// Build an array of keyframes (each a single transform)
359  
-	NSMutableArray* arrayTop = [NSMutableArray arrayWithCapacity:frameCount + 1];
360  
-	NSMutableArray* arrayUpperFold = [NSMutableArray arrayWithCapacity:frameCount + 1];
361  
-	NSMutableArray* arrayLowerFold = [NSMutableArray arrayWithCapacity:frameCount + 1];
362  
-	NSMutableArray* arrayBottom = [NSMutableArray arrayWithCapacity:frameCount + 1];
363  
-	CGFloat toProgress = finish? 1 : 0;
364  
-	CGFloat progress;
365  
-	CGFloat verticalOffset;
366  
-	CATransform3D upperFold, lowerFold;
367  
-	for (int frame = 0; frame <= frameCount; frame++)
368  
-	{
369  
-		progress = [self lastProgress] + (((toProgress - [self lastProgress]) * frame) / frameCount);
370  
-		verticalOffset = [self calculateFold:progress upperFold:&upperFold lowerFold:&lowerFold];
371  
-		[arrayTop addObject:[NSNumber numberWithFloat:verticalOffset]];
372  
-		[arrayUpperFold addObject:[NSValue valueWithCATransform3D:upperFold]];
373  
-		[arrayLowerFold addObject:[NSValue valueWithCATransform3D:lowerFold]];
374  
-		[arrayBottom addObject:[NSNumber numberWithFloat:-verticalOffset]];
375  
-	}
376  
-	
  355
+		
377 356
 	// Create a transaction
378 357
 	[CATransaction begin];
379 358
 	[CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];
@@ -382,38 +361,242 @@ - (void)animateFold:(BOOL)finish
382 361
 		[self postFold:finish];
383 362
 	}];
384 363
 	
385  
-	// Create the 4 animations
386  
-	CAKeyframeAnimation *animationTop = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"]; 
387  
-	[animationTop setValues:[NSArray arrayWithArray:arrayTop]]; 		
388  
-	[animationTop setRemovedOnCompletion:YES];
  364
+	[self.animationView setCenter:[self animationCenter]];
  365
+
  366
+	BOOL vertical = YES;
  367
+	BOOL forwards = finish != [self isFolded];
  368
+	BOOL inverse = [self isInverse];
  369
+	NSString *rotationKey = vertical? @"transform.rotation.x" : @"transform.rotation.y";
  370
+	double factor = (vertical? 1 : - 1) * M_PI / 180;
  371
+	CGFloat fromProgress = [self lastProgress];
  372
+	if (finish == [self isFolded])
  373
+		fromProgress = 1 - fromProgress;
  374
+
  375
+	// fold the first (top) joint away from us
  376
+	CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:rotationKey];
  377
+	[animation setFromValue:forwards? [NSNumber numberWithDouble:-90*factor*fromProgress] : [NSNumber numberWithDouble:-90*factor*(1-fromProgress)]];
  378
+	[animation setToValue:forwards? [NSNumber numberWithDouble:-90*factor] : [NSNumber numberWithDouble:0]];
  379
+	[animation setFillMode:kCAFillModeForwards];
  380
+	[animation setRemovedOnCompletion:NO];
  381
+	[self.firstJointLayer addAnimation:animation forKey:nil];
  382
+	
  383
+	// fold the second joint back towards us at twice the angle (since it's connected to the first fold we're folding away)
  384
+	animation = [CABasicAnimation animationWithKeyPath:rotationKey];
  385
+	[animation setFromValue:forwards? [NSNumber numberWithDouble:180*factor*fromProgress] : [NSNumber numberWithDouble:180*factor*(1-fromProgress)]];
  386
+	[animation setToValue:forwards? [NSNumber numberWithDouble:180*factor] : [NSNumber numberWithDouble:0]];
  387
+	[animation setFillMode:kCAFillModeForwards];
  388
+	[animation setRemovedOnCompletion:NO];
  389
+	[self.secondJointLayer addAnimation:animation forKey:nil];
  390
+	
  391
+	// fold the bottom sleeve (3rd joint) away from us, so that net result is it lays flat from user's perspective
  392
+	animation = [CABasicAnimation animationWithKeyPath:rotationKey];
  393
+	[animation setFromValue:forwards? [NSNumber numberWithDouble:-90*factor*fromProgress] : [NSNumber numberWithDouble:-90*factor*(1-fromProgress)]];
  394
+	[animation setToValue:forwards? [NSNumber numberWithDouble:-90*factor] : [NSNumber numberWithDouble:0]];
  395
+	[animation setFillMode:kCAFillModeForwards];
  396
+	[animation setRemovedOnCompletion:NO];
  397
+	[self.bottomSleeve addAnimation:animation forKey:nil];
  398
+	
  399
+	// fold top sleeve towards us, so that net result is it lays flat from user's perspective
  400
+	animation = [CABasicAnimation animationWithKeyPath:rotationKey];
  401
+	[animation setFromValue:forwards? [NSNumber numberWithDouble:90*factor*fromProgress] : [NSNumber numberWithDouble:90*factor*(1-fromProgress)]];
  402
+	[animation setToValue:forwards? [NSNumber numberWithDouble:90*factor] : [NSNumber numberWithDouble:0]];
  403
+	[animation setFillMode:kCAFillModeForwards];
  404
+	[animation setRemovedOnCompletion:NO];
  405
+	[self.topSleeve addAnimation:animation forKey:nil];
  406
+
  407
+	// Build an array of keyframes for perspectiveLayer.bounds.size.height
  408
+	NSMutableArray* arrayHeight = [NSMutableArray arrayWithCapacity:frameCount + 1];
  409
+	NSMutableArray* arrayUpperShadow = inverse? nil : [NSMutableArray arrayWithCapacity:frameCount + 1];
  410
+	NSMutableArray* arrayLowerShadow = [NSMutableArray arrayWithCapacity:frameCount + 1];
  411
+	CGFloat progress;
  412
+	CGFloat cosine;
  413
+	CGFloat cosHeight;
  414
+	CGFloat cosShadow;
  415
+	for (int frame = 0; frame <= frameCount; frame++)
  416
+	{
  417
+		progress = fromProgress + (((1 - fromProgress) * frame) / frameCount);
  418
+		//progress = (((float)frame) / frameCount);
  419
+		cosine = forwards? cos(radians(90 * progress)) : sin(radians(90 * progress));
  420
+		if ((forwards && frame == frameCount) || (!forwards && frame == 0 && fromProgress == 0))
  421
+			cosine = 0;
  422
+		cosHeight = ((cosine)* FOLD_HEIGHT); // range from 2*height to 0 along a cosine curve
  423
+		[arrayHeight addObject:[NSNumber numberWithFloat:cosHeight]];
  424
+		
  425
+		cosShadow = FOLD_SHADOW_OPACITY * (1 - cosine);
  426
+		if (!inverse)
  427
+			[arrayUpperShadow addObject:[NSNumber numberWithFloat:cosShadow]];
  428
+		[arrayLowerShadow addObject:[NSNumber numberWithFloat:inverse? cosShadow : cosShadow * (FOLD_SHADOW_ADJUSTMENT_FACTOR)]];
  429
+	}
389 430
 	
390  
-	CAKeyframeAnimation *animationUpperFold = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; 
391  
-	[animationUpperFold setValues:[NSArray arrayWithArray:arrayUpperFold]]; 		
392  
-	[animationUpperFold setRemovedOnCompletion:YES];
  431
+	// resize height of the 2 folding panels along a cosine curve.  This is necessary to maintain the 2nd joint in the center
  432
+	// Since there's no built-in sine timing curve, we'll use CAKeyframeAnimation to achieve it
  433
+	CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animationWithKeyPath:vertical? @"bounds.size.height" : @"bounds.size.width"];
  434
+	[keyAnimation setValues:[NSArray arrayWithArray:arrayHeight]];
  435
+	[keyAnimation setFillMode:kCAFillModeForwards];
  436
+	[keyAnimation setRemovedOnCompletion:NO];
  437
+	[self.perspectiveLayer addAnimation:keyAnimation forKey:nil];
  438
+	
  439
+	// Dim the 2 folding panels as they fold away from us
  440
+	// Note that 2 slightly different endpoint opacities are used to help distinguish the upper and lower panels
  441
+	if (!inverse)
  442
+	{
  443
+		keyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
  444
+		[keyAnimation setValues:arrayUpperShadow];
  445
+		[keyAnimation setFillMode:kCAFillModeForwards];
  446
+		[keyAnimation setRemovedOnCompletion:NO];
  447
+		[self.upperFoldShadow addAnimation:keyAnimation forKey:nil];
  448
+	}
393 449
 	
394  
-	CAKeyframeAnimation *animationLowerFold = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; 
395  
-	[animationLowerFold setValues:[NSArray arrayWithArray:arrayLowerFold]]; 		
396  
-	[animationLowerFold setRemovedOnCompletion:YES];
  450
+	keyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
  451
+	[keyAnimation setValues:arrayLowerShadow];
  452
+	[keyAnimation setFillMode:kCAFillModeForwards];
  453
+	[keyAnimation setRemovedOnCompletion:NO];
  454
+	[self.lowerFoldShadow addAnimation:keyAnimation forKey:nil];
  455
+					
  456
+	// commit the transaction
  457
+	[CATransaction commit];
  458
+}
  459
+
  460
+- (void)buildLayers
  461
+{
  462
+	[CATransaction begin];
  463
+	[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
  464
+
  465
+	BOOL vertical = YES;
397 466
 	
398  
-	CAKeyframeAnimation *animationBottom = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"]; 
399  
-	[animationBottom setValues:[NSArray arrayWithArray:arrayBottom]]; 		
400  
-	[animationBottom setRemovedOnCompletion:YES];
  467
+	CGRect bounds = self.centerBar.bounds;
  468
+	CGFloat scale = [[UIScreen mainScreen] scale];
401 469
 	
402  
-	// add the animations
403  
-	[self.topBar.layer addAnimation:animationTop forKey:@"transformTop"];
404  
-	[self.foldTop.layer addAnimation:animationUpperFold forKey:@"transformUpperFold"];
405  
-	[self.foldBottom.layer addAnimation:animationLowerFold forKey:@"transformLowerFold"];
406  
-	[self.bottomBar.layer addAnimation:animationBottom forKey:@"transformBottom"];
  470
+	// we inset the folding panels 1 point on each side with a transparent margin to antialiase the edges
  471
+	UIEdgeInsets foldInsets = vertical? UIEdgeInsetsMake(0, 1, 0, 1) : UIEdgeInsetsMake(1, 0, 1, 0);
407 472
 	
408  
-	// set final states
409  
-	[self.topBar.layer setTransform:CATransform3DMakeTranslation(0, [[[animationTop values] lastObject] floatValue], 0)];
410  
-	[self.foldTop.layer setTransform:[[[animationUpperFold values] lastObject] CATransform3DValue]];
411  
-	[self.foldBottom.layer setTransform:[[[animationLowerFold values] lastObject] CATransform3DValue]];
412  
-	[self.bottomBar.layer setTransform:CATransform3DMakeTranslation(0, [[[animationBottom values] lastObject] floatValue], 0)];
  473
+	CGRect upperRect = bounds;
  474
+	if (vertical)
  475
+		upperRect.size.height = bounds.size.height / 2;
  476
+	else
  477
+		upperRect.size.width = bounds.size.width / 2;
  478
+	CGRect lowerRect = upperRect;
  479
+	if (vertical)
  480
+		lowerRect.origin.y += upperRect.size.height;
  481
+	else
  482
+		lowerRect.origin.x += upperRect.size.width;
  483
+		
  484
+	// Create 4 images to represent 2 halves of the 2 views
  485
+	[self.centerBar setHidden:NO];
  486
+	UIImage * foldUpper = [MPAnimation renderImageFromView:self.centerBar withRect:upperRect transparentInsets:foldInsets];
  487
+	UIImage * foldLower = [MPAnimation renderImageFromView:self.centerBar withRect:lowerRect transparentInsets:foldInsets];
  488
+	UIImage *slideUpper = [MPAnimation renderImageFromView:self.topBar];
  489
+	UIImage *slideLower = [MPAnimation renderImageFromView:self.bottomBar];
  490
+	
  491
+	UIView *actingSource = self.contentView;
  492
+	UIView *containerView = [actingSource superview];
  493
+	[actingSource setHidden:YES];
  494
+	
  495
+	CATransform3D transform = CATransform3DIdentity;	
  496
+	CALayer *upperFold;
  497
+	CALayer *lowerFold;
  498
+	
  499
+	CGFloat width = vertical? bounds.size.width : bounds.size.height;
  500
+	CGFloat height = vertical? bounds.size.height/2 : bounds.size.width/2;
  501
+	CGFloat upperHeight = roundf(height * scale) / scale; // round heights to integer for odd height
  502
+	CGFloat lowerHeight = (height * 2) - upperHeight;
  503
+
  504
+	// view to hold all our sublayers
  505
+	CGRect mainRect = [containerView convertRect:self.centerBar.frame fromView:actingSource];
  506
+	self.animationView = [[UIView alloc] initWithFrame:mainRect];
  507
+	self.animationView.backgroundColor = [UIColor clearColor];
  508
+	[containerView addSubview:self.animationView];
  509
+	[self setAnimationCenter:[self.animationView center]];
  510
+	
  511
+	// layer that covers the 2 folding panels in the middle
  512
+	self.perspectiveLayer = [CALayer layer];
  513
+	self.perspectiveLayer.frame = CGRectMake(0, 0, vertical? width : height * 2, vertical? height * 2 : width);
  514
+	[self.animationView.layer addSublayer:self.perspectiveLayer];
  515
+	
  516
+	// layer that encapsulates the join between the top sleeve (remains flat) and upper folding panel
  517
+	self.firstJointLayer = [CATransformLayer layer];
  518
+	self.firstJointLayer.frame = self.animationView.bounds;
  519
+	[self.perspectiveLayer addSublayer:self.firstJointLayer];
  520
+	
  521
+	// This remains flat, and is the upper half of the destination view when moving forwards
  522
+	// It slides down to meet the bottom sleeve in the center
  523
+	self.topSleeve = [CALayer layer];
  524
+	self.topSleeve.frame = (CGRect){CGPointZero, slideUpper.size};
  525
+	self.topSleeve.anchorPoint = CGPointMake(vertical? 0.5 : 1, vertical? 1 : 0.5);
  526
+	self.topSleeve.position = CGPointMake(vertical? width/2 : 0, vertical? 0 : width/2);
  527
+	[self.topSleeve setContents:(id)[slideUpper CGImage]];
  528
+	[self.firstJointLayer addSublayer:self.topSleeve];
  529
+	
  530
+	// This piece folds away from user along top edge, and is the upper half of the source view when moving forwards
  531
+	upperFold = [CALayer layer];
  532
+	upperFold.frame = (CGRect){CGPointZero, foldUpper.size};
  533
+	upperFold.anchorPoint = CGPointMake(vertical? 0.5 : 0, vertical? 0 : 0.5);
  534
+	upperFold.position = CGPointMake(vertical? width/2 : 0, vertical? 0 : width / 2);
  535
+	upperFold.contents = (id)[foldUpper CGImage];
  536
+	[self.firstJointLayer addSublayer:upperFold];
  537
+	
  538
+	// layer that encapsultates the join between the upper and lower folding panels (the V in the fold)
  539
+	self.secondJointLayer = [CATransformLayer layer];
  540
+	self.secondJointLayer.frame = self.animationView.bounds;
  541
+	self.secondJointLayer.frame = CGRectMake(0, 0, vertical? width : height * 2, vertical? height*2 : width);
  542
+	self.secondJointLayer.anchorPoint = CGPointMake(vertical? 0.5 : 0, vertical? 0 : 0.5);
  543
+	self.secondJointLayer.position = CGPointMake(vertical? width/2 : upperHeight, vertical? upperHeight : width / 2);
  544
+	[self.firstJointLayer addSublayer:self.secondJointLayer];
  545
+	
  546
+	// This piece folds away from user along bottom edge, and is the lower half of the source view when moving forwards
  547
+	lowerFold = [CALayer layer];
  548
+	lowerFold.frame = (CGRect){CGPointZero, foldLower.size};
  549
+	lowerFold.anchorPoint = CGPointMake(vertical? 0.5 : 0, vertical? 0 : 0.5);
  550
+	lowerFold.position = CGPointMake(vertical? width/2 : 0, vertical? 0 : width / 2);
  551
+	lowerFold.contents = (id)[foldLower CGImage];
  552
+	[self.secondJointLayer addSublayer:lowerFold];
  553
+	
  554
+	// This remains flat, and is the lower half of the destination view when moving forwards
  555
+	// It slides up to meet the top sleeve in the center
  556
+	self.bottomSleeve = [CALayer layer];
  557
+	self.bottomSleeve.frame = (CGRect){CGPointZero, slideLower.size};
  558
+	self.bottomSleeve.anchorPoint = CGPointMake(vertical? 0.5 : 0, vertical? 0 : 0.5);
  559
+	self.bottomSleeve.position = CGPointMake(vertical? width/2 : lowerHeight, vertical? lowerHeight : width / 2);
  560
+	[self.bottomSleeve setContents:(id)[slideLower CGImage]];
  561
+	[self.secondJointLayer addSublayer:self.bottomSleeve];
  562
+	
  563
+	self.firstJointLayer.anchorPoint = CGPointMake(vertical? 0.5 : 0, vertical? 0 : 0.5);
  564
+	self.firstJointLayer.position = CGPointMake(vertical? width/2 : 0, vertical? 0 : width / 2);
  565
+	
  566
+	// Shadow layers to add shadowing to the 2 folding panels
  567
+	self.upperFoldShadow = [CALayer layer];
  568
+	[upperFold addSublayer:self.upperFoldShadow];
  569
+	self.upperFoldShadow.frame = CGRectInset(upperFold.bounds, foldInsets.left, foldInsets.top);
  570
+	self.upperFoldShadow.backgroundColor = [UIColor blackColor].CGColor;
  571
+	self.upperFoldShadow.opacity = 0;
  572
+	
  573
+	self.lowerFoldShadow = [CALayer layer];
  574
+	[lowerFold addSublayer:self.lowerFoldShadow];
  575
+	self.lowerFoldShadow.frame = CGRectInset(lowerFold.bounds, foldInsets.left, foldInsets.top);
  576
+	self.lowerFoldShadow.backgroundColor = [UIColor blackColor].CGColor;
  577
+	self.lowerFoldShadow.opacity = 0;
  578
+		
  579
+	[self setDropShadowForLayer:self.topSleeve];
  580
+	[self setDropShadowForLayer:upperFold];
  581
+	[self setDropShadowForLayer:lowerFold];
  582
+	[self setDropShadowForLayer:self.bottomSleeve];
  583
+	
  584
+	// reduce shadow on topSleeve slightly so it won't shade the upperFold panel so much
  585
+	CGRect topBounds = self.topSleeve.bounds;
  586
+	topBounds.size.height -= 1; // make it shorter by 1
  587
+	[self.topSleeve setShadowPath:[[UIBezierPath bezierPathWithRect:topBounds] CGPath]];	
  588
+	
  589
+	[upperFold setShadowPath:[[UIBezierPath bezierPathWithRect:CGRectInset([upperFold bounds], foldInsets.left, foldInsets.top)] CGPath]];	
  590
+	[lowerFold setShadowPath:[[UIBezierPath bezierPathWithRect:CGRectInset([lowerFold bounds], foldInsets.left, foldInsets.top)] CGPath]];
  591
+	[self.bottomSleeve setShadowPath:[[UIBezierPath bezierPathWithRect:[self.bottomSleeve bounds]] CGPath]];
  592
+	
  593
+	// Perspective is best proportional to the height of the pieces being folded away, rather than a fixed value
  594
+	// the larger the piece being folded, the more perspective distance (zDistance) is needed.
  595
+	// m34 = -1/zDistance
  596
+	transform.m34 = [self skew];
  597
+	self.perspectiveLayer.sublayerTransform = transform;
413 598
 	
414  
-	// commit the transaction
415 599
 	[CATransaction commit];
416 600
 }
417 601
 
418  
-
419 602
 @end
130  EnterTheMatrix/en.lproj/MainStoryboard_iPad.storyboard
@@ -940,6 +940,9 @@ www.glyphish.com</string>
940 940
                                             <segment title="Normal"/>
941 941
                                             <segment title="High"/>
942 942
                                         </segments>
  943
+                                        <connections>
  944
+                                            <action selector="skewValueChanged:" destination="ueP-0g-58F" eventType="valueChanged" id="vI2-oj-OyF"/>
  945
+                                        </connections>
943 946
                                     </segmentedControl>
944 947
                                     <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Skew" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="AWZ-BL-e0F">
945 948
                                         <rect key="frame" x="20" y="24" width="42" height="21"/>
@@ -1621,6 +1624,133 @@ www.glyphish.com</string>
1621 1624
         <image name="matrix_01.png" width="480" height="360"/>
1622 1625
         <image name="matrix_02.png" width="480" height="360"/>
1623 1626
     </resources>
  1627
+    <classes>
  1628
+        <class className="AnchorPointTable" superclassName="UITableViewController">
  1629
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/AnchorPointTable.h"/>
  1630
+        </class>
  1631
+        <class className="Arc" superclassName="UIView">
  1632
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/Arc.h"/>
  1633
+        </class>
  1634
+        <class className="BasicAnimationViewController" superclassName="UIViewController">
  1635
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/BasicAnimationViewController.h"/>
  1636
+            <relationships>
  1637
+                <relationship kind="action" name="animationChanged:"/>
  1638
+                <relationship kind="action" name="goPressed:"/>
  1639
+                <relationship kind="outlet" name="bar" candidateClass="UIView"/>
  1640
+                <relationship kind="outlet" name="controlFrame" candidateClass="UIView"/>
  1641
+                <relationship kind="outlet" name="modeSegment" candidateClass="UISegmentedControl"/>
  1642
+                <relationship kind="outlet" name="rotateLabel" candidateClass="UILabel"/>
  1643
+                <relationship kind="outlet" name="rotateSwitch" candidateClass="UISwitch"/>
  1644
+                <relationship kind="outlet" name="scaleLabel" candidateClass="UILabel"/>
  1645
+                <relationship kind="outlet" name="scaleSwitch" candidateClass="UISwitch"/>
  1646
+                <relationship kind="outlet" name="speedSwitch" candidateClass="UISwitch"/>
  1647
+                <relationship kind="outlet" name="translateLabel" candidateClass="UILabel"/>
  1648
+                <relationship kind="outlet" name="translateSwitch" candidateClass="UISwitch"/>
  1649
+            </relationships>
  1650
+        </class>
  1651
+        <class className="CATransformController" superclassName="TransformController">
  1652
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/CATransformController.h"/>
  1653
+        </class>
  1654
+        <class className="CGAffineController" superclassName="TransformController">
  1655
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/CGAffineController.h"/>
  1656
+        </class>
  1657
+        <class className="FlipViewController" superclassName="UIViewController">
  1658
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/FlipViewController.h"/>
  1659
+            <relationships>
  1660
+                <relationship kind="action" name="durationValueChanged:"/>
  1661
+                <relationship kind="action" name="skewValueChanged:"/>
  1662
+                <relationship kind="outlet" name="contentView" candidateClass="UIView"/>
  1663
+                <relationship kind="outlet" name="controlFrame" candidateClass="UIView"/>
  1664
+                <relationship kind="outlet" name="imageView" candidateClass="UIImageView"/>
  1665
+                <relationship kind="outlet" name="skewLabel" candidateClass="UILabel"/>
  1666
+                <relationship kind="outlet" name="skewSlider" candidateClass="UISlider"/>
  1667
+            </relationships>
  1668
+        </class>
  1669
+        <class className="FoldViewController" superclassName="UIViewController">
  1670
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/FoldViewController.h"/>
  1671
+            <relationships>
  1672
+                <relationship kind="action" name="skewValueChanged:" candidateClass="UISegmentedControl"/>
  1673
+                <relationship kind="outlet" name="bottomBar" candidateClass="UIView"/>
  1674
+                <relationship kind="outlet" name="centerBar" candidateClass="UIView"/>
  1675
+                <relationship kind="outlet" name="contentView" candidateClass="UIView"/>
  1676
+                <relationship kind="outlet" name="controlFrame" candidateClass="UIView"/>
  1677
+                <relationship kind="outlet" name="skewSegment" candidateClass="UISegmentedControl"/>
  1678
+                <relationship kind="outlet" name="topBar" candidateClass="UIView"/>
  1679
+            </relationships>
  1680
+        </class>
  1681
+        <class className="GridView" superclassName="UIView">
  1682
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/GridView.h"/>
  1683
+        </class>
  1684
+        <class className="GridViewController" superclassName="UIViewController">
  1685
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/GridViewController.h"/>
  1686
+            <relationships>
  1687
+                <relationship kind="action" name="lockPressed:"/>
  1688
+                <relationship kind="action" name="resetPressed:"/>
  1689
+                <relationship kind="action" name="rotateHorzChanged:"/>
  1690
+                <relationship kind="action" name="rotateVertChanged:"/>
  1691
+                <relationship kind="action" name="scaleHorzChanged:"/>
  1692
+                <relationship kind="action" name="scaleVertChanged:"/>
  1693
+                <relationship kind="action" name="translateHorzChanged:"/>
  1694
+                <relationship kind="action" name="translateVertChanged:"/>
  1695
+                <relationship kind="outlet" name="grid" candidateClass="GridView"/>
  1696
+                <relationship kind="outlet" name="horizontalPanel" candidateClass="UIView"/>
  1697
+                <relationship kind="outlet" name="lockButton" candidateClass="UIButton"/>
  1698
+                <relationship kind="outlet" name="resetButton" candidateClass="UIButton"/>
  1699
+                <relationship kind="outlet" name="verticalPanel" candidateClass="UIView"/>
  1700
+            </relationships>
  1701
+        </class>
  1702
+        <class className="InfoView" superclassName="UIViewController">
  1703
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/InfoView.h"/>
  1704
+            <relationships>
  1705
+                <relationship kind="outlet" name="label" candidateClass="UITextView"/>
  1706
+            </relationships>
  1707
+        </class>
  1708
+        <class className="KeyframeViewController" superclassName="UIViewController">
  1709
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/KeyframeViewController.h"/>
  1710
+            <relationships>
  1711
+                <relationship kind="action" name="arcPositionValueChanged:"/>
  1712
+                <relationship kind="outlet" name="arc" candidateClass="Arc"/>
  1713
+                <relationship kind="outlet" name="arcPositionSegment" candidateClass="UISegmentedControl"/>
  1714
+                <relationship kind="outlet" name="bottomLabel" candidateClass="UILabel"/>
  1715
+                <relationship kind="outlet" name="controlFrame" candidateClass="UIView"/>
  1716
+                <relationship kind="outlet" name="modeSegment" candidateClass="UISegmentedControl"/>
  1717
+                <relationship kind="outlet" name="speedSwitch" candidateClass="UISwitch"/>
  1718
+                <relationship kind="outlet" name="topLabel" candidateClass="UILabel"/>
  1719
+            </relationships>
  1720
+        </class>
  1721
+        <class className="TransformController" superclassName="UIViewController">
  1722
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/TransformController.h"/>
  1723
+            <relationships>
  1724
+                <relationship kind="action" name="anchorPressed:"/>
  1725
+                <relationship kind="action" name="infoPressed:"/>
  1726
+                <relationship kind="action" name="resetPressed:"/>
  1727
+                <relationship kind="action" name="transformPressed:"/>
  1728
+                <relationship kind="outlet" name="contentView" candidateClass="UIView"/>
  1729
+                <relationship kind="outlet" name="toolbar" candidateClass="UIToolbar"/>
  1730
+            </relationships>
  1731
+        </class>
  1732
+        <class className="TransformTable" superclassName="UITableViewController">
  1733
+            <source key="sourceIdentifier" type="project" relativePath="./Classes/TransformTable.h"/>
  1734
+            <relationships>
  1735
+                <relationship kind="action" name="resetRotationPressed:"/>
  1736
+                <relationship kind="action" name="resetScalePressed:"/>
  1737
+                <relationship kind="action" name="resetSkewPressed:"/>
  1738
+                <relationship kind="action" name="resetTranslatePressed:"/>
  1739
+                <relationship kind="action" name="rotateXChanged:"/>
  1740
+                <relationship kind="action" name="rotateYChanged:"/>
  1741
+                <relationship kind="action" name="rotateZChanged:"/>
  1742
+                <relationship kind="action" name="rotationAngleChanged:"/>
  1743
+                <relationship kind="action" name="scaleLockPressed:"/>
  1744
+                <relationship kind="action" name="scaleXChanged:"/>
  1745
+                <relationship kind="action" name="scaleYChanged:"/>
  1746
+                <relationship kind="action" name="scaleZChanged:"/>
  1747
+                <relationship kind="action" name="skewChanged:"/>
  1748
+                <relationship kind="action" name="translateXChanged:"/>
  1749
+                <relationship kind="action" name="translateYChanged:"/>
  1750
+                <relationship kind="action" name="translateZChanged:"/>
  1751
+            </relationships>
  1752
+        </class>
  1753
+    </classes>
1624 1754
     <simulatedMetricsContainer key="defaultSimulatedMetrics">
1625 1755
         <simulatedStatusBarMetrics key="statusBar" statusBarStyle="blackTranslucent"/>
1626 1756
         <simulatedOrientationMetrics key="orientation"/>

0 notes on commit 23a54f8

Please sign in to comment.
Something went wrong with that request. Please try again.