Programmatic UIScrollview with Autolayout - ios

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

Related

Scrollview Height Changes when Alert Controller is presented

I have a scrollview with an image and text. The scrollview correctly displays its height encapsulating the image and text. When an alert controller is presented to the screen and dismissed, the scrollview's height changes. Any thoughts on why this happens and how to fix this?
View heirarchy before alert is presented:
After alert is presented:
The view is added after viewDidLoad and programmatically using constraints:
let offlineView = OfflineView()
view.addSubview(offlineView)
offlineView.translatesAutoresizingMaskIntoConstraints = false
offlineView.topAnchor.constraint(equalTo: navBar.bottomAnchor, constant: 0).isActive = true
offlineView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
offlineView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0).isActive = true
offlineView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0).isActive = true
OfflineView Xib constraints:
My label in the scrollview constraints were Center X axis and Static width.
When I changed the label constraints to leading to superview = 45, trailing to superview >= 45, the error no longer persisted.
I don't know why/how this happened, but it fixed my problem.. If anyone could explain this phenomenon.
This behavior seems to be weird. But you can give the below solution a try:
You are directly adding a UIScrollView to the main view. Instead, you should take one container view on main view and then add your scrollView on to that view and attach the constraints to that container view. And that container view should have the constraints wrt main view. This will solve your problem.

Why safeAreaLayoutGuide is property of UIView? Why it is not associated with UIViewController?

Before iOS11, there was topLayoutGuide and bottomLayoutGuide are the two instance properties of UIViewController. That are deprecated in iOS11.
In iOS11, apple introduces safeAreaLayoutGuide. As per my observation it is an instance property of UIView. What is the main reason behind this strategy?
The question arises in my mind now is: Right now I have a UIView instance that is associated with UIViewController. Now I added UIScrollView as subview to main UIView. For that I apply constraints with safeAreaLayoutGuide. See below code
view.addSubview(scrollView)
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor).isActive = true
Next I have to add another UIView as subview to UIScrollView. I am doing it with respect to safeAreaLayoutGuide of UIScrollView. See below code.
scrollView.addSubview(vwAvatar)
vwAvatar.leadingAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.leadingAnchor).isActive = true
vwAvatar.topAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.topAnchor).isActive = true
vwAvatar.widthAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.widthAnchor).isActive = true
vwAvatar.heightAnchor.constraint(equalTo: vwAvatar.widthAnchor, multiplier: 3.0 / 4.0).isActive = true
Now my question is whether I am doing this in right way while adding Subview to UIScrollView? Or I should go with typical approach of adding constraints with leading and trailing anchors, while adding subview to UIScrollView?

iOS, the height of scrollView content size is equal to the content view's height, but it can still scroll vertically?

[_contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(_scrollTabs);
make.height.mas_equalTo(_scrollTabs);
}];
[_scrollTabs mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self);
}];
self is a custom view.
I set the constraints like this. Am I doing something wrong? Thanks!
Bottom line, if you unambiguously define the constraints between a content view and the scroll view (which will define the contentSize pursuant to TN2154) and then unambiguously define the constraints between the content view and its subviews, then, yes, it will correctly define the size of the scroll view's contentSize.
I would suggest using the view debugger, , and look at the constraints in the right panel:
In the particular screen snapshot, I've selected the third subview (dark blue) inside the content view inside the scroll view, and it tells us that the active constraints are:
offset 10 from leading edge of container (green)
offset 10 from trailing edge of container (green)
offset 10 from the subview above (dark red)
fixed height of 200 (I do this because this has no implicit height)
a width of the main view (bright red) less 20 (so that it occupies an appropriate amount of horizontal space, again because there is no implicit width)
offset 10 from the subview below (light blue)
So, you just click on your various subviews and the container, and confirm that the constraints are precisely what you intended. It's all to easy to miss a constraint and/or fail to activate one, and the whole thing falls apart.
By the way, sometimes it's not obvious what views the constraints are between, but if you tap on the constraints button, , when a view is selected, it will highlight just the views to which you have constraints (in this example, to the content view, to the subviews above and below, and the main view; since neither the scroll view (yellow) nor the first subview (purple) have any constraints to this third subview, so you just see their wire-frame, not their content):
Note, this is an example, I thought that I'd show you the constraints I used so that auto layout can correctly calculate the contentSize based upon a content view and subviews with fully satisfied, unambiguous constraints:
let contentView = ContentView()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = randomColor()
scrollView.addSubview(contentView)
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
])
var previousView: UIView?
for _ in 0 ..< 4 {
let subview = SomeSubview()
subview.translatesAutoresizingMaskIntoConstraints = false
subview.backgroundColor = self.randomColor()
contentView.addSubview(subview)
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
subview.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
subview.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20),
subview.heightAnchor.constraint(equalToConstant: 200)
])
if previousView != nil {
subview.topAnchor.constraint(equalTo: previousView!.bottomAnchor, constant: 10).isActive = true
} else {
subview.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10).isActive = true
}
previousView = subview
}
previousView?.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10).isActive = true
What strikes me odd in your example is that you are setting both edges (which I assume is setting the top, bottom, leading, and trailing constraints) and height. Setting the height of the content view is not needed. You can just define the edges and you should be good. The height is dictated by the constraints of its subviews.
If your subviews appear to be laid out correctly but your scroll view's contentSize is not getting set correctly, then the culprit may be a missing bottom constraint between the last subview and your content view.
If you're still having problems, I'd suggest you create a simplified, yet complete example of your problem. The code that you've shared is insufficient. But we don't want to see all of your code nor your specific UI, either. Instead, create a stand-alone simplified example that manifests the problem you describe. Only if we can reproduce your problem can we help you solve it.

Is it possible for UIStackView to scroll?

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

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.

Resources