Swift constraint doesn't update [duplicate] - swift

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

Related

NSGridView custom view intrinsic size

I'm building a simple NSGridView, and want to have a custom NSView as each element of the grid. Eventually, each NSView will be a xib based label (NSTextField) centered in the NSView.
The problem I am having is with the intrinsic size of the NSView. I want to define the size of the NSView and have auto layout work based on that. I added this code to the custom view (labelView):
override var intrinsicContentSize: NSSize {
return NSSize(width:100, height:100);
};
And it is indeed called; but apparently ignored. As a test, I have on the same row some other labels, and the height for the row is always set to the largest of the row text heights (including the label in the custom view); but the length is set to the longest of column text fields, ignoring the label in the custom view. And anyway, I want to arbitrarily make the NSView a certain height and length, as I tried (but failed) to do with the intrinsicContentSize.
NSStackview seems to do the right thing; but NSGridView does not.
I can force the width of a particular column with
grid.column(at:0).width = 400;
but want I really want to do is define the size of the NSView, and let autolayout use that as a building block.
This strikes me as a conceptual error on my part, so if someone could explain these NSGridView-autolayout-NSView subtleties, I think many might benefit.
I was having the exact same issue, tried to use custom NSView's inside a NSGridView and couldn't get them to draw correctly. What finally worked for me was setting the following:
let gridSize = 5
let cellSize: CGFloat = 50
gridView.xPlacement = .fill // this was key part of the solution
gridView.yPlacement = .fill
for i in 0 ..< gridSize {
gridView.row(at: i).height = cellSize
gridView.column(at: i).width = cellSize
}
Note that I'm setting the size of each cell with the row height and column width of the NSGridView, and not using the NSView size, but this is the only way I got it working.

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
}

Controls at the bottom of UIScrollView not working

The UIButton in the scrollView is visible, but not accessible. I am using Constraints.
My UI structure is this:
- UIView
- scrollView: UIScrollView
- contentView: UIView
- UIButton
- UIButton
- UIButton
- ....
- UIButton
I've already tried to set the contentSize of the scrollView. And the height of the contentView. Next to that I've tried to uncheck the checkbox Adjust Scroll View Insets in the storyboard of that ViewController without any luck. I've also set the priority of the Align Center Y to 250, and the priority bottom space to 250 of the contentView.
func updateScrollViewSize() {
var contentRect = CGRect.zero
for view in contentView.subviews {
contentRect = contentRect.union(view.frame)
}
contentRect.size = CGSize(width: scrollView.contentSize.width, height: contentRect.height + 50)
scrollView.contentSize = contentRect.size
contentView.frame.size = contentRect.size
contentView.layoutIfNeeded()
}
The button I try to reach has a Y value of: 1030.0
The height of the contentView is: 871.0
Step-by-step:
Add a scroll view to your view, background color red, constrain it 20-pts on each side:
Add a UIView as your "content view" to the scroll view (green background to make it easy to see), constrain it 0-pts on each side, constrain equal width and equal height to the scroll view. Important: change the Priority of the Height constraint to 250!
Add a UILabel to the contentView, constrain it 30-pts from the top, centered horizontally:
Add another label to the contentView, constrain it 300-pts from the first label, centered horizontally:
Add a UIButton to the contentView, constrain it 30-pts from the bottom, centered horizontally:
Now add a vertical constraint from the bottom of the second label to the top of the button, and set it to 400-pts:
As you see, this pushes the button off-screen past the bottom of the scroll view.
Run the app. You will be able to scroll down to see the button, and you'll be able to tap it.
Absolutely no code needed!
If you use AutoLayout, you don't needed to install frames manually.
You can try install constraints properly and content size will be right in this case and you won't have to install content size manually.
If you achieve this, I guess everything will work correctly.
You can follow this or this guide

How to programmatically set constraints when View sizes are different?

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()
}

UIButton action is not triggered after constraint layouts changed

I have got an UIButton on a storyboard ViewController. When I load data into the form and the layout is significantly changing the button does not recognise the touch action.
I have figured out that when button is visible on the scrollview right after it if filled with data, the touch action works.
If the data too long and the button is not visible at first, just when it is scrolled into the display, the touch action does not work.
I was checking if something is above the button, but nothing. I have tried to change the zPosition of the button, not solved the problem.
What can be the issue?
I have made custom classes from the UIScrollView and the UIButton to check how the touches event triggered. It is showing the same behaviour, which is obvious. If the button is visible right at the beginning, the UIButton's touchesBegan event is triggered. If the button moves down and not visible at the beginning, it is never triggered, but the scrollview's touchesBegan is called instead.
Depending on the size of the data I load into the page sometimes the button is visible at the beginning, but the form can be still scrolled a bit. In this case the button still work, so it seems that this behaviour is not depending on if the scrollview is scrolled before or not, just on the initial visibility of the button.
Is there any layout or display refresh function which should be called to set back the behaviour to the button?
The code portion which ensures that the contentview is resized for the scroll if the filled data requires bigger space.
func fillFormWithData() {
dispDescription.text = jSonData[0]["advdescription"]
dispLongDescription.text = jSonData[0]["advlongdesc"]
priceandcurrency.text = jSonData[0]["advprice"]! + " " + jSonData[0]["advpricecur"]!
validitydate.text = jSonData[0]["advdate"]!
contentview.layoutIfNeeded()
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
contentview.frame.size.height = contentRect.size.height
scrollview.contentSize = contentview.bounds.size
}
Ok, so another update. I have coloured the contentview background to blue and the scrollview background to white. When I load the data and resize the layout constraints, the contentview is resizing as expected, however now the scrollview is going to the bottom. After I scroll the view it is resizing to the original size which fits the screen. Now the button is only recognised when I touch the are which is blue behind. With the white background it is not recognised anymore, so it seems that the scrollview is hiding the button.
Let me get this clear the button is added in storyboard and it is a spritekit project?? If you are using zPosition?? Why don’t u connect the UIButton via the assistant editor as an IBAction then the action is always tied to the button.
You can also do it differently
Create an SKLabelNode and put it on the screen where you want to have the button and then set a name to it as myButton
override func touchesBegan(_ touches: Set<UITouch>, with event:
UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let tappedNodes = nodes(at: location)
for node in tappedNodes {
if node.name == "myButton" {
// call your action here
}
}
}
}
EDIT 1:
You could also try auto resizing your scrollView.content this works also if you are adding any views via the app or programmatically
private func resizeScrollView(){
print("RESIZING THE SCROLLVIEW from \(scrollView.contentSize)")
for view in scrollView.subviews {
contentRect = contentRect.union(view.frame)
}
scrollView.contentSize = CGSize(width: contentRect.size.width, height: contentRect.size.height + 150)
print("THE CONTENT SIZE AFTER RESIZING IS: \(scrollView.contentSize)")
}
EDIT 2: I think I found the issue with your project. You need to move the MessageButton(UzenetButton) above DispDescription label in the object inspector in that way it will always be above your message textView.
At the moment the UzeneButton is at the very far back in your view hierarchy so if your textView is resizing whilst editing it covers the button that is why you cannot click on it.
See #Endre Olah,
To make situation more clear do one more thing, set clipToBound property of contentview to true.
you will notice that after loading of data your button not fully visible, it means it is shifting out of bound of its parentView (ContentView)
And that's why button is not taking your touch. However, if you carefully touch upper part of button it still do its job. Because upper part is still in bound of ContentView
Solution :
After loading of data you have to make sure that you increase height of ContentView such that button should never go out of bound of its parentView(ContentView).
FOR EXAMPLE
#IBOutlet var heightConstraintOfContentView : NSLayoutConstraint!
After loading of data
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
heightConstraintOfContentView.constant = contentRect.size.height
contentView.layoutIfNeeded()
I use following steps when I need to use scrollview with dynamic content:
1) Firstly add a scrollView with top, bottom, trailing and leading is 0 to super view.
2) Add a view to scrollView and view's trailing, leading bottom and top space to scrollView can be set to 0 (or you can add margin optionally).
3) Now, you should add UI elements like buttons, labels with appropriate top, bottom, trailing and leading margins to each other.
4) Lastly, add equal height and equal width constraint to view with Safe Area:
and change equal height priority of view to 250:
It should solve your problem with UIScrollView.
Finally, I have found the solution in another chain, once it became clear that the scrollview's contentview is resizing on scroll event to the original size. (Not clear why this is like this, but that is the fact.)
So I had to add a height constraint to the contentview in the storyboard and create an outlet to it and adjust this constraint when the content size is changing, like this:
#IBOutlet weak var ContentViewHeight: NSLayoutConstraint!
func fillFormWithData() {
dispDescription.text = jSonData[0]["advdescription"]
dispLongDescription.text = jSonData[0]["advlongdesc"]
priceandcurrency.text = jSonData[0]["advprice"]! + " " + jSonData[0]["advpricecur"]!
validitydate.text = jSonData[0]["advdate"]!
contentview.layoutIfNeeded()
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
contentview.bounds = contentRect
scrollview.contentSize = contentRect.size
----------- This is the key line to the success ----------
ContentViewHeight.constant = contentRect.size.height
----------------------------------------------------------
}
After this is added, it works perfectly.