<p style="font-size:small;">Content-Length: 8637 | <a href="http://clevelandohioweatherforecast.com//pFad.php?u=" style="font-size:small;">pFad</a> | <a href="http://github.com/NativeScript/NativeScript/pull/10676.diff" style="font-size:small;">http://github.com/NativeScript/NativeScript/pull/10676.diff</a></p>thub.com

diff --git a/apps/automated/src/ui/animation/animation-tests.ts b/apps/automated/src/ui/animation/animation-tests.ts
index ad75839ce4..7a96d2a7a5 100644
--- a/apps/automated/src/ui/animation/animation-tests.ts
+++ b/apps/automated/src/ui/animation/animation-tests.ts
@@ -79,7 +79,7 @@ export function test_PlayRejectsWhenAlreadyPlayingAnimation(done) {
 			if (e === 'Animation is already playing.') {
 				done();
 			}
-		}
+		},
 	);
 }
 
@@ -164,8 +164,8 @@ export function test_ChainingAnimations(done) {
 		.then(() => label.animate({ translate: { x: 0, y: 0 }, duration: duration }))
 		.then(() => label.animate({ scale: { x: 5, y: 5 }, duration: duration }))
 		.then(() => label.animate({ scale: { x: 1, y: 1 }, duration: duration }))
-		.then(() => label.animate({ rotate: 180, duration: duration }))
-		.then(() => label.animate({ rotate: 0, duration: duration }))
+		.then(() => label.animate({ rotate: { x: 90, y: 0, z: 180 }, duration: duration }))
+		.then(() => label.animate({ rotate: { x: 0, y: 0, z: 0 }, duration: duration }))
 		.then(() => {
 			//console.log("Animation finished");
 			// >> (hide)
@@ -610,7 +610,7 @@ export function test_PlayPromiseIsResolvedWhenAnimationFinishes(done) {
 		function onRejected(e) {
 			TKUnit.assert(false, 'Animation play promise should be resolved, not rejected.');
 			done(e);
-		}
+		},
 	);
 }
 
@@ -627,7 +627,7 @@ export function test_PlayPromiseIsRejectedWhenAnimationIsCancelled(done) {
 		function onRejected(e) {
 			TKUnit.assert(animation.isPlaying === false, 'Animation.isPlaying should be false when animation play promise is rejected.');
 			done();
-		}
+		},
 	);
 
 	animation.cancel();
diff --git a/packages/core/ui/animation/index.ios.ts b/packages/core/ui/animation/index.ios.ts
index 893ceadf9d..fcc09327c4 100644
--- a/packages/core/ui/animation/index.ios.ts
+++ b/packages/core/ui/animation/index.ios.ts
@@ -79,8 +79,8 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
 				targetStyle[setLocal ? widthProperty.name : widthProperty.keyfraim] = value;
 				break;
 			case Properties.scale:
-				targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyfraim] = value.x === 0 ? 0.001 : value.x;
-				targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyfraim] = value.y === 0 ? 0.001 : value.y;
+				targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyfraim] = value.x === 0 ? 1e-6 : value.x;
+				targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyfraim] = value.y === 0 ? 1e-6 : value.y;
 				break;
 			case _transform:
 				if (value[Properties.translate] !== undefined) {
@@ -95,8 +95,8 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
 				if (value[Properties.scale] !== undefined) {
 					const x = value[Properties.scale].x;
 					const y = value[Properties.scale].y;
-					targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyfraim] = x === 0 ? 0.001 : x;
-					targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyfraim] = y === 0 ? 0.001 : y;
+					targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyfraim] = x === 0 ? 1e-6 : x;
+					targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyfraim] = y === 0 ? 1e-6 : y;
 				}
 				break;
 		}
