Is it possible for UIStackView to scroll? - ios

Let's say I have added more views in UIStackView which can be displayed, how I can make the UIStackView scroll?

In case anyone is looking for a solution without code, I created an example to do this completely in the storyboard, using Auto Layout.
You can get it from github.
Basically, to recreate the example (for vertical scrolling):
Create a UIScrollView, and set its constraints.
Add a UIStackView to the UIScrollView
Set the constraints: Leading, Trailing, Top & Bottom should be equal to the ones from UIScrollView
Set up an equal Width constraint between the UIStackView and UIScrollView.
Set Axis = Vertical, Alignment = Fill, Distribution = Equal Spacing, and Spacing = 0 on the UIStackView
Add a number of UIViews to the UIStackView
Run
Exchange Width for Height in step 4, and set Axis = Horizontal in step 5, to get a horizontal UIStackView.

I present you the right solution
For Xcode 11+
Step 1:
Add a ScrollView and resize it
Step 2:
Add Constraints for a ScrollView
Step 3:
Add a StackView into ScrollView, and resize it.
Step 4:
Add Constraints for a StackView (Stask View -> Content Layout Guide -> "Leading, Top, Trailing, Bottom")
Step 4.1:
Correct Constraints -> Constant (... -> Constant = 0)
Step 5:
Add Constraints for a StackView (Stask View -> Frame Layout Guide -> "Equal Widths")
Step 6 Example:
Add two UIView(s) with HeightConstraints and RUN
I hope it will be useful for you like

Apple's Auto Layout Guide includes an entire section on Working with Scroll Views. Some relevant snippets:
Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines
the scroll view’s content area.
(Optional) To disable horizontal scrolling, set the content view’s width equal to the scroll view’s width. The content view now fills the
scroll view horizontally.
(Optional) To disable vertical scrolling, set the content view’s height equal to the scroll view’s height. The content view now fills
the scroll view horizontally.
Furthermore:
Your layout must fully define the size of the content view (except
where defined in steps 5 and 6). … When the content view is taller than the scroll view, the scroll view enables vertical scrolling. When the content view is wider than the scroll view, the scroll view enables horizontal scrolling.
To summarize, the scroll view's content view (in this case, a stack view) must be pinned to its edges and have its width and/or height otherwise constrained. That means that the contents of the stack view must be constrained (directly or indirectly) in the direction(s) in which scrolling is desired, which might mean adding a height constraint to each view inside a vertically scrolling stack view, for example. The following is an example of how to allow for vertical scrolling of a scroll view containing a stack view:
// Pin the edges of the stack view to the edges of the scroll view that contains it
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
// Set the width of the stack view to the width of the scroll view for vertical scrolling
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true

Up to date for 2020.
100% storyboard OR 100% code.
This example is vertical:
Here's the simplest possible explanation:
Have a blank full-screen scene
Add a scroll view. Control-drag from the scroll view to the base view, add left-right-top-bottom, all zero.
Add a stack view in the scroll view. Control-drag from the stack view to the scroll view, add left-right-top-bottom, all zero.
Put two or three labels inside the stack view.
For clarity, make the background color of the label red. Set the label height to 100.
Now set the width of each UILabel:
Surprisingly, control-drag from the UILabel to the scroll view, not to the stack view, and select equal widths.
To repeat:
Don't control drag from the UILabel to the UILabel's parent - go to the grandparent. (In other words, go all the way to the scroll view, do not go to the stack view.)
It's that simple. That's the secret.
Secret tip - Apple bug:
It will not work with only one item! Add a few labels to make the demo work.
You're done.
Tip: You must add a height to every new item. Every item in any scrolling stack view must have either an intrinsic size (such as a label) or add an explicit height constraint.
The alternative approach:
To recap the above approach: surprisingly, set the widths of the labels to the width of the scroll view (not the stack view).
Here is an alternate approach...
Drag from the stack view to the scroll view, and add a "width equal" constraint. This seems strange because you already pinned left-right, but that is how you do it. No matter how strange it seems that's the secret.
So you have two options:
Surprisingly, set the width of each item in the stack view to the width of the scrollview grandparent (not the stackview parent).
or
Surprisingly, set a "width equal" of the stackview to the scrollview - even though you do have the left and right edges of the stackview pinned to the scrollview anyway.
To be clear, do ONE of those methods, do NOT do both.

The constraints in the top-voted answer here worked for me, and I've pasted an image of the constraints below, as created in my storyboard.
I did hit two issues though that others should be aware of:
After adding constraints similar to those in in the accepted answer, I'd get the red autolayout error Need constraints for: X position or width. This was solved by adding a UILabel as a subview of the stack view.
I'm adding the subviews programmatically, so I originally had no subviews on the storyboard. To get rid of the autolayout errors, add a subview to the storyboard, then remove it on load before adding your real subviews and constraints.
I originally attempted to add UIButtons to the UIStackView. The buttons and views would load, but the scroll view would not scroll. This was solved by adding UILabels to the Stack View instead of buttons. Using the same constraints, this view hierarchy with the UILabels scrolls but the UIButtons does not.
I'm confused by this issue, as the UIButtons do seem to have an IntrinsicContentSize (used by the Stack View). If anyone knows why the buttons don't work, I'd love to know why.
Here is my view hierarchy and constraints, for reference:

