48

I am trying some stuffs out with CATiledLayer inside UIScrollView.

Somehow, the size of UIView inside the UIScrollView gets changed to a large number. I need to find out exactly what is causing this resize.

Is there a way to detect when the size of UIView(either frame, bounds) or the contentSize of UIScrollView is resized?

I tried

override var frame: CGRect {
    didSet {
        println("frame changed");
    }
}

inside UIView subclass,

but it is only called once when the app starts, although the size of UIView is resized afterwards.

Juan Catalan
  • 2,299
  • 1
  • 17
  • 23
Joon. P
  • 2,238
  • 7
  • 26
  • 53

9 Answers9

61

There's an answer here:

https://stackoverflow.com/a/27590915/5160929

Just paste this outside of a method body:

override var bounds: CGRect {
    didSet {
        // Do stuff here
    }
}
Community
  • 1
  • 1
bearacuda13
  • 1,779
  • 3
  • 24
  • 32
  • 1
    Best for UIScrollView (because viewWillLayoutSubviews() is not accessible) Just notice that it may be called many times and be careful not to change the bounds in it, it will be infinitly – Yitzchak Jan 03 '18 at 12:44
  • 9
    Detecting `bounds` change is not guaranteed to work, depending on where you put your view in the view hierarchy. I'd override `layoutSubviews` instead. See [this](https://stackoverflow.com/a/50901862/1452174) and [this](https://stackoverflow.com/a/5003296/1452174) answers. – HuaTham Jun 18 '18 at 02:06
46

viewWillLayoutSubviews() and viewDidLayoutSubviews() will be called whenever the bounds change. In the view controller.

simons
  • 2,374
  • 2
  • 18
  • 20
9

You can also use KVO:

You can set a KVO like this, where view is the view you want to observe frame changes for:

self.addObserver(view, forKeyPath: "center", options: NSKeyValueObservingOptions.New, context: nil)

And you can get the changes with this notification:

override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: NSDictionary!, context: CMutableVoidPointer) {
    }

The observeValueForKeyPath will be called whenever the frame of the view you are observing changes.

Also remember to remove the observer when your view is about to be deallocated:

view.removeObserver(self, forKeyPath:"center")
Community
  • 1
  • 1
Lefteris
  • 14,550
  • 2
  • 56
  • 95
  • 7
    KVO on UIKit may break at anytime. So should be used only as a last resort and be prepared for it to fail in future versions of iOS. Look at this answer from an iOS UIKit engineer: http://stackoverflow.com/a/6051404/251394 – srik May 09 '16 at 08:38
  • UIKit properties are in general not KVO-compliant nor are they documented to be so. I'd stay away from this approach. – HuaTham Jun 18 '18 at 02:06
9

You can create a custom class, and use a closure to get the updated rect comfortably. Especially handy when dealing with classes (like CAGradientLayer which want you to give them a CGRect):

GView.swift:

import Foundation
import UIKit

class GView: UIView {

    var onFrameUpdated: ((_ bounds: CGRect) -> Void)?

    override func layoutSublayers(of layer: CALayer) {
      super.layoutSublayers(of: layer)
      self.onFrameUpdated?(self.bounds)
    }
}

Example Usage:

let headerView = GView()
let gradientLayer = CAGradientLayer()
headerView.layer.insertSublayer(gradientLayer, at: 0)
    
gradientLayer.colors = [
    UIColor.mainColorDark.cgColor,
    UIColor.mainColor.cgColor,
]
    
gradientLayer.locations = [
    0.0,
    1.0,
]
    
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
    
headerView.onFrameUpdated = { _ in // here you have access to `bounds` and `frame` with proper values
    gradientLayer.frame = headerView.bounds
}

If you are not adding your views through code, you can set the Custom Class property in storyboard to GView.

Please note that the name GView was chosen as a company measure and probably choosing something like FrameObserverView would be better.

Sean Goudarzi
  • 1,244
  • 1
  • 10
  • 23
3

This is a simple and not-too-hacky solution: You remember the last size of your view, compare it to the new size in an overridden layoutSubviews method, and then do something when you determine that the size has changed.

/// Create this as a private property in your UIView subclass
private var lastSize: CGSize = .zero

open override func layoutSubviews() {
    // First call super to let the system update the layout
    super.layoutSubviews()
    
    // Check if:
    // 1. The view is part of the view hierarchy
    // 2. Our lastSize var doesn't still have its initial value
    // 3. The new size is different from the last size
    if self.window != nil, lastSize != .zero, frame.size != lastSize {
        // React to the size change
    }
    
    lastSize = frame.size
}

Note that you don't have to include the self.window != nil check, but I assume that in most cases you are only interested in being informed of size changes for views that are part of the view hierarchy.

Note also that you can remove the lastSize != .zero check if you want to be informed about the very first size change when the view is initially displayed. Often we are not interested in that event, but only in subsequent size changes due to device rotation or a trait collection change.

Enjoy!

Johannes Fahrenkrug
  • 42,912
  • 19
  • 126
  • 165
2

The answers are correct, although for my case the constraints I setup in storyboard caused the UIView size to change without calling back any detecting functions.

Joon. P
  • 2,238
  • 7
  • 26
  • 53
  • Perhaps I am misunderstanding. But AFAIK anything which is happening as a result of constraints should show in the layout preview - in the Assistant editor. – simons Jun 03 '15 at 10:35
1

For UIViews, as easy as:

    override func layoutSubviews() {
        super.layoutSubviews()

        setupYourNewLayoutHereMate()
    }
oskarko
  • 3,382
  • 1
  • 26
  • 26
  • 1
    It's not that easy. `layoutSubviews` can be called multiple times. Also, the view's `frame` and `bounds` properties may be .zero. So you have to check each time it is called, and act appropriately (e.g. remove/rebuild layers) – Womble Jul 15 '21 at 06:54
1

You can use the FrameObserver Pod.

It is not using KVO or Method Swizzling so won't be breaking your code if the underlying implementation of UIKit ever changes.

whateverUIViewSubclass.addFrameObserver { frame, bounds in // get updates when the size of view changes
    print("frame", frame, "bounds", bounds)
}

You can call it on a UIView instance or any of its subclasses, like UILabel, UIButton, UIStackView, etc.

Sean Goudarzi
  • 1,244
  • 1
  • 10
  • 23
-2

STEP 1:viewWillLayoutSubviews

Called to notify the view controller that its view is about to layout its subviews

When a view's bounds change, the view adjusts the position of its subviews. Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.

STEP 2:viewDidLayoutSubviews

Called to notify the view controller that its view has just laid out its subviews.

When the bounds change for a view controller's view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted. Each subview is responsible for adjusting its own layout.

Your view controller can override this method to make changes after the view lays out its subviews. The default implementation of this method does nothing.

Above these methods are called whenever bounds of UIView is changed

bearacuda13
  • 1,779
  • 3
  • 24
  • 32
user3182143
  • 9,459
  • 3
  • 32
  • 39