如何:在iOS中使用StackView View从XIB自我调整自定义视图

我最近再次开始使用Swift进入iOS开发,现在我正在研究定制的UIControl。 对于布局和灵活性,此自定义视图使用StackView构建。

我想要实现的目标……

…基本上只是具有与StackView相同的function,当我将它直接拖到故事板中时我通常拥有它 – 在那里它resize以适应没有问题。 这与自我调整tableview单元格基本相同。

橱窗

我可以将我的CustomView简化为一个简单的例子:它是一个StackView,里面有三个标签, Alignment设置为CenterDistribution Equal Centering 。 StackView将固定到左,右和顶部布局指南(或尾随,前导和顶部)。 我可以轻松地使用CustomView重新创建,从XIB加载转换为具有相同参数的CustomView – 我将它们作为屏幕截图附加。

Simple 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文件,覆盖布局子视图以使框架适应实际视图,就是这样。

问题

所以我的问题与通常如何接近有关。 我的StackView中的标签具有内在的内容大小,即,通常StackView只是适应标签的高度而不会抱怨 – 如果您在故事板中设计它。 但是,如果您将所有内容放在XIB中并保持布局原样,那么如果视图固定在三个方向(如顶部,前导,尾随),则IB会抱怨未知高度。

我阅读了自动布局指南并观看了WWDCvideo“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)) } 

基本上我确实拉伸视图以适应故事板中我的UIView的整个宽度,并限制它的顶部和底部到StackView的顶部和底部。

我使用这个时遇到了很多问题,XCode 8崩溃了,所以我不太清楚它是怎么做的。

(2)或者我应该覆盖intrinsicContentSize

我也可以通过检查所有arrangeSubviews的高度来简单地覆盖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://stackoverflow.com/a/24441368中的关键短语:

“通常,自动布局以自上而下的方式执行。换句话说,首先执行父视图布局,然后执行任何子视图布局。”

..但我真的不太确定。

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

假设我禁用了translatesAutoresizingMaskIntoConstraints那么我不能像IB中的tableview单元那样设置约束? 它总是转换为类似label.top = top的东西,因此标签将被拉伸而不是inheritance尺寸/高度的StackView。 另外,我无法将StackView固定到IB的IBIB容器视图中。

希望有人可以帮助我。 干杯!

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

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

假设我禁用了translatesAutoresizingMaskIntoConstraints,那么我不能像IB中的tableview单元那样设置约束? 它总是转换为类似label.top = top的东西,因此标签将被拉伸而不是inheritance尺寸/高度的StackView。 另外,我无法将StackView固定到IB的IBIB容器视图中。

在此示例中,您必须在代码中添加约束,因为您从nib加载任意视图,然后将其添加到编码的CustomView 。 笔尖中的视图没有预先知道它将被置于什么上下文或层次结构中,因此不可能提前定义如何将自身约束到未知的未来超视图。 对于故事板上的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 ,它会将子视图的边缘固定到其自己的边缘,如您所愿。

(4)为什么我必须首先这样做?

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

实际上,这里的关键问题是你的StackView的.center alignment属性。 这就是说StackView中的标签将垂直居中, 但它不会将它们限制在StackView的顶部或底部 。 这就像在视图内的子视图中添加centerY约束一样。 您可以将外部视图设置为所需的任何高度,子视图将居中,但它不会“推出”外部视图的顶部和底部,因为中心约束仅限制中心,而不是顶部和底部边缘。

在StackView的Storyboard版本中,StackView上方的视图层次结构是已知的,并且还有通过resize掩码自动生成约束的选项。 无论哪种方式,UIKit都知道最终的StackView高度,并且标签将在该高度内垂直居中,但实际上不会影响它。 您需要将StackView的对齐方式设置为.fill,或者将标签顶部和底部的其他约束添加到StackView的顶部和底部,以便自动布局以确定StackView的高度。

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

这没有太大帮助,因为内部内容大小本身不会推出父视图的边缘,除非子视图的边缘也被约束到父视图的边缘。 在任何情况下,只要您解决上面提到的.center对齐问题,您将自动获得所需的所有正确的内在内容大小行为。

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

这通常是一种有效的方法。 如上所述,如果您将UIStackView子类化,则不需要执行任何操作。

综上所述

如果你:

  1. 将NewView创建为UIStackView的子类,如上面的示例代码所示

  2. 在故事板上创建一个视图,设置为自定义类CustomView 。 如果希望CustomView正确地自我resize,则需要为其赋予约束,而不是使用resize掩码中的自动生成约束。

  3. 将笔尖中的StackView更改为将alignment设置为.fill或者在至少一个标签(最可能是大中心标签)的顶部和底部以及堆栈视图的顶部和底部之间设置其他约束

然后自动布局应该正常工作,您的自定义视图与StackView应按预期布局。