Autolayout two views vertically with dynamic height aligned top - ios

I have two views that can be dynamic height.
Dependent on content either view1 or view2 is the highest.
The highest view should determinate the distance to view3.
In my current implementation view1 and view2 each have a constraint for x pixels for view3. But the problem is view1 and view2 then always will be same height (all the content will not align top)

Try this. Make sure both views 1 and 2, have some constraint to the top of the superview and to the sides (or fixed width, whatever it takes to fix the views in the horizontal direction). Lets say you want that distance to the bottom view to be 30. Give view 1 a constraint to view 3 of =30 with a priority of 200, and another one of >=30 with a priority of 1000 (default). View 2 should just need one constraint to view 3 of >=30 with 1000 priority.
The low priority constraint (of 200) will fix the position of view 3 initially, but is low enough that if view 2 expands, view 3 will move down, and also low enough that the default Content Hugging Priority of view 1 will keep it from expanding its height if it doesn't need to based on its text size.

swift
import SnapKit
view3.snp.makeConstraints {
$0.top.greaterThanOrEqualTo(view1.snp.bottom).offset(30)
$0.top.greaterThanOrEqualTo(view2.snp.bottom).offset(30)
$0.left.right.equalTo(0)
$0.height.equalTo(50)
}

There are two ways actually
With constraints (as described rdelmar)
class MyViewController: ViewController {
let view1 = View()
let view2 = View()
let view3 = View()
override func buildUI() {
super.buildUI()
body {
view1
.edgesToSuperview(top: 16, leading: 16)
view2
.edgesToSuperview(top: 16, trailing: -16)
.width(to: view1)
.leadingTo(view1, 16)
view3
.topTo(view1, 30 ! 200) // 200 is priority, ! is priority operator
.topTo(view2, >=30)
.edgesToSuperview(leading: 16, trailing: -16, bottom: -16)
}
}
}
I'm not sure here about =30 with 200 priority, maybe it should be >=30 as with view2, but you can easily figure it out with Live preview
With StackViews
class MyViewController: ViewController {
override func buildUI() {
super.buildUI()
body {
VStack {
HStack {
View() // view 1
View() // view 2
}
.distribution(.fillEqually) // to make view 1&2 width equally
.alignment(.top)
Space() // flexible space
VSpace(30) // fixed 30pt height space
View() // view 3
}
.edgesToSuperview()
}
}
}
All the examples are by using awesome UIKitPlus library

The accepted answer did not work for me (although it did put me on the right path). In my case, view 1 or view 2 could be any size and view 3 had to be below the highest one. This code got it to work, though I must say I find autolayout extremely unintuitive:
let view3TopAnchor1 = statusL.topAnchor.constraint(greaterThanOrEqualTo: view1.bottomAnchor, constant: 10.0)
view3TopAnchor1.priority = UILayoutPriority(100)
view3TopAnchor1.isActive = true
let view3TopAnchor2 = statusL.topAnchor.constraint(greaterThanOrEqualTo: view2.bottomAnchor, constant: 10.0)
view3TopAnchor2.priority = UILayoutPriority(100)
view3TopAnchor2.isActive = true
view1.setContentHuggingPriority(UILayoutPriority(101), for: .vertical)
view2.setContentHuggingPriority(UILayoutPriority(101), for: .vertical)

float yPos = MAX(view1.frame.size.height,view2.frame.size.height);
view3.frame.origin.y = view1.frame.origin.y + maxHeight;

Related

UIScrollView with dynamically sized content

