sak
sak

Reputation: 3307

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

I'm attempting to add perspective to a view in UIKit.

Currently my implementation looks like this:

let view: UIView

...

vat perspective = CATransform3DIdentity

view.layer.sublayerTransform = perspective

view.layer.transform = CATransform3DConcat(
    perspective,
    someTransform
)


Now according to the documentation, it seems like it should be enough to set sublayerTransform, but in practice it seems I also have to concatenate the perspective transform in the layer.transform property.

I.e. I should be able to set my layer transform like so:

view.layer.transform = someTransform

Or like so:

view.layer.transform = CATransform3DConcat(
    CATransform3DIdentity,
    someTransform
)

What is actually the intended way to introduce perspective?

Upvotes: 0

Views: 373

Answers (1)

DonMag
DonMag

Reputation: 77690

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:

enter image description here

enter image description here

enter image description here

enter image description here

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

Upvotes: 2

Related Questions