110

I have a view that has rows and columns of imageviews in it.

If this view is resized, I need to rearrange the imageviews positions.

This view is a subview of another view that gets resized.

Is there a way to detect when this view is being resized?

live-love
  • 48,840
  • 22
  • 240
  • 204

7 Answers7

105

As Uli commented below, the proper way to do it is override layoutSubviews and layout the imageViews there.

If, for some reason, you can't subclass and override layoutSubviews, observing bounds should work, even when being kind of dirty. Even worse, there is a risk with observing - Apple does not guarantee KVO works on UIKit classes. Read the discussion with Apple engineer here: When does an associated object get released?

original answer:

You can use key-value observing:

[yourView addObserver:self forKeyPath:@"bounds" options:0 context:nil];

and implement:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == yourView && [keyPath isEqualToString:@"bounds"]) {
        // do your stuff, or better schedule to run later using performSelector:withObject:afterDuration:
    }
}
Community
  • 1
  • 1
Michal
  • 4,846
  • 3
  • 33
  • 25
  • 2
    Really, laying out subviews is exactly what layoutSubviews is for, so observing size changes, while functional, is IMO bad design. – uliwitness Jul 22 '11 at 12:49
  • @uliwitness well they do keep changing this stuff. Anyway, now you should use `viewWillTransition` etc. etc. – Dan Rosenstark Jan 12 '17 at 19:49
  • viewWillTransition of for UIViewControllers, not UIView. – Armand Feb 07 '18 at 07:21
  • Is using `layoutSubviews` still a recommended method if, depending on the current size of the view, different subviews needed to be added/removed and different constraints need to be added/removed? – Chris Prince May 04 '19 at 18:41
  • 1
    Well, I think I just answered this for my case. When I do the setup (dependent on size) I need in an override of bounds, it works. When I do it in layoutSubviews it doesn't. – Chris Prince May 04 '19 at 18:48
103

In a UIView subclass, property observers can be used:

override var bounds: CGRect {
    didSet {
        // ...
    }
}

Without subclassing, key-value observation with smart key-paths will do:

var boundsObservation: NSKeyValueObservation?

func beginObservingBounds() {
    boundsObservation = observe(\.bounds) { capturedSelf, _ in
        // ...
    }
}
Rudolf Adamkovič
  • 31,030
  • 13
  • 103
  • 118
  • 2
    @Ali Why do you think so? – Rudolf Adamkovič Mar 12 '15 at 07:41
  • on load frame changes but it looks like bounds does not (I know it is strange and maybe I am dong something wrong ) – Ali Mar 12 '15 at 09:32
  • 3
    @Ali: as it has been mentioned a couple of times here: `frame` is a derived and runtime calculated property. do not override this, unless you have a very smart and knowing reason to do so. otherwise: use `bounds` or (even better) `layoutSubviews`. – auco Mar 13 '15 at 13:45
  • `layoutSubviews` gets called multiple times. If you want to set layout constraints with more precise calculation, then `layoutSubviews` won't work. – amol-c Feb 02 '16 at 17:00
  • 3
    Such a great way to create a circle. Thanks! `override var bounds: CGRect { didSet { layer.cornerRadius = bounds.size.width / 2 }}` – SimplGy Jul 12 '16 at 14:22
  • 5
    Detecting `bounds` or `frame` 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:10
  • Apple REALLY need to fix this API. It's broken, IMO. We need a property or callback that will tell us just once when our view is ready for customisation, with a fully populated frame/bounds. – Womble Jul 15 '21 at 07:09
34

Create subclass of UIView, and override layoutSubviews

gwang
  • 425
  • 5
  • 8
  • 13
    the trouble with this is that subviews can not only change their size, but they can animate that size change. When UIView runs the animation, it does not call layoutSubviews each time. – David Jeske Feb 13 '13 at 01:12
  • 2
    @DavidJeske UIViewAnimationOptions has an option called UIViewAnimationOptionLayoutSubviews. I think this maybe help. : ) – Neal.Marlin Jul 27 '19 at 02:56
