How to Do Transforms on a Calayer

How to do transforms on a CALayer?

Basics

There are a number of different transforms you can do on a layer, but the basic ones are

  • translate (move)
  • scale
  • rotate

Sample Image

To do transforms on a CALayer, you set the layer's transform property to a CATransform3D type. For example, to translate a layer, you would do something like this:

myLayer.transform = CATransform3DMakeTranslation(20, 30, 0)

The word Make is used in the name for creating the initial transform: CATransform3DMakeTranslation. Subsequent transforms that are applied omit the Make. See, for example, this rotation followed by a translation:

let rotation = CATransform3DMakeRotation(CGFloat.pi * 30.0 / 180.0, 20, 20, 0)
myLayer.transform = CATransform3DTranslate(rotation, 20, 30, 0)

Now that we have the basis of how to make a transform, let's look at some examples of how to do each one. First, though, I'll show how I set up the project in case you want to play around with it, too.

Setup

For the following examples I set up a Single View Application and added a UIView with a light blue background to the storyboard. I hooked up the view to the view controller with the following code:

import UIKit

class ViewController: UIViewController {

var myLayer = CATextLayer()
@IBOutlet weak var myView: UIView!

override func viewDidLoad() {
super.viewDidLoad()

// setup the sublayer
addSubLayer()

// do the transform
transformExample()
}

func addSubLayer() {
myLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 40)
myLayer.backgroundColor = UIColor.blue.cgColor
myLayer.string = "Hello"
myView.layer.addSublayer(myLayer)
}

//******** Replace this function with the examples below ********

func transformExample() {

// add transform code here ...


}

}

There are many different kinds of CALayer, but I chose to use CATextLayer so that the transforms will be more clear visually.

Translate

The translation transform moves the layer. The basic syntax is

CATransform3DMakeTranslation(_ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat)

where tx is the change in the x coordinates, ty is the change in y, and tz is the change in z.

Example

Sample Image

In iOS the origin of the coordinate system is in the top left, so if we wanted to move the layer 90 points to the right and 50 points down, we would do the following:

myLayer.transform = CATransform3DMakeTranslation(90, 50, 0)

Notes

  • Remember that you can paste this into the transformExample() method in the project code above.
  • Since we are just going to deal with two dimensions here, tz is set to 0.
  • The red line in the image above goes from the center of the original location to the center of the new location. That's because transforms are done in relation to the anchor point and the anchor point by default is in the center of the layer.

Scale

The scale transform stretches or squishes the layer. The basic syntax is

CATransform3DMakeScale(_ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat)

where sx, sy, and sz are the numbers by which to scale (multiply) the x, y, and z coordinates respectively.

Example

Sample Image

If we wanted to half the width and triple the height, we would do the following

myLayer.transform = CATransform3DMakeScale(0.5, 3.0, 1.0)

Notes

  • Since we are only working in two dimensions, we just multiply the z coordinates by 1.0 to leave them unaffected.
  • The red dot in the image above represents the anchor point. Notice how the scaling is done in relation to the anchor point. That is, everything is either stretched toward or away from the anchor point.

Rotate

The rotation transform rotates the layer around the anchor point (the center of the layer by default). The basic syntax is

CATransform3DMakeRotation(_ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat)

where angle is the angle in radians that the layer should be rotated and x, y, and z are the axes about which to rotate. Setting an axis to 0 cancels a rotation around that particular axis.

Example

Sample Image

If we wanted to rotate a layer clockwise 30 degrees, we would do the following:

let degrees = 30.0
let radians = CGFloat(degrees * Double.pi / 180)
myLayer.transform = CATransform3DMakeRotation(radians, 0.0, 0.0, 1.0)

Notes

  • Since we are working in two dimentions, we only want the xy plane to be rotated around the z axis. Thus we set x and y to 0.0 and set z to 1.0.
  • This rotated the layer in a clockwise direction. We could have rotated counterclockwise by setting z to -1.0.
  • The red dot shows where the anchor point is. The rotation is done around the anchor point.

Multiple transforms

In order to combine multiple transforms we could use concatination like this

CATransform3DConcat(_ a: CATransform3D, _ b: CATransform3D)

However, we will just do one after another. The first transform will use the Make in its name. The following transforms will not use Make, but they will take the previous transform as a parameter.

Example

Sample Image

This time we combine all three of the previous transforms.

let degrees = 30.0
let radians = CGFloat(degrees * Double.pi / 180)

// translate
var transform = CATransform3DMakeTranslation(90, 50, 0)

