UIStackView doesn't fill if removing and adding the same views - ios

I found an issue in iOS 9 where the layout is not working properly when removing and adding the same views to UIStackView and uses intrinsic content size of the views instead of the specified fillEqually distribution type.
For example, this code:
class ViewController: UIViewController {
let stackView = UIStackView()
let red = UIButton()
let blue = UIButton()
let green = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
stackView.distribution = .fillEqually
red.backgroundColor = .red
blue.backgroundColor = .blue
green.backgroundColor = .green
let views = [red, blue, green]
for view in views {
stackView.addArrangedSubview(view)
}
view.backgroundColor = .lightGray
view.addSubview(stackView)
stackView.frame = view.bounds.insetBy(dx: 0, dy: view.frame.height / 3)
let button = UIButton()
button.frame = view.bounds
view.addSubview(button)
button.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
}
func buttonClicked() {
for view in stackView.arrangedSubviews {
view.removeFromSuperview()
}
stackView.addArrangedSubview(red)
stackView.addArrangedSubview(blue)
}
}
Shows the following result after tapping the screen:
But on iOS 10 it shows the expected result:
Creating new views instead of reusing the old ones outputs the expected layout on iOS 9:
func buttonClicked() {
for view in stackView.arrangedSubviews {
view.removeFromSuperview()
}
let red = UIButton()
let blue = UIButton()
red.backgroundColor = .red
blue.backgroundColor = .blue
stackView.addArrangedSubview(red)
stackView.addArrangedSubview(blue)
}
But the whole point of this code is reusing views to improve performance.
Is this an issue in my code? A bug on iOS 9? Is there any known workaround?

for view in stackView.arrangedSubviews {
stackView.removeArrangedSubview(view)
view.removeFromSuperview()
}
I think you forget to call removeArrangedSubview.

Solved it by explicitly calling layoutIfNeeded method after removing all the views:
func buttonClicked() {
for view in stackView.arrangedSubviews {
view.removeFromSuperview()
}
stackView.layoutIfNeeded() // Required to avoid layout issues in iOS 9
stackView.addArrangedSubview(red)
stackView.addArrangedSubview(blue)
}

Related

UILabel not clickable in stack view programmatically created Swift

