One way to fix your problem is to control more directly how the purple view is shown and hidden. What you're doing now (I assume) is setting isHidden
property to true and then letting the stack view do whatever it wants. Instead, let's put the purple view inside a container view, and animate the container view's height down to zero. Then it can look like this:

The reason to use a container view instead of just animating the purple view's height directly is that you might (in general) have other constraints controlling the purple view's height, so also constraining its height to zero would fill up your console with unsatisfiable constraint errors.
So here's what I did for the demo. I made a “Hello, world!” label with a purple background. I constrained its height to 80. I put the label inside a container view (just a plain UIView
). I constrained the top, leading, and trailing edges of the label to the container view, as normal. I also constrained the bottom edge of the label to the container view, but at priority 999* (which is less than the default, “required” priority of 1000). This means that the container view will try very hard to be the same size as the label, but if the container view is forced to change height, it will do so without affecting the label's height.
The container also has clipsToBounds
set, so if the container becomes shorter than the label, the bottom part of the label is hidden.
To toggle the visibility of the label, I activate or deactivate a required-priority height constraint on the container view that sets its height to zero. Then I ask the window to lay out its children, inside an animation block.
In my demo, I also have the stack view's spacing
set to 12. If I just leave the container view “visible” (not isHidden
) with a height of zero, the stack view will put 12 points of space after the button, which can look incorrect. On iOS 11 and later, I fix this by setting a custom spacing of 0 after the button when I “hide” the container, and restore the default spacing when I “show” it.
On iOS version before iOS 11, I just go ahead and really hide the container (setting its isHidden
to true) after the hiding animation completes. And I show the container (setting its isHidden
to false) before running the showing animation. This results in a little bump as the spacing instantly disappears or reappears, but it's not too bad.
Handling the stack view spacing makes the code substantially bigger, so if you're not using spacing in your stack view, you can use simpler code.
Anyway, here's my code:
class TaskletViewController: UIViewController {
@IBAction func buttonWasTapped() {
if detailContainerHideConstraint == nil {
detailContainerHideConstraint = detailContainer.heightAnchor.constraint(equalToConstant: 0)
}
let wantHidden = !(detailContainerHideConstraint?.isActive ?? false)
if wantHidden {
UIView.animate(withDuration: 0.25, animations: {
if #available(iOS 11.0, *) {
self.stackView.setCustomSpacing(0, after: self.button)
}
self.detailContainerHideConstraint?.isActive = true
self.view.window?.layoutIfNeeded()
}, completion: { _ in
if #available(iOS 11.0, *) { } else {
self.detailContainer.isHidden = true
}
})
} else {
if #available(iOS 11.0, *) { } else {
detailContainer.isHidden = false
}
UIView.animate(withDuration: 0.25, animations: {
if #available(iOS 11.0, *) {
self.stackView.setCustomSpacing(self.stackView.spacing, after: self.button)
}
self.detailContainerHideConstraint?.isActive = false
self.view.window?.layoutIfNeeded()
})
}
}
override func loadView() {
stackView.axis = .vertical
stackView.spacing = 12
stackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.green.withAlphaComponent(0.2)
button.setTitle("Tap to toggle", for: .normal)
button.addTarget(self, action: #selector(buttonWasTapped), for: .touchUpInside)
button.setContentHuggingPriority(.required, for: .vertical)
button.setContentCompressionResistancePriority(.required, for: .vertical)
stackView.addArrangedSubview(button)
detailLabel.translatesAutoresizingMaskIntoConstraints = false
detailLabel.text = "Hello, world!"
detailLabel.textAlignment = .center
detailLabel.backgroundColor = UIColor.purple.withAlphaComponent(0.2)
detailLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true
detailContainer.translatesAutoresizingMaskIntoConstraints = false
detailContainer.clipsToBounds = true
detailContainer.addSubview(detailLabel)
let bottomConstraint = detailLabel.bottomAnchor.constraint(equalTo: detailContainer.bottomAnchor)
bottomConstraint.priority = .init(999)
NSLayoutConstraint.activate([
detailLabel.topAnchor.constraint(equalTo: detailContainer.topAnchor),
detailLabel.leadingAnchor.constraint(equalTo: detailContainer.leadingAnchor),
detailLabel.trailingAnchor.constraint(equalTo: detailContainer.trailingAnchor),
bottomConstraint
])
stackView.addArrangedSubview(detailContainer)
self.view = stackView
}
private let stackView = UIStackView()
private let button = UIButton(type: .roundedRect)
private let detailLabel = UILabel()
private let detailContainer = UIView()
private var detailContainerHideConstraint: NSLayoutConstraint?
}