Auto Layout Constraint on Calayer iOS

CALayer Auto Layout

You need to provide the correct arcCenter as below and no need to set a frame for the shapLayer.

override func layoutSubviews() {
super.layoutSubviews()

let center = CGPoint(x: bounds.midX, y: bounds.midY)
let progressCircle = UIBezierPath(arcCenter: center, radius: 50, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)

displayLayer.path = progressCircle.cgPath
trackLayer.path = progressCircle.cgPath
}

Result

Sample Image

Note: I used the first ProgressView class without KVO.

CALayer not resizing with Autolayout

The default layer of a UIView does resize with its view, but sublayers don't (as you found out). One way to make this work is to create a custom view class, move the code you have in your question to it, and override layoutSublayersOfLayer where you can set the gradient layer to be the same size as the view. Because this code is now in a custom class, I also created a property percentageCompleted (instead of a local variable), and added a willSet clause so the bar's appearance is updated any time you change the percentageCompleted property.

class RDProgressView: UIView {

private let gradient : CAGradientLayer = CAGradientLayer()

var percentageCompleted: Double = 0.0 {
willSet{
gradient.locations = [newValue, newValue]
}
}

override func awakeFromNib() {
self.layer.cornerRadius = 4
self.layer.masksToBounds = true

// create color array
let arrayColors: [AnyObject] = [
UIColor (red: 255/255, green: 138/255, blue: 1/255, alpha: 1).CGColor,
UIColor (red: 110/255, green: 110/255, blue: 118/255, alpha: 1).CGColor]

// set gradient's color array
gradient.colors = arrayColors

//Set progress(progressBar)
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.locations = [percentageCompleted, percentageCompleted]
gradient.endPoint = CGPoint(x: 1, y: 0.5)

self.layer.insertSublayer(gradient, atIndex: 0)
}

override func layoutSublayersOfLayer(layer: CALayer!) {
super.layoutSublayersOfLayer(layer)
gradient.frame = self.bounds
}
}

In IB, you would change the class of your view to RDProgressView (in my example), and in cellForRowAtIndexPath, you would only need to get a reference to the view, and set its percentageCompleted property.

progressBar =  cell.contentView.viewWithTag(3) as RDProgressView! 
progressBar.percentageCompleted = 0.2

Animate UIView's Layer with constrains (Auto Layout Animations)

I know it's a tricky question. Actually, you don't need to care about layoutSubViews at all. The key here is when you set the shapeLayer. If it's setup well, i.e. after the constraints are all working, you don't need to care that during the animation.

//in CustomView, comment out the layoutSubViews() and add updateLayer()

 func updateLayer(){
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: layer.bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}

// override func layoutSubviews() {
// super.layoutSubviews()
//
// // While animating `myView` height, this method gets called
// // So new bounds for layer will be calculated and set immediately
// // This result in not proper animation
//
// // check by making below condition always true
//
// if !self.isAnimating{ //if true{
// guard let layer = self.layer as? CAShapeLayer else { return }
//
// layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
// layer.shadowPath = layer.path
// }
// }

in ViewController: add viewDidAppear() and remove other is animation block

  override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myView.updateLayer()
}

@objc func animateView(_ sender: UIButton){

CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))

UIView.animate(withDuration: 5.0, animations: {

self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()

}) { (success) in
self.myView.isAnimating = false
}
....

Then you are good to go. Have a wonderful day.

How setting up shadow of the CALayer breaks AutoLayout constraints?

But it breaks imageView sizes and constraints.

Yes, because you forgot to start by saying

