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:
Related
I have a horizontal stackView inside a vertical stackView. The problem is that i want to register a UITapGesture on the whole row, not just the elements inside. Since the parent(vertical) stackView 's alignment = .leading, it causes the remaining width not occupied of my stackView to become unresponsive to clicks.
In essence, i'd like for the whole row to be clickable and not just the elements inside of it. I've set the horizontal bg color to black to give an idea of the clickable width.
Thanks in advance.
Here is the general idea of my code:
// Parent vertical stackView
private(set) var optionStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .leading
return stackView
}()
// Child horizontal stackView inside optionStackView
private(set) var takePictureStackView: UIStackView = {
let stackView = UIStackView()
stackView.backgroundColor = .black
return stackView
}()
...
takePictureStackView.addArrangedSubview(takePictureImageView)
takePictureStackView.addArrangedSubview(takePictureLabel)
optionStackView.addArrangedSubview(takePictureStackView)
...
// Attaching tapGesture recognizer from VC
takePictureStackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.takePicture)))
Add a constraint for width of takePictureStackView to be equal to the width of optionStackView.
This will expand takePictureStackView horizontally and will increase the tappable area.
I've got a horizontal scroll view with content as follows:
When I run it scrolls horizontally partially - I can scroll until about half of the red view is visible but then it bounces back.
How can I get it so that it can scroll all the way so all the red view is visible and then stays there?
I have this in the view controller, but it makes no difference if there or not.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("Content view bounds: \(contentView.bounds)")
scrollView.layoutIfNeeded()
scrollView.contentSize = contentView.bounds.size
}
In the screenshot of the storyboard there is no trailing edge constraint for the red view, however if I add one between the red view and the content view then when I run on the device it stops scrolling and looks like this:
You shouldn't need to be explicitly setting the .contentSize to begin with -- you can let auto-layout handle it all for you.
First, delete your Content View.width = width constraint:
Having that constraint told auto-layout to make your contentView only as wide as the scroll view, so you wouldn't get any horizontal scrolling. By explicitly setting the .contentSize you got some scrolling, but as you found it didn't give you what you wanted.
After deleting that constraint, add a 20-pt trailing constraint from Red View to the trailing edge of the content view:
Now, you have a complete chain of horizontal constraints...
- Blue View.leading = leading + 20
- Blue View Width
- Red View.leading = Blue View.trailing + 40
- Red View Width
- trailing = Red View.trailing + 20
This satisfies auto-layout and properly defines the width of Content View... and since Content View is constrained to leading and trailing of your Scroll View, you get correct horizontal scrolling.
No need for any code.
I got it to work by adding the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scrollView.contentSize = CGSize(width: 750,height: 812)
}
But why does the above work but not the following?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("Content view bounds: \(contentView.bounds)")
scrollView.layoutIfNeeded()
scrollView.contentSize = contentView.bounds.size
}
At runtime contentView.bounds is (0.0, 0.0, 750.0, 812.0)
How should I setup auto layout constrains so that multiline label stays vertically centered inside scrollview until it's text content becomes too long to be shown at once? When the text length becomes too long text should be aligned at top with the scrollview so the user can see the beginning of text and scroll for more. This is how I tried to setup constrains
for scrollView:
Equal Height to: Superview
Align Trailing to: Safe Area, Equals = -8
Align Leading to: Safe Area, Equals = 8
Align Top to: Safe Area
for label:
Leading Space to: Superview
Equal Width to: Superview
Align Center Y to: Superview
I also added following code to viewDidLoad()
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
The problem is that but I still get some warnings and also text ends up "vertically centered" inside scrollview without possibility to really scroll to the beginning or the end of it, even when it can not fit whole inside. For scrollview I get warning that it "has ambiguous scrollable content width", while for the label I get warning "trailing constraint is missing, which may cause overlapping with other views"
How about constraining the label height to the height of the scrollView's superview? In that case the label will be always as big as the screen where it is presented, and since by default the text in a UILabel is centered vertically, you would get what you want. See the following Playgrounds example for reference:
import PlaygroundSupport
import UIKit
class A: UIViewController {
let scrollView = UIScrollView()
let label = UILabel()
override func viewDidLoad() {
self.view.backgroundColor = .white
self.view.addSubview(scrollView)
self.scrollView.addSubview(label)
label.numberOfLines = 0
label.text = "How should I setup auto layout constrains so that multiline label stays vertically centered inside scrollview until it's text content becomes too long to be shown at once?"
scrollView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
scrollView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor),
label.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor),
label.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor),
label.topAnchor.constraint(equalTo: self.scrollView.topAnchor),
label.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor),
label.heightAnchor.constraint(greaterThanOrEqualTo: self.view.heightAnchor),
])
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = A()
I would like to setup a UIViewStack so that it will center the two views inside, even though they have different widths. Here is an example:
Is it possible to achieve this type of configuration with UIStackView? I cannot seem to figure it out!
Any help would be appreciated.
You should use nested StackView. Firstly embed View1 and View2 in a Horizontal StackView. Set alignment property center and distribution fill-proportionally. Then embed the Horizontal StackView in a Vertical Stackview. Here I have attached my demo screenshot:
No , you can't . From the apple's Doc
The stack view uses Auto Layout to position and size its arranged views. The stack view aligns the first and last arranged view with its edges along the stack’s axis. In a horizontal stack, this means the first arranged view’s leading edge is pinned to the stack’s leading edge, and the last arranged view’s trailing edge is pinned to the stack’s trailing edge.
You can use Constraints instead.
I can make UIStackView auto grow width after adding views there
Create UIViewController and give UIStackView be center there
Constraint
StackView.leading Priority set to 250 to avoid warning issue in xib
Codes
class StackSampleViewController: UIViewController {
#IBOutlet weak var stackView: UIStackView!
//Keep center auto grow with subviews
#IBAction func touchUpAdd(_ sender: Any) {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
if (stackView.subviews.count % 2) == 0 {
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
view.backgroundColor = .black
} else {
view.widthAnchor.constraint(equalToConstant: 50).isActive = true
view.backgroundColor = .red
}
stackView.addArrangedSubview(view)
}
}
Result
Yes you can, firstly you create main vertical stackview and set aligment center and distribution fill. Then you create second horizontal stackview in main stack and set alignment and distribition fill. Add the last elements you want to add Thats it.
First stackview:
Second stackview:
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.