9

Swift 4 keypath KVO -- This is how I detect autorotate and moving to iPad side panel. Should work work any view. Had to observe the UIView's layer.

private var observer: NSKeyValueObservation?

override func viewDidLoad() {
    super.viewDidLoad()
    observer = view.layer.observe(\.bounds) { object, _ in
        print(object.bounds)
    }
    // ...
}
override func viewWillDisappear(_ animated: Bool) {
    observer?.invalidate()
    //...
}
Warren Stringer
  • 1,692
  • 1
  • 17
  • 14
  • This solution worked for me. I'm new to Swift and I'm unsure on the \.bounds syntax you used here. What does that mean exactly? – WBuck Jan 19 '18 at 00:49
  • here is more discussion: https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift – Warren Stringer Jan 20 '18 at 01:16
  • `.layer` did the trick! Do you know why using `view.observe` doesn't work? – d4Rk Jan 26 '18 at 14:56
  • @d4Rk - I don't know. My guess is that layer bounds less ambiguous than UIView frames. For example, UITableView's contentOffset will affect the final coordinates of its subViews. Not sure. – Warren Stringer Jan 28 '18 at 17:58
  • THIS IS A GREAT ANSWER FOR ME. My case is that I'm using AutoLayout for laying out my views. BUT UICollectionViewFlowLayout forces you to give an explicit itemSize. but when your collectionView size changes (like in my case when I'm showing a toolbar with AutoLayout) - the itemSize remains the same and throws =[ the observer is the cleanest approach I got so far. and the best one!! – Yitzchak Jun 03 '18 at 08:11
7

Pretty old but still a good question. In Apple's sample code, and in some of their private UIView subclasses, they override setBounds roughly like:

-(void)setBounds:(CGRect)newBounds {
    BOOL const isResize = !CGSizeEqualToSize(newBounds.size, self.bounds.size);
    if (isResize) [self prepareToResizeTo:newBounds.size]; // probably saves 
    [super setBounds:newBounds];
    if (isResize) [self recoverFromResizing];
}

Overriding setFrame: is NOT a good idea. frame is derived from center, bounds, and transform, so iOS will not necessarily call setFrame:.

Adlai Holler
  • 830
  • 7
  • 10
  • 2
    This is true (in theory). However, `setBounds:` is not called either when setting the frame property (at least on iOS 7.1). This could be an optimization Apple added to avoid the additional message. – nschum Aug 11 '14 at 10:46
  • 1
    … and bad design on Apple's side not to call their own accessors. – osxdirk Oct 09 '14 at 12:03
  • 2
    In fact, _both_ `frame` and `bounds` are derived from the view's underlying `CALayer`; they just call through to the layer's getter. And `setFrame:` sets the layer's frame, while `setBounds:` sets the layer bounds. So you cannot just override one or the other. Also, `layoutSubviews` gets called excessively (not just on geometry changes), so it may not always be a good option either. Still looking... – big_m Dec 21 '15 at 20:28
7

You can create a subclass of UIView and override the

setFrame:(CGRect)frame

method. This is the method called when the frame (i.e. the size) of the view is changed. Do something like this:

- (void) setFrame:(CGRect)frame
{
  // Call the parent class to move the view
  [super setFrame:frame];

  // Do your custom code here.
}
rekle
  • 2,375
  • 1
  • 18
  • 9
-3

If you're in a UIViewController instance, overriding viewDidLayoutSubviews does the trick.

override func viewDidLayoutSubviews() {
    // update subviews
}
Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
  • UIView size changed needed, not UIViewController. – Gastón Antonio Montes Oct 05 '17 at 13:55
  • @GastónAntonioMontes thanks for that! You might have noticed a relationship between `UIView` instances and `UIViewController` instances. So if you have a `UIView` instance with no VC attached, the other answers are great, but if you happen to be attached to a VC, this is your man. Sorry it's not applicable to your case. – Dan Rosenstark Oct 05 '17 at 15:48