如何在iOS中使用StackView从XIB创建自适应大小的自定义视图

33

最近我重新开始学习使用Swift进行iOS开发,并且现在正在研究自定义UIControl。为了布局和灵活性,此自定义视图是使用StackView构建的。

我想要实现的目标是...

...基本上只是希望使用StackView时与将其直接拖入Storyboard中相同的功能-可以轻松调整大小。这本质上与自适应大小的表格视图单元格相同。

演示

我可以将我的CustomView简化为一个简单的例子:其中包含三个标签的StackView,Alignment设置为CenterDistribution Equal Centering。 StackView将固定到左侧、右侧和顶部布局引导(或尾随、前导和顶部)。 我可以使用从XIB加载的CustomView重新创建具有相同参数的CustomView - 我已附上两者的截图。

简单StackView的屏幕截图

CustomView的屏幕截图。

加载XIB文件并设置它。

import UIKit

@IBDesignable
class CustomView: UIView {

    // loaded from NIB
    private weak var view: UIView!

    convenience init() {
        self.init(frame: CGRect())
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.loadNib()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.loadNib()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // we need to adjust the frame of the subview to no longer match the size used
        // in the XIB file BUT the actual frame we got assinged from the superview
        self.view.frame = bounds
    }

    private func loadNib ()  {
        self.view = Bundle (for: type(of: self)).loadNibNamed(
            "CustomView", owner: self, options: nil)! [0] as! UIView
        self.addSubview(self.view)
    }
}

在这里您可以看到,我只是加载了XIB文件,覆盖了layoutSubviews以适应实际视图的框架,就是这样。

问题:

我的问题与通常的方法有关。我的StackView中的标签具有固有内容大小,即通常情况下,如果您在Storyboard中设计它,则StackView会根据标签的高度自适应而不会抱怨。但是,如果您将所有内容放入XIB并将布局保留为原样,则IB将抱怨未知的高度,如屏幕截图所示(顶部,前导,尾随)。我阅读了"Auto-Layout Guide"并观看了WWDC视频"Mysteries of AutoLayout",但是这个主题对我来说仍然很新,我认为我还没有找到正确的方法。因此,这就是我需要帮助的地方。

(1) 我应该设置约束并禁用translatesAutoresizingMaskIntoConstraints吗?

我首先可以并且应该做的是将translatesAutoresizingMaskIntoConstraints设置为false,以便我不会获得自动生成的约束并定义自己的约束。因此,我可以将loadNib更改为:

private func loadNib ()  {
    self.view = Bundle (for: type(of: self)).loadNibNamed(
        "CustomView", owner: self, options: nil)! [0] as! UIView
    self.view.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(self.view)

    self.addConstraint(NSLayoutConstraint (
        item: self
        , attribute: .bottom
        , relatedBy: .equal
        , toItem: self.view
        , attribute: .bottom
        , multiplier: 1.0
        , constant: 0))

    self.addConstraint(NSLayoutConstraint (
        item: self
        , attribute: .top
        , relatedBy: .equal
        , toItem: self.view
        , attribute: .top
        , multiplier: 1.0
        , constant: 0))

    self.addConstraint(NSLayoutConstraint (
        item: self.view
        , attribute: .leading
        , relatedBy: .equal
        , toItem: self
        , attribute: .leading
        , multiplier: 1.0
        , constant: 0))

    self.addConstraint(NSLayoutConstraint (
        item: self.view
        , attribute: .trailing
        , relatedBy: .equal
        , toItem: self
        , attribute: .trailing
        , multiplier: 1.0
        , constant: 0))
}

基本上,我将视图在Storyboard中拉伸以适应UIView的整个宽度,并将其顶部和底部约束到StackView的顶部和底部。

我在使用这个时遇到了很多XCode 8崩溃的问题,所以我不太确定该怎么做。

(2)或者我应该覆盖intrinsicContentSize?

我也可以通过检查所有arrangedSubviews的高度并取最高的一个来简单地重写intrinsicContentSize,并将我的视图高度设置为它:

override var intrinsicContentSize: CGSize {
    var size = CGSize.zero

    for (_ , aSubView) in (self.view as! UIStackView).arrangedSubviews.enumerated() {
        let subViewHeight = aSubView.intrinsicContentSize.height
        if  subViewHeight > size.height {
            size.height = aSubView.intrinsicContentSize.height
        }
    }

    // add some padding
    size.height += 0
    size.width = UIViewNoIntrinsicMetric
    return size
}

虽然这显然给了我更多的灵活性,但我还必须使内在内容大小无效,检查动态类型和其他许多我宁愿避免的东西。

(3) 我甚至是以“拟议”的方式加载XIB吗?

或者有更好的方法,例如使用UINib.instantiate?我真的找不到最佳实践的做法。

(4) 首先为什么要这样做?

真的,为什么?我只是将StackView移到CustomView中。那么为什么StackView现在停止自适应,为什么我必须设置顶部和底部约束?

