110

I have a UIView which has about 8 different CALayer sublayers added to its layer. If I modify the view's bounds (animated), then the view itself shrinks (I checked it with a backgroundColor), but the sublayers' size remains unchanged.

How to solve this?

iain
  • 5,660
  • 1
  • 30
  • 51
Geri Borbás
  • 15,810
  • 18
  • 109
  • 172

8 Answers8

160

I used the same approach that Solin used, but there's a typo in that code. The method should be:

- (void)layoutSubviews {
  [super layoutSubviews];
  // resize your layers based on the view's new bounds
  mylayer.frame = self.bounds;
}

For my purposes, I always wanted the sublayer to be the full size of the parent view. Put that method in your view class.

Lukas Würzburger
  • 6,543
  • 7
  • 41
  • 75
Chadwick Wood
  • 2,012
  • 1
  • 15
  • 9
  • 8
    @fishinear are you sure? it sounds like you are describing frame. bounds should, theoretically, always have 0,0 as its origin. – griotspeak Feb 11 '12 at 02:32
  • 3
    @griotspeak - that's actually not the case. See the description of the bounds property here: http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/UIView/UIView.html#//apple_ref/doc/uid/TP40006816-CH3-SW1 -- "By default, the origin of the bounds rectangle is set to (0, 0) but you can change this value to display different portions of the view." – jdc Mar 22 '13 at 22:43
  • Many thanks, I have since learned of a few cases (scroll view, for one) where that is not true but I forgot about this comment. – griotspeak Mar 23 '13 at 13:41
  • 32
    Don't forget to call [super layoutSubviews], as this can cause unexpected behavior in various UIKit classes. – Kpmurphy91 May 26 '14 at 03:51
  • 2
    This didn't work very well for me with a self-sizing `UITextView` (scrollEnabled = NO) and auto layout. I had a text view pinned to its superview, which in turn had a `CALayer` at the bottom of itself (a border), and I wanted it to update border position when the text view's height changed. For some reason `layoutSubiews` was called way too late (and the layer position was animated). – iosdude May 30 '17 at 04:02
  • Found a solution: To disable layer frame animation use `[CATransaction setDisableActions:YES]` and perform changes inside a transaction. – iosdude May 30 '17 at 04:09
27

Since CALayer on the iPhone does not support layout managers, I think you have to make your view's main layer a custom CALayer subclass in which you override layoutSublayers to set the frames of all sublayers. You must also override your view's +layerClass method to return the class of your new CALayer subclass.

Ole Begemann
  • 135,006
  • 31
  • 278
  • 256
  • Override +layerClass like in the case of OpenGL layer override? Think so. Set the frames of the sublayers? Set to what? I have to calculate all the sublayers' frames/positions individually depending on the superlayers frame size? Isn't there any "constraint-like", or "link-to-superlayer"-like solution? Oh, god, another day out of time-frame. CGLayers got resized, but was too laggy, CALayers are fast enough, but did not get resized. So many surprise. Thanks for the reply, anyway. – Geri Borbás Mar 24 '10 at 11:46
  • 3
    CALayer on the Mac has layout managers for this but they are not available on the iPhone. So yes, you have to calculate the frame sizes yourself. Another option could be to use subviews instead of sublayers. Then you could set the subviews' autoresizing masks accordingly. – Ole Begemann Mar 24 '10 at 11:52
  • 2
    Ya, UIViews are too performance expensive classes, thatswhy I use CALayers. – Geri Borbás Mar 24 '10 at 16:35
  • I tried the way you suggested. It works, and it is not. I resize the view animated, but the sublayers resizes in a different animation. Bloody hell, what to do now? What is the method to override in the CALayer subclass what invokes on EVERY (!) frame of the view's animation? Is there any? – Geri Borbás Mar 24 '10 at 19:32
  • Just bumped into this as well. Seems like the best option is to subclass CALayer as Ole said, but it seems unnecessarily cumbersome. – Dimitri May 27 '10 at 15:53
  • Regarding this very old QA, @OleBegemann 's solution of using layoutSublayers ... now `layoutSublayers#oflayer` ... But i had never heard of it before; have no idea how it works; and in extensive testing can't seem to get it to work !!! Heh. – Fattie Dec 11 '17 at 17:31
17