As Eik says, UIStackView and UIScrollView play together nicely, see here.
The key is that the UIStackView handles the variable height/width for different contents and the UIScrollView then does its job well of scrolling/bouncing that content:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = CGSize(width: stackView.frame.width, height: stackView.frame.height)
}

Horizontal Scrolling (UIStackView within UIScrollView)
For horizontal scrolling. First, create a UIStackView and a UIScrollView and add them to your view in the following way:
let scrollView = UIScrollView()
let stackView = UIStackView()
scrollView.addSubview(stackView)
view.addSubview(scrollView)
Remembering to set the translatesAutoresizingMaskIntoConstraints to false on the UIStackView and the UIScrollView:
stackView.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
To get everything working the trailing, leading, top and bottom anchors of the UIStackView should be equal to the UIScrollView anchors:
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
But the width anchor of the UIStackView must the equal to or greater than the width of the UIScrollView anchor:
stackView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor).isActive = true
Now anchor your UIScrollView, for example:
scrollView.heightAnchor.constraint(equalToConstant: 80).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo:view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo:view.trailingAnchor).isActive = true
Next, I would suggest trying the following settings for the UIStackView alignment and distribution:
topicStackView.axis = .horizontal
topicStackView.distribution = .equalCentering
topicStackView.alignment = .center
topicStackView.spacing = 10
Finally you'll need to use the addArrangedSubview: method to add subviews to your UIStackView.
Text Insets
One additional feature that you might find useful is that because the UIStackView is held within a UIScrollView you now have access to text insets to make things look a bit prettier.
let inset:CGFloat = 20
scrollView.contentInset.left = inset
scrollView.contentInset.right = inset
// remember if you're using insets then reduce the width of your stack view to match
stackView.widthAnchor.constraint(greaterThanOrEqualTo: topicScrollView.widthAnchor, constant: -inset*2).isActive = true

I was looking to do the same thing and stumbled upon this excellent post. If you want to do this programmatically using the anchor API, this is the way to go.
To summarise, embed your UIStackView in your UIScrollView, and set the anchor constraints of the UIStackView to match those of the UIScrollView:
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true

Just add this to viewdidload:
let insets = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
scrollVIew.contentInset = insets
scrollVIew.scrollIndicatorInsets = insets
source:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html

You can try ScrollableStackView : https://github.com/gurhub/ScrollableStackView
It's Objective-C and Swift compatible library. It's available through CocoaPods.
Sample Code (Swift)
import ScrollableStackView
var scrollable = ScrollableStackView(frame: view.frame)
view.addSubview(scrollable)
// add your views with
let rectangle = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 55))
rectangle.backgroundColor = UIColor.blue
scrollable.stackView.addArrangedSubview(rectangle)
// ...
Sample Code (Objective-C)
#import ScrollableStackView
ScrollableStackView *scrollable = [[ScrollableStackView alloc] initWithFrame:self.view.frame];
scrollable.stackView.distribution = UIStackViewDistributionFillProportionally;
scrollable.stackView.alignment = UIStackViewAlignmentCenter;
scrollable.stackView.axis = UILayoutConstraintAxisVertical;
[self.view addSubview:scrollable];
UIView *rectangle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 55)];
[rectangle setBackgroundColor:[UIColor blueColor]];
// add your views with
[scrollable.stackView addArrangedSubview:rectangle];
// ...

Adding some new perspective for macOS Catalyst. Since macOS apps support window resizing, it is possible that your UIStackView will transition from an unscrollable status to a scrollable one, or vice versa. There are two subtle things here:
UIStackView is designed to fit all area it can.
During the transition, UIScrollView will attempt to resize its bounds to account for the newly gained/lost area underneath your navigation bar (or toolbar in the case of macOS apps).
This will unfortunately create an infinite loop. I am not extremely familiar with UIScrollView and its adjustedContentInset, but from my log in its layoutSubviews method, I am seeing the following behavior:
One enlarges the window.
UIScrollView attempts to shrink its bounds (since no need for the area underneath the toolbar).
UIStackView follows.
Somehow UIScrollView is unsatisfied, and decide to restore to the larger bounds. This feels very odd to me since what I am seeing from the log is that UIScrollView.bounds.height == UIStackView.bounds.height.
UIStackView follows.
Then loop to step 2.
It appears to me that two steps would fix the issue:
Align UIStackView.top to UIScrollView.topMargin.
Set contentInsetAdjustmentBehavior to .never.
Here I am concerned with a vertically scrollable view with a vertically growing UIStackView. For a horizontal pair, change the code accordingly.
Hope it helps anyone in the future. Couldn't find anyone mentioning this on the Internet and it costed me quite a long time to figure out what happened.