override func layoutSubviews() {
super.layoutSubviews() // important

Since autolayout happens in layoutSubviews, if you don’t call super you prevent it from operating.

How do I adjust the anchor point of a CALayer, when Auto Layout is being used?

[EDIT: Warning: The entire ensuing discussion will be possibly outmoded or at least heavily mitigated by iOS 8, which may no longer make the mistake of triggering layout at the time that a view transform is applied.]

Autolayout vs. View Transforms

Autolayout does not play at all well with view transforms. The reason, as far as I can discern, is that you're not supposed to mess with the frame of a view that has a transform (other than the default identity transform) - but that is exactly what autolayout does. The way autolayout works is that in layoutSubviews the runtime comes dashing through all the constraints and setting the frames of all the views accordingly.

In other words, the constraints are not magic; they are just a to-do list. layoutSubviews is where the to-do list gets done. And it does it by setting frames.

I can't help regarding this as a bug. If I apply this transform to a view:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

I expect to see the view appear with its center in the same place as before and at half the size. But depending on its constraints, that may not be what I see at all.

[Actually, there's a second surprise here: applying a transform to a view triggers layout immediately. This seems to me be another bug. Or perhaps it's the heart of the first bug. What I would expect is to be able to get away with a transform at least until layout time, e.g. the device is rotated - just as I can get away with a frame animation until layout time. But in fact layout time is immediate, which seems just wrong.]

Solution 1: No Constraints

One current solution is, if I'm going to apply a semipermanent transform to a view (and not merely waggle it temporarily somehow), to remove all constraints affecting it. Unfortunately this typically causes the view to vanish from the screen, since autolayout still takes place, and now there are no constraints to tell us where to put the view. So in addition to removing the constraints, I set the view's translatesAutoresizingMaskIntoConstraints to YES. The view now works in the old way, effectively unaffected by autolayout. (It is affected by autolayout, obviously, but the implicit autoresizing mask constraints cause its behavior to be just like it was before autolayout.)

Solution 2: Use Only Appropriate Constraints

If that seems a bit drastic, another solution is to set the constraints to work correctly with an intended transform. If a view is sized purely by its internal fixed width and height, and positioned purely by its center, for example, my scale transform will work as I expect. In this code, I remove the existing constraints on a subview (otherView) and replace them with those four constraints, giving it a fixed width and height and pinning it purely by its center. After that, my scale transform works:

NSMutableArray* cons = [NSMutableArray array];
for (NSLayoutConstraint* con in self.view.constraints)
if (con.firstItem == self.otherView || con.secondItem == self.otherView)
[cons addObject:con];

[self.view removeConstraints:cons];
[self.otherView removeConstraints:self.otherView.constraints];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterX relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.otherView.center.x]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:self.otherView.center.y]];
[self.otherView addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.width]];
[self.otherView addConstraint:
[NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.height]];

The upshot is that if you have no constraints that affect a view's frame, autolayout won't touch the view's frame - which is just what you're after when a transform is involved.

Solution 3: Use a Subview

The problem with both the above solutions is that we lose the benefits of constraints to position our view. So here's a solution that solves that. Start with an invisible view whose job is solely to act as a host, and use constraints to position it. Inside that, put the real view as a subview. Use constraints to position the subview within the host view, but limit those constraints to constraints that won't fight back when we apply a transform.

Here's an illustration:

Sample Image

The white view is host view; you are supposed to pretend that it is transparent and hence invisible. The red view is its subview, positioned by pinning its center to the host view's center. Now we can scale and rotate the red view around its center without any problem, and indeed the illustration shows that we have done so:

self.otherView.transform = CGAffineTransformScale(self.otherView.transform, 0.5, 0.5);
self.otherView.transform = CGAffineTransformRotate(self.otherView.transform, M_PI/8.0);

And meanwhile the constraints on the host view keep it in the right place as we rotate the device.

Solution 4: Use Layer Transforms Instead

Instead of view transforms, use layer transforms, which do not trigger layout and thus do not cause immediate conflict with constraints.

For example, this simple "throb" view animation may well break under autolayout:

[UIView animateWithDuration:0.3 delay:0
options:UIViewAnimationOptionAutoreverse
animations:^{
v.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
v.transform = CGAffineTransformIdentity;
}];

Even though in the end there was no change in the view's size, merely setting its transform causes layout to happen, and constraints can make the view jump around. (Does this feel like a bug or what?) But if we do the same thing with Core Animation (using a CABasicAnimation and applying the animation to the view's layer), layout doesn't happen, and it works fine:

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.autoreverses = YES;
ba.duration = 0.3;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1)];
[v.layer addAnimation:ba forKey:nil];

Swift: Mask Alignment + Auto-Layout Constraints

I found a way around it. Not sure if this is the best way but here we go...

http://imgur.com/pUIZbNA

Just make sure you change the name of the UIView class in the storyboard inspector too. Apparently, the trick is to set the mask frame for each layoutSubviews call.

class MaskView : UIView {

override func layoutSubviews() {
super.layoutSubviews()
if let mask = self.layer.mask {
mask.frame = self.bounds
}
}

}

class ViewController: UIViewController {

@IBOutlet weak var viewForLayer: MaskView!

override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "TopBump")!.CGImage!
let maskLayer = CALayer()
maskLayer.contents = image
maskLayer.frame = viewForLayer.bounds
viewForLayer.layer.mask = maskLayer
viewForLayer.backgroundColor = UIColor.orangeColor()

// Do any additional setup after loading the view, typically from a nib.
viewForLayer.layer.borderColor = UIColor.redColor().CGColor
viewForLayer.layer.borderWidth = 10

}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

}

Combining NSLayoutConstraints with CALayer backed views does not animate correctly

This is a known issue (if you google it you'll see a couple of good hits, though I think this answer, while admittedly focusing of different symptoms, is the most articulate answer I've seen thus far describing the problem UIView scaling during animation ... it's an old answer, but still applicable, I believe).

The easiest solution, IMHO, would be to put the layer in a view that doesn't resize upon animation (e.g. a new view, of fixed size, whose constraints keep it centered on your content view, but doesn't change size as your content view does). That way, you can enjoy the constraints-based animations, but not lose your custom layer's aspect ratio/size.



Related Topics



Leave a reply



Submit