I've recently started diving into iOS development with Swift again and am now looking into a custom UIControl. For layout and flexibility this custom view is built using a StackView.
What I want to achieve ...
... is basically to simply have the same functionality with the StackView that I usually have when I drag it directly into the storyboard - where it resizes to fit no problem. This essentially the same as self-sizing tableview cells.
Showcase
I can reduce my CustomView to a simple example: It's a StackView with three labels in it, Alignment
set to Center
, Distribution
Equal Centering
. The StackView would be pinned to left, right and top layout guides (or trailing, leading and top). This I can easily re-create with a CustomView, loaded from XIB translate into a CustomView with the same parameters - I attached both as screenshot.
Screenshot of Simple StackView
Loading the XIB File and setting it up.
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)
}
}
What you can see here is that I simply load the XIB file, override layout Subviews to adapt the frame to the actual view and that's it.
Questions
So my questions are related to how this is usually approached. The labels in my StackView have an intrinsic content size, i.e., usually the StackView simply adapts to the height of the labels without complaining - IF you design it in the storyboard. It doesn't though, if you put all of it in a XIB and leave the layout as it is - the IB will complain about an unknown height if the view is pinned three ways as in the screenshot (top, leading, trailing).
I read the Auto-Layout Guide and watched the WWDC videos "Mysteries of AutoLayout" but the topic is still quite new to me and I don't think I got the right approach yet. So this is where I need help
(1) Should I set constraints and disable translatesAutoresizingMaskIntoConstraints
?
What I first could and should do is to set translatesAutoresizingMaskIntoConstraints
false
such that I don't get auto-generated constraints and define my own constraints. So I could change loadNib
into this:
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))
}
Basically I did stretch the view to fit the full width of my UIView in the Storyboard and restrained it's top and bottom to top and bottom of the StackView.
I had tons of problems with XCode 8 crashing on me when using this one so I'm not quite sure what to make of it.
(2) Or should I override intrinsicContentSize
?
I could also simply override intrinsicContentSize
by checking the height of all my arrangedSubviews
and take the one that is the highest, set my view height to it:
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
}
While this surely gives me more flexibility I'd also have to invalidate the intrinsic content size, check the Dynamic Type and lots of other stuff I'd rather avoid.
(3) Am I even loading the XIB in the "proposed" way ?
Or is there a better way, e.g. by using UINib.instantiate
? I really couldn't find a best-practice way to do that.
(4) Why do I have to do this in the first place?
Really, why? I only moved the StackView into a CustomView. So why does the StackView stop adapting itself now, why do I have to set the top and bottom constraints?
What was helping the most so far is that key-phrase in https://stackoverflow.com/a/24441368 :
"In general, auto layout is performed in a top-down fashion. In other words, a parent view layout is performed first, and then any child view layouts are performed."
.. but I'm really not so sure about it.
(5) And why do I have to add the constraints in code?
Assuming I disable translatesAutoresizingMaskIntoConstraints
then I can't set constraints like for tableview cells in IB? It'll always translate into something like label.top = top and thus the label would be stretched instead of the StackView inheriting the size/height. Also I can't really pin the StackView to it's container view in the IB for the XIB file.
Hope someone can help me out there. Cheers!
Here are some answers to your questions, although in a different order:
You have to add the constraints in code, in this example, because you are loading an arbitrary view from a nib, and then adding it to a your coded
CustomView
. The view in the nib has no pre-existing knowledge of what context or hierarchy it will be placed into, so it can't possible defined ahead of time how to constrain itself to an unknown future superview. In the case of the StackView on the storyboard, or inside a prototype cell, that context is known as well as its parent view.One nice alternative you can consider for this case, if you don't want to manually write constraints, is to make your CustomView subclass UIStackView instead of UIView. Since UIStackView generates appropriate constraints automatically, it will save you the manual coding. See next answer for some sample code.
I'm not aware of a "standard" or "right" way to handle loading a nib into a custom view. I've seen a variety of approaches that all work. However, I will note that you are doing more work than needed in your sample code. Here is a more minimal way of accomplishing the same outcome, but also using a UIStackView subclass instead of a UIView subclass (as mentioned above):
Note that because CustomView is now a subclass of UIStackView, it will create the constraints needed for any subviews automatically. In this case, because
distribution
andalignment
are both set to.fill
, it will pin the edges o the subview to its own edges, as you want.Actually, the key issue here is your StackView's
alignment
property of.center
. That is saying that the labels in StackView will be centered vertically, but it does not constrain them to the top or the bottom of the StackView. It's just like adding a centerY constraint to a subview inside a view. You can set the outer view to any height you want, and the subview will be centered, but it does not "push out" the top and bottom of the outer view, because center constraints only constrain centers, not top and bottom edges.In your storyboard version of the StackView, the view hierarchy above that StackView is known, and there are also options to generate constraints automatically from resizing masks. Either way, the final StackView heigh is known to UIKit and the labels will be centered vertically within that height, but will not actually effect it. You need to either set the alignment of your StackView to be .fill, or add additional constraints from the tops and bottoms of the labels to the top and bottom of the StackView in order for auto layout to determine how tall the StackView should be.
This wouldn't help much, because the intrinsic content size by itself won't push out the edges of a parent view unless the edges of the subview are also constrained to the edges of the parent view. In any event, you will get all the correct intrinsic content size behavior you need in this case automatically, so long as you address the
.center
alignment issue mentioned above.This would normally be a valid approach. If you subclass UIStackView as I describe above, though, you don't need to do either.
In summary
If you:
Create your CustomView as a subclass of UIStackView as in the sample code above
Create a view on your storyboard set to your custom class
CustomView
. If you want the CustomView to self-size correctly, you need to give it constraints and not use auto-generated constraints from the resizing mask.Change the StackView in your nib to either have the
alignment
set to.fill
or to have additional constraints between the top and bottom of at least one of the labels (most likely the large center one) and the top and bottom of the stack viewThen auto layout should work properly and your custom view with the StackView should layout as expected.