Reputation: 3307
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
Reputation: 77690
We can transform:
.layer
, which transforms everything together.layer.sublayerTransform
, which transforms the sublayers together,someSubLayer
, which transforms an individual layerHere'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:
Play around with that example code to get a better idea of what's going on.
Upvotes: 2