My question and code is based on this answer to one of my previous questions. I have programmatically created stackview where several labels are stored and I'm trying to make these labels clickable. I tried two different solutions:
Make clickable label. I created function and assigned it to the label in the gesture recognizer:
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag)
}
but it does not work. Then below the second way....
I thought that maybe the 1st way does not work because the labels are in UIStackView so I decided to assign click listener to the stack view and then determine on which view we clicked. At first I assigned to each of labels in the stackview tag and listened to clicks:
let tap = UITapGestureRecognizer(target: self, action: #selector(didTapCard(sender:)))
labelsStack.addGestureRecognizer(tap)
....
#objc func didTapCard (sender: UITapGestureRecognizer) {
(sender.view as? UIStackView)?.arrangedSubviews.forEach({ label in
print((label as! UILabel).text)
})
}
but the problem is that the click listener works only on the part of the stack view and when I tried to determine on which view we clicked it was not possible.
I think that possibly the problem is with that I tried to assign one click listener to several views, but not sure that works as I thought. I'm trying to make each label in the stackview clickable, but after click I will only need getting text from the label, so that is why I used one click listener for all views.
Applying a transform to a view (button, label, view, etc) changes the visual appearance, not the structure.
Because you're working with rotated views, you need to implement hit-testing.
Quick example:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
Assuming you're still working with the MyCustomView class and layout from your previous questions, we'll build on that with a few changes for layout, and to allow tapping the labels.
Complete example:
class Step5VC: UIViewController {
// create the custom "left-side" view
let myView = MyCustomView()
// create the "main" stack view
let mainStackView = UIStackView()
// create the "bottom labels" stack view
let bottomLabelsStack = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
guard let img = UIImage(named: "pro1") else {
fatalError("Need an image!")
}
// create the image view
let imgView = UIImageView()
imgView.contentMode = .scaleToFill
imgView.image = img
mainStackView.axis = .horizontal
bottomLabelsStack.axis = .horizontal
bottomLabelsStack.distribution = .fillEqually
// add views to the main stack view
mainStackView.addArrangedSubview(myView)
mainStackView.addArrangedSubview(imgView)
// add main stack view and bottom labels stack view to view
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
bottomLabelsStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomLabelsStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain Top/Leading/Trailing
mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
//mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// we want the image view to be 270 x 270
imgView.widthAnchor.constraint(equalToConstant: 270.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
// constrain the bottom lables to the bottom of the main stack view
// same width as the image view
// aligned trailing
bottomLabelsStack.topAnchor.constraint(equalTo: mainStackView.bottomAnchor),
bottomLabelsStack.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
bottomLabelsStack.widthAnchor.constraint(equalTo: imgView.widthAnchor),
])
// setup the left-side custom view
myView.titleText = "Gefährdung"
let titles: [String] = [
"keine / gering", "mittlere", "erhöhte", "hohe",
]
let colors: [UIColor] = [
UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
]
for (c, t) in zip(colors, titles) {
// because we'll be using hitTest in our Custom View
// we don't need to set .isUserInteractionEnabled = true
// create a "color label"
let cl = colorLabel(withColor: c, title: t, titleColor: .black)
// we're limiting the height to 270, so
// let's use a smaller font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .light)
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapRotatedLeftLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
// add the label to the custom myView
myView.addLabel(cl)
}
// rotate the left-side custom view 90-degrees counter-clockwise
myView.rotateTo(-.pi * 0.5)
// setup the bottom labels
let colorDictionary = [
"Red":UIColor.systemRed,
"Green":UIColor.systemGreen,
"Blue":UIColor.systemBlue,
]
for (myKey,myValue) in colorDictionary {
// bottom labels are not rotated, so we can add tap gesture recognizer directly
// create a "color label"
let cl = colorLabel(withColor: myValue, title: myKey, titleColor: .white)
// let's use a smaller, bold font for the left-side labels
cl.font = .systemFont(ofSize: 12.0, weight: .bold)
// by default, .isUserInteractionEnabled is False for UILabel
// so we must set .isUserInteractionEnabled = true
cl.isUserInteractionEnabled = true
// create a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTapBottomLabel(_:)))
// add the recognizer to the label
cl.addGestureRecognizer(t)
bottomLabelsStack.addArrangedSubview(cl)
}
}
#objc func didTapRotatedLeftLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Label in Rotated Custom View:", title)
// do something based on the tapped label/view
}
}
#objc func didTapBottomLabel (_ sender: UITapGestureRecognizer) {
if let v = sender.view as? UILabel {
let title = v.text ?? "label with no text"
print("Tapped Bottom Label:", title)
// do something based on the tapped label/view
}
}
func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
let newLabel = PaddedLabel()
newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
newLabel.backgroundColor = color
newLabel.text = title
newLabel.textAlignment = .center
newLabel.textColor = titleColor
newLabel.setContentHuggingPriority(.required, for: .vertical)
return newLabel
}
}
class MyCustomView: UIView {
public var titleText: String = "" {
didSet { titleLabel.text = titleText }
}
public func addLabel(_ v: UIView) {
labelsStack.addArrangedSubview(v)
}
public func rotateTo(_ d: Double) {
// get the container view (in this case, it's the outer stack view)
if let v = subviews.first {
// set the rotation transform
if d == 0 {
self.transform = .identity
} else {
self.transform = CGAffineTransform(rotationAngle: d)
}
// remove the container view
v.removeFromSuperview()
// tell it to layout itself
v.setNeedsLayout()
v.layoutIfNeeded()
// get the frame of the container view
// apply the same transform as self
let r = v.frame.applying(self.transform)
wC.isActive = false
hC.isActive = false
// add it back
addSubview(v)
// set self's width and height anchors
// to the width and height of the container
wC = self.widthAnchor.constraint(equalToConstant: r.width)
hC = self.heightAnchor.constraint(equalToConstant: r.height)
guard let sv = v.superview else {
fatalError("no superview")
}
// apply the new constraints
NSLayoutConstraint.activate([
v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC,
outerStack.widthAnchor.constraint(equalTo: sv.heightAnchor),
])
}
}
// our subviews
private let outerStack = UIStackView()
private let titleLabel = UILabel()
private let labelsStack = UIStackView()
private var wC: NSLayoutConstraint!
private var hC: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// stack views and label properties
outerStack.axis = .vertical
outerStack.distribution = .fillEqually
labelsStack.axis = .horizontal
// let's use .fillProportionally to help fit the labels
labelsStack.distribution = .fillProportionally
titleLabel.textAlignment = .center
titleLabel.backgroundColor = .lightGray
titleLabel.textColor = .white
// add title label and labels stack to outer stack
outerStack.addArrangedSubview(titleLabel)
outerStack.addArrangedSubview(labelsStack)
outerStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(outerStack)
wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)
NSLayoutConstraint.activate([
outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
wC, hC,
])
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// convert the point to the labels stack view coordinate space
let pt = labelsStack.convert(point, from: self)
// loop through arranged subviews
for i in 0..<labelsStack.arrangedSubviews.count {
let v = labelsStack.arrangedSubviews[i]
// if converted point is inside subview
if v.frame.contains(pt) {
return v
}
}
return super.hitTest(point, with: event)
}
}
class PaddedLabel: UILabel {
var padding: UIEdgeInsets = .zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: padding))
}
override var intrinsicContentSize : CGSize {
let sz = super.intrinsicContentSize
return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
}
}
The problem is with the the stackView's height. Once the label is rotated, the stackview's height is same as before and the tap gestures will only work within stackview's bounds.
I have checked it by changing the height of the stackview at the transform and observed tap gestures are working fine with the rotated label but with the part of it inside the stackview.
Now the problem is that you have to keep the bounds of the label inside the stackview either by changing it axis(again a new problem as need to handle the layout with it) or you have to handle it without the stackview.
You can check the observation by clicking the part of rotated label inside stackview and outside stackview.
Code to check it:
class ViewController: UIViewController {
var centerLabel = UILabel()
let mainStackView = UIStackView()
var stackViewHeightCons:NSLayoutConstraint?
var stackViewTopsCons:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
mainStackView.axis = .horizontal
mainStackView.alignment = .top
mainStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainStackView)
mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackViewTopsCons = mainStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300)
stackViewTopsCons?.isActive = true
stackViewHeightCons = mainStackView.heightAnchor.constraint(equalToConstant: 30)
stackViewHeightCons?.isActive = true
centerLabel.textAlignment = .center
centerLabel.text = "Let's rotate this label"
centerLabel.backgroundColor = .green
centerLabel.tag = 11
setTapListener(centerLabel)
mainStackView.addArrangedSubview(centerLabel)
// outline the stack view so we can see its frame
mainStackView.layer.borderColor = UIColor.red.cgColor
mainStackView.layer.borderWidth = 1
}
public func setTapListener(_ label: UILabel){
let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapGesture)
}
#objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
print(gesture.view?.tag ?? 0)
var yCor:CGFloat = 300
if centerLabel.transform == .identity {
centerLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
yCor = mainStackView.frame.origin.y - (centerLabel.frame.size.height/2)
} else {
centerLabel.transform = .identity
}
updateStackViewHeight(topCons: yCor)
}
private func updateStackViewHeight(topCons:CGFloat) {
stackViewTopsCons?.constant = topCons
stackViewHeightCons?.constant = centerLabel.frame.size.height
}
}
Sorry. My assumption was incorrect.
Why are you decided to use Label instead of UIButton (with transparence background color and border line)?
Also you can use UITableView instead of stack & labels
Maybe this documentation will help too (it is written that usually in one view better to keep one gesture recognizer): https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers

Using custom UIView class with Auto Layout returns incorrect bounds

I'm presenting a simple view controller. To follow MVC, I moved my programmatic view code into a separate UIView subclass. I followed the advice here of how to set up the custom view.
This works ok in iPhone. But, on iPad, the view controller is automatically presented with the new post-iOS 13 default modal style .pageSheet, which causes one of my buttons to be too wide. I looked into the view debugger and it's because the width constraint is set to self.bounds.width * 0.7, and self.bounds returns the full width of the iPad (1024 points) at the time the constraint is set, not the actual final view (which is only 704 points wide).
iPhone simulator screenshot
iPad 12.9" simulator screenshot
Three questions:
In general, am I setting up the custom view + view controller correctly? Or is there a best practice I'm not following?
How do I force an update to the constraints so that the view recognizes it's being presented in a .pageSheet modal mode on iPad, and automatically update the width of self.bounds? I tried self.view.setNeedsLayout() and self.view.layoutIfNeeded() but it didn't do anything.
Why is it that the Snooze button is laid out correctly on iPad, but not the Turn Off Alarm button... the Snooze button relies on constraints pinned to self.leadingAnchor and self.trailingAnchor. Why do those constraints correctly recognize the modal view they're being presented in, but self.bounds.width doesn't?
Code:
Xcode project posted here
View Controller using a custom view:
class CustomViewController: UIViewController {
var customView: CustomView {
return self.view as! CustomView
}
override func loadView() {
let customView = CustomView(frame: UIScreen.main.bounds)
self.view = customView // Set view to our custom view
}
override func viewDidLoad() {
super.viewDidLoad()
print("View's upon viewDidLoad is: \(self.view.bounds)") // <-- This gives incorrect bounds
// Add actions for buttons
customView.snoozeButton.addTarget(self, action: #selector(snoozeButtonPressed), for: .touchUpInside)
customView.dismissButton.addTarget(self, action: #selector(dismissButtonPressed), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
print("View's bounds upon viewDidAppear is: \(self.view.bounds)") // <-- This gives correct bounds
// This doesn't work
// //self.view.setNeedsLayout()
// // self.view.layoutIfNeeded()
}
#objc func snoozeButtonPressed() {
// Do something here
}
#objc func dismissButtonPressed() {
self.dismiss(animated: true, completion: nil)
}
}
Custom view code:
class CustomView: UIView {
public var snoozeButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Snooze", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.white.cgColor
button.layer.cornerRadius = 14
return button
}()
public var dismissButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Turn off alarm", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = UIColor.clear
button.layer.cornerRadius = 14
button.layer.borderWidth = 2
button.layer.borderColor = UIColor.white.cgColor
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
self.backgroundColor = .systemPurple
// Add subviews
self.addSubview(snoozeButton)
self.addSubview(dismissButton)
}
func setupConstraints() {
print("Self.bounds when setting up custom view constraints is: \(self.bounds)") // <-- This gives incorrect bounds
NSLayoutConstraint.activate([
dismissButton.heightAnchor.constraint(equalToConstant: 60),
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This is the constraint that's wrong
dismissButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
dismissButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
snoozeButton.heightAnchor.constraint(equalToConstant: 60),
snoozeButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
snoozeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
snoozeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -40)
])
}
}
.. and finally, the underlying view controller doing the presenting:
class BlankViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
let customVC = CustomViewController()
self.present(customVC, animated: true, completion: nil)
}
}
Thanks.
There's no need to reference bounds -- just use a relative width constraint.
Change this line:
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This gives incorrect bounds
to this:
dismissButton.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7),
Now, the button will be 70% of the view's width. As an added benefit, it will adjust itself if/when the view width changes, instead of being stuck at a calculated Constant value.

Button in UIStackView not clickable

I'm trying to add a button to my stack view. The button has a buttonTapped method that should be called when it is tapped. The problem is it is never being called, the button does not seem to be clickable.
class CustomButton: UIViewController {
var buttonDelegate: ButtonDelegate?
let button = UIButton(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width - 40, height: 30))
init(label: String) {
button.setTitle(label, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .systemBlue
super.init(nibName: nil, bundle: nil)
}
#objc func buttonTapped() {
print("this never gets printed")
buttonDelegate?.buttonTapped(buttonType: .submit)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
}
}
And then my main view controller:
protocol ButtonDelegate {
func buttonTapped(buttonType: ButtonType)
}
class DynamicViewController: UIViewController, ButtonDelegate {
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
lazy var contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private func setupViews() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(stackView)
let btn = CustomButton(label: "hi")
btn.buttonDelegate = self
self.stackView.addArrangedSubview(btn.view)
}
func buttonTapped(buttonType: ButtonType) {
print("also never gets printed")
}
}
There is nothing overlapping the button or anything like that:
My question is why the button is not clickable.
You are adding the view controller as a subview. So you also need to add as a child.
Add bellow code after self.stackView.addArrangedSubview(btn.view) this line.
self.addChild(btn)
btn.didMove(toParent: self)

iOS 13 navigationItem.titleView broken

So, I have this piece of code that until now (< iOS 12) just adds a blue rectangle in the UINavigationBar of a UINavigationController-embedded UIViewController. In iOS 13 it doesn't work. Is there something that I'm missing? Maybe because it has no given size? Or is there a new method for setting the NavigationBar's view?
navigationItem.titleView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
It looks like this (iOS 12 on the left, iOS 13 on the right):
Actually, while the animation is happening, it's visible but after that it disappears.
Edit:
If you set the titleView in viewDidAppear it works. But with a delay, not so elegant.
This is what I would like to do:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.titleView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
}
But what works, as mentioned, is:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationItem.titleView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
}

UIStackView's spacing vanishes when items are removed and re-added

If a UIStackView object contains three or more items, the spacing between the items remains normal when I remove the items from the stack view and then re-add them.
However, if the same UIStackView object contains two items, the spacing between them vanishes when I remove and re-add them.
What is causing this behavior and how can I fix it?
Here is a simple visual:
And here is the simple code associated with my simple visual:
import UIKit
class StackViewTest : UIViewController {
let stackView = UIStackView()
let stackViewLabels = [
//When I use three labels instead of two, everything works normally!
//UILabel(),
UILabel(),
UILabel()
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.whiteColor()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "reloadStackView"))
//initialize stackView
stackView.spacing = 30
stackView.axis = .Horizontal
stackView.distribution = .EqualSpacing
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor).active = true
//initialize stackView labels
var labelIndex = 0
for label in stackViewLabels {
label.text = "Label \(labelIndex++)"
}
//initialize tap count label
tapCountLabel.numberOfLines = 3
tapCountLabel.lineBreakMode = .ByWordWrapping
view.addSubview(tapCountLabel)
updateTapCount()
}
//this function is called each time the screen is tapped
func reloadStackView() {
for view in stackView.arrangedSubviews {
stackView.removeArrangedSubview(view)
view.removeFromSuperview()
}
for label in stackViewLabels {
stackView.addArrangedSubview(label)
}
updateTapCount()
}
//for updating the number of screen taps:
let tapCountLabel = UILabel(frame: CGRectMake(0, 30, 400, 90))
var screenTapCount = 0
func updateTapCount() {
tapCountLabel.text = "Tap Count: \(screenTapCount++)\nstackView.spacing: \(stackView.spacing)\n(tap the screen to reload the stackview)"
}
}

Resources