Search code examples
swiftcalayer

CALayers transforming incorrectly when rotated


Given the arrangement of CALayers as shown on the left (a background, a frame and a text layer), I wish to rotate two of them by 45° (layer.transform = CATransform3DMakeRotation(.pi / 4, 0, 0, 1)), resulting in the diamond on the right, but what I see is the diagonal line in the middle.

Unrotated, rotated, expected

The element comprises a parent CALayer, a background layer, a border frame layer and the text layer. I want to be able to scale and rotate the element as a whole, hence the invisible parent layer and the two pre-rotated children:

class ScoreLayer: CALayer {
    var score = 0 {
        didSet {
            updateValue()
        }
    }

    let bgLayer: CALayer = {
        let layer = CALayer()
        layer.contentsScale = UIScreen.main.scale
        layer.backgroundColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
        layer.shadowOffset = .zero
        layer.shadowRadius = 3
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.4
        return layer
    }()

    let textLayer: CATextLayer = {
        let layer = CATextLayer()
        layer.contentsScale = UIScreen.main.scale
        layer.alignmentMode = .center
        layer.foregroundColor = UIColor.white.cgColor
        layer.fontSize = 20
        layer.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        return layer
    }()

    let frameLayer: CALayer = {
        let layer = CALayer()
        layer.contentsScale = UIScreen.main.scale
        layer.borderColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
        layer.borderWidth = 3
        layer.shadowOffset = .zero
        layer.shadowRadius = 3
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.7
        return layer
    }()

    override init() {
        super.init()
        contentsScale = UIScreen.main.scale

        addSublayer(bgLayer)
        addSublayer(textLayer)
        addSublayer(frameLayer)

        updateValue()
    }

    override func layoutSublayers() {
        bgLayer.frame = bounds
        frameLayer.frame = bounds
        textLayer.frame = bounds

        bgLayer.transform = CATransform3DMakeRotation(.pi / 4, 0, 0, 1)
        frameLayer.transform = CATransform3DMakeRotation(.pi / 4, 0, 0, 1)
    }

    private func updateValue() {
        textLayer.string = "\(score)"
    }
}

Animating the rotation (by setting a new transform via CADisplayLink) it looks as if the rotating layers are also rotating about their own x-axis. I am not setting any transform than the two in the code listing.

What is causing this?


Solution

  • Don't update the frame after a transform has been applied. layoutSublayers is called multiple times and the compounding effect is what results in the unwanted distortion. Set the frame once elsewhere before applying a transform, or, if frame updates are required (perhaps in response to an orientation change), be sure to remove the transform update the frame and then reapply the transform.

    Edited to add:

    The docs actually explicitly say "Do not set the frame if the transform property applies a rotation transform that is not a multiple of 90 degrees."