If any one looking for horizontally scrollview
func createHorizontalStackViewsWithScroll() {
self.view.addSubview(stackScrollView)
stackScrollView.translatesAutoresizingMaskIntoConstraints = false
stackScrollView.heightAnchor.constraint(equalToConstant: 85).isActive = true
stackScrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
stackScrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
stackScrollView.bottomAnchor.constraint(equalTo: visualEffectViews.topAnchor).isActive = true
stackScrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: stackScrollView.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: stackScrollView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: stackScrollView.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: stackScrollView.bottomAnchor).isActive = true
stackView.heightAnchor.constraint(equalTo: stackScrollView.heightAnchor).isActive = true
stackView.distribution = .equalSpacing
stackView.spacing = 5
stackView.axis = .horizontal
stackView.alignment = .fill
for i in 0 ..< images.count {
let photoView = UIButton.init(frame: CGRect(x: 0, y: 0, width: 85, height: 85))
// set button image
photoView.translatesAutoresizingMaskIntoConstraints = false
photoView.heightAnchor.constraint(equalToConstant: photoView.frame.height).isActive = true
photoView.widthAnchor.constraint(equalToConstant: photoView.frame.width).isActive = true
stackView.addArrangedSubview(photoView)
}
stackView.setNeedsLayout()
}

One simple way for dynamic elements in stackview embed in scrollview. In XIB, add a UIStackView inside an UIScrollView and add constraints that the stackview fit the scrollview (top, bottom, lead, trail) and add a constraint to match horizontal center between them. But marked "remove at build time" the last constraint. It make XIB happy and avoid errors.
Example for horizontal scroll:
Then:
Then in your code, just add elements like buttons in your stackview like this:
array.forEach { text in
let button = ShadowButton(frame: .zero)
button.setTitle(text, for: .normal)
myStackView.addArrangedSubview(button)
button.heightAnchor.constraint(equalToConstant: 40).isActive = true
button.widthAnchor.constraint(equalToConstant: 80).isActive = true
}

If you have a constraint to center the Stack View vertically inside the scroll view, just remove it.

Example for a vertical stackview/scrollview (using the EasyPeasy for autolayout):
let scrollView = UIScrollView()
self.view.addSubview(scrollView)
scrollView <- [
Edges(),
Width().like(self.view)
]
let stackView = UIStackView(arrangedSubviews: yourSubviews)
stackView.axis = .vertical
stackView.distribution = .fill
stackView.spacing = 10
scrollView.addSubview(stackView)
stackView <- [
Edges(),
Width().like(self.view)
]
Just make sure that each of your subview's height is defined!

First and foremost design your view, preferably in something like Sketch or get an
idea of what do you want as a scrollable content.
After this make the view controller free form (choose from attribute
inspector) and set height and width as per the intrinsic content
size of your view (to be chosen from the size inspector).
After this in the view controller put a scroll view and this is a
logic, which I have found to be working almost all the times in iOS (it may require going through the documentation of that view class which one can obtain via command + click on that class or via googling)
If you are working with two or more views then first start with a view, which has been introduced earlier or is more primitive and then go to the view which has been introduced
later or is more modern. So here since scroll view has been
introduced first, start with the scroll view first and then go to the
stack view. Here put scroll view constraints to zero in all direction vis-a-vis its super view. Put all your views inside this scroll view and then put them in stack view.
While working with stack view
First start with grounds up(bottoms up approach), ie., if you have labels, text fields and images in your view, then lay out these views first (inside the scroll view) and after that put them in the stack view.
After that tweak the property of stack view. If desired view is still not achieved, then use another stack view.
If still not achieved then play with compression resistance or content hugging priority.
After this add constraints to the stack view.
Also think of using an empty UIView as filler view, if all of the above is not giving satisfactory results.
After making your view, put a constraint between the mother stack view and the scroll view, while constraint children stack view with the mother stack view.
Hopefully by this time it should work fine or you may get a warning from Xcode giving suggestions, read what it says and implement those. Hopefully now you should have a working view as per your expectations:).

For nested or single Stack view scroll view must be set a fixed width with the root view. Main stack view which is inside of scroll view must set the same width. [My scroll view is bellow of a View ignore it]
Set up an equal Width constraint between the UIStackView and
UIScrollView.

Place a scroll view on your scene, and size it so that it fills the scene. Then, place a stack view inside the scroll view, and place the add item button inside the stack view. As soon as everything’s in place, set the following constraints:
Scroll View.Leading = Superview.LeadingMargin
Scroll View.Trailing = Superview.TrailingMargin
Scroll View.Top = Superview.TopMargin
Bottom Layout Guide.Top = Scroll View.Bottom + 20.0
Stack View.Leading = Scroll View.Leading
Stack View.Trailing = Scroll View.Trailing
Stack View.Top = Scroll View.Top
Stack View.Bottom = Scroll View.Bottom
Stack View.Width = Scroll View.Width
code:Stack View.Width = Scroll View.Width is the key.