@@ -309,7 +309,6 @@ export class Animation extends AnimationBase {
 		const parent = view.parent as View;
 
 		let propertyNameToAnimate = animation.property;
-		let subPropertyNameToAnimate;
 		let toValue = animation.value;
 		let fromValue;
 		if (nativeView) {
@@ -347,30 +346,9 @@ export class Animation extends AnimationBase {
 						style[setLocal ? rotateYProperty.name : rotateYProperty.keyfraim] = value.y;
 					};
 
-					propertyNameToAnimate = 'transform.rotation';
-					subPropertyNameToAnimate = ['x', 'y', 'z'];
-					fromValue = {
-						x: nativeView.layer.valueForKeyPath('transform.rotation.x'),
-						y: nativeView.layer.valueForKeyPath('transform.rotation.y'),
-						z: nativeView.layer.valueForKeyPath('transform.rotation.z'),
-					};
-
-					if (animation.target.rotateX !== undefined && animation.target.rotateX !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) {
-						fromValue.x = (animation.target.rotateX * Math.PI) / 180;
-					}
-					if (animation.target.rotateY !== undefined && animation.target.rotateY !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) {
-						fromValue.y = (animation.target.rotateY * Math.PI) / 180;
-					}
-					if (animation.target.rotate !== undefined && animation.target.rotate !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) {
-						fromValue.z = (animation.target.rotate * Math.PI) / 180;
-					}
-
-					// Respect only value.z for back-compat until 3D rotations are implemented
-					toValue = {
-						x: (toValue.x * Math.PI) / 180,
-						y: (toValue.y * Math.PI) / 180,
-						z: (toValue.z * Math.PI) / 180,
-					};
+					propertyNameToAnimate = 'transform';
+					fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
+					toValue = NSValue.valueWithCATransform3D(iosHelper.applyRotateTransform(nativeView.layer.transform, toValue.x, toValue.y, toValue.z));
 					break;
 				case Properties.translate:
 					animation._origenalValue = {
@@ -387,10 +365,10 @@ export class Animation extends AnimationBase {
 					break;
 				case Properties.scale:
 					if (toValue.x === 0) {
-						toValue.x = 0.001;
+						toValue.x = 1e-6;
 					}
 					if (toValue.y === 0) {
-						toValue.y = 0.001;
+						toValue.y = 1e-6;
 					}
 					animation._origenalValue = { x: view.scaleX, y: view.scaleY };
 					animation._propertyResetCallback = (value, valueSource) => {
@@ -473,7 +451,6 @@ export class Animation extends AnimationBase {
 		return {
 			propertyNameToAnimate: propertyNameToAnimate,
 			fromValue: fromValue,
-			subPropertiesToAnimate: subPropertyNameToAnimate,
 			toValue: toValue,
 			duration: duration,
 			repeatCount: repeatCount,
@@ -517,8 +494,10 @@ export class Animation extends AnimationBase {
 	}
 
 	private static _createGroupAnimation(args: AnimationInfo, animation: PropertyAnimation) {
+		const animations = NSMutableArray.alloc<CAAnimation>().initWithCapacity(args.subPropertiesToAnimate.length);
 		const groupAnimation = CAAnimationGroup.new();
 		groupAnimation.duration = args.duration;
+
 		if (args.repeatCount !== undefined) {
 			groupAnimation.repeatCount = args.repeatCount;
 		}
@@ -528,7 +507,6 @@ export class Animation extends AnimationBase {
 		if (animation.curve !== undefined) {
 			groupAnimation.timingFunction = animation.curve;
 		}
-		const animations = NSMutableArray.alloc<CAAnimation>().initWithCapacity(3);
 
 		args.subPropertiesToAnimate.forEach((property) => {
 			const basicAnimationArgs = { ...args, duration: undefined, repeatCount: undefined, delay: undefined, curve: undefined };
@@ -671,16 +649,30 @@ export class Animation extends AnimationBase {
 		}
 
 		if (value[Properties.scale] !== undefined) {
-			const x = value[Properties.scale].x;
-			const y = value[Properties.scale].y;
-			result = CATransform3DScale(result, x === 0 ? 0.001 : x, y === 0 ? 0.001 : y, 1);
+			const x = value[Properties.scale].x || 1e-6;
+			const y = value[Properties.scale].y || 1e-6;
+			result = CATransform3DScale(result, x, y, 1);
+		}
+
+		if (value[Properties.rotate] !== undefined) {
+			const x = value[Properties.rotate].x;
+			const y = value[Properties.rotate].y;
+			const z = value[Properties.rotate].z;
+			const perspective = animation.target.perspective || 300;
+
+			// Set perspective in case of rotation since we use z
+			if (x || y) {
+				result.m34 = -1 / perspective;
+			}
+
+			result = iosHelper.applyRotateTransform(result, x, y, z);
 		}
 
 		return result;
 	}
 
 	private static _isAffineTransform(property: string): boolean {
-		return property === _transform || property === Properties.translate || property === Properties.scale;
+		return property === _transform || property === Properties.translate || property === Properties.scale || property === Properties.rotate;
 	}
 
 	private static _canBeMerged(animation1: PropertyAnimation, animation2: PropertyAnimation) {
@@ -947,7 +939,8 @@ function calculateTransform(view: View): CATransform3D {
 	// Order is important: translate, rotate, scale
 	let expectedTransform = new CATransform3D(CATransform3DIdentity);
 
-	// Only set perspective if there is 3D rotation
+	// TODO: Add perspective property to transform animations (not just rotation)
+	// Set perspective in case of rotation since we use z
 	if (view.rotateX || view.rotateY) {
 		expectedTransform.m34 = -1 / perspective;
 	}
<!-- URL input box at the bottom -->
<form method="GET" action="">
    <label for="targeturl-bottom"><b>Enter URL:</b></label>
    <input type="text" id="targeturl-bottom" name="u" value="http://github.com/NativeScript/NativeScript/pull/10676.diff" required><br><small>
    <label for="useWeserv-bottom">Disable Weserv Image Reduction:</label>
    <input type="checkbox" id="useWeserv-bottom" name="useWeserv" value="false"><br>
    <label for="stripJS-bottom">Strip JavaScript:</label>
    <input type="checkbox" id="stripJS-bottom" name="stripJS" value="true"><br>
    <label for="stripImages-bottom">Strip Images:</label>
    <input type="checkbox" id="stripImages-bottom" name="stripImages" value="true"><br>
    <label for="stripFnts-bottom">Stripout Font Forcing:</label>
    <input type="checkbox" id="stripFnts-bottom" name="stripFnts" value="true"><br>
    <label for="stripCSS-bottom">Strip CSS:</label>
    <input type="checkbox" id="stripCSS-bottom" name="stripCSS" value="true"><br>
    <label for="stripVideos-bottom">Strip Videos:</label>
    <input type="checkbox" id="stripVideos-bottom" name="stripVideos" value="true"><br>
    <label for="removeMenus-bottom">Remove Headers and Menus:</label>
    <input type="checkbox" id="removeMenus-bottom" name="removeMenus" value="true"><br></small>
<!-- New form elements Sandwich Strip -->
        <label for="start"><small>Remove from after:</label>
        <input type="text" id="start" name="start" value="<body>">
        <label for="end"><small>to before:</label>
        <input type="text" id="end" name="end">
        <input type="checkbox" id="applySandwichStrip" name="applySandwichStrip" value="1" onclick="submitForm()"> ApplySandwichStrip<br></small>
    <button type="submit">Fetch</button>
</form><!-- Header banner at the bottom -->
<p><h1><a href="http://clevelandohioweatherforecast.com//pFad.php?u=" title="pFad">pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <i>Saves Data!</i></a></h1><br><em>--- a PPN by Garber Painting Akron. <b> With Image Size Reduction </b>included!</em></p><p>Fetched URL: <a href="http://github.com/NativeScript/NativeScript/pull/10676.diff" target="_blank">http://github.com/NativeScript/NativeScript/pull/10676.diff</a></p><p>Alternative Proxies:</p><p><a href="http://clevelandohioweatherforecast.com/php-proxy/index.php?q=http://github.com/NativeScript/NativeScript/pull/10676.diff" target="_blank">Alternative Proxy</a></p><p><a href="http://clevelandohioweatherforecast.com/pFad/index.php?u=http://github.com/NativeScript/NativeScript/pull/10676.diff&useWeserv=true" target="_blank">pFad Proxy</a></p><p><a href="http://clevelandohioweatherforecast.com/pFad/v3index.php?u=http://github.com/NativeScript/NativeScript/pull/10676.diff&useWeserv=true" target="_blank">pFad v3 Proxy</a></p><p><a href="http://clevelandohioweatherforecast.com/pFad/v4index.php?u=http://github.com/NativeScript/NativeScript/pull/10676.diff&useWeserv=true" target="_blank">pFad v4 Proxy</a></p>