到目前为止,最有帮助的是https://dev59.com/8WAf5IYBdhLWcg3woj7N#24441368中的关键短语:

“一般来说,自动布局是从上到下执行的。换句话说,首先执行父视图布局,然后执行任何子视图布局。”

..但我真的不太确定。

(5) 为什么我必须在代码中添加约束?

假设我禁用了translatesAutoresizingMaskIntoConstraints,那么我不能像在IB中的tableview cells那样设置约束吗?它将总是转换为类似于label.top = top这样的东西,从而标签会被拉伸,而不是StackView继承大小/高度。此外,我无法真正将StackView固定到其XIB文件中的容器视图。

希望有人能帮助我。干杯!


没有人理解你的问题,这就是为什么它仍然是开放的。首先,您需要完全将堆栈视图固定到自定义视图上。然后它将根据自定义视图的框架进行调整大小。但是,如果您希望自定义视图根据堆栈视图调整大小,则需要将自定义视图固定到堆栈视图上(反向/翻转约束)。自定义视图将扩展以适应堆栈视图(堆栈视图将首先进行布局)。 - Brandon
我当时正确地固定了引脚,如果我没有从XIB文件中加载它,它就可以工作。自那以后我没有碰过这个项目,这就是为什么我没有更改描述的原因。 - martin
1个回答

17

以下是您的问题的一些答案,尽管顺序不同:

(5) 为什么我必须在代码中添加约束条件?

假设我禁用了translatesAutoresizingMaskIntoConstraints,那么我不能像在IB中的tableview单元格那样设置约束条件吗?它会始终被转换成诸如label.top = top这样的内容,因此标签将被拉伸,而不是StackView继承大小/高度。此外,对于XIB文件,我无法将StackView固定到其容器视图中。

在这个例子中,您必须在代码中添加约束条件,因为您正在从nib中加载任意视图,然后将其添加到代码中的CustomView中。nib中的视图没有预先知道自己将被放置到什么上下文或层次结构中,因此无法提前定义如何将自身限制到未知的未来超级视图中。在故事板上的StackView或原型单元格内部,已知该上下文以及其父视图。

如果您不想手动编写约束条件,则可以考虑一个不错的替代方案,即使您的CustomView子类UIStackView而不是UIView。由于UIStackView自动生成适当的约束条件,它将节省您手动编码的时间。请参见下一个答案中的一些示例代码。

(3) 我是否正在以“建议”的方式加载XIB?

还是有更好的方法,例如使用UINib.instantiate?我确实找不到最佳实践的方法。

我不知道如何处理将nib加载到自定义视图中的“标准”或“正确”方式。我看过各种方法,它们都有效。但是,我将指出,在样本代码中,您正在进行比所需工作更多的工作。以下是一种更简单的方法,可以达到相同的结果,但也使用了UIStackView子类而不是UIView子类(如上所述):

class CustomView: UIStackView {
    required init(coder: NSCoder) {
        super.init(coder: coder)
        distribution = .fill
        alignment = .fill
        if let nibView = Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)?.first as? UIView {
            addArrangedSubview(nibView)
        }
    }
}
注意,因为CustomView现在是UIStackView的子类,它会自动创建任何子视图所需的约束。 在这种情况下,由于distributionalignment都设置为.fill,因此它将将子视图的边缘固定到其自己的边缘,就像您想要的那样。
实际上,这里的关键问题是StackView的alignment属性为.center。 这意味着StackView中的标签将在垂直居中,但它不会将它们限制在StackView的顶部或底部。 这就像在视图内部添加一个子视图的centerY约束一样。 您可以将外层视图设置为任何高度,并且子视图将居中,但它不会“推出”外层视图的顶部和底部,因为中心约束仅约束中心,而不是顶部和底部边缘。
您需要将StackView的对齐方式设置为.fill,或者将标签的顶部和底部与StackView的顶部和底部之间添加其他约束,以便自动布局确定StackView应该有多高。
如果您按照上面的示例代码,将CustomView创建为UIStackView的子类,则不需要设置约束并禁用translatesAutoresizingMaskIntoConstraints。如果您希望CustomView自我调整大小正确,则需要为其提供约束并且不使用从调整大小掩模生成的自动生成约束。
总之,如果您遵循上述步骤,则自动布局应该正常工作,且带有StackView的自定义视图应按预期进行布局。

2
非常感谢您的回复 :) 我回到 iOS 后会尝试一下,非常感激! - martin
如果在Storyboard上添加自定义视图,这将起作用,但是如果您要以编程方式将自定义视图添加到现有的UIStackView中呢? - JCutting8
继承UIStackView而不是UIView是一个很好的想法,感谢您提供这个优秀的答案。 - alobaili
哇哦,为什么我之前没想到呢。我浪费了很多工作时间。这太重要了。在创建自定义视图时,我总是会忘记很多东西,但现在一切都改变了。特别感谢@daniel-hall。 - Sandeep Rana

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