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.
Related
I have the following horizontal UIStackView
Yellow background
Leading, top and trailing same as safe area
Alignment = Top
Distribution = Fill Proportionally
UILabel as children
If I add UILabel as its children, it will automatically wrap height content as below
UIImage as children
I replace UILabel with 3 identical UIImageView.
Every UIImageView has the following properties
Content mode = Aspect Fit
It looks as following
I was wondering, how can I make the horizontal UIStackView wrap to its height content?
I expect the UIStackView's will wrap the UIImageView's content height. As an outcome, we will not observe any yellow background of UIStackView.
May I know, how can I achieve so?
So far, I have tried to play around with
Content hugging priority of both UIStackView and UIImageView
Content compression resistance priority for both UIStackView and UIImageView
Still not able to achieve my desired outcome.
The following is the original image source, which is 606x404
You cannot get this result with only Storyboard setup -- you will need some code.
Based on your image showing your desired output, you want each "row" to:
have 3 images
show them at original aspect-ratio
small spacing between images (such as 4-pts)
fit so the widths and heights maintain the ratios
First comment - forget Distribution = Fill Proportionally on the stack views.
For this layout, the stack view should be:
Axis: Horizontal
Alignment: Fill
Distribution: Fill
Spacing: 4
So, what we need to do is constrain each imageView's aspect ratio based on its image's aspect ratio (height / width).
Here is a full example... it uses these 6 images:
We'll also embed the horizontal stack view(s) in a vertical stack view, using the same settings (expect Axis: Vertical).
class AdjustStackViewController: UIViewController {
var vStack = UIStackView()
var picNames: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
// six images, named "p1" - "p6"
for i in 1...6 {
picNames.append("p\(i)")
}
// vertical stack view
vStack.axis = .vertical
vStack.spacing = 4
// use auto-layout
vStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(vStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain vStack
// Top + 20
// Leading and Trailing 0
vStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
])
fillStacks()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// re-fill images in new (shuffled) order
fillStacks()
}
func fillStacks() -> Void {
// remove existing horizontal stack views (if needed)
vStack.arrangedSubviews.forEach { v in
v.removeFromSuperview()
}
let shuffledImages = picNames.shuffled()
// two horizontal stack views each with 3 images
var picNum: Int = 0
for _ in 1...2 {
let hStack = UIStackView()
hStack.spacing = 4
vStack.addArrangedSubview(hStack)
for _ in 1...3 {
// make sure we can load the images
guard let img = UIImage(named: shuffledImages[picNum]) else {
fatalError("Could not load images!")
}
// create Image View
let imgView = UIImageView()
// set the image
imgView.image = img
// proportional constraint based on image dimensions
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: img.size.height / img.size.width).isActive = true
// add to hStack
hStack.addArrangedSubview(imgView)
// increment pic number
picNum += 1
}
}
}
}
Here's how it looks:
When you run this, each time you tap the view it will shuffle the order of the images, so you can see how the layout adapts.
I'm trying to use a custom view as an accessory view over the keyboard, for various reasons, in this case, it is much preferred over manual keyboard aligning because of some other features.
Unfortunately, this is a dynamic view that defines its own height. The constraints all work fine outside of the context of an accessoryView without errors, and properly resizing
When added as a keyboardAccessoryView it seems to impose a height of whatever the frame is at the time and break other height constraints
It appears as:
"<NSLayoutConstraint:0x600003e682d0 '_UIKBAutolayoutHeightConstraint' Turntable.ChatInput:0x7fb629c15050.height == 0 (active)>"
(where 0 would correspond to whatever height had been used at initialization
It is also labeled accessoryHeight which should make it easy to remove, but unfortunately, before I can do this, I'm getting unsatisfiable constraints and the system is tossing my height constraints
Tried:
in the inputAccessoryView override, I tried to check for the constraints and remove it, but it doesn't exist at this time
setting translatesAutoresizing...Constraints = false
tl;dr
Using a view as a KeyboardAccessoryView is adding its own height constraint after the fact, can I remove this?
Looks like keyboard doesn't like inputAccessoryView with height constraint. However you still can have inputAccessoryView with dynamic height by using frame (it is still possible to use constraints inside your custom inputAccessoryView).
Please check this example:
import UIKit
final class ViewController: UIViewController {
private let textField: UITextField = {
let view = UITextField()
view.frame = .init(x: 100, y: 100, width: 200, height: 40)
view.borderStyle = .line
return view
}()
private let customView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.frame.size.height = 100
view.autoresizingMask = .flexibleHeight // without this line height won't change
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(textField)
textField.inputAccessoryView = customView
textField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.customView.frame.size.height = 50
self.textField.reloadInputViews()
}
}
}
I am trying to use UIStackView with dynamic content on my project. The stackview is pinned to the leading & trailing of the container view. Why have I pinned it? Because Xcode will otherwise complained that I have to specify width or X position. But maybe this is not the right approach - let me know.
The stack view is currently configured with:
alignment: center (i've tried 'Filled' too)
distribution: equal spacing
Anyhow, the content of the stackview itself is created from a XIB file, which gets expanded into a custom UIView. This kind of works, but I'm having several issues with the layout:
When there are only a few items in the stackview, then they are sparsely distributed. Ideally I want the UIView (the orange button with the green area) to resize as big as possible to fill in these gaps
When there are a lot of items in the stackview, they are currently stacked on top of each other. This is an issue. The individual UIView should be resized to the biggest size that will make them fit horizontally in the UIStackview.
What I've done to add the UIView(s) to the stackview is as follow:
labelStackView.arrangedSubviews.forEach { (view) in
view.removeFromSuperview()
}
for (index, character) in model.modelName.enumerated() {
// CharacterPlaceholder extends UIView
let characterPlaceHolder = CharacterPlaceholder()
...
labelStackView.addArrangedSubview(characterPlaceHolder)
}
while CharacterPlaceholder roughly looks like below
class CharacterPlaceholder: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
customInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
customInit()
}
private func customInit(){
let nib = UINib(nibName: "CharacterPlaceholder", bundle: nil)
if let view = nib.instantiate(withOwner: self, options: nil).first as? UIView {
addSubview(view)
view.frame = self.bounds
}
}
I have uploaded the project to my github account:
https://github.com/alexwibowo/Flipcard
Any idea what I've done wrong? Should I grab the screensize at the start (or during screen rotation), and then manually calculate the width required for the buttons? That seems painful & very manual. I'm hoping that there is some autolayout magic that I can use here.
Thank you in advance for the help!
Edit: I gather that I need to use 'fill equally' for the distribution, so that the individual letter blocks will have the same size. But also, so that they are constrained to the available stackview space. However, now they can still overlap.
Ok.. I've figured a way to do this. First, as I've mentioned, I need to set the distribution to 'fill equally' so that each view in the stackview will be sized equally.
But doing that by itself is not enough.
I need to get the screen size. i.e. through
view.frame.width
Then, when I expand the xib file, i need to resize it manually using simple maths. E.g.:
let viewWidth = view.frame.width
let numberOfCharacters = model.modelName.count
let widthOfEach = Int(viewWidth) / numberOfCharacters
then the resizing part:
characterPlaceHolder.widthConstraint.constant = widthOfEachIncludingMargin
That widthConstraint is an outlet that I've set on the characterPlaceHolder view.
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
Assume I have a layout as on image below:
Root view - is a viewController's view. I'd like to have my footer text to be
sticked to bottom of screen, if content fits in one screen (main text is small)
have 15pt from main text & 15 pt from bottom of screen in other cases
I understand, that I can calculate main text heigt & compare it to current screen size, but I'd like to find another way (ideally only with constraints).
Is it possible?
To achieve what you are trying to do you need to set the following constraints:
ScrollView:
- top, leading, trailing and bottom equal to RootView's top, leading, trailing and bottom
WrapperView:
- top, leading, trailing and bottom equal to ScrollView's top, leading, trailing and bottom
- width equal to ScrollView's width
- height greaterThanOrEqual to ScrollView's height
TextView:
- top, leading and trailing equal to WrapperView's top, leading and trailing
FooterView:
- leading and trailing equal to WrapperView's leading and trailing
- bottom equal to WrapperView's bottom (with constant 15)
- top greaterThanOrEqual to TextView's bottom (with constant 15)
The key are the two constraints that use the greaterThanOrEqual relation: The WrapperView is at least as high as the ScrollView and the FooterView`s top has at least a distance of 15 from the TextView's bottom.
Here are the constraints when you use a Storyboard:
And this is how you would do it programatically (using SnapKit):
import UIKit
import SnapKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let scrollView = UIScrollView()
view.addSubview(scrollView)
let wrapperView = UIView()
wrapperView.backgroundColor = .cyan
scrollView.addSubview(wrapperView)
let textView = UILabel()
textView.numberOfLines = 0
textView.font = UIFont.systemFont(ofSize: 30)
textView.text = "You think water moves fast? You should see ice. It moves like it has a mind. Like it knows it killed the world once and got a taste for murder. After the avalanche, it took us a week to climb out. Now, I don't know exactly when we turned on each other, but I know that seven of us survived the slide... and only five made it out. Now we took an oath, that I'm breaking now. We said we'd say it was the snow that killed the other two, but it wasn't. Nature is lethal but it doesn't hold a candle to man."
//textView.text = "You think water moves fast? You should see ice. It moves like it has a mind."
textView.backgroundColor = .yellow
wrapperView.addSubview(textView)
let footer = UILabel()
footer.textAlignment = .center
footer.text = "Footer text"
footer.backgroundColor = .orange
wrapperView.addSubview(footer)
scrollView.snp.makeConstraints { (make) in
make.edges.equalTo(view)
}
wrapperView.snp.makeConstraints { (make) in
make.edges.equalTo(scrollView)
make.width.equalTo(scrollView)
make.height.greaterThanOrEqualTo(scrollView)
}
textView.snp.makeConstraints { (make) in
make.top.left.right.equalTo(0)
}
footer.snp.makeConstraints { (make) in
make.top.greaterThanOrEqualTo(textView.snp.bottom).offset(15)
make.left.right.equalTo(0)
make.bottom.equalTo(-15)
}
}
}
Screenshot with long text:
Screenshot with short text: