Remove custom spacing in UIStackView - ios

I have a stackview with three subviews:
stackView.spacing = 8.0
stackView.addArrangeSubview(view1)
stackView.addArrangeSubview(view2)
stackView.addArrangeSubview(view3)
At some point, I add custom spacing after view2:
stackView.setCustomSpacing(24.0, after: view2)
Now, I want to remove the custom spacing. Is this possible using UIStackView? The only way I can think of is
stackView.setCustomSpacing(8.0, after: view2)
but this will break if I ever change the spacing of my stackView to something other than 8.0, because spacing will remain 8.0 after view2.

You should re-create stackView with specific spacing and replace the old one with the new stackView.

Try this extension:
extension UIStackView {
// remove all custom spacing
func removeCustomSpacing() -> Void {
let a = arrangedSubviews
arrangedSubviews.forEach {
$0.removeFromSuperview()
}
a.forEach {
addArrangedSubview($0)
}
}
// remove custom spacing after only a single view
func removeCustomSpacing(after arrangedSubview: UIView) -> Void {
guard let idx = arrangedSubviews.firstIndex(of: arrangedSubview) else { return }
removeArrangedSubview(arrangedSubview)
insertArrangedSubview(arrangedSubview, at: idx)
}
}
Use it simply as:
myStackView.removeCustomSpacing()
or, if you are setting custom spacing on multiple views, and you want to remove it from only one view, use:
theStackView.removeCustomSpacing(after: view2)

let spacingView = UIView()
[View1, View2, spacingView, View3].forEach { view in
stackView.addArrangeSubview(view)
}
when you want to remove it just remove the spacingView from the array and you can play with width and height of the spacingView like you prefer

Related

How do you remove constraint centerXAnchor?

I have a button placed in the center using centerXAnchor of superview, but now I have to change the position of the button from centerX to align leading from code. However, it's not moving to the left. Instead, it gets full width button.
buttonView!.translatesAutoresizingMaskIntoConstraints = false
buttonView!.removeConstraints(buttonView!.constraints)
NSLayoutConstraint.activate([
buttonView!.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 12),
buttonView!.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 20),
])
Removing/add constraints doesn't cause them to be (re)applied.
Call .setNeedsUpdateConstraints() on your view. The system will then call updateConstraints as part of its next layout pass. For complex constraint scenarios you may need your own implementation of updateConstraints, but for most cases, and definitely yours, this won't be needed (and should generally be avoided unless there is a specific reason to use it - see the docs)
first remove all Constraint
extension UIView {
public func removeAllConstraints() {
var _superview = self.superview
while let superview = _superview {
for constraint in superview.constraints {
if let first = constraint.firstItem as? UIView, first == self {
superview.removeConstraint(constraint)
}
if let second = constraint.secondItem as? UIView, second == self {
superview.removeConstraint(constraint)
}
}
_superview = superview.superview
}
self.removeConstraints(self.constraints)
self.translatesAutoresizingMaskIntoConstraints = true
self.setNeedsUpdateConstraints()
}
}
to use it
DispatchQueue.main.async { [weak self] in
self?.buttonView.removeAllConstraints()
// then you can add your constraint as you like
}
ref
There are various ways to do this, depending on exactly what you need to accomplish.
First, you said you're laying out your views in Storyboard, so...
If we're talking about one (or a few) specific views, we can create #IBOutlet vars for the constraints you want to change.
In Storyboard:
give your buttonView a centerX constraint, with Priority: Required (1000)
give your buttonView your desired leading constraint, with Priority: Low (250)
Connect them to outlets:
#IBOutlet var buttonCenterConstraint: NSLayoutConstraint!
#IBOutlet var buttonLeadingConstraint: NSLayoutConstraint!
Then, to switch from centered to leading:
buttonCenterConstraint.priority = .defaultLow
buttonLeadingConstraint.priority = .required
and you can "toggle" it back to centered with:
buttonLeadingConstraint.priority = .defaultLow
buttonCenterConstraint.priority = .required
Perhaps do the same thing with centerY and bottom constraints.
If you want a little more "flexibility," you could do something like this:
in Storyboard, set only the centerX constraint
Then, to change that to a leading constraint:
// find the centerX constraint and de-activate it
if let cxConstraint = mainView.constraints.first(where: { ($0.firstAttribute == .centerX && $0.firstItem === buttonView) }) {
cxConstraint.isActive = false
} else if let cxConstraint = mainView.constraints.first(where: { ($0.firstAttribute == .centerX && $0.secondItem === buttonView) }) {
cxConstraint.isActive = false
}
// add a new leading constraint
buttonView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 12.0).isActive = true
You could also use an extension as suggested by someone else to "remove all constraints" ... but you risk removing constraints that you do not want changed.

Updating Main.storyboard constraints programatically (Swift)

I want to update the bottom anchor of my textview to a constant equal to the height of the keyboard when it appears so that it doesn't cover the text in the textView. I have a constraint identifier in Main.storyboard set as "bottomTextViewConstraint" for my textView, we well as the following code:
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
print("This prints")
for constraint in self.textView.constraints {
print("This does not print")
if constraint.identifier == "bottomTextViewConstraint" {
constraint.constant = keyboardSize.height
print("This does not print")
}
}
textView.updateConstraints()
}
}
}
self.textView.constraints is nil... It seems that programatically I can't access what I have set up in the storyboard. Any ideas why?
A constraint relating the bottom anchor of a text view and its super view's bottom anchor will be added to the text view's super view, so you cannot find it in textView.constraints.
A much simpler way to do this is to add an IBOutlet from the storyboard:
#IBOutlet var bottomConstraint: NSLayoutConstraint!
Right click on your view controller in the storyboard, then connect bottomConstraint to the constraint shown in the outline view:
Then you can remove your entire for loop and replace it with just:
bottomConstraint.constant = keyboardSize.height

Animate the height of a UIScrollView based on it's content

My situation:
I have a horizontal ScrollView containing a StackView.
Inside this StackView there are some Views, that can be expanded/collapsed.
When I want to expand one of these Views, I first unhide some subViews in the View. After that I need to change the height of the ScrollView based on the new height of this View.
But this is not working...
I try this code:
UIView.animate(withDuration: 0.3) { [self] in
// Toggle hight of all subViews
stackView.arrangedSubviews.forEach { itemView in
guard let itemView = itemView as? MyView else { return }
itemView.toggleView()
}
// Now update the hight of the StackView
// But here the hight is always from the previous toggle
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print(height)
heightConstraint.constant = height
}
This code nicely animates, but always to the wrong height.
So the ScrollView animates to collapsed when it should be expanded and expanded when it should be collapsed.
Anyone with on idea how to solve this?
The problem is that, whatever you are doing here:
itemView.toggleView()
may have done something to change the height a view, but then you immediately call:
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
before UIKit has updated the frames.
So, you can either track your own height property, or...
get the frame heights after the update - such as with:
DispatchQueue.main.async {
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print("h", height)
self.scrollHeightConstraint.constant = height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}

Iterate through all labels in a static table view

I have a static tableViews and a button to change the colour scheme (theme) of the app. Is there a fast way for me to change all the label colours throughout the table from white to black?
im thinking something like the following.
Pseudo code:
for subview in view.subviews {
if subview is UILabel {
subview.fontColor = .black
}
}
So when the orange switch is on the "Light" side I would like all labels to go black. I have used story board to construct it, so it would be nice if I could avoid having to connect all labels to the .swift file.
I am writing about code for your sudo code you need to iterate with recursion.
func getLabelsInView(view: UIView) -> [UILabel] {
var results = [UILabel]()
for subview in view.subviews as [UIView] {
if let label = subview as? UILabel {
results += [label]
} else {
results += getLabelsInView(view: subview)
}
}
return results
}
Call any where from you need to change color
let labels = getLabelsInView(self.view) // or any other view / table view
for label in labels {
label.textColor = .black
}

UIStackView children equally spaced (around and between)

How can I have an UIStackView with the same space as padding and gap between views?
How can I achieve this layout:
When this one doesn't suit me:
Neither does this:
I just need the around views space to be the same as the between views space.
Why is it so hard?
Important
I'm using my fork of TZStackView to support iOS 7. So no layoutMargins for me :(
I know this is an older question, but the way I solved it was to add two UIViews with zero size at the beginning and end of my stack, then use the .equalSpacing distribution.
Note: this only guarantees equal around spacing along the main axis of the stack view (i.e. the left and right edges in my example)
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .equalSpacing
// add content normally
// ...
// add extra views for spacing
stack.insertArrangedSubview(UIView(), at: 0)
stack.addArrangedSubview(UIView())
You can almost achieve what you want using a UIStackView. When you set some constraints yourself on the UIViews inside the UIStackView you can come up with this:
This is missing the left and right padding that you are looking for. The problem is that UIStackView is adding its own constraints when you add views to it. In this case you can add top and bottom constraints to get the vertical padding, but when you try to add a trailing constraint for the right padding, UIStackView ignores or overrides that constraint. Interestingly adding a leading constraint for the left padding works.
But setting constraints on UIStackView's arranged subviews is not what you want to do anyway. The whole point of using a UIStackView is to just give it some views and let UIStackView handle the rest.
To achieve what you are trying to do is actually not too hard. Here is an example of a UIViewController that contains a custom stack view that can handle padding on all sides (I used SnapKit for the constraints):
import UIKit
import SnapKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let padding: CGFloat = 30
let customStackView = UIView()
customStackView.backgroundColor = UIColor(white: 0, alpha: 0.1)
view.addSubview(customStackView)
customStackView.snp_makeConstraints { (make) -> Void in
make.top.left.equalTo(padding)
make.right.equalTo(-padding)
}
// define an array of subviews
let views = [UIView(), UIView(), UIView()]
// UIView does not have an intrinsic contentSize
// so you have to set some heights
// In a real implementation the height will be determined
// by the views' content, but for this example
// you have to set the height programmatically
views[0].snp_makeConstraints { (make) -> Void in
make.height.equalTo(150)
}
views[1].snp_makeConstraints { (make) -> Void in
make.height.equalTo(120)
}
views[2].snp_makeConstraints { (make) -> Void in
make.height.equalTo(130)
}
// Iterate through the views and set the constraints
var leftHandView: UIView? = nil
for view in views {
customStackView.addSubview(view)
view.backgroundColor = UIColor(white: 0, alpha: 0.15)
view.snp_makeConstraints(closure: { (make) -> Void in
make.top.equalTo(padding)
make.bottom.lessThanOrEqualTo(-padding)
if let leftHandView = leftHandView {
make.left.equalTo(leftHandView.snp_right).offset(padding)
make.width.equalTo(leftHandView)
} else {
make.left.equalTo(padding)
}
leftHandView = view
})
}
if let lastView = views.last {
lastView.snp_makeConstraints(closure: { (make) -> Void in
make.right.equalTo(-padding)
})
}
}
}
This produces the following results:
For those who keep getting here looking for a solution for this problem. I found that the best way (in my case) would be to use a parent UIView as background and padding, like this:
In this case the UIStackView is contrained to the edges of the UIView with a padding and separate the subviews with spacing.

Resources