(Xcode 11, Swift)
Being a newbie to iOS and Autolayout, I'm struggling with implementing a fairly simple (IMHO) view which displays a [vertical] list of items. The only problem is that items are decided dynamically and each of them could be either text or image (where either of those could be fairly large so scrolling would be required). WebView is not an option, so it has to be implemented natively.
This is how I understand the process:
Make in IB a UIScrollView and size it to the size of the outer frame.
Make a container view as a subview of UIScrollView (again, in IB) and size it the same.
Set constraint on equal width of both
At runtime, populate container view with UILabels/UIImageViews and also set constraints programmatically to ensure proper layout.
"Tell" scrollview about the subview height in order to make it manage the scrolling thereof.
Is this the right approach? It doesn't seem to work for me (for a toy example of dynamically adding a very tall image to a container view - I cannot get the scrolling to work). What would be the proper way to do the last step in the process above - just force the contentSize of the scrollview to the size of the populated container view (it doesn't seem to work for me). Any help would be appreciated.
When adding multiple elements to a scroll view at run-time, you may find it much easier to use a UIStackView... when setup properly, it will automatically grow in height with each added object.
As a simple example...
1) Start by adding a UIScrollView (I gave it a blue background to make it easier to see). Constrain it to Zero on all 4 sides:
Note that we see the "red circle" indicating missing / conflicting constraints. Ignore that for now.
2) Add a UIView as a "content view" to the scroll view (I gave it a systemYellow background to make it easier to see). Constrain it to Zero on all 4 sides to the Content Layout Guide -- this will (eventually) define the scroll view's content size. Also constrain it equal width and equal height to the Frame Layout Guide:
Important Step: Select the Height constraint, and in the Size Inspector pane select the Placeholder - Remove at build time checkbox. This will satisfy auto-layout in IB during design time, but will allow the height of that view to shrink / grow as necessary.
3) Add a Vertical UIStackView to the "content view". Constrain it to Zero on all 4 sides. Configure its properties to Fill / Fill / 8 (as shown below):
4) Add an #IBOutlet connection to the stack view in your view controller class. Now, at run-time, as you add UI elements to the stack view, all of your "scrollability" will be handled by auto-layout.
Here is an example class:
class DynaScrollViewController: UIViewController {
#IBOutlet var theStackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
// local var so we can reuse it
var theLabel = UILabel()
var theImageView = UIImageView()
// create a new label
theLabel = UILabel()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theLabel.translatesAutoresizingMaskIntoConstraints = false
// multi-line
theLabel.numberOfLines = 0
// cyan background to make it easy to see
theLabel.backgroundColor = .cyan
// add 9 lines of text to the label
theLabel.text = (1...9).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .yellow
// add 5 lines of text to the label
theLabel.text = (1...5).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// create a new UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load an image for it - I have one named background
if let img = UIImage(named: "background") {
theImageView.image = img
}
// let's give the image view a 4:3 width:height ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 4.0/3.0).isActive = true
// add it to the stack view
theStackView.addArrangedSubview(theImageView)
// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .green
// add 2 lines of text to the label
theLabel.text = (1...2).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// add another UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load a different image for it - I have one named AquariumBG
if let img = UIImage(named: "AquariumBG") {
theImageView.image = img
}
// let's give this image view a 1:1 width:height ratio
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor, multiplier: 1.0).isActive = true
// add it to the stack view
theStackView.addArrangedSubview(theImageView)
}
}
If the steps have been followed, you should get this output:
and, after scrolling to the bottom:
Alignment constraints (leading/trailing/top/bottom)
The alignment constraint between Scroll View and Content View defines the scrollable range of the content. For example,
If scrollView.bottom = contentView.bottom, it means Scroll View is
scrollable to the bottom of Content View.
If scrollView.bottom = contentView.bottom + 100, the scrollable
bottom end of Scroll View will exceed the end of Content View by 100
points.
If scrollView.bottom = contentView.bottom — 100, the bottom of
Content View will not be reached even the scrollView is scrolled to
the bottom end.
That is, the (bottom) anchor on Scroll View indicates the (bottom) edge of the outer frame, i.e., the visible part of Content View; the (bottom) anchor on Content View refers to the edge of the actual content, which will be hidden if not scrolled to.
Unlike normal use cases, alignment constraints between Scroll View and Content View have nothing to do with the actual size of Content View. They affect only “scrollable range of content view” but NOT “actual content size”. The actual size of Content View must be additionally defined.
Size constraints (width/height)
To actually size Content View, we may set the size of Content View to a specific length, like width/height of 500. If the width/height exceeds the width/height of Scroll View, there will be a scrollbar for users to scroll.
However, a more common case will be, we want Content View to have the same width (or height) as Scroll View. In this case, we will have
contentView.width = scrollView.width
The width of Content View refers to the actual full width of content. On the other hand, the width of Scroll View refers to the outer container frame width of Scroll View. Of course, it doesn’t have to be the same width, but can be other forms like a * scrollView.width + b.
And if we have Content View higher (or wider) than Scroll View, a scrollbar appears.
Content View can not only be a single view, but also multiple views, as long as they are appropriately constrained using alignment and size constraints to Scroll View.
For details, you may follow this article: Link.

UILabel breaks early inside UIStackView

