How to detect when UIView size is changed in swift ios xcode?

53,357

Solution 1

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

Solution 2

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
    }
}

Solution 3

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")

Solution 4

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.

Solution 5

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!

Share:
53,357
Joon. P
Author by

Joon. P

Updated on July 09, 2022

Comments

  • Joon. P
    Joon. P almost 2 years

    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.

  • simons
    simons almost 9 years
    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.
  • srik
    srik about 8 years
    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: stackoverflow.com/a/6051404/251394
  • Yitzchak
    Yitzchak over 6 years
    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
  • HuaTham
    HuaTham almost 6 years
    UIKit properties are in general not KVO-compliant nor are they documented to be so. I'd stay away from this approach.
  • HuaTham
    HuaTham almost 6 years
    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 and this answers.
  • Womble
    Womble almost 3 years
    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)
  • Oliver Pearmain
    Oliver Pearmain almost 3 years
    This is a UIView**Controller** method, not a UIView method like requested.