I have a Screen like this which is built in interface builder:
The control at the bottom is a UICollectionView and the other two are a UIButton and a segmentedControl. They sit on a UIView which is a child of the UIController's view but has the same frame. In my code, I add the UIViewController to a UINavigationController so there will be a UINavigationBar on the top of the screen. I can set the auto layout constraint to force the top of this screen to move below the navigationbar. However when I scroll down the UICollectionView I couldn't scroll to the bottom to view the rest of the items. I can only see half size of the last two items.
I have updated the code and put some log in viewDidAppear:
UICollectionView frame height = 527.000000
View frame = 504.000000
So the UICollectionView's height is bigger than the view's height. I want the collection view to fit right in both 3.5" screen and 4" screen.
Any idea what I have done wrong? How can I fix this?
This is because your collection view's height is exceeding that of the frame, try to reduce the height of your collection view which fits that of your UIView's height - your collection view's yOrigin.
The problem is that when the navigation bar is added, the UICollectionView gets "pushed" down but its height remains the same.
You need to add a layout constraint that will cause the UICollectionView to reduce height when adding the navigation bar. The constraint should be something along the lines of "bottom space to bottom layout guide = 0"
Swift 4.1
Try to define the constraints as follows to consider the Navigation Bar (this should work even with autorotation, when the status bar can change in height) It also considers the chante in iOS 11 for the safeArea.
let collectionView = UICollectionView()
self.view.addSubview(collectionView)
if #available(iOS 11.0, *) {
let safeArea = self.view.safeAreaLayoutGuide
collectionView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 0).isActive = true
} else {
let topGuide = self.topLayoutGuide
collectionView.topAnchor.constraint(equalTo: topGuide.bottomAnchor, constant: 0).isActive = true
}
collectionView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0).isActive = true
collectionView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0).isActive = true
collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
Related
I have two views, one UIView and one UIStackView
let mainView = UIView()
mainView.translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
They are both added to the superView with these constraints
NSLayoutConstraint.activate([
mainView.topAnchor.constraint(equalTo: view.topAnchor),
mainView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mainView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackView.topAnchor.constraint(equalTo: mainView.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
When both the UIView and the UIStackView doesn't hold any other views, I get warnings on both views in the debugger
Height is ambiguous for uiview
Height and vertical position is ambiguous for uistackview
But if I add a UIButton to the UIStackView, and then set it to hidden with .isHidden = true, both warnings are gone, except for iOS10 where both warnings remain.
With or without the debugger warnings I get the result I want, that if the UIStackView is empty (or all subviews hidden), the UIView covers the whole screen.
What is going on here, why is one hidden view not the same as an empty stackView, and why does the behavior differ on iOS10? And last but not least, how can I satisfy iOS10 without compromising my current layout, that seemingly works for everything after iOS10?
Here's what you got — mainView and stackView are have no heights and edges are clipped.
There's no way to calculate vertical position and height base on that constraints. You can add some height constraint on either view to make it clear for auto layout
mainView.heightAnchor.constraint(greaterThanOrEqualToConstant: 1)
UIScrollView lets us set a scrollIndicatorInset. However, when setting a bottom inset for this value on a landscape iPhone with a safeAreaInset (i.e. a 'notch'), the horizontal position of the scrollbar is unexpectedly updated.
Here is a scroll view on an iPhone X with no changes to scrollIndicatorInset - note that the scrollbar on the right edge is horizontally flush with the edge of the screen.
Now I add one line of code:
scrollView.scrollIndicatorInsets.bottom = 1
The bottom edge of the scroll indicator is inset as expected. But the scrollbar is now also relocated horizontally to align with the safe area, rather than the screen edge.
What is the cause of this horizontal inset and how can I prevent it?
Setting the bottom scroll inset to anything other than 0 adds the additional margin, and setting it back to 0 removes it. The same thing applies when setting any of the scroll insets edges.
Some logging before and after setting the scrollIndicatorInset shows no change to the safeAreaInset nor the layoutMargins on the view, the scroll view, or the content view inside the scroll view.
One place this is a problem is when placing a text field inside a scroll view and adjusting the bottom inset to accommodate the keyboard. The scrollbar jumps around as the keyboard is presented and dismissed.
I am providing a small view controller below in case you want to try it out for yourself.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = UIScrollView()
scrollView.backgroundColor = .white
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.widthAnchor.constraint(equalToConstant: 50).isActive = true
contentView.heightAnchor.constraint(equalToConstant: 500).isActive = true
// this causes the issue
scrollView.scrollIndicatorInsets.bottom = 1
}
}
This is an old question, but as a workaround you can also set, in your case, the scroll view's right indicator inset to an appropriate negative distance, such as the parent view's right safe area inset.
override func viewDidAppear(_ animated: Bool) {
scrollView.verticalScrollIndicatorInsets.right = -self.view.safeAreaInsets.right
}
In iOS 13 and above, we should be using horizontalScrollIndicatorInsets and verticalScrollIndicatorInsets as scrollIndicatorInsets has been deprecated.
This isn't a perfect workaround, as you'll find the top and bottom insets also need adjusting. For instance, maybe you're trying to set the bottom inset to end halfway up the screen, and you're happy with that. But after these adjustments the top inset will now be zero by default, so you'll probably want to adjust that, too, or the scroll indicator will go all the way up into the rounded corner of the device.
I have a view that is composed of an image, a form with 11 UITextfield and a button, but the form is too big for my screen that is why I tried to use a UIScrollview.
The error I have is that my UIScrollview does not work as I can solve this problem.
This is my code:
import UIKit
class LoginCtrl: UIViewController {
let scrollView: UIScrollView = {
let sv = UIScrollView(frame: UIScreen.main.bounds)
sv.isScrollEnabled = true
sv.contentSize = CGSize(width: 2000, height: 5678)
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(r: 0, g: 150, b: 136)
self.view.addSubview(scrollView)
scrollView.addSubview(contenedorCampos)
setear_posicion_scrollView()
setear_posicion_contenedor()
}
func setear_posicion_scrollView(){
//definir x,y,width,height constraints
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
var heightContenedor: NSLayoutConstraint?
func setear_posicion_contenedor(){
//definir x,y,width,height constraints
contenedorCampos.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
contenedorCampos.topAnchor.constraint(equalTo: tabsInicio.bottomAnchor, constant: 12).isActive = true
contenedorCampos.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -24).isActive = true
heightContenedor = contenedorCampos.heightAnchor.constraint(equalToConstant: 400)
heightContenedor?.isActive = true
contenedorCampos.addSubview(txtNombres)
contenedorCampos.addSubview(divider_txtNombres)....
}
}
Thanks
You've defined the relationship between the scrollview and its superview(which defines its frame), but not the relationship between the scrollview and its subviews (which defines its contentSize). As a result, the actual contentSize of the scrollview will be just be (0, 0).
In other words, you never actually laid anything out, at least not in the code you posted.
What you need to do is define layout constraints for the actual child views (everything that is a subview of the scrollview). Make sure to set up constraints definitively pinning these components to the edges of their parent (the scrollview). Once you have defined the constraints sufficiently, the scrollview should have a content size.
Technical note about this
In general, Auto Layout considers the top, left, bottom, and right
edges of a view to be the visible edges. That is, if you pin a view to
the left edge of its superview, you’re really pinning it to the
minimum x-value of the superview’s bounds. Changing the bounds origin
of the superview does not change the position of the view.
The UIScrollView class scrolls its content by changing the origin of
its bounds. To make this work with Auto Layout, the top, left, bottom,
and right edges within a scroll view now mean the edges of its content
view.
The constraints on the subviews of the scroll view must result in a
size to fill, which is then interpreted as the content size of the
scroll view. (This should not be confused with the
intrinsicContentSize method used for Auto Layout.) To size the scroll
view’s frame with Auto Layout, constraints must either be explicit
regarding the width and height of the scroll view, or the edges of the
scroll view must be tied to views outside of its subtree.
I couldn't get my UIScrollView to scroll.
Here is my code:
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame : CGRect ( x:0,y:0,width:UIScreen.main.bounds.width,height:UIScreen.main.bounds.height))
scrollview.delegate = self
view.addSubview(scrollView)
for i in 0...14 {
let numLabel = UILabel(frame : CGRect( x : 0 , y : 10+(i*40) , width : UIScreen.main.bounds.width - 20 : height : 40))
numlabel.text = "\(i)"
scrollView.addSubview(numLabel)
}
}
}
This is making the views appear but not scrolling.
Scroll view scrolls to its content size.
Whenever you add a subview to scroll view you should make sure that your scroll view's content size is enough to fit the new view.
In your case you are not taking care of that.
Ideally whenever you add a subview you should correspondingly adjust the hight of the scroll view content size.
In your case after you have added all of the labels to scroll view i.e. after for loop add following line
self.scrollView.contentSize = CGSize(width:self.scrollView.bounds.size.width,height:10+(15*40))
or in the for loop after adding label you can do the following
self.scrollView.contentSize = CGSize(width:self.scrollView.bounds.size.width,height:10+((i+1)*40))
The second approach is better. Because if you add more labels to scroll view it will take care of that. Again, make it a rule of thumb, whenever adding a view to scroll view make sure that its content size is updated to fit all the subviews.
While you can manually set the contentSize, I would not advise doing that.
Instead, I'd use constraints for the subviews of the scroll view. The auto-layout engine will calculate the contentSize for you automatically. It will also take care of adjusting everything if the device rotates.
I'd also suggest using a stack view, you don't have to mess around with either manual frames for the labels nor with constraints between them.
So, you can do something like:
var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
for i in 0 ... 140 {
let numLabel = UILabel()
numLabel.translatesAutoresizingMaskIntoConstraints = false
numLabel.text = "\(i)"
numLabel.font = .preferredFont(forTextStyle: .body)
stackView.addArrangedSubview(numLabel)
}
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
])
}
Note the use of UIFont.preferredFont(forTextStyle: .body) to enjoy Dynamic Text. This means that if the user has a larger font specified in their settings, this will automatically show the larger font in this app. But more importantly, we didn't have to calculate the size of the label for that font. Constraints and the stack view took care of both the frames of the labels as well as the scroll view's contentSize.
For the sake of completeness, it's worth noting that the alternative is to use a UITableView or UICollectionView. This is a scalable, memory efficient way of viewing data within a scroll view. It's beyond the scope of this question, but it's worth remembering as you consider creating large scroll views.
I have a table view which displays table view cells and a header view (search bar). I want the width of the table view cells to be 0.9 of the device screen size, and the header view to have the same size. However, this is not possible because cells and the header view are all contained in a table view.
When I use layout anchor constraints everything gets resized (including the header view).
self.tableView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
// here's the 0.9 multiplier
self.tableView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.9).isActive = true
self.tableView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
How can I make sure that the table view header has the same width of the view controller, while keeping the table view width to be 0.9 of the main view?
It is best not to try to adjust the constraints of a TableViewCell or its contentView. However, you can just set both those views to have a background color of .clear and then add subviews to the contentView with the constraints you want.
class CustomCell: UITableViewCell {
let smallerView = UIView()
override init(frame: CGRect) {
super.init(frame)
smallerView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(smallerView)
smallerView.widthAnchor.contraint(equalTo: contentView.widthAnchor, multiplier: 0.9).isActive = true
smallerView.heightAnchor.contraint(equalTo: contentView.heightAnchor).isActive = true
}
}
Your custom cell "starts with" a ContentView, which uses uses the table view's insets to set its width...
So, if you have a bunch of elements / objects that you are adding to the content view...
Your best bet is to probably first add a standard UIView as a "container view"... (just my naming of it)...
Then, set the constraints on that "containing view" to be 0.9 of the width of the contentView...
All of the elements you add to that "containing view" will be constrained relative to it and thus will all stay inside the 0.9-width overall...