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.
Related
I have this UIVIew extension method
func addSubviewWithConstrainedBounds(subview:UIView) {
subview.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(subview)
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
subview.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
subview.topAnchor.constraint(equalTo: self.topAnchor, constant: 0),
subview.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0),
])
}
The idea being that I can programmatically add a view into another, where I'll leverage autolayout to match the bounds of the subview to the superview
When I call this to add my subview to a UIView superview, it works
However, when I try to add my subview to a UITextView superview, the subview insists on taking some width and height constraints from "content size". As you can see my trailing and bottom constraints are ignored (no exceptions in console either)
For comparison, here are the runtime constraints when I added the UILabel into a UIView instead of a UITextView
am I potentially missing a property here? something that I need to set? or is this just a quirk of trying to add a subview to a UITextView?
The reason behind this behavior is that a UITextView is a UIScrollView. That means that if your subview does not have a height/width, your layout is ambiguous, so the layout engine adds the intrinsic size to resolve that issue.
It depends on how or where you want to position your subview in your textview, in that case you need to be more specific about the alignment. Check out the options for content hugging priority if that helps you.
But in the end i think you might be better off to not add a label as a subview, if you can use TextKit's features to style and position the content of your planned label inside your textview.
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
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?
[_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.
I am using Auto Layout in a storyboard, no code, and am having difficulties in getting the view inside the scroll view's content view to stretch to fill the device width. I understand the issue is an ambiguous scroll view width, but I'm not sure how to make it non-ambiguous when I want it to always stretch to fill available width (with some padding).
In a view controller, I added a scroll view with 4 constraints: top, bottom, leading, trailing to superview. I added a view to the scroll view that will act as the content view - all subviews will be added to the content view. It has 4 constraints: top, bottom, leading, trailing to scroll view. I then added the view I want to be visible (a simple red box that has a fixed height but stretches to fill the screen width) to the content view. Its constraints are: trailing to superview (15), leading to superview (15), top to superview (15), bottom to superview (15), and height equals 60.
This results in an ambiguous scroll view width, and the frames are misplaced - it wants to set the box view's width to 0.
How can I set this up so the box view stretches to fill the device screen, resolving the scroll view content size width ambiguity?
In iOS 11 and later, scroll views have two sets of layout guides, one for the scrollable content, contentLayoutGuide (which dictates the scrolling behavior), and one for its frame, frameLayoutGuide (which dictates the size of the subview).
For example, this adds a subview that is inset by 20 points all the way around, whose width is set relative to the frame of the scroll view, but has a fixed height:
let subview = UIView()
subview.translatesAutoresizingMaskIntoConstraints = false
subview.backgroundColor = .red
scrollView.addSubview(subview)
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 20),
subview.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -20),
subview.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 20),
subview.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -20),
subview.heightAnchor.constraint(equalToConstant: 1000),
subview.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -40),
])
You can also do this in IB, without any coding at all:
Prior to iOS 11, in the absence of the frameLayoutGuide, you had to set the subview’s constraints based upon the scroll view’s superview, and I outline that process below. But since iOS 11, the above is the more intuitive solution.
The constraints you described in your question are equivalent to the following VFL:
The scroll view occupies the entire view
H:|[scrollView]|
V:|[scrollView]|
The red view is 60 pt tall and has a margin of 15 to the edges of the scroll view's contentSize:
H:|-(15)-[redView]-(15)-|
V:|-(15)-[redView(60)]-(15)-|
The red view is ambiguous because there is nothing that defines its width. (The horizontal constraints between the red view and the scroll view define the scroll view's contentSize, not the red view's width. See Apple Technical Note 2154.)
You resolve this ambiguity by adding a constraint that says that the red view is 30pt narrower than the main view. So add constraint between red view an the scroll view's superview by control-dragging from the red view to the scroll view (and this is probably easiest to do from the document outline):
Then choose "equal widths":
Having defined the red view to be the same width of the main view, you now have to alter that constraint to modify the constant to adjust for the margins you supplied between the red view and the scroll view's contentSize. Thus, select that constraint you just added and edit it, changing the constant to -30:
Frankly, that Interface Builder technique is a little cumbersome. It may be easier to illustrate how to do this programmatically:
view.addConstraint(NSLayoutConstraint(item: redView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1.0, constant: -30))