iPhone UIView - 调整框架以适应子视图大小

43

在添加子视图后,应该有一种方法可以调整UIView的框架大小,使其框架大小适合所有子视图。如果您的子视图是动态添加的,如何确定容器视图的框架需要的大小?这个方法行不通:

[myView sizeToFit];
11个回答

44

你还可以添加以下代码来计算子视图的位置。

[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

2
在for循环中,我会使用tmpFrame = CGRectUnion(tmpFrame, v.frame),其中tmpFrame将在for循环之前声明为CGRectZero。 - mrd3650
1
不适用于自动布局,因为子视图的框架稍后计算。 :( - Maciej Swic
2
不错,谢谢节省时间 :D 我在你的代码中添加了两个方法 https://gist.github.com/Dudi00/7600551 - Błażej
@MaciejSwic 你可以通过编程设置宽度/高度约束来使用自动布局实现相同的效果。 - teebot
1
对于您的自定义视图:重写sizeThatFits:(CGSize)size并将上述代码添加到其中,使用return CGSizeMake(w, h);代替。然后您的sizeToFit将起作用。 - ullstrm
显示剩余2条评论

26

我需要适应具有负原点的子视图,CGRectUnion是ObjC实现它的方式,正如评论中提到的那样。首先,让我们看一下它的工作原理:

正如下图所示,我们假设一些子视图位于外面,因此我们需要做两件事情来使其看起来更好,而不影响子视图的定位:

  1. 将视图的框架移动到左上角位置
  2. 将子视图相反地移动以抵消效果。

图片胜过千言万语。

演示

代码胜过亿万言语。这是解决方案:

@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
我认为用Swift 2.0写作很有趣…我是正确的!
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
    }
}

还有操场上的证明:

注意visualAidView不会移动,并帮助您看到superview在保持子视图位置的同时调整大小。

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

操场图片


19

看起来Kenny的答案上面指向了参考问题的正确解决方案,但可能已经误解了错误的概念。UIView类引用明确建议了一个系统,使sizeToFit适用于您的自定义视图。

重写sizeThatFits,而不是sizeToFit

您的自定义UIView需要覆盖sizeThatFits以返回“适合接收者子视图的新大小”,但是您可以根据自己的需要计算。您甚至可以使用另一个答案中的数学来确定您的新大小(但不重新创建内置的sizeToFit系统)。

sizeThatFits返回与其状态相关的数字之后,对自定义视图调用sizeToFit将开始导致预期的调整大小。

sizeThatFits如何工作

没有重写的情况下,sizeThatFits只是返回传入的尺寸参数(对于从sizeToFit调用而言,默认为self.bounds.size)。虽然我只在couple sources上看到这个问题,但似乎传入的尺寸并不被视为严格要求。

[sizeThatFits] 返回最适合符合传递给它的约束条件的控件的大小。如果无法满足约束条件,该方法可以(强调他们的话)决定忽略约束条件。


16

4
好的。虽然有一种方法实际上可以做到“自适应大小”,但这种方法还是很好的。 - sol
10
根据UIView类的参考文档,“sizeToFit [...] 您不应该覆盖此方法。” - Bored Astronaut

4

更新了@Mazyod的答案到Swift 3.0,运行得很顺利!

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

这几乎完美地解决了我的问题,即自定义UIView无法调整大小并返回新高度。为了消除“调用'offsetBy(dx:dy:)'的结果未使用”的警告,我更新了函数以在有问题的行的开始处使用“_ =”。 - commbot

1

虽然这是一个老问题,但你也可以使用递归函数来解决。

您可能希望找到一种无论子视图和子子视图有多少都能正常工作的解决方案。

更新:之前的代码只有一个getter函数,现在还有一个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)
        }
    }
}

其实,协议扩展只有在您想要在 UIViewCALayer 之间共享此代码时才有用。如果是针对 UIView 子类型,则只需使用普通扩展而无需使用协议。 - Mazyod
@Mazyod 即使协议扩展不是您首选的方法,使用递归函数仍比被接受或得到最高投票答案更有用。 - R Menke
我认为递归方法只在某些情况下有用,其中你可能遇到过其中之一。但对我来说,我宁愿保持扁平化。我的意思是说,递归方法应该根据用例进行评估,并不是一种普遍更好的方法,在我看来。 - Mazyod
@Mazyod,你可以编写递归和非递归函数,并在适当的时候使用其中之一。具有递归选项很有用且合乎逻辑,因为任何子视图都可以拥有自己的子视图。这在答案中没有提到,所以我加上了它。 - R Menke
是的,我认为您的贡献在这方面非常有价值。 - Mazyod

1

虽然已经有很多答案了,但没有一个适用于我的情况。如果您使用autoresizingMasks并且想要调整视图大小并移动子视图以使矩形的大小与子视图匹配,请参考以下方法。

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

0
这是被接受答案的一个 Swift 版本,也有细微改动,不是扩展它,而是将该方法作为变量获取视图并返回。
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
}

这是扩展版本:

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

0

当您创建自己的UIView类时,请考虑在子类中重写IntrinsicContentSize。操作系统会调用此属性以了解建议的视图大小。我将放置C#(Xamarin)的代码片段,但是思路是相同的:

[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*/);
        }
    }
}

这个返回的大小完全取决于你的设计。例如,如果你只添加了一个标签,那么宽度和高度就是该标签的宽度和高度。如果你有更复杂的视图,你需要返回适合所有子视图的大小。


0

无论是哪个模块动态添加了所有这些子视图,都必须知道将它们放在哪里(以便它们正确关联或不重叠等)。一旦你知道了这一点,再加上当前视图的大小和子视图的大小,你就有了确定是否需要修改封闭视图的所有必要信息。


1
是的,我可以手动设置或通过进行大量计算来完成。但是UIView有一个sizeToFit方法,在文档中说:“调整接收方视图的大小和位置,以便其仅包含其子视图。”难道它不应该做到吗? - sol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接