Auto Layout constraint on CALayer IOS
Asked Answered
R

8

83

Hi I am developing iPhone application in which I tried to set one side border for edittext. I did this in following way:

 int borderWidth = 1;
CALayer *leftBorder = [CALayer layer];

leftBorder.borderColor = [UIColor whiteColor].CGColor;
leftBorder.borderWidth = borderWidth;

leftBorder.frame = CGRectMake(0, textField.frame.size.height - borderWidth, textField
                              .frame.size.width, borderWidth);
[textField.layer addSublayer:leftBorder];

I put some constraints on my edittext in IB so that when I rotate my device it will adjust width of text field according to that. My problem is that adjusts the width of edittext not adjusting the width of CALayer which I set for my edit text. So I think I have to put some constraints for my CALayer item as well. But I dont know how to do that. ANy one knows about this? Need Help. Thank you.

Ryder answered 19/6, 2014 at 9:30 Comment(1)
not possible. Constraints only work with UIViewKrp
J
131

the whole autoresizing business is view-specific. layers don't autoresize.

what you have to do -- in code -- is to resize the layer yourself

e.g.

in a viewController you would do

- (void) viewDidLayoutSubviews {
  [super viewDidLayoutSubviews]; //if you want superclass's behaviour... 
  // resize your layers based on the view's new frame
  self.editViewBorderLayer.frame = self.editView.bounds;
}

or in a custom UIView you could use

- (void)layoutSubviews {
  [super layoutSubviews]; //if you want superclass's behaviour...  (and lay outing of children)
  // resize your layers based on the view's new frame
  layer.frame = self.bounds;
}
Jeanne answered 19/6, 2014 at 9:35 Comment(7)
If you are using layoutSubviews you should call it on superIvory
@rmp251 neither docs nor header files say so and it seems weird. i mean I would only retain the default behaviour -- auto layout (on ios51+).. might be what one wants but it isn't always right ... so no AFAIKJeanne
This solution doesn't work if the view that owns the layer is sized via autolayout constraints. In this case the bounds and frame properties don't reflect the actual bounds/frame as determined by the constraints. Any suggestions?Bagehot
@AlfieHanssen you mean if it is the view's backing layer? no then of course not as the view and the layer are basically one.Jeanne
@AlfieHanssen they do reflect the actual frame and bounds. LayoutSubviews does that. You can also fire after ViewDidAppear. Also calling layoutIfNeeded, InvalidateIntrinsic, etc...Lightness
@AlfieHanssen updating the frame in viewDidAppear worked for me as suggested Nick Turner.Ideography
This also doesn't work when you are animating autolayout constraints with layoutIfNeeded on a view which has some sublayers that should reflect the view's frame..Skinny
S
14

In Swift 5, you can try the following solution:

Use KVO, for the path "YourView.bounds" as given below.

self.addObserver(self, forKeyPath: "YourView.bounds", options: .new, context: nil)

Then handle it as given below.

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "YourView.bounds" {
            YourLayer.frame = YourView.bounds
            return
        }
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    }

More info about this here

Sudiesudnor answered 26/1, 2017 at 7:6 Comment(4)
This worked superbly and I think is the best approach to take with "autolayout" as opposed to doing it in *layoutsubviewsDirectory
The URL in the answer is broken.Ascetic
URL is still brokenTaunton
Not sure what is the issue. The ref url still works for me. marcosantadev.com/calayer-auto-layout-swiftSudiesudnor
W
9

According to this answer, layoutSubviews does not get called in all needed cases. I have found this delegate method more effective:

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if(self != nil) {
        [self.layer addSublayer:self.mySublayer];
    }
    return self;
}

- (void)layoutSublayersOfLayer:(CALayer*)layer
{
    self.mySublayer.frame = self.bounds;
}
Winter answered 23/10, 2017 at 13:32 Comment(0)
T
7

My solution when I create with dashed border

class DashedBorderView: UIView {

   let dashedBorder = CAShapeLayer()

   override init(frame: CGRect) {
       super.init(frame: frame)
       commonInit()
   }

   required init?(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)
       commonInit()
   }

   private func commonInit() {
       //custom initialization
       dashedBorder.strokeColor = UIColor.red.cgColor
       dashedBorder.lineDashPattern = [2, 2]
       dashedBorder.frame = self.bounds
       dashedBorder.fillColor = nil
       dashedBorder.cornerRadius = 2
       dashedBorder.path = UIBezierPath(rect: self.bounds).cgPath
       self.layer.addSublayer(dashedBorder)
   }

   override func layoutSublayers(of layer: CALayer) {
       super.layoutSublayers(of: layer)
       dashedBorder.path = UIBezierPath(rect: self.bounds).cgPath
       dashedBorder.frame = self.bounds
   }
}
Tellus answered 26/1, 2018 at 7:7 Comment(0)
B
1

Swift 3.x KVO Solution (Updated @arango_86's answer)

Add Observer

self.addObserver(
    self, 
    forKeyPath: "<YOUR_VIEW>.bounds", 
    options: .new, 
    context: nil
)

Observe Values

override open func observeValue(
    forKeyPath keyPath: String?, 
    of object: Any?, 
    change: [NSKeyValueChangeKey : Any]?, 
    context: UnsafeMutableRawPointer?
) {
    if (keyPath == "<YOUR_VIEW.bounds") {
      updateDashedShapeLayerFrame()
      return
    }

    super.observeValue(
        forKeyPath: keyPath, 
        of: object, 
        change: change, 
        context: context
    )
}
Bog answered 1/10, 2018 at 13:31 Comment(0)
P
1

When I have to apply KVO I prefer to do it with RxSwift (Only if I am using it for more stuff, do not add this library just for this.)

You can apply KVO with this library too, or in the viewDidLayoutSubviews but I've had better results with this.

  view.rx.observe(CGRect.self, #keyPath(UIView.bounds))
        .subscribe(onNext: { [weak self] in
            guard let bounds = $0 else { return }
            self?.YourLayer.frame = bounds
        })
        .disposed(by: disposeBag)
Palazzo answered 3/11, 2019 at 16:35 Comment(0)
N
1

Riffing off arango_86's answer – if you are applying the KVO fix within your own UIView subclass, the more "Swifty" way to do this is to override bounds and use didSet on it, like so:

override var bounds: CGRect {
    didSet {
        layer.frame = bounds
    }
}
Nitty answered 3/8, 2020 at 7:58 Comment(0)
A
-1

I had a similar problem - needing to set the frame of a 'CALayer' when using auto layout with views (in code, not IB).

In my case, I had a slightly convoluted hierarchy, having a view controller within a view controller. I ended up at this SO question and looked into the approach of using viewDidLayoutSubviews. That didn't work. Just in case your situation is similar to my own, here's what I found...

Overview

I wanted to set the frame for the CAGradientLayer of a UIView that I was positioning as a subview within a UIViewController using auto layout constraints.

Call the subview gradientView and the view controller child_viewController.

child_viewController was a view controller I'd set up as a kind of re-usable component. So, the view of child_viewController was being composed into a parent view controller - call that parent_viewController.

When viewDidLayoutSubviews of child_viewController was called, the frame of gradientView was not yet set.

(At this point, I'd recommend sprinkling some NSLog statements around to get a feel for the sequence of creation of views in your hierarchy, etc.)

So I moved on to try using viewDidAppear. However, due to the nested nature of child_viewController I found viewDidAppear was not being called.

(See this SO question: viewWillAppear, viewDidAppear not being called, not firing).

My current solution

I've added viewDidAppear in parent_viewController and from there I'm calling viewDidAppear in child_viewController.

For the initial load I need viewDidAppear as it's not until this is called in child_viewController that all of the subviews have their frames set. I can then set the frame for the CAGradientLayer...

I've said that this is my current solution because I'm not particularly happy with it.

After initially setting the frame of the CAGradientLayer - that frame can become invalid if the layout changes - e.g. rotating the device.

To handle this I'm using viewDidLayoutSubviews in child_viewController - to keep the frame of the CAGradientLayer within gradientView, correct.

It works but doesn't feel good. (Is there a better way?)

Actinolite answered 2/5, 2015 at 23:56 Comment(1)
Generally you should not call view appearance/lifecycle methods manually. Daij-Djan has the right idea in this particular case.Conversazione

© 2022 - 2024 — McMap. All rights reserved.