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!