// rotate
transform = CATransform3DRotate(transform, radians, 0.0, 0.0, 1.0)

// scale
transform = CATransform3DScale(transform, 0.5, 3.0, 1.0)

// apply the transforms
myLayer.transform = transform

Notes

  • The order that the transforms are done in matters.
  • Everything was done in relation to the anchor point (red dot).

A Note about Anchor Point and Position

We did all our transforms above without changing the anchor point. Sometimes it is necessary to change it, though, like if you want to rotate around some other point besides the center. However, this can be a little tricky.

The anchor point and position are both at the same place. The anchor point is expressed as a unit of the layer's coordinate system (default is 0.5, 0.5) and the position is expressed in the superlayer's coordinate system. They can be set like this

myLayer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
myLayer.position = CGPoint(x: 50, y: 50)

If you only set the anchor point without changing the position, then the frame changes so that the position will be in the right spot. Or more precisely, the frame is recalculated based on the new anchor point and old position. This usually gives unexpected results. The following two articles have an excellent discussion of this.

  • About the anchorPoint
  • Translate rotate translate?

See also

  • Border, rounded corners, and shadow on a CALayer
  • Using a border with a Bezier path for a layer

Applying multiple transforms to a UIView / CALayer

I realize this doesn't answer the question entirely (or come close), but if you're only working with CGAffineTransforms you can use CGAffineTransformConcat() to combine multiple transforms.

This will work just fine when some transforms are animated and others are not, as long as you concat the transformations properly. I don't know how this works when you're also doing layer transforms.

How exactly do I have to use the perspective transform on CALayer?

We can transform:

  • .layer, which transforms everything together
  • .layer.sublayerTransform, which transforms the sublayers together,
  • someSubLayer, which transforms an individual layer

Here's a quick example...

We'll use this UIImageView subclass, adding a CAShapeLayer and a CATextLayer, and then transform them in different ways:

class TransformImageView: UIImageView {
let textLayer = CATextLayer()
let shapeLayer = CAShapeLayer()

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {

shapeLayer.strokeColor = UIColor.yellow.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 8
layer.addSublayer(shapeLayer)

textLayer.string = "TEST"
textLayer.foregroundColor = UIColor.red.cgColor
let font: UIFont = .systemFont(ofSize: 40.0, weight: .bold)
textLayer.font = font
textLayer.alignmentMode = .center
textLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(textLayer)

}
override func layoutSubviews() {
super.layoutSubviews()

let pth = UIBezierPath(ovalIn: bounds.insetBy(dx: bounds.width * 0.1, dy: bounds.height * 0.1))
shapeLayer.path = pth.cgPath
shapeLayer.frame = bounds

guard let font = textLayer.font else { return }
textLayer.frame = CGRect(x: bounds.minX, y: bounds.midY - (font.pointSize * 0.5), width: bounds.maxX, height: font.pointSize)
}
public func doTransform(_ idx: Int) {

var tr: CATransform3D = CATransform3DIdentity

// make sure everything is at identity
self.layer.transform = tr
self.layer.sublayerTransform = tr
self.textLayer.transform = tr
self.shapeLayer.transform = tr

let v: CGFloat = 60.0

switch idx {
case 1:
// transform entire view, including sublayers
tr.m34 = 1.0 / 200.0
tr = CATransform3DRotate(tr, -v * .pi / 180.0, 1.0, 0.0, 0.0)
self.layer.transform = tr
case 2:
// transform only sublayers
tr = CATransform3DIdentity
tr.m34 = 1.0 / 200.0
tr = CATransform3DRotate(tr, -v * .pi / 180.0, 1.0, 0.0, 0.0)
self.layer.sublayerTransform = tr
case 3:
// transform layer with one transform
// only sublayers with another transform
tr.m34 = 1.0 / 200.0
tr = CATransform3DRotate(tr, v * .pi / 180.0, 1.0, 0.0, 0.0)
self.layer.transform = tr
tr = CATransform3DIdentity
tr.m34 = 1.0 / 200.0
tr = CATransform3DRotate(tr, v * .pi / 180.0, 0.0, 1.0, 0.0)
self.layer.sublayerTransform = tr
case 4:
// transform each sublayer individually
tr.m34 = 1.0 / 200.0
tr = CATransform3DRotate(tr, v * .pi / 180.0, 0.0, 0.0, 1.0)
self.textLayer.transform = tr
tr = CATransform3DIdentity
tr.m34 = 1.0 / 200.0
tr = CATransform3DRotate(tr, v * .pi / 180.0, 0.0, 1.0, 0.0)
self.shapeLayer.transform = tr
default:
// no transforms
break
}

}
}