Given the view hierarchy:
UIStackView
--UILabel
--UISwitch
The label breaks too early, even if it can be fit to a single line.
Setting numberOfLines = 1 forces the label to be laid out correctly.
How to make UILabel perform line break only when needed?
Code:
private lazy var title: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
private lazy var toggle = UISwitch()
private lazy var stack = UIStackView(axis: .horizontal,
distribution: .equalSpacing,
alignment: .center,
views: [title,
toggle])
func setupConstraints() {
stack.snp.makeConstraints { (make) in
make.edges.equalTo(contentView.layoutMarginsGuide)
}
}
Result:
Setting numberOfLines = 1 gets me what I'd like to achieve, but the label looses its multi-line functionality:
How to force the desired behavior without losing support for multi-line labels?
When there is a lot of horizontal space, the label gets laid out correctly no matter of the numberOfLines property:
Set your UISwitch's content hugging and resistance priority to 1000.
And stack view distribution and alignment to fill.
Extra Note - If you want label and switch to be top aligned, then set alignment to top.
In your stack view you can give a constraint to your label and switch...
1) give your label leading, top , trailing and bottom constraint. Don't give Width constraint. In trailing constraint take second item Switch.
2) give your switch trailing, top, bottom and Fix width.
Hope it Will work.
Add label inside another stack view.
UIStackView
--UIStackView
--UILabel
--UISwitch

Stack view - but with "proportional" gaps

Imagine a stack view with four items, filling something. (Say, filling the screen).
Notice there are three gaps, ABC.
(Note - the yellow blocks are always some fixed height each.)
(Only the gaps change, depending on the overall height available to the stack view.)
Say UISV is able to draw everything, with say 300 left over. The three gaps will be 100 each.
In the example, 9 is left over, so A B and C are 3 each.
However.
Very often, you want the gaps themselves to enjoy a proportional relationship.
Thus - your designer may say something like
If the screen is too tall, expand the spaces at A, B and C. However. Always expand B let's say 4x as fast as the gaps at A and B."
So, if "12" is left over, that would be 2,8,2. Whereas when 18 is left over, that would be 3,12,3.
Is this concept available in stack view? Else, how would you do it?
(Note that recently added to stack view, you can indeed specify the gaps individually. So, it would be possible to do it "manually", but it would be a real mess, you'd be working against the solver a lot.)
You can achieve that by following workaround. Instead of spacing, for each space add a new UIView() that would be a stretchable space. And then just add constraints between heights of these "spaces" that would constrain their heights together based on the multipliers you want, so e.g.:
space1.heightAnchor.constraint(equalTo: space2.heightAnchor, multiplier: 2).isActive = true
And to make it work I think you'd have to add one constraint that would try to stretch those spaces in case there is free space:
let stretchingConstraint = space1.heightAnchor.constraint(equalToConstant: 1000)
// lowest priority to make sure it wont override any of the rest of constraints and compression resistances
stretchingConstraint.priority = UILayoutPriority(rawValue: 1)
stretchingConstraint.isActive = true
The "normal" content views would have to have intrinsic size or explicit constraints setting their heights to work properly.
Here is an example:
class MyViewController: UIViewController {
fileprivate let stack = UIStackView()
fileprivate let views = [UIView(), UIView(), UIView(), UIView()]
fileprivate let spaces = [UIView(), UIView(), UIView()]
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(stack)
// let stack fill the whole view
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: self.view.topAnchor),
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
stack.leftAnchor.constraint(equalTo: self.view.leftAnchor),
stack.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
stack.alignment = .fill
// distribution must be .fill
stack.distribution = .fill
stack.spacing = 0
stack.axis = .vertical
for (index, view) in views.enumerated() {
stack.addArrangedSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
// give it explicit height (or use intrinsic height)
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
view.backgroundColor = .orange
// intertwin it with spaces
if index < spaces.count {
stack.addArrangedSubview(spaces[index])
spaces[index].translatesAutoresizingMaskIntoConstraints = false
}
}
// constraints for 1 4 1 proportions
NSLayoutConstraint.activate([
spaces[1].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 4),
spaces[2].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 1),
])
let stretchConstraint = spaces[0].heightAnchor.constraint(equalToConstant: 1000)
stretchConstraint.priority = UILayoutPriority(rawValue: 1)
stretchConstraint.isActive = true
}
}
Remarkably, #MilanNosáľ 's solution works perfectly.
You do not need to set any priorities/etc - it works perfectly "naturally" in the iOS solver!
Set the four content areas simply to 50 fixed height. (Use any intrinsic content items.)
Simply don't set the height at all of "gap1".
Set gap2 and gap3 to be equal height of gap1.
Simply - set the ratios you want for gap2 and gap3 !
Versus gap1.
So, gap2 is 0.512 the height of gap1, gap3 is 0.398 the height of gap1, etc.
It does solve it in all cases.
Fantastic!!!!!!!!!!
So: in the three examples (being phones with three different screen heights). In fact the relative heights of the gaps, is always the same. Your design department will rejoice! :)
Created: a gist with a storyboard example
The key here is Equal Heights between your arranged views and your reference view:
And then change the 'Multiplier` to your desired sizes:
In this example I have 0.2 for the main view sizes (dark grey), 0.05 within the pairs (black), and 0.1 between the pairs (light grey)
Then simply changing the size of the containing view will cause the views to re-size proportionally:
This is entirely within the storyboard, but you could do the same thing in code.
Note that I'm using only proportions within the StackView to avoid having an incorrect total size, (and making sure they add up to 1.0), but it should be possible to also have some set heights within the StackView if done correctly.

Auto Layout how to move the view2 from right to the bottom?

I have two views in the cell at the left UIImageView and at the right UIView Containing some other views as in picture
Now I want if the width of Cell is => 300 then View2 should be at the right of Screen as it is in picture other wise it should be moved to the bottom of View1
Do one thing. Set another constraints for that view
1) leading to main view
2) top to imageview (you can give this constraint by right click from view to drag imageview then vertical spacing.)
Now give outlets for this two constraints programatically.
And also give outlets for constraints which are:
1) leading from view to imageview
2) top of view.
Now as per your requirement,set if condition
e.g.
if(width => 300)
{
topOfViewToImageviewConstraint.isActive = false
leadingOfViewToMainViewConstraint.isActive = false
}
else
{
leadingOfViewToImageViewConstraint.isActive = false
topOfviewToMainView.isActive = false
leadingOfViewToMainViewConstraint.constant = 0
topOfViewToImageviewConstraint.constant = 20
}
This will work. I am not currently in Xcode, so this is just some sample code, but the idea will work.
in cellForRowAt
check if self.tableView.cellForRowAt(IndexPath.row).frame.width => 300
if it is, make no change, but if it is not find the y coordinate of the bottom of view 1 and make a new rect at (x: 0, y: botOfView1, width, height). Then set the frame of view 2 equal to that.

Changing the x position of button elements inside its container in swift 3

I have a scroller at the bottom of my view controller which contains buttons. Buttons are not always active, they change as the user adds them to his favourites. Which means I have to decide on the x position of the buttons programmatically.
What I have done is created outlets of all the buttons from the view controller in the storyboard to the script and I am attempting to access them like this (simplified code):
var Items = [String : Bool]
#IBOutlet weak var item1: UIButton!
...
if Items[item] == true {
item1.isEnabled = true
\\ here comes a line of code where i should position item1's x position but I haven't managed to find the right way
}
The question is how to change the x position of UIButton programmatically, and are there better ways to handle the problem? I am using Xcode 8 and swift 3
EDIT :
Method for constraints:
func activeConstraint(element: UIButton, constant: Int) {
let widthConstraint100 = NSLayoutConstraint(item: element,
attribute:NSLayoutAttribute.width,
relatedBy:NSLayoutRelation.equal,
toItem:nil,
attribute:NSLayoutAttribute.notAnAttribute,
multiplier:1.0,
constant: 100)
let widthConstraint0 = NSLayoutConstraint(item: element,
attribute:NSLayoutAttribute.width,
relatedBy:NSLayoutRelation.equal,
toItem:nil,
attribute:NSLayoutAttribute.notAnAttribute,
multiplier:1.0,
constant: 0)
if constant == 0 {
element.removeConstraint(widthConstraint100)
element.addConstraint(widthConstraint0)
} else {
element.removeConstraint(widthConstraint0)
element.addConstraint(widthConstraint100)
}
element.updateConstraints()
}
Example of call to the function:
if Items[item] == true {
activeConstraint(element: item, constant: 100)
} else {
activeConstraint(element: item, constant: 0)
}
EDIT 2:
Although I have seen many questions here referring UIStackView (like this one), none of the solutions worked for me. I have an horizontal scroller at the bottom of the screen with many buttons that get activated or deactivated programmatically with their width constraint set to 0 or 100. Here is the final result that works in my case (XCode 8.0) :
Hierarchy:
image
UIScrollView HAS to have one element: View (lets call it Container View) that holds all the contents of the scroller. Container view needs to have 4 constraints - top, bottom, leading and trailing set towards superView (UIScrollView). (preferably all 4 set to 0). Also set vertical constraint to center horizontally from Container view to superView - UIScrollView.
StackView holds all the buttons - it needs to have all 4 constraints (top, bottom, leading, trailing) set towards superView which is Container View.
All buttons need to have their width constraint set to a constant of desire.

Resources