StackView returns the ex-height in viewDidLayoutSubviews - swift

So I have a scrollView, inside that scrollView is a stackView that contains every views in the screen.
The problem is my stackView has many hide-able views so I need to adjust my containsVieww's height base on my stackView's height
my psuedo code should be like this :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(stackView.bounds.height)
// do logic to change contentView size here
}
func setupView(){
scrollView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
scrollView.backgroundColor = .white
scrollView.contentSize = CGSize(width: view.frame.width, height: view.frame.height)
scrollView.addSubview(stackView)
stackView.setArrangedSubView(views: [label1, label 2, label 3, label 4, ..., label n]
[label1, label 2, label 3].isHidden = true
}
func onHitButton(){
if isHidden {
[label1, label 2, label 3].isHidden = false
isHidden = false
} else {
[label1, label 2, label 3].isHidden = true
isHidden = true
}
print(stackView.bounds.height) // still return the ex height
}
Here is the problem:
On first init, my [label1,2,3].isHidden = true, my stackViewHeight is 500
When my onHitButton is called, my [label1,2,3].isHidden = false, my stackViewHeight is still 500 but the screen displays correctly, those labels are visible now and my stackView is stretched. And of course my scrollView is not displays correctly.
Then I hit my onHitButton again, those labels are hidden, my stackView is shrink on screen but the stackViewHeight returned 850?
It's supposed to be the other way around.
I also tried to print the height on another button call and it return the right height? Seem like viewDidLayoutSubviews called too early.
To sum it up: stackView return the height before it resize it self

Use auto-layout to constrain the stack view to the scroll view's Content Layout Guide:
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
// reference to scrollView's Content Layout Guide
let cGuide = scrollView.contentLayoutGuide
// reference to scrollView's Frame Layout Guide
let fGuide = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stackView to scrollView's Content Layout Guide
stackView.topAnchor.constraint(equalTo: cGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: cGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: cGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: cGuide.bottomAnchor),
// constrain stackView's Width to scrollView's Frame Layout Guide
stackView.widthAnchor.constraint(equalTo: fGuide.widthAnchor),
])
This will completely avoid the need to ever set .contentSize -- it will all be handled by auto-layout.

Related

Why does NSScrollView display a white track?

I am programming with macos, in swift. I have a few paragraphs of text (Lopem Ipsum - to test) inside a scroll view. Also a title, which is a NSTextView, positioned above.
When I present it in a popover, all looks fine.
However in another part of the project, I present a the same contents (using a copy of the same layout code - below), but this time in a modal window.
It has a ugly white track! That must be connected to the scroll view, since it only appears when I set the vertical scrollbar - hasVerticalScroller, which is necessary because I want vertical scrolling. As it should, the knob highlights on mouse-hover and it scolls properly.
It probably would be fine with a solid white background. But that's not what I want! Thus I have the setting scrollView.drawsBackground = false.
I've tried: scrollView.verticalScroller?.highlight(false); scrollView.verticalScroller?.wantsLayer = true; scrollView.verticalScroller?.layer?.backgroundColor = NSColor.clear.cgColor - no luck!
Even tried with only one paragraph (ie. no scroll), still the same.
I am using code similar to this:
class ViewController: NSViewController {
let scrollView = NSScrollView()
let textView = NSTextView()
override func viewDidLoad() {
super.viewDidLoad()
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.autoresizingMask = .width
textView.isVerticallyResizable = true
textView.textContainer?.widthTracksTextView = true
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView = textView
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
What can I do to have a clear (no background) track on the scrollbar?
scrollView.scrollerStyle = .overlay

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

iOS - Constraint to UIImage with centrally aligned UILabel's text

I have UITableViewCell that has a UIlabel aligned center I'm setting image in default imageView property of the UITableViewCell but since text is aligned center there is a gap between text and the image.
I want image then little space then text all center to UITableViewCell I have tried following code,
cell.imageView?.image = image
cell.imageView?.translatesAutoresizingMaskIntoConstraints = false
cell.imageView?.centerYAnchor.constraint(equalTo: label.centerYAnchor).isActive = true
let rect: CGRect = label.textRect(forBounds: label.bounds, limitedToNumberOfLines: 1)
cell.imageView?.leadingAnchor.constraint(equalTo: label.leadingAnchor, constant: rect.origin.x - padding).isActive = true
That works for me but when I switch device from iPhone 11 Max Pro to iPhone 8 image overlaps the text because label.textRect always brings the same text irrespective of screen size
I have also tried using range of the first later and using it's rect but same problem of not being changed per screen size.
Can this be achieved without putting custom UIImageView in UITableViewCell?
You could use a stackView that you center inside your cell and add your imageView and your label as arranged subViews. Note that you would need to create a custom cell.
Create your stackView:
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 10 // You can set the spacing accordingly
return stackView
}()
Layout as follows:
contentView.addSubview(stackView)
// Swap these two lines if instead you want label then image
stackView.addArrangedSubview(image)
stackView.addArrangedSubview(label)
// StackView
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
stackView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true

Snapkit centerY constraint centers item above the center Y axis

I'm trying to make a custom UICollectionView cell class. The cell consists of a content view and a label. I want the label to be in the center of the view, horizontally and vertically, but instead the label is placed above the content view's center y axis.
I've made sure that the constraints are set, no other constraints are being set, and that the issue affects all views in the content view (I added another view and set its center Y axis as a test, and that also didn't work). I also set the content view and the label's background colors to be contrasting, and have confirmed that the label is not lying on the content view's center y anchor.
Here is how I set the consraints:
label.snp.makeConstraints{make in
make.centerX.centerY.equalToSuperview()
}
Here is what I get instead. Clearly the label is not centered vertically. You can see the blue UIView, which I added as a test, is also not centered vertically.
I used to add my constraints programmatically in this way
self.view.addSubview(image)
image.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
image.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
image.heightAnchor.constraint(equalToConstant: 30).isActive = true
image.widthAnchor.constraint(equalToConstant: 30).isActive = true
and my image is declarated in this way
let image: UIImageView = {
let theImageView = UIImageView()
theImageView.image = UIImage(named: "ico_return")
theImageView.translatesAutoresizingMaskIntoConstraints = false
return theImageView
}()
Hope it helps
Can you try Following Code.
class FilterCollectionViewCell: UICollectionViewCell {
let labelTemp = UILabel()
override func awakeFromNib() {
labelTemp.backgroundColor = .white
labelTemp.textColor = .black
labelTemp.text = "testing"
self.contentView.addSubview(labelTemp)
labelTemp.snp.makeConstraints { (make) in
make.centerX.centerY.equalTo(self.contentView)
}
}
}
Fast and easy:
myLabel.snp.makeConstraints { (make) in
make.center.equalTo(self.topView.snp.center)
}

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?
}