and use this example controller class to show 4 different options:

class ExampleViewController: UIViewController {

let strs: [String] = [
".layer.transform",
".layer.sublayerTransform",
"Different transform for .layer and .sublayerTransform",
"no .layer transform, different transforms for each sublayer",
]
let infoLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 12.0, weight: .light)
v.textAlignment = .center
v.numberOfLines = 0
return v
}()

var imgView: TransformImageView!

var idx: Int = 0

override func viewDidLoad() {
super.viewDidLoad()

guard let img = UIImage(named: "test") else {
fatalError("Could not load image!")
}

let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false

let seg = UISegmentedControl(items: ["1", "2", "3", "4"])
seg.addTarget(self, action: #selector(segChanged(_:)), for: .valueChanged)

stackView.addArrangedSubview(seg)

let v = UILabel()
v.font = .systemFont(ofSize: 12.0, weight: .light)
v.textAlignment = .center
v.text = "Original - no Transforms"
stackView.addArrangedSubview(v)

let defImgView = TransformImageView(frame: .zero)
defImgView.image = img
defImgView.heightAnchor.constraint(equalTo: defImgView.widthAnchor, multiplier: 2.0 / 3.0).isActive = true
stackView.addArrangedSubview(defImgView)

stackView.setCustomSpacing(40.0, after: defImgView)

stackView.addArrangedSubview(infoLabel)

imgView = TransformImageView(frame: .zero)
imgView.image = img
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 2.0 / 3.0).isActive = true
stackView.addArrangedSubview(imgView)

view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
stackView.widthAnchor.constraint(equalToConstant: 240.0),
stackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])

seg.selectedSegmentIndex = 0
segChanged(seg)
}

@objc func segChanged(_ sender: UISegmentedControl) {
let idx = sender.selectedSegmentIndex
imgView.doTransform(idx + 1)
infoLabel.text = strs[idx]
}

}

The output looks like this:

Sample Image

Sample Image

Sample Image

Sample Image

Play around with that example code to get a better idea of what's going on.

Change a CALayer's rotation transform to a value without changing the layer's scale transform

You can reapply your transform again having xx value unchanged

    func rotateLayer(by angle: CGFloat) {
layer.transform = CATransform3DConcat(CATransform3DMakeScale(xx, xx, 1),
CATransform3DMakeRotation(angle, 0, 0, 1))
}

Edit

Based on this answer you can get and reuse uniform scale factor like this

    func rotateLayer(by angle: CGFloat) {
let scale = sqrt(pow(layer.transform.m11, 2) + pow(layer.transform.m12, 2))
print(scale)
layer.transform = CATransform3DConcat(CATransform3DMakeScale(scale, scale, 1), CATransform3DMakeRotation(angle, 0, 0, 1))
}

How to create mirrored transformation of CALayer

Hope this will help.

CGAffineTransformMakeScale(1.0, -1.0);

image.transform = CGAffineTransformMakeScale(1.0, -1.0); // For Top to bottom

image.transform = CGAffineTransformMakeScale(-1.0, 1.0); // For left to right

How do I use CALayer renderInContext and keep the perspective transform?

You might want to have a look at the method drawViewHierarchyInRect:afterScreenUpdates: which is offered since iOS 7.0. Applying the method to the view containing the CALayers will keep their 3D transformations.

Sample code:

BOOL opaqueBackground = YES;
UIGraphicsBeginImageContextWithOptions(theViewContainingYourCALayers.bounds.size, !opaqueBackground, 0);
[theViewContainingYourCALayers drawViewHierarchyInRect:theViewContainingYourCALayers.bounds afterScreenUpdates:YES];
[UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

As matt pointed out, relying solely on renderInContext: is not possible since it disregards any transformation other than affine ones.

Late answer but I hope it still helps.

CALayer transforming

Is there a reason you're not directly setting the layer's transform instead of using an animation?

This should do what you want:

- (void)respondToGesture:(UIRotationGestureRecognizer *)rec {
if (rec.state == UIGestureRecognizerStateChanged) {
CGAffineTransform currentTransform = squareLayer.affineTransform;
squareLayer.affineTransform = CGAffineTransformRotate(currentTransform, gesture.rotation);
gesture.rotation = 0;
}
}

You also need to adjust squareLayer.anchorPoint if you want the rotation to happen around the centre of the user's rotation instead of the centre of the layer.



Related Topics



Leave a reply



Submit