In my case the number of views inside the stackView was variable and I wanted to center the items. So, for instance, with one view in the stackView, I wanted this view to be centered in the middle of the screen, and if all the views did not fit inside the screen, I wanted the view to be scrollable.
This is the hierarchy of my view.
So I set a fixed width for the button, then, for the stackView:
Same fixed width as the button but with 650 priority.
Align X center to containerView
Trailing >= 0 and leading >= 0 to containerView
Bottom and top space to containerView
For the containerView:
Trailing, leading, bottom, top, equal height, equal width (250 priority) to superview
Fixed height
For the scrollView:
Trailing, leading, bottom, top to superview
The scrollView is also embedded in a view that has leading and trailing constraints.
And about the code I used to approach this:
for index in 0...array.count - 1 {
if index == 0 {
firstButton.setTitle(title, for: .normal)
} else {
let button = UIButton()
button.setTitle(title, for: .normal)
stackView.addArrangedSubview(button)
stackView.setNeedsLayout()
}
}
containerView.layoutIfNeeded()
stackView.distribution = .fillEqually
stackView.spacing = 10
stackView.alignment = .fill

something everyone seems to have missed when doing this with Storyboard is FIX THE MULTIPLIER!
When you're following the steps above in anyones tutorial and resetting the constant to 0 also check the multiplier and reset it to 1, it will have taken on some other factor when visually linking to remain in place
I found I can make a LOOOOOOOOOOOONG text block squash or stretch in a UIStackView simply with
Simply
Add constraints to the scrollview
Top left bottom right, cons 0
Add constraints to the stack view pointing at the scrollbars Content Layout Guide
Then add equal width or equal height constraint from the Frame Layout Guide.
Pick : width if the content needs to scroll vertically, height if it needs to scroll horizontally.
Now here is the key. Edit each constraint and reset the constant to 0 AND set the multiplier back to 1!!!!!
It gets wonky if you don't
If it works you can click on the inner content and mouse scroll

Related

Programmatic UIScrollview with Autolayout

After reading the technical notes on apple's website and reading matt neuburg's book on programming iOS 11 with a UIScrollview held in place with Autolayout, I have not been able to fully understand the concept of how it all works.
Basically what I want to have is a Scrollview that would have a child view ChildView where this child view then has a Textview.
Below I have attached the mockup of what I am trying to achieve Programmatically no-nibs, no storyboards.
and as for the code, This is what I usually come up with:
Code
let Scroller: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
scroll.backgroundColor = UIColor.alizarinColor()
return scroll
}()
// Content view
let ContentView : UIView = {
let content = UIView()
content.translatesAutoresizingMaskIntoConstraints = false
content.backgroundColor = UIColor.blue
return content
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(Scroller)
// Auto layout
Scroller.leftAnchor.constraint(equalTo: view.leftAnchor, constant:0).isActive = true
Scroller.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
Scroller.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
Scroller.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
Scroller.addSubview(ContentView)
// Undefined Content view
}
Please Note: for the ContentView, I normally define constraints to anchor the edges inside the scrollview but not in this case with Autolayout and the fact that I want it to scroll vertically upwards when the keyboard becomesFirstResponder. Another way I came up with this to try to work is to create a UIView that spans larger than the Scrollview to allow the child view to be a subview of this larger view that has the scroll view as its parent.
My Problem: How can I achieve this from here onwards? Any suggestions?
I have been giving it a thought to something like this: (ContentView would be the larger view that will allow this to be scrollable, and the child view would be the 3rd child view in the hierarchy)
You don't need to create a faux content view, you can add subviews directly to the scroll view (which I prefer). Apple does not recommend creating one, they only suggest that you can.
Subviews of the scroll view shall not rely on the scroll view to determine their sizes, only their positions.
Your constraints must define the left-most, right-most, top-most, and bottom-most edges in order for auto layout to create the content view for you.
When you create a scroll view, you may give its frame the bounds of the controller's view:
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
You must then set the boundaries of the content view by anchoring its subviews to the edges of the scroll view. To achieve vertical-only scrolling, your top-most view must be anchored to the top of the scroll view and none of the subviews anchored to the leading and trailing edges must exceed the width of the scroll view.
topMostView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(topMostView)
topMostView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
topMostView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
topMostView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
topMostView.heightAnchor.constraint(equalToConstant: 1000).isActive = true
Notice the topMostView does not rely on the scroll view to determine its size, only its position. The content in your scroll view now has a height of 1000 but it won't scroll because nothing is anchored to the bottom of the scroll view. Therefore, do that in your bottom-most view.
bottomMostView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(bottomMostView)
bottomMostView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
bottomMostView.topAnchor.constraint(equalTo: topMostView.bottomAnchor).isActive = true
bottomMostView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
bottomMostView.heightAnchor.constraint(equalToConstant: 1000).isActive = true
bottomMostView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
The last anchor may seem odd because you're anchoring a view that is 1,000 points tall to an anchor that you just anchored to the bottom of the view which is definitely less than 1,000 points tall. But this is how Apple wants you to do it. By doing this, you do not need to create a content view, auto layout does it for you.
Defining the "edge constraints" (left-most, right-most, top-most, bottom-most) goes beyond scroll views. When you create a custom UITableViewCell, for example, using auto layout, defining the four edge constraints (i.e. where the top-most subview is anchored to the top of the cell topMostView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true, the bottom-most subview to the bottom of the cell bottomMostView.topAnchor.constraint(equalTo: self.bottomAnchor).isActive = true, etc.) is how you create self-sizing cells. Defining the edge constraints is how you create any self-sizing view, really.
When you create a scrollView , apple recommends to put a contentView in it and give that contentView the width of the viewController's view and pin it's top , bottom,leading,trailing constraints to the scrollview , then begin by placing items from top to bottom as you want and pin the bottom most item to the bottom of the scollview's contentView , so the scrollview can render it's height , this bottom constraint can be as you like and according to it scrollview will continue scrolling until finishes it

UIScrollView that expands with contents' instrinsic size until height X, and then scrolls

I'm basically trying to reproduce the behavior of the title and message section of an alert.
The title and message labels appear to be in a scroll view. If the label text increases then the alert height also increases along with the intrinsic content size of the labels. But at a certain height, the alert height stops increasing and the title and message text become scrollable.
What I have read:
Articles
Auto Layout Magic: Content Sizing Priorities
Editing Auto Layout Constraints (documentation)
A Fixed Width Dynamic Height ScrollView in AutoLayout
Using UIScrollView with Auto Layout in iOS
Stack Overflow
Adding priority to layout constraints
Inequality Constraint Ambiguity
UIScrollView Scrollable Content Size Ambiguity
Ambiguity with two inequality constraints
IOS scrollview ambiguous scrollable content height in case of autolayout
The answer may be in there but I was not able to abstract it.
What I have tried:
Only focusing on the scroll view with the two labels I tried to make a minimal example in which a parent view would resize according to the intrinsic height of the scrollview. I've played around with a lot of constraints. Here is one combo (among many) that doesn't work:
I've worked with auto layout and normal constraints and even intrinsic content sizes. Also, I do know how to get a basic scroll view working with auto layout. However, I've never done anything with priorities and content hugging and compression resistance. From the reading I've done, I have a superficial understanding of their meanings, but I am at a loss of how to apply them in this instance. My guess is I need to do something with content hugging and priorities.
I think I have achieved an effect similar to the one you wanted with pure Auto Layout.
THE STRUCTURE
First, let me show you my structure:
Content View is the view that has the white background, Caller View and Bottom View have a fixed height. Bottom View has your button, Caller View has your title.
THE SOLUTION
So, after setting the basic constraints (note that the view inside scroll view has top, left, right and bottom to the scroll view AND an equal width) the problem is that the scroll view doesn't know what size should have.
So here comes what I have done:
I wanted that the scroll could grow until a max. So I added a proportional height to the superview that sets that max:
However, this brings two problems: Scroll View still doesn't know what height should have and now you can resize and the scroll view will pass the size of his content (if the content is smaller than the max size).
So, to solve both issues I have added an equal height with a smaller priority from the View inside of the Scroll View and the Scroll View
I hope this can help you out.
Your problem can't be solved with constraints alone, you have to use some code. That's because the scroll view doesn't have an intrinsic content size.
So, create a subclass of scroll view. Maybe give it a property or a delegate or something to tell it what its maximum height should be.
In the subclass, invalidate the intrinsic content size whenever the content size changes, and calculate the new intrinsic size as the minimum of the content size and the maximum allowed size.
Once the scroll view has an intrinsic size your constraints between it and its super view will actually do something meaningful for your problem.
It can be done in Interface builder using auto layout without any difficulties.
set outer container view ("Parent container for scrollview" in your sample) height constraint with "less than or equal" relation.
2.add "equal heights" constraint to content view and scroll view. Then set low priority for this constraint.
That's all. Now your scrollview will be resized by height if content height changed, but only to max height limited by outer view height constraint.
You should be able to achieve this solution via pure autolayout.
Typically if I want labels to grow as their content grows vertically I do this
[label setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
In order for your scrollview to comply to your requirements you will need to make sure a line can be drawn connecting the top of the scrollview all the way through the labels to the bottom of the scrollview so it can calculate it's height. In order for the scrollview to confine to it's parent you can set a height constraint with a multiplier of the superview of say 0.8
You can do this fairly simply with two constraints
Constraint 1: ScrollView.height <= MAX_SIZE. Priority = Required
Constraint 2: ScrollView.height = ScrollView.contentSize.height. Priority = DefaultHigh
AutoLayout will 'try' to keep the scrollView to the contentSize, but will 'give up' when it matches the max height and will stop there.
the only tricky part is setting the height for Constraint 2.
When my UIStackView is in a UIViewController, I do that in viewWillLayoutSubviews
If you're subclassing UIScrollView to achieve this, you could do it in updateConstraints
something like
override func viewWillLayoutSubviews() {
scrollViewHeightConstraint?.constant = scrollView.contentSize.height
super.viewWillLayoutSubviews()
}
To my project, I have a similar problem. You can using the following way to make it work around.
First, Title and bottom action height are fixed. Content has variable height. You can add it the mainView as one child using the font-size, then call layoutIfNeeded, then its height can be calculated and saved as XX. Then removed it from mainView.
Second, using normal constraint to layout the content part with scrollView, mainView has a height constraint of XX and setContentCompressionResistancePriority(.defaultLow, for: .vertical).
Finally, alert can show exact size when short content and show limited size when long size with scrolling.
I have been able to achieve this exact behavior with only AutoLayout constraints. Here is a generic demo of how to do it: It can be applied to your view hierarchy as you see fit.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let kTestContentHeight: CGFloat = 1200
// Subview that will shrink to fit content and expand up to 50% of the view controller's height
let modalView = UIView()
modalView.backgroundColor = UIColor.gray
// Scroll view that will facilitate scrolling if the content > 50% of view controller's height
let scrollView = UIScrollView()
scrollView.backgroundColor = .yellow
// Content which has an intrinsic height
let contentView = UIView()
contentView.backgroundColor = .green
// add modal view
view.addSubview(modalView)
modalView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([modalView.leftAnchor.constraint(equalTo: view.leftAnchor),
modalView.rightAnchor.constraint(equalTo: view.rightAnchor),
modalView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor,
multiplier: 0.5),
modalView.bottomAnchor.constraint(equalTo: view.bottomAnchor)])
let expandHeight = modalView.heightAnchor.constraint(equalTo: view.heightAnchor)
expandHeight.priority = UILayoutPriority.defaultLow
expandHeight.isActive = true
// add scrollview to modal view
modalView.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([scrollView.topAnchor.constraint(equalTo: modalView.topAnchor),
scrollView.leftAnchor.constraint(equalTo: modalView.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: modalView.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: modalView.bottomAnchor)])
// add content to scrollview
scrollView.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
contentView.widthAnchor.constraint(equalTo: modalView.widthAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.heightAnchor.constraint(equalToConstant: kTestContentHeight)])
let contentBottom = contentView.bottomAnchor.constraint(equalTo: modalView.bottomAnchor)
contentBottom.priority = .defaultLow
contentBottom.isActive = true
}
}

Disable horizontal scroll in UIScrollView with autolayout

I want to create view with only vertical scroll. As it turned out it is epic hard to do in iOs. I have done this steps:
1) Create UIViewController in storyboard;
2) Add ScrollView inside View in UIViewController and add 0 constrains to each side.
3) Add elements in scrollview, as result:
After I launch my app all works, but:
1) How should I disable horizontal scroll? I add 0 constrain to the right to my scrollView + 0 constrain to the right to my uilabel (as u see on screen for some reasons it is not attached to the right, it has different constrain, but in property I set constrain = 0) and, as I thought, label text supposed to be in my screen bounds, but when I launch app I can scroll to right, i.e. uilable text didn't wrap, my scrollview just resize to fit the text.I tried to set my scrollView in code: scrollView.contentSize = CGSize(UIScreen.mainScreen().bounds.width, height: 800), but didn't help.
2) If I scroll to much, then blank space appears and this is not cool, how to fix it?
1) Horizontal scroll enables automatically when the content width in scrollView more than width of scrollView. Therefore, in order to avoid horizontal scrolling is necessary to make width of the content inside scrollView less than or equal to scrollView width.
Leading space and trailing space can't set specific width to views, they just stretch them. In regular views, they no stretch for more than width of view, but scrollView is a special view, actually, with an infinite content width. Therefore, trailing space and leading space constraints in scrollView change the width of views to their maximum possible values (In case with UILabel you can see resize to fit the text).
To avoid horizontal scrolling, you need to set specific width of each view, less than or equal to scrollView width. Specific width of views may be set with width constraints.
Instead of setting each view width, much better to add a view–container and set width to it, and inside it place the views as needed.
Views hierarchy:
View
-> ScrollView
-> ContainerView
-> UILabel
-> UILabel
-> ... other views that you need
Autolayout constraints:
ScrollView
-> leading space to View : 0
-> trailing space to View : 0
-> top space to View : 0
-> bottom space to View : 0
Container View
-> leading space to ScrollView : 0
-> trailing space to ScrollView : 0
-> top space to ScrollView : 0
-> bottom space to ScrollView : 0
-> width equal to ScrollView : 0
To set width equal constraint ctrl+drag from containerView to scrollView.
2) Vertical scroll is dependent on the total height of content. Blank space can be, if the last element inside containerView has a large value of the bottom space to superview.
Or do you mean bounce effect? You can disable vertical bounce of scrollView.
You can also easily do this in code. In my case, I have a UIStackView that's the only subview of a UIScrollView
// Create the stack view
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
// Add things to the stack view....
// Add it as a subview to the scroll view
scrollView.addSubview(stackView)
// Use auto layout to pin the stack view's sides to the scroll view
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
])
// Now make sure the thing doesn't scroll horizontally
let margin: CGFloat = 40
scrollView.contentInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
let stackViewWidthConstraint = stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
stackViewWidthConstraint.constant = -(margin * 2)
stackViewWidthConstraint.isActive = true
The NSLayoutConstraint.activate bit is taken from Dave DeLong's excellent UIView extension here: https://github.com/davedelong/MVCTodo/blob/master/MVCTodo/Extensions/UIView.swift#L26

Create a vertical UIScrollView programmatically in Swift

I've been looking for days for a (working) tutorial or even an example app that uses UIScrollView to scroll vertically, programatically that is. There's tons of tutorials on using storyboard, leaving me at a loss.
I looked through apple's documentation, and their "guide" still not having a solid example or hint as to where to start. What I've attempted so far, is doing some of the following.
Making my view a scrollview Directly in the class
let scrollView = UIScrollView(frame: UIScreen.mainScreen().bounds)
Then assigning it to the view in my viewDidLoad function
self.view = scollView
Attempting to change the content size.
self.scrollView.contentSize = CGSize(width:2000, height: 5678)
Trying to enable scrolling with
scrollView.scrollEnabled = true
And the last suggestion I could find on doing this programatically
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.frame = view.bounds
}
Currently I havn't tried to start adding my objects to the scrollview, (I don't need to zoom, just do vertical scrolling), but I havn't managed to get anything working whatsoever :( In fact, running the app with those additions simply causes UIProblems, The screen is moved up weirdly, and it doesn't fit the entire width of my screen? I tried fixing that making the frame bounds equal to the width, but still didn't work
I'm not getting any errors.
I would love to see an example viewcontroller that you can scroll vertically in! Or any help would be hugely appreciated!
Here is a screenshot of the disaster, attempting to make my view scrollable causes.
(I made the scrollview background red, to see if it was even showing up correctly. Which it seems to be. Yet I can't scroll anywhere
As suggested in the comments, instead of doing self.view = self.scrollview, I tried adding scrollview as a subview of self.view instead, but with no positive results.
Adding
scrollView.contentSize = CGSize(width:2000, height: 5678)
to viewDidLayoutSubviews, as suggested in the comments below made my view scrollable!
However my layout still looks like a complete mess for some reason (it looks as it's supposed to, before I made it scrollable).
Here's an example constraint for my topBar (blue one), that's supposed to take up the entire horizontal space.
self.scrollView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[topBar]|", options: nil, metrics: nil, views: viewsDictionary))
Any ideas why this doesn't work?
Swift 4.2
I make simple and complete example of scroll a stack view using auto layout.
All view are in code and don't need story board.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.addSubview(scrollViewContainer)
scrollViewContainer.addArrangedSubview(redView)
scrollViewContainer.addArrangedSubview(blueView)
scrollViewContainer.addArrangedSubview(greenView)
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollViewContainer.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
scrollViewContainer.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
scrollViewContainer.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
scrollViewContainer.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
// this is important for scrolling
scrollViewContainer.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let scrollViewContainer: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 10
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let redView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 500).isActive = true
view.backgroundColor = .red
return view
}()
let blueView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 200).isActive = true
view.backgroundColor = .blue
return view
}()
let greenView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 1200).isActive = true
view.backgroundColor = .green
return view
}()
}
hope this help!
Strictly I feel problem is at below line.
self.view = scollView
It should be self.view.addSubview(scollView)
Then add all label, buttons, etc in scrollview and then give content size.
Content size is the parameter that will tell scrollview to scroll.
Put self.scrollView.contentSize = CGSize(width:2000, height: 5678) inside viewDidLayoutSubviews.
Thanks to Tom Insam who helped me put this answer together:
You need to add all constraints as you usually do i.e.
add constraints for the scrollview and its superview.
add constraints for the contents of the scrollview.
Then pause. Understand that at this point, even if your scrollView's frame is known to be e.g. 400 * 600, still the size of its content is unknown. It could be 400 * 6000 or 400 * 300 or any other size.
There's no other edge (leading, trailing, left, right, top, bottom, margins) based constraint that you can use to calculate the scrollview's content size.
Unless your views have some intrinsicContentSize, then at this point if you run the code, you'll get a run-time error saying: ScrollView Content size is ambiguous. Continue reading to learn more about intrinsicContentSize.
What's the solution?
https://developer.apple.com/library/archive/technotes/tn2154/_index.html
To use the pure autolayout approach do the following:
Set translatesAutoresizingMaskIntoConstraints to NO on all views involved.
Position and size your scroll view with constraints external to the scroll view.
Use constraints to lay out the subviews within the scroll view, being sure that the constraints tie to all four edges of the scroll view and do not rely on the scroll view to get their size.
The part I bolded is tho whole focus of my answer. You need to set a (horizontal/vertical) constraint that's independent of edges. Because otherwise it would rely on the scrollview to get its size.
Instead you need to explicitly set constraints on the width, height or attach to the center of the axis. The 3rd bullet is unneeded if the view has intrinsicContentSize e.g. labels or textviews can calculate their contentsize based on font, character length, and line breaks. To dig deepinter into intrinsicContentSize, see here
To say things differently:
ScrollView needs:
external constraints against its superview
internal constraints for its content
edge-to-edge / chained subviews in a way that its height (if scrolling vertically can be calculated). To the Layout engine setting a contentSize with a height of 1000 shouldn't be any different from chaining multiple subviews where the height of the all the content can be calculated as 1000. e.g. you have two labels together they have 400 lines. Each lines takes 2points and there's 100points of line space and 100 points of space between the two labels equaling to 1000 ( 400 * 2 + 100 + 100) points.
You see, the OS can calculate that without looking into the surroundings of the label. That's what I mean by calculating the size without relying on the scrollview
Also see docs from here as well.

In autolayout, how can I have view take up half of the screen, regardless of orientation?

In portrait mode, I have a tableview on the left-side, and a webview on the right-side. They both take half of the screen. I have autolayout enabled.
But when I rotate the screen to landscape mode, the tableview's width takes up less than half the screen's size, and the webview ends up taking more, instead of them being an even split.
Why does this happen? How can I have the views take up only half the screen in regard to their widths, regardless of screen orientation?
The first half of the answer address the case in which we want to split the view evenly between view A (blue) and view B (red). The second half will address the case in which we want to have view A take up half the screen, but view B does not exist.
Step 1:
Set up blue's auto-layout constraints as pictured. Top, left, bottom of 0 to the superview. Right of 0 to the red view.
Step 2:
Set up the same, but mirrored, constraints for the red view.
If you've completed the first two steps correctly, you should have some auto-layout errors and warnings:
We need one more constraint to fix these errors/warnings and get what we need.
Step 3:
Hold control, click and drag from one view to the other and select "equal widths". Now our views will always maintain the same width. All of our auto layout warnings and errors disappear, and our views will always be half the screen no matter the orientation or device.
To add these constraints in code using VFL, we need the following constraints:
#"H:|[blueView(==redView)][redView]|"
#"V:|[blueView]|"
#"V:|[redView]|"
Now, suppose the case where we want a single view to take up half the screen, but we don't have a view for the other half. We can still do this with auto layout, but it's a slightly different set up. In this example, our view is blue, and its parent view is green.
Step 1:
This is similar to step 1 above, except we don't add a right side constraint (this will obviously vary if we want our view to take up a different half).
Step 2:
Like before, we want to assign an "equal widths" constraint. In this case, from our view to the parent view. Again, hold control and click drag from one to the other.
At this point, we have an auto layout warning. Auto layout wants our frame to match its parent's width. Clicking the warning and choosing "Update constraints" will put a hardcoded value in. We don't want this.
Step 3:
Select the view and go to its size inspector. Here, we'll be able to edit the constraints.
Click "Edit" next to the "Equal Width to:" constraint. We need to change the multiplier value.
We need to change the multiplier value to 2.
The constraint now changes to a "Proportional Width to:", and all of our auto layout warnings and errors disappear. Now our view will always take up exactly half of the super view.
To add these constraints in code, we can add some using VFL:
#"H:|[blueView]"
#"V:|[blueView]|"
But the proportional width constraint can't be added with VFL. We must add it as such:
NSLayoutConstraint *constraint =
[NSLayoutConstraint constraintWithItem:blueView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeWidth
multiplier:2.0
constant:0.0];
Create a UIView in XIB or Storyboard
Set top and left constraints
Set a height constraint
Set width constraint to equal width of the super view *Important
Edit the width of the constraint by converting it to decimal
Set the decimal to the width you'd like (In my case 0.5 or half)
Bind both of them to the superview edges (table view to the leading edge, web view to the trailing edge) and set constraint for them to have same width.
Programatically in Swift 4:
import UIKit
class ViewController: UIViewController {
let halfScreenViewLeft: UIView = {
let view = UIView()
view.backgroundColor = .blue
return view
}()
let halfScreenViewRight: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(halfScreenViewLeft)
view.addSubview(halfScreenViewRight)
//Enable Autolayout
halfScreenViewLeft.translatesAutoresizingMaskIntoConstraints = false
//Place the top side of the view to the top of the screen
halfScreenViewLeft.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
//Place the left side of the view to the left of the screen.
halfScreenViewLeft.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
//Set the width of the view. The multiplier indicates that it should be half of the screen.
halfScreenViewLeft.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true
//Set the same height as the view´s height
halfScreenViewLeft.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
//We do the same for the right view
//Enable Autolayout
halfScreenViewRight.translatesAutoresizingMaskIntoConstraints = false
//Place the top side of the view to the top of the screen
halfScreenViewRight.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
//The left position of the right view depends on the position of the right side of the left view
halfScreenViewRight.leadingAnchor.constraint(equalTo: halfScreenViewLeft.trailingAnchor).isActive = true
//Place the right side of the view to the right of the screen.
halfScreenViewRight.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
//Set the same height as the view´s height
halfScreenViewRight.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
}
The constraint system can't read your mind. You must say what you want. Half is half. If you want half, say half. To make a view half the width of its superview, give it a width constraint that has a multiplier of .5 in relation to its superview's width.

Resources