I used this in the UIView.

-(void)layoutSublayersOfLayer:(CALayer *)layer
{
    if (layer == self.layer)
    {
        _anySubLayer.frame = layer.bounds;
    }

super.layoutSublayersOfLayer(layer)
}

Works for me.

Fattie
  • 27,874
  • 70
  • 431
  • 719
Runo Sahara
  • 715
  • 8
  • 23
  • Yes, this worked for me in iOS 8. Presumably would work in earlier versions. I had a gradient background layer and needed to resize the layer when the device is rotated. – EPage_Ed Mar 13 '15 at 23:00
  • Worked for me, but it did need a call through to super – entropy Apr 18 '16 at 15:10
  • 1
    This is the best answer! Yeah I have tried layoutSubviews but it only breaks layout of my UITextField like leftView and rightView disappearing and more Text in UITextField disappearing. Maybe I have used extension instead of inheritance of UIView, but it was very buggy. THIS WORKS GREAT! THX – Michał Ziobro Sep 17 '18 at 17:19
  • For a layer with custom drawing (`CALayerDelegate`'s `draw(_:in:)`), I had to also call `_anySubLayer.setNeedsDisplay()`. Then this worked for me. – jedwidz Aug 02 '20 at 11:43
9

I had the same problem. In a custom view's layer I added two more sublayers. In order to resize the sublayers (every time the custom view's boundaries change), I implemented the method layoutSubviews of my custom view; inside this method I just update each sublayer's frame to match the current boundaries of my subview's layer.

Something like this:

-(void)layoutSubviews{
   //keep the same origin, just update the width and height
   if(sublayer1!=nil){
      sublayer1.frame = self.layer.bounds;
   }
}
nschum
  • 15,322
  • 5
  • 58
  • 56
Solin
  • 756
  • 1
  • 6
  • 8
  • 16
    FYI, you don't need the if(sublayer1!=nil), since ObjC defines messages to nil (in this case, -setFrame:) as a no-op returning 0. – Andrew Pouliot Sep 20 '11 at 23:14
  • Don't forget to call `[super layoutSubviews]`. Omitting this call can cause unexpected behavior. – McKinley Nov 29 '21 at 07:07
9

2017:

The literal answer to this question:

"CALayers didn't get resized on its UIView's bounds change. Why?"

is that for better or worse:

needsDisplayOnBoundsChange

defaults to false (!!) in CALayer.

solution,

class CircularGradientViewLayer: CALayer {
    
    override init() {
        
        super.init()
        needsDisplayOnBoundsChange = true
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override open func draw(in ctx: CGContext) {
        go crazy drawing in .bounds
    }
}

Indeed, I direct you to this QA

https://stackoverflow.com/a/47760444/294884

which explains, what the hell the critical contentsScale does. You usually need to set that, when you set needsDisplayOnBoundsChange.

Fattie
  • 27,874
  • 70
  • 431
  • 719
5

Swift 3 Version

In Custom cell, Add Following lines

Declare first

let gradientLayer: CAGradientLayer = CAGradientLayer()

Then add following lines

override func layoutSubviews() {
    gradientLayer.frame = self.YourCustomView.bounds
}
Ashish Gupta
  • 737
  • 11
  • 18
  • 1
    Don't forget to call `super.layoutSubviews()`. Omitting this call can cause unexpected behavior in some views. – McKinley Nov 29 '21 at 07:05
0

As [Ole] wrote CALayer does not support autoresizing on iOS. So you should adjust layout manually. My option was to adjust layer's frame within (iOS 7 and earlier)

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration 

or (as of iOS 8)

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinato
DevGansta
  • 5,564
  • 2
  • 16
  • 16
-1

In your custom view, you need declare variable for your custom layer, don't declare variable in scope init. And just init it once time, don't try set null value and reinit

class CustomView:UIView {
    var customLayer:CALayer = CALayer()
    
    override func  layoutSubviews() {
        super.layoutSubviews()
        //        guard let _fillColor = self._fillColor else {return}
        initializeLayout()
    }
    private func initializeLayout() { 
        customLayer.removeFromSuperView()
        customLayer.frame = layer.bounds
                   
        layer.insertSubview(at:0)
    }
}
Kozmotronik
  • 2,080
  • 3
  • 10
  • 25