Last active
January 12, 2025 19:39
-
-
Save ninjaprox/b48583ece232ff5417bce5ee978e34da to your computer and use it in GitHub Desktop.
Understanding UIView's tranform property
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground - noun: a place where people can play | |
import UIKit | |
import XCPlayground | |
// MARK: - Helpers | |
extension UIView { | |
func addBorderWith(color: UIColor, width: CGFloat, alpha: CGFloat = 1) { | |
self.layer.borderColor = color.colorWithAlphaComponent(alpha).CGColor | |
self.layer.borderWidth = width | |
} | |
} | |
// Reference: http://www.informit.com/articles/article.aspx?p=1951182 | |
func scaleOf(transform: CGAffineTransform) -> CGPoint { | |
let xscale = sqrt(transform.a * transform.a + transform.c * transform.c) | |
let yscale = sqrt(transform.b * transform.b + transform.d * transform.d) | |
return CGPoint(x: xscale, y: yscale) | |
} | |
func rotationOf(transform: CGAffineTransform) -> CGFloat { | |
return CGFloat(atan2f(Float(transform.b), Float(transform.a))) | |
} | |
// MARK: - Setup | |
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) | |
containerView.backgroundColor = UIColor.grayColor() | |
XCPlaygroundPage.currentPage.liveView = containerView | |
let originalView = UIView(frame: CGRect(x: 150, y: 150, width: 150, height: 150)) | |
originalView.backgroundColor = UIColor.greenColor() | |
containerView.addSubview(originalView) | |
// MARK: - Experiment | |
let transformedView = UIView(frame: originalView.frame) | |
transformedView.backgroundColor = UIColor.blueColor().colorWithAlphaComponent(0.5) | |
transformedView.transform = CGAffineTransformMakeRotation(CGFloat(M_PI) / 12) | |
containerView.addSubview(transformedView) | |
/** | |
When `transform` is applied, view's `frame` is invalid. It is now actually of boundary view. | |
*/ | |
assert(!CGRectEqualToRect(originalView.frame, transformedView.frame)) | |
let boundaryView = UIView(frame: transformedView.frame) | |
boundaryView.addBorderWith(UIColor.redColor(), width: 1) | |
containerView.addSubview(boundaryView) | |
/** | |
However, either view's `bounds` or `center` keeps the same. | |
*/ | |
assert(CGRectEqualToRect(originalView.bounds, transformedView.bounds)) | |
assert(CGPointEqualToPoint(originalView.center, transformedView.center)) | |
/** | |
To persist transformed view's location, size, it is recommended to persist its `center`, `size` of `bounds` and `transform` instead `origin` of `frame`. | |
However, if the data is `origin` of `frame`, there is way to display it properly. | |
Solution: | |
- Init view with its size (`size` of `bounds`) regarless its origin (`origin` of `frame`). | |
- Set `transform`. | |
- Calculate `center` based on `frame` after `tranform` applied. | |
*/ | |
let clonedTransformedView = UIView(frame: CGRect(x: 0, y: 0, width: transformedView.bounds.width, height: transformedView.bounds.height)) | |
let persistedOrigin = transformedView.frame.origin | |
let persistedSize = transformedView.bounds.size | |
clonedTransformedView.addBorderWith(UIColor.blueColor(), width: 2) | |
clonedTransformedView.transform = transformedView.transform | |
clonedTransformedView.center = CGPoint(x: persistedOrigin.x + clonedTransformedView.frame.width / 2, y: persistedOrigin.y + clonedTransformedView.frame.height / 2) | |
containerView.addSubview(clonedTransformedView) | |
/** | |
Effect of `tranform` to child views. | |
*/ | |
let childView1 = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) | |
childView1.backgroundColor = UIColor.brownColor().colorWithAlphaComponent(0.5) | |
transformedView.addSubview(childView1) | |
transformedView.transform = CGAffineTransformScale(transformedView.transform, 2, 2) // Comment this line to see the actual position and size before scale transform | |
/** | |
There is difference between changing `frame` size and `bounds` size of child views. | |
Changing `frame` size keeps its `frame` origin, whereas `bounds` size does not. | |
Changing `bounds` size grows or shrinks the view relative to its center point. | |
*/ | |
let scale = scaleOf(transformedView.transform) | |
let childView2 = UIView(frame: childView1.frame) | |
childView2.frame.size = CGSize(width: childView2.bounds.width / scale.x, height: childView2.bounds.height / scale.y) | |
childView2.addBorderWith(UIColor.brownColor(), width: 2) | |
transformedView.addSubview(childView2) | |
let childView3 = UIView(frame: childView1.frame) | |
childView3.contentMode = .TopLeft | |
childView3.bounds.size = CGSize(width: childView3.bounds.width / scale.x, height: childView3.bounds.height / scale.y) | |
childView3.addBorderWith(UIColor.brownColor(), width: 2) | |
transformedView.addSubview(childView3) | |
assert(!CGRectEqualToRect(childView2.frame, childView3.frame)) | |
assert(CGRectEqualToRect(childView2.bounds, childView3.bounds)) | |
/** | |
The above is correct even when `transform` is identity transform. | |
*/ | |
let childView4 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) | |
let childView5 = UIView(frame: childView4.frame); | |
childView4.backgroundColor = UIColor.redColor() | |
childView5.backgroundColor = UIColor.redColor().colorWithAlphaComponent(0.5) | |
childView4.bounds.size = CGSize(width: 50, height: 50) | |
originalView.addSubview(childView4) | |
originalView.addSubview(childView5) | |
assert(CGPointEqualToPoint(childView4.center, childView5.center)) | |
/** | |
Re-create transformed view (with scale and rotation) by using real size after scale and rotation. | |
Therefore, `transform` will be rotation only transformation instead of both scale and rotation | |
This results in another way to persist view's information. Persist after scale size of the view, center and rotation only (not scale-rotation transformation as ealier) | |
*/ | |
let clonedTransformedView2 = UIView(frame: CGRect(x: 0, y: 0, width: transformedView.bounds.width * scale.x, height: transformedView.bounds.height * scale.y)) | |
let rotation = rotationOf(transformedView.transform) | |
clonedTransformedView2.addBorderWith(UIColor.redColor(), width: 2) | |
clonedTransformedView2.transform = CGAffineTransformMakeRotation(rotation) | |
clonedTransformedView2.center = transformedView.center | |
containerView.addSubview(clonedTransformedView2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment