Determining when a view created programmatically did appear on screen - swift

I have a textField which, when tapped, pops up a vertical stackView created programatically. The stackView is a child of the textField. textField.addSubview(stackView) places the stackView's origin at the textField's origin. I wish to move the stackView's origin up vertically by an amount equal to the height of the stackView. If I do:
func textFieldDidBeginEditing(_ textField: UITextField) {
stackView = UIStackView() // var stackView: UIStackView!
stackView.axis = .vertical
stackView.alignment = .leading
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 0
stackView.backgroundColor = .white
stackView.layer.borderWidth = 1
stackView.layer.borderColor = UIColor.gray.cgColor
for title in titleList {
stackView.addArrangedSubview(createButton(title))
}
textField.addSubview(stackView)
stackView.transform = CGAffineTransform(translationX: 0, y: -stackView.frame.height)
}
It doesn't work. The stackView's origin remains at the textField's origin. But if I use a constant:
textField.addSubview(stackView)
stackView.transform = CGAffineTransform(translationX: 0, y: -144)
it works. I have confirmed through debugging that stackView.frame.height is 0 at the time the CGAffineTransform is performed. If I check its value after it has exited the function, I do see that its height is 144. That means the stackView hasn't actually appeared yet within the function.
Is there a way to catch when the programmatically-created stackView appears (sort of like the way viewDidAppear(_:) works) so that I can then set its position on the screen?

I don't think there is a canonical way to know when an individual view has appeared on screen. You might be able to hook into the draw method (which is hacky), but a stack view doesn't draw anything itself, so I don't think that works.
You could probably use UIViewController.viewDidLayoutSubviews, but you might need to add the stack view to the root view for that to work.
In any case, adding the UIStackView as a subview of the UITextField is bad practice. You probably want to add it to the parent view instead. And yes, using AutoLayout is the way to go here. Let the framework handle the layout, don't try to do it using transforms.

Related

How do I make a Swift UIStackView size dynamically based on content?

I have added the following UIStackView to my ViewController. As the views change the values of textOneLabel and textTwoLabel change.
With the following code the initial StackView is centered and the portions filled proportionally. However with subsequent text combinations the bounds of the StackView don't change, leaving the content off center. How can I change the StackView properties so it will adapt to the content and always stay centered?
headerStackView.axis = .horizontal
headerStackView.distribution = .fillProportionally
headerStackView.spacing = 8
headerStackView.layer.borderWidth = 1
headerStackView.layer.borderColor = UIColor.red.cgColor
headerStackView.translatesAutoresizingMaskIntoConstraints = false
headerStackView.topAnchor.constraint(equalTo: timerHeader.topAnchor, constant: 4).isActive = true
headerStackView.centerXAnchor.constraint(equalTo: timerHeader.centerXAnchor).isActive = true
headerStackView.addArrangedSubview(textOneLabel)
headerStackView.addArrangedSubview(textTwoLabel)
First, forget you ever heard of .fillProportionally...
it doesn't do what you think it does
you'll encounter very unexpected layout issues if your stack view has spacing greater than Zero
if your stack view has no width (neither width anchor nor leading/trailing anchors), .fillProportionally doesn't do anything
So, change your .distribution to .fill.
Add these lines to control what auto-layout does with your labels:
textOneLabel.setContentHuggingPriority(.required, for: .horizontal)
textOneLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
textTwoLabel.setContentHuggingPriority(.required, for: .horizontal)
textTwoLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
Now, your stackView FRAME will remain centered.

UIStackView and a placeholder view in another UIStackView problem

There is a problem if you have a UIStackView(testStack) and a placeholder UIView(testView) inside another UIStackView(mainStack). It is meant that if there is no content in the testStack it will collapse, and the testView will take all the space. There is even a content hugging priority set to maximum for the testStack so it should collapse its height to 0 when there are no subviews. But it does not. How to make it collapse when there is no content?
PS If there are items in the testStack, everything works as expected: testView takes all available space, testStack takes only the space to fit its subviews.
class AView: UIView {
lazy var mainStack: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.backgroundColor = .gray
stack.addArrangedSubview(self.testStack)
stack.addArrangedSubview(self.testView)
return stack
}()
let testStack: UIStackView = {
let stack = UIStackView()
stack.backgroundColor = .blue
stack.setContentHuggingPriority(.init(1000), for: .vertical)
return stack
}()
let testView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
init() {
super.init(frame: .zero)
backgroundColor = .yellow
addSubview(mainStack)
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.topAnchor.constraint(equalTo: topAnchor).isActive = true
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When auto-layout arranges subviews in a UIStackView, it looks at:
the stack view's .distribution property
the subviews' height constraints (if given)
the subviews' Intrinsic Content Size
Since you have not specified a .distribution property, mainStack is using the default of .fill.
A UIStackView has NO Intrinsic Content Size, so auto-layout says "testStack has a height of Zero"
A UIView has NO Intrinsic Content Size, so auto-layout says "testView has a height of Zero"
Since the distribution is fill, auto-layout effectively says: "the heights of the arranged subviews are ambiguous, so let's give the last subview a height of Zero, and fill mainStack with the first subview.
Setting .setContentHuggingPriority will have no effect, because there is no intrinsic height to "hug."
If you set mainStack's .distribution = .fillEqually, you will get (as expected) testStack filling the top half, and testView filling the bottom half.
If you set mainStack's .distribution = .fillProportionally, you will get the same result... testStack filling the top half, and testView filling the bottom half, because .fillProportionally uses the arranged subviews' Intrinsic Content Sizes... in this case, they are both Zero, so "proportional" will be equal.
If you set mainStack's .distribution = .equalSpacing or .distribution = .equalCentering, you won't see either testStack or testView ... auto-layout will give each of them a height of Zero, and fill the rest of mainStack with (empty) "spacing."
If your goal is to have testStack "disappear" if it is empty, you can either:
set it hidden, or
subclass it and give it an intrinsic height

How to programmatically set the constraints of the subViews of a UIPageViewController?

I have contained the subViews of a UIPageViewController within a UIView so that my screen has a partial scrollView container. However, the subViewControllers extend beyond both, the UIView that is supposed to contain the (horizontal/swiping page style) scrollView and the screen of the device.
I have already tried to use autolayout constraints but the subViews still go beyond the device screen.
Here is the UIView that contains the subViews of the UIPVC:
let pagingContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
and here is the set up within viewDidLoad():
let pageController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
addChild(pageController)
pageController.didMove(toParent: self)
pagingContainer.addSubview(pageController.view)
In case I haven't articulated properly:
What I wish for to happen is that the bottom half of my screen is a horizontal-page-style swiping scrollView that contains x number of subViewControllers (under UIPVC), and the size of subViewControllers are limited to the size of the UIView(pagingContainer).
I think I might understand what you're asking.
It should be pretty simple, set your left/right/top/bottom constraints for the pageController.view to be equal to the pagingContainer
In my example, I'm using SnapKit, so I set the edges equal to superview (which is the paingContainer).
let pageController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
self.addChild(pageController)
pageController.didMove(toParent: self)
pagingContainer.addSubview(pageController.view)
// I set up constraints with SnapKit (since I mostly use that pod)
pageController.view.snp.makeConstraints({ (make) in
make.edges.equalToSuperview()
})
// But if I remember correctly, you can also set it like so:
pageController.view.translatesAutoresizingMaskIntoConstraints = false
pageController.view.widthAnchor.constraint(equalTo: self.pagingContainer.widthAnchor).isActive = true
pageController.view.heightAnchor.constraint(equalTo: self.pagingContainer.heightAnchor).isActive = true
pageController.view.centerXAnchor.constraint(equalTo: self.pagingContainer.centerXAnchor).isActive = true
Here is a quick gif of what it looks like. Main view controller only has red background and a pagingContainer on the bottom half and inset of 30 on each side (to demonstrate the size of pageController being within the pagingContainer and not overflowing)

UIStackView Animation Issue

I have a subStackView inside a stackView and when I hide/show the contents of ONE subStackView, the animation goes all the way up over the other stack views: https://www.youtube.com/watch?v=vKXwX7OpkxU
This is how I create the subStackView. I tried with and without clipToBounds and with an without translatedAutoresizingMaskIntoConstraints. Also tried layoutIfNeeded in the animation part.
let subStackView = UIStackView(arrangedSubviews: [self.innerView[0], self.innerView[1])
subStackView.translatesAutoresizingMaskIntoConstraints = false
subStackView.axis = .vertical
subStackView.distribution = .fillEqually
subStackView.alignment = .fill
subStackView.spacing = 0
subStackView.clipsToBounds = true
This subStackView is then loaded into a mainStackView which results in the issue.
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?
}

Reduce space around a textfield using auto layout

I'm currently using a stackView that contains a few vertically aligned elements. I'm doing everything in code.
I've marked every stack view's subview. Here's a screenshot:
This is the code for the stackview:
func setUpVerticalLayoutStackView() {
stackViewForVerticalLayout = UIStackView(arrangedSubviews: [viewTitleButton, stackViewForTheTwoPersons, owedButton, amountTextField, doneButton])
stackViewForVerticalLayout.axis = .vertical
stackViewForVerticalLayout.distribution = .equalCentering
stackViewForVerticalLayout.alignment = .center
stackViewForVerticalLayout.spacing = 20
stackViewForVerticalLayout.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(stackViewForVerticalLayout)
}
My question is: why does the textfield occupy so much space? Can I reduce it somehow?
Don't mind the "r" at the center of the view, it's just something I mistakenly added in an image editor.
Change stackViewForVerticalLayout.distribution to .equalSpacing or .fillEqually depending on what you want. This will reduce this size of the field