I have a complex view hierarchy, built in Interface Builder, with nested UIStackViews. I get \"unsatisfiable constraints\" notices every time I hide some of my inner stackviews.
Another approach
Try to avoid nested UIStackViews. I love them and build almost everything with them. But as I recognized that they secretly add constraints I try to only use them at the highest level and non-nested where possible. This way I can specify the 2nd highest priority .defaultHigh
to the spacing constraint which resolves my warnings.
This priority is just enough to prevent most layout issues.
Of course you need to specify some more constraints but this way you have full control of them and make your view layout explicit.
Here's implementation of Senseful's suggestion #3 written as Swift 3 class using SnapKit constraints. I also tried overriding the properties, but never got it working without warnings, so I'll stick with wrapping UIStackView:
class NestableStackView: UIView {
private var actualStackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame);
addSubview(actualStackView);
actualStackView.snp.makeConstraints { (make) in
// Lower edges priority to allow hiding when spacing > 0
make.edges.equalToSuperview().priority(999);
}
}
convenience init() {
self.init(frame: CGRect.zero);
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addArrangedSubview(_ view: UIView) {
actualStackView.addArrangedSubview(view);
}
func removeArrangedSubview(_ view: UIView) {
actualStackView.removeArrangedSubview(view);
}
var axis: UILayoutConstraintAxis {
get {
return actualStackView.axis;
}
set {
actualStackView.axis = newValue;
}
}
open var distribution: UIStackViewDistribution {
get {
return actualStackView.distribution;
}
set {
actualStackView.distribution = newValue;
}
}
var alignment: UIStackViewAlignment {
get {
return actualStackView.alignment;
}
set {
actualStackView.alignment = newValue;
}
}
var spacing: CGFloat {
get {
return actualStackView.spacing;
}
set {
actualStackView.spacing = newValue;
}
}
}
This is a known problem with hiding nested stack views.
There are essentially 3 solutions to this problem:
innerStackView.removeFromSuperview()
, but then you'll need to remember where to insert the stack view.The 3rd option is the best in my opinion. For more information about this problem, why it happens, the different solutions, and how to implement solution 3, see my answer to a similar question.
I hit a similar problem with UISV-hiding. For me, the solution was to reduce the priorities of my own constraints from Required (1000) to something less than that. When UISV-hiding constrains are added, they take priority and the constraints no longer clash.
Ideally we could just set the priority of the UISV-spacing
constraint to a lower value, but there doesn't appear to be any way to do that. :)
I am having success setting the spacing
property of the nested stack views to 0 before hiding, and restoring to the proper value after making it visible again.
I think doing this recursively on nested stack views would work. You could store the original value of the spacing
property in a dictionary and restore it later.
My project only has a single level of nesting, so I am unsure if this would result in FPS problems. As long as you don't animate the changes in spacing, I don't think it would create too much of a hit.
In my case I was adding width and height constraint to a navigation bar button, as per the advice above I only added lower priority to the constraints.
open func customizeNavigationBarBackButton() {
let _selector = #selector(UIViewController._backButtonPressed(_:))
let backButtonView = UIButton(type: .custom)
backButtonView.setImage(UIImage(named: "icon_back"), for: .normal)
backButtonView.imageEdgeInsets = UIEdgeInsets.init(top: 0, left: -30, bottom: 0, right: 0)
backButtonView.snp.makeConstraints { $0.width.height.equalTo(44).priority(900) }
backButtonView.addTarget(self, action: _selector, for: .touchUpInside)
let backButton = UIBarButtonItem(customView: backButtonView)
self.navigationItem.leftBarButtonItem = backButton
}