43

Shouldn't there be a way to resize the frame of a UIView after you've added subviews so that the frame is the size needed to enclose all the subviews? If your subviews are added dynamically how can you determine what size the frame of the container view needs to be? This doesn't work:

[myView sizeToFit];
Forge
  • 6,538
  • 6
  • 44
  • 64
sol
  • 6,402
  • 13
  • 47
  • 57

11 Answers11

44

You could also add the following code to calculate subviews position.

[myView resizeToFitSubviews]

UIViewUtils.h

#import <UIKit/UIKit.h>

@interface UIView (UIView_Expanded)

-(void)resizeToFitSubviews;

@end

UIViewUtils.m

#import "UIViewUtils.h"

@implementation UIView (UIView_Expanded)

-(void)resizeToFitSubviews
{
    float w = 0;
    float h = 0;

    for (UIView *v in [self subviews]) {
        float fw = v.frame.origin.x + v.frame.size.width;
        float fh = v.frame.origin.y + v.frame.size.height;
        w = MAX(fw, w);
        h = MAX(fh, h);
    }
    [self setFrame:CGRectMake(self.frame.origin.x, self.frame.origin.y, w, h)];
}

@end
user1204936
  • 449
  • 1
  • 4
  • 2
  • 2
    In the for loop I would use `tmpFrame = CGRectUnion(tmpFrame, v.frame)` where tmpFrame will be declared just before the forloop as CGRectZero – mrd3650 Apr 11 '12 at 14:48
  • I didn't even know you could add method this way to a native class. This works wonders and I learned something :) Thanks ! – Yahel Aug 03 '12 at 16:58
  • 1
    Does not work for autolayout as the frames for the subviews are calculated later :( – Maciej Swic Nov 21 '12 at 13:11
  • 2
    nice one,thanks for saved time :D I add two methods to your code https://gist.github.com/Dudi00/7600551 – Błażej Nov 22 '13 at 14:13
  • @MaciejSwic you can achieve the same effect with autolayout by setting width/height constraints programmatically – teebot Apr 08 '14 at 11:41
  • 1
    For your custom views: Override sizeThatFits:(CGSize)size and add the above code to it with return CGSizeMake(w, h); instead. Then your sizeToFit will work. – ullstrm May 09 '14 at 08:55
  • @teebot Can you elaborate? I have never used constraints programmatically, how would it help to calculate 'faster' than constraints made in storyboard? – CyberMew Dec 30 '14 at 11:39
26

I needed to fit subviews had a negative origin point, and CGRectUnion is the ObjC way of doing it, honestly, as someone mentioned in the comments. First, let's see how it works:

As you can see below, we assume some subviews are lying outside, so we need to do two things to make this look good, without affecting the positioning of the subviews:

  1. Move the frame of the view to the top left most position
  2. Move the subviews the opposite direction to negate the effect.

A picture is worth a thousand words.

demonstration

Code is worth a billion words. Here is the solution:

@interface UIView (UIView_Expanded)

- (void)resizeToFitSubviews;

@end

@implementation UIView (UIView_Expanded)

- (void)resizeToFitSubviews
{
    // 1 - calculate size
    CGRect r = CGRectZero;
    for (UIView *v in [self subviews])
    {
        r = CGRectUnion(r, v.frame);
    }

    // 2 - move all subviews inside
    CGPoint fix = r.origin;
    for (UIView *v in [self subviews])
    {
        v.frame = CGRectOffset(v.frame, -fix.x, -fix.y);
    }

    // 3 - move frame to negate the previous movement
    CGRect newFrame = CGRectOffset(self.frame, fix.x, fix.y);
    newFrame.size = r.size;

    [self setFrame:newFrame];
}

@end

I thought it would be fun to write in Swift 2.0 .. I was right!

extension UIView {

    func resizeToFitSubviews() {

        let subviewsRect = subviews.reduce(CGRect.zero) {
            $0.union($1.frame)
        }

        let fix = subviewsRect.origin
        subviews.forEach {
            $0.frame.offsetInPlace(dx: -fix.x, dy: -fix.y)
        }

        frame.offsetInPlace(dx: fix.x, dy: fix.y)
        frame.size = subviewsRect.size
    }
}

And the playground proof:

Notice the visualAidView doesn't move, and helps you see how the superview resizes while maintaining the positions of the subviews.

let canvas = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
canvas.backgroundColor = UIColor.whiteColor()

let visualAidView = UIView(frame: CGRect(x: 5, y: 5, width: 70, height: 70))
visualAidView.backgroundColor = UIColor(white: 0.8, alpha: 1)
canvas.addSubview(visualAidView)

let superview = UIView(frame: CGRect(x: 15, y: 5, width: 50, height: 50))
superview.backgroundColor = UIColor.purpleColor()
superview.clipsToBounds = false
canvas.addSubview(superview)

[
    {
        let view = UIView(frame: CGRect(x: -10, y: 0, width: 15, height: 15))
        view.backgroundColor = UIColor.greenColor()
        return view
    }(),
    {
        let view = UIView(frame: CGRect(x: -10, y: 40, width: 35, height: 15))
        view.backgroundColor = UIColor.cyanColor()
        return view
    }(),
    {
        let view = UIView(frame: CGRect(x: 45, y: 40, width: 15, height: 30))
        view.backgroundColor = UIColor.redColor()
        return view
    }(),

].forEach { superview.addSubview($0) }

playground image

Mazyod
  • 22,319
  • 10
  • 92
  • 157
19

It looks like Kenny 's answer above points to the right solution in the referenced question, but may have taken away the wrong concept. The UIView class reference definitely suggests a system for making sizeToFit relevant to your custom views.

Override sizeThatFits, not sizeToFit

Your custom UIViews needs to override sizeThatFits to return "a new size that fits the receiver's subviews", however you wish to calculate this. You could even use the math from another answer to determine your new size (but without recreating the built-in sizeToFit system).

After sizeThatFits returns numbers relevant to its state, calls to sizeToFit on your custom views will start causing the expected resizes.

How sizeThatFits works

Without an override, sizeThatFits simply returns the passed-in size parameter (defaulted to self.bounds.size for calls from sizeToFit. While I only have a couple sources on the issue, it appears that the passed-in size is not to be seen as a strict demand.

[sizeThatFits] returns the "most appropriate" size for the control that fits the constraints passed to it. The method can (emphasis theirs) decide to ignore the constraints if they cannot be met.

Community
  • 1
  • 1
patridge
  • 26,385
  • 18
  • 89
  • 135
16

Check out Having trouble getting UIView sizeToFit to do anything meaningful

The gist is that sizeToFit is for subclasses to override, and doesn't do anything in UIView. It does stuff for UILabel, because it overrides sizeThatFits: which is called by sizeToFit

Community
  • 1
  • 1
Kenny Winker
  • 11,919
  • 7
  • 56
  • 78
4

Updated @Mazyod's answer to Swift 3.0, worked like a charm!

extension UIView {

func resizeToFitSubviews() {

    let subviewsRect = subviews.reduce(CGRect.zero) {
        $0.union($1.frame)
    }

    let fix = subviewsRect.origin
    subviews.forEach {
        $0.frame.offsetBy(dx: -fix.x, dy: -fix.y)
        }

        frame.offsetBy(dx: fix.x, dy: fix.y)
        frame.size = subviewsRect.size
    }
}
mr. sudo
  • 347
  • 1
  • 13
  • This worked almost perfectly to fix my problem of a custom UIView not resizing and returning the new height. To silence the warning of `Result of call to 'offsetBy(dx:dy:)' is unused `, I updated the function to use `_ = ` at the beginning of the offending lines – commbot Apr 30 '20 at 02:47
1

Old question but you could also do this with a recursive function.

You might want a solution that always works no matter how many subviews and subsubviews,...

Update : Previous piece of code only had a getter function, now also a setter.

extension UIView {

    func setCGRectUnionWithSubviews() {
        frame = getCGRectUnionWithNestedSubviews(subviews: subviews, frame: frame)
        fixPositionOfSubviews(subviews, frame: frame)
    }

    func getCGRectUnionWithSubviews() -> CGRect {
        return getCGRectUnionWithNestedSubviews(subviews: subviews, frame: frame)
    }

    private func getCGRectUnionWithNestedSubviews(subviews subviews_I: [UIView], frame frame_I: CGRect) -> CGRect {

        var rectUnion : CGRect = frame_I
        for subview in subviews_I {
            rectUnion = CGRectUnion(rectUnion, getCGRectUnionWithNestedSubviews(subviews: subview.subviews, frame: subview.frame))
        }
        return rectUnion
    }

    private func fixPositionOfSubviews(subviews: [UIView], frame frame_I: CGRect) {

        let frameFix : CGPoint = frame_I.origin
        for subview in subviews {
            subview.frame = CGRectOffset(subview.frame, -frameFix.x, -frameFix.y)
        }
    }
}
R Menke
  • 8,183
  • 4
  • 35
  • 63
  • Well, actually the protocol extension would only be useful if you want to share this code between `UIView` and `CALayer`. If it's for `UIView` subtypes, just use a normal extension without protocols. – Mazyod Oct 12 '15 at 03:02
  • @Mazyod even if the protocol extension is not your preferred method. Using a recursive function still is more useful than the accepted or highest voted answer. – R Menke Oct 12 '15 at 18:53
  • I believe the recursive approach is only useful in some scenarios, one of which you may have encountered. But for me, I'd rather keep it flat. What I mean to say, the recursive approach should be evaluated per use case, and is not a generally better approach, IMHO. – Mazyod Oct 12 '15 at 19:01
  • @Mazyod you could make a recursive and non recursive function and use one or the other when appropriate. Having a recursive option is useful and logical since any subview can have subviews of its own. It was not mentioned in the answers, which is why I added it. – R Menke Oct 12 '15 at 19:04
  • true, I think your contribution is very valuable in that sense – Mazyod Oct 12 '15 at 19:05
1

Although quite a few answers already, none worked for my use case. In case you use autoresizingMasks and want to resize view and move subviews so the size of rectangle matches subviews.

public extension UIView {

    func resizeToFitSubviews() {
        var x = width
        var y = height
        var rect = CGRect.zero
        subviews.forEach { subview in
            rect = rect.union(subview.frame)
            x = subview.frame.x < x ? subview.frame.x : x
            y = subview.frame.y < y ? subview.frame.y : y
        }
        var masks = [UIView.AutoresizingMask]()
        subviews.forEach { (subview: UIView) in
            masks.add(subview.autoresizingMask)
            subview.autoresizingMask = []
            subview.frame = subview.frame.offsetBy(dx: -x, dy: -y)
        }
        rect.size.width -= x
        rect.size.height -= y
        frame.size = rect.size
        subviews.enumerated().forEach { index, subview in
            subview.autoresizingMask = masks[index]
        }
    }
}
Renetik
  • 5,887
  • 1
  • 47
  • 66
0

Here is a swift version of the accepted answer, also small change, instead of extending it this method gets the view as a variable and returns it.

func resizeToFitSubviews(#view: UIView) -> UIView {
    var width: CGFloat = 0
    var height: CGFloat = 0

    for someView in view.subviews {
        var aView = someView as UIView
        var newWidth = aView.frame.origin.x + aView.frame.width
        var newHeight = aView.frame.origin.y + aView.frame.height
        width = max(width, newWidth)
        height = max(height, newHeight)
    }

    view.frame = CGRect(x: view.frame.origin.x, y: view.frame.origin.y, width: width, height: height)
    return view
}

Heres the extending version:

/// Extension, resizes this view so it fits the largest subview
func resizeToFitSubviews() {
    var width: CGFloat = 0
    var height: CGFloat = 0
    for someView in self.subviews {
        var aView = someView as! UIView
        var newWidth = aView.frame.origin.x + aView.frame.width
        var newHeight = aView.frame.origin.y + aView.frame.height
        width = max(width, newWidth)
        height = max(height, newHeight)
    }

    frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: width, height: height)
}
Esqarrouth
  • 38,543
  • 21
  • 161
  • 168
0

Whatever module that dynamically added all these subviews had to know where to put them (so they relate properly, or so they don't overlap, etc.) Once you know that, plus the size of the current view, plus the size of the subview, you have all you need to determine if the enclosing view needs to be modified.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153
  • 1
    true, I could set it manually or by doing a lot of math. But there is a sizeToFit method of UIView, that in the documentation says "Resizes and moves the receiver view so it just encloses its subviews." Shouldn't that do it? – sol Aug 31 '10 at 01:41
0

When you make your own UIView class, consider overriding IntrinsicContentSize in the subclass. This property is called by OS to know the recommended size of your view. I will put code snippet for C# (Xamarin), but the idea is the same:

[Register("MyView")]
public class MyView : UIView
{
    public MyView()
    {
        Initialize();
    }

    public MyView(RectangleF bounds) : base(bounds)
    {
        Initialize();
    }

    Initialize()
    {
        // add your subviews here.
    }

    public override CGSize IntrinsicContentSize
    {
        get
        {
            return new CGSize(/*The width as per your design*/,/*The height as per your design*/);
        }
    }
}

This returned size depends completely on your design. For example, if you just added a label, then width and height is just the width and height of that label. If you have more complex view, you need to return the size that fits all your subviews.

Ibrahim
  • 183
  • 1
  • 6
-4
[myView sizeToFit];

Should work, why don't you check the CGRect before and after?

Jordan
  • 21,746
  • 10
  • 51
  • 63
  • I have the view's background color set to Red, and it does not change when I call sizeToFit, even though some of the subviews are located outside the frame. – sol Aug 31 '10 at 18:57