How to programmatically set constraints when View sizes are different? - swift4

Using Xcode 10, iOS 11.4+, Swift 4
I have a series of UIViewControllers that I am navigating through,
each of which is being placed into a ContainerView.
Based on which VC is pushed, I am hiding/showing the Top or Bottom views (shown in gray) in the Main Controller while the ContainerView is always in the middle (light blue).
What I would like to do is set the constraints so that the ContainerView is appropriately sized when the Top or Bottom views are shown/hidden.
For example, when the "Main Menu" is shown, the ContainerView should fill the entire Main Container view (Top and Bottom views will be hidden)
When "Item1" is shown, the Top view will be shown and the Bottom view hidden. Therefore, ContainerView should fill the Main Container view left, right, and bottom, but the ContainerView Top should be constrained to the Top view bottom.
When "Item5" is shown, the Top and Bottom views will also be shown.
Therefore, ContainerView should fill Main Container view left, right, and be constrained to the Top view bottom, and the Bottom view top (as shown in the Main Container)
I've tried using code like this to fill the entire Main view:
NSLayoutConstraint.activate([
ContainerView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0),
ContainerView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0),
ContainerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
ContainerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0)
])
However, the ContainerView never changes, and Xcode gives me a lot of warnings like:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want.
Here's another screenshot from the Storyboard:
Here's a link to download my sample project:
https://gitlab.com/whoit/containerviews
How can I correctly modify the constraints to accomplish this?

As I've mentioned in the comment, you should have used UIStackView for your top / bottom views visibility controlling.
You will need a UIStackView with following attributes:
Axis: Vertical
Alignment: Fill
Distribution: Fill
Spacing: As per your need
The stack view will contain the Top View, Container View and Bottom View respectively as its sub views.
You need to pin all the sides (leading, trailing, top & bottom) of this stack view to its superview (view controller's view). And as you need some height constraints for your top & bottom view, you give them as your need.
So basically your stack view is now capable of resizing its sub views when you hide any of them. You just need to perform:
theSubViewNeedsTobeHidden.isHidden = true
I've made a working demo for you that can be found here.

Issues
1) You're adding new constraints all the time, as this lines create a new constraints, each time they're getting called:
ContainerView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0)
2) In the beginning, the MainContainerView is not constraint at all
Solution
I would suggest following:
add those four constraints in the storyboard instead
create IBOutlets for the height-constraints of your top and bottom views.
set the constants of those to 0 (when hiding them)
(optional) for sure you can still fade them in/out by setting their alpha as well, and then add the height constant to the completion block.
Note:
Hidden views (alpha = 0 or isHidden = true) are still taken into account for layouting. That means, your constraints are still valid, when the views are hidden. But to make them look like hidden for the layouting as well, you'll then have to set theirs height constants to 0.
Code
#objc func hideControlViews(_ notification: Notification){
let userInfo = notification.userInfo! as! [String : Bool]
//print("Got top view notification: \(String(describing: userInfo["hide"]))")
if (userInfo["hidetop"]!) {
self.topViewHeightConstraint.constant = 0
} else {
self.topViewHeightConstraint.constant = 64
}
if (userInfo["hidebottom"]!) {
self.bottomViewHeightConstraint.constant = 0
} else {
self.bottomViewHeightConstraint.constant = 108
}
self.view.layoutIfNeeded()
}

Related

How I can show/hide view inside another view with height changing of root swift?

I have two views one inside another, it looks like picture below:
when I press on orange arrow I would like to show/hide view above grey line and grey line too. I did it by such way:
#objc func showHide(tapGestureRecognizer: UITapGestureRecognizer)
{
let tappedImage = tapGestureRecognizer.view as! UIImageView
UIView.animate(withDuration: 0.2, animations: { () -> Void in
self.jobDataView.isHidden = !self.jobDataView.isHidden
self.view.layoutIfNeeded()
})
tappedImage.image = self.jobDataView.isHidden ? UIImage(systemName: "arrow.down"):UIImage(systemName: "arrow.up")
}
and my view above gray line can be hidden and shown. But root view doesn't change its' height and it has similar sizes before and after btn click. I tried to add constraint of height, but I it didn't solve my problem. Maybe someone knows how to solve my problem?
You need to use UIStackView either through Storyboard or from code. when you hide subview inside stackview it will automatically change stackview height. what you need to do is to make stackView distribution = fill and use vertical stack...
Hide view on button tap and when you need to show it .. add it in stack at. 0th index ..

How to get a custom scrollingStackView's height

I have a custom view, it's a scrolling view with a stack view in it. And I added 3 container views inside this stack view to stack and scroll vertically. Inside each container view I add either a UIImage or a UILabel view.
I don't want my scrollview to scroll if all the views are already visible on screen.
For this I need to know the height of my scrollingStackView, so I can compare it with the current screen size.
Here is how I set them up in viewDidLoad:
setupScrollingSV()
setupContainer1()
setupContainer2()
setupContainer3()
setupImageViewInContainer1()
setupImageViewInContainer2()
setupLabelViewInContainer3()
My custom scrolling stack view doesn't have a size, I lay it out programmatically giving left, right, top anchors.
scrollingSV.leftAnchor.constraint(equalTo: stackView.leftAnchor).isActive = true
scrollingSV.rightAnchor.constraint(equalTo: stackView.rightAnchor).isActive = true
scrollingSV.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
And there is another stack view that is in safe area of my view, which has this scrolling stack view and another container view ContainerView4.
I layout ContainerView4 like this:
ContainerView4.leftAnchor.constraint(equalTo: stackView.leftAnchor).isActive = true
ContainerView4.rightAnchor.constraint(equalTo: stackView.rightAnchor).isActive = true
ContainerView4.topAnchor.constraint(equalTo: scrollingSV.bottomAnchor).isActive = true
ContainerView4.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
ContainerView4.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
Lastly, when I use debug view hierarchy I do see the height and width for my scrollingSV, and the views inside the containers.
Here is the code I am trying to get a size for the scrollingSV, which returns 0:
print("Scrollview height: \(scrollingSV.contentSize.height )")
I already tried to get the content size by using union as told in this stackoverflow answer: How do I auto size a UIScrollView to fit its content
But this also returns 0.
How can I get this scrolling stack view's size?
Or is it not calculated yet in viewDidLoad?
Thank you
Apparently in viewDidLoad the views were not finished with laying out their subviews. So the height was not meaningful yet.
When I did this check in viewDidAppear, the problem is fixed and I could reach the scrolling stack view's contentSize and frame height.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scrollingSV.isScrollEnabled = scrollingSV.contentSize.height > scrollingSV.frame.height
}

Swift constraint doesn't update [duplicate]

(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.

Autolayout for Single UIView and Multiple TableViews

I'm trying to get the following (single) UIView to appear static within the phone screen while either TableView 2 or TableView 3 is being viewed, then horizontally scroll out of view when the user moves to TableView 1. Essentially, the UIView scrolls in when moving from TableView 1 to TableView 2, remains static from 2 to 3 or 3 to 2, and scrolls away when going back to 1.
I have the following in my ViewController in an attempt to change the constraints programmatically, however I don't have much experience with this.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.scrollView.contentOffset.x <= 400 {
singleView.centerXAnchor.constraint(equalTo: TableView2.centerXAnchor, constant: 0).isActive = true
} else {
singleView.centerXAnchor.constraint(equalTo: TableView2.centerXAnchor, constant: self.scrollView.contentOffset.x).isActive = true
}
It's not working correctly; is there a more appropriate approach I should be taking? Thanks!
Here’s how I would do it.
Use a paging scroll view.
Start with the View constrained to the second table view.
When the user starts to scroll from page 2 to page 3, remove all constraints on the View and constrain it instead to the view controller main view so that it is stationary.
When the user starts to scroll from page 2 to page 1, reverse that.

Add layoutMargins to one element in a UIStackView

I would like to create a vertical stackview with 3 elements in it.
I want a bit more space only between the 2nd and the last element. So I thought about adding to the last element :
mylastelement.layoutMargins = UIEdgeInsets(top:30, left:0,bottom:0, right:0)
But the layoutmargins are not applied in my stackview. Is there any easy way to achieve that (Id like to avoid to modify the last element inner height).
EDIT : I just tried to increase 2nd element height (+50) within its frame by doing :
my2ndElementLabel.sizeToFit()
my2ndElementLabel.frame = CGRect(x:my2ndElementLabel.frame.origin.x,y:lmy2ndElementLabel.frame.origin.y,
width:my2ndElementLabel.frame.width, height:my2ndElementLabel.frame.height + 50)
but it has no effect.
EDIT2 : I tried to add a random view to my UIStackView, but the the view is just ignored ! May have missed something in understanding how UIKit work ?... :
let v = UIView(frame:CGRect(x:0,y:0,width:100,height:400))
v.backgroundColor = .red
myStackView.addArrangedSubview(v)
//...
Here is an extension I made that helps to achieve fast such margins :
extension UIStackView {
func addArrangedSubview(_ v:UIView, withMargin m:UIEdgeInsets )
{
let containerForMargin = UIView()
containerForMargin.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: containerForMargin.topAnchor, constant:m.top ),
v.bottomAnchor.constraint(equalTo: containerForMargin.bottomAnchor, constant: m.bottom ),
v.leftAnchor.constraint(equalTo: containerForMargin.leftAnchor, constant: m.left),
v.rightAnchor.constraint(equalTo: containerForMargin.rightAnchor, constant: m.right)
])
addArrangedSubview(containerForMargin)
}
}
What you could do is set a custom spacing between the second and third element.
myStackView.setCustomSpacing(30.0, after: my2ndElementLabel)
In the same general vein, you can constrain the top (or bottom) anchor of your view relative to the corresponding edge of any view in which it's embedded. What's ugly being somewhat a matter of taste, I find autolayout constraints easy to use and easy to reason about.
A simple example from Mac OS rather than iOS:
let button = ControlFactory.labeledButton("Filter")
addSubview(button)
button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20).isActive = true
button.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
This particular code lives in the view initializer, and positions a button in the middle of a view, 20 points up from the bottom.
I found myself : It looks like UIStackView doesn't work at all with old sizing system (with .frame). It seems you have to constraint height and width, and StackView will constraint left/top/right/bottom position for you when you add the arrangedSubview.
My second view was a label : I wanted a margin of 40, under the text. So i first computed the label height into its .frame property, and constraint the height at frame.height + 40(= my margin)
labelDesc.sizeToFit()
labelDesc.heightAnchor.constraint(equalToConstant:40).isActive = true
I find my own solution utterly ugly though. I'm sure UIKit provide a better way to achieve such a simple goal, without having to make these kind of DIY solutions. So please if you're used to work with UIKit, tell me if there is any better solution.
Consider adding a "margin" by inserting a correctly-sized UIView within the Stack View as needed.
If you need a 40px margin between 2 specific elements... add a UIView with a height constraint of 40px. Assign a clearColor background to make it invisible.
You can add IBOutlets to this view and hide it as you would any other item in the Stack View.