Error UIScrollView in swift 3 - ios

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.

Related

Setting a bottom scroll indicator inset on a UIScrollView view has unexpected effect on horizontal scrollbar position

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.

How to setup auto layout constrains so that UILabel stays vertically centered inside UIScrollView

How should I setup auto layout constrains so that multiline label stays vertically centered inside scrollview until it's text content becomes too long to be shown at once? When the text length becomes too long text should be aligned at top with the scrollview so the user can see the beginning of text and scroll for more. This is how I tried to setup constrains
for scrollView:
Equal Height to: Superview
Align Trailing to: Safe Area, Equals = -8
Align Leading to: Safe Area, Equals = 8
Align Top to: Safe Area
for label:
Leading Space to: Superview
Equal Width to: Superview
Align Center Y to: Superview
I also added following code to viewDidLoad()
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
The problem is that but I still get some warnings and also text ends up "vertically centered" inside scrollview without possibility to really scroll to the beginning or the end of it, even when it can not fit whole inside. For scrollview I get warning that it "has ambiguous scrollable content width", while for the label I get warning "trailing constraint is missing, which may cause overlapping with other views"
How about constraining the label height to the height of the scrollView's superview? In that case the label will be always as big as the screen where it is presented, and since by default the text in a UILabel is centered vertically, you would get what you want. See the following Playgrounds example for reference:
import PlaygroundSupport
import UIKit
class A: UIViewController {
let scrollView = UIScrollView()
let label = UILabel()
override func viewDidLoad() {
self.view.backgroundColor = .white
self.view.addSubview(scrollView)
self.scrollView.addSubview(label)
label.numberOfLines = 0
label.text = "How should I setup auto layout constrains so that multiline label stays vertically centered inside scrollview until it's text content becomes too long to be shown at once?"
scrollView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
scrollView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor),
label.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor),
label.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor),
label.topAnchor.constraint(equalTo: self.scrollView.topAnchor),
label.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor),
label.heightAnchor.constraint(greaterThanOrEqualTo: self.view.heightAnchor),
])
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = A()

ScrollView not scrolling with subviews

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.

UIScrollView not scrolling at all when added programmatically in Swift?

So, I have looked through almost all of Stackoverflow's answers to this particular question and even looked through tutorials that supposedly teach you how to use a scroll view but It doesn't seem to apply for my project..
Here is what I know so far, in order for a Scroll View to properly work you first need to give it a content size. This determines the scrollable height etc.
I have provided some code to give you all a better idea of how I am adding said items into my scrollview. If there is something that I am doing wrong or if there is a better way to go about doing this please let me know, I am still fairly new to Swift and iOS development and in my mind it feels like I am doing it correctly.
The steps I am taking
Create items that I want to display (Input fields, Imageviews etc..)
Add said items to the view of the viewcontroller. (view.addsubview(etc..))
Create a scrollView and set its constraints to be same as the screen / view
Add our view with all the items in it into said scroll view
Relax and everything should work out perfect?????
Here is my code, I know it might be lengthy but I think it might be needed so that the scope of my question is understood
class JobRegistrationController: UIViewController {
// ... Omitted for clarity
lazy var scrollView: UIScrollView = {
let view = UIScrollView(frame: UIScreen.main.bounds)
view.backgroundColor = .red
view.contentSize = CGSize(width: self.view.bounds.width, height: self.view.bounds.height * 2)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//... Omitted for clarity
let scrollContentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Need so that view controller is not behind nav controller
self.edgesForExtendedLayout = []
view.addSubview(scrollView)
scrollView.addSubview(scrollContentView)
scrollContentView.addSubview(jobTypeField)
scrollContentView.addSubview(jobTypeDividerLine)
// x, y, width and height constraints
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).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
// x, y, width and height constraints
scrollContentView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollContentView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollContentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollContentView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
// x, y, width and height constraints
jobTypeField.leftAnchor.constraint(equalTo: scrollContentView.leftAnchor, constant: 20).isActive = true
jobTypeField.topAnchor.constraint(equalTo: scrollContentView.topAnchor).isActive = true
jobTypeField.rightAnchor.constraint(equalTo: scrollContentView.rightAnchor, constant: -8).isActive = true
jobTypeField.heightAnchor.constraint(equalToConstant: 30).isActive = true
// x, y, width and height constraints
jobTypeDividerLine.leftAnchor.constraint(equalTo: scrollContentView.leftAnchor, constant: 20).isActive = true
jobTypeDividerLine.topAnchor.constraint(equalTo: jobTypeField.bottomAnchor).isActive = true
jobTypeDividerLine.rightAnchor.constraint(equalTo: scrollContentView.rightAnchor).isActive = true
jobTypeDividerLine.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
Use this method in your class
override func viewDidLayoutSubviews()
{
scrollView.delegate = self
scrollView.contentSize = CGSize(width:self.view.frame.size.width, height: 1000) // set height according you
}
view.contentSize = CGSize(width: self.view.bounds.width, height: self.view.bounds.height * 2)
You should try to log the contentSize in your console after trying to access it. I am not sure if you are setting the correct contentSize here if the self.view.bounds has been calculated correctly when this gets called at that moment. Since it takes time for self.view frame and bounds to be calculated.
Try setting your contentSize after you have added the actual content to it based on the actual total content size.
EDIT:
Add a single UIView inside the scrollView, with the constraints set to top-bottom-leading-trailing, and add your subviews to it. Also, set the same constraints on the scrollView to the superView top-bottom-leading-trailing.
I believe the line of code below is the problem
scrollContentView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
You are setting your content view to the top of the view, when you should be setting it to the top of the scrollview.
I've just overcome a similar issue were I was setting the topAnchor of my first view to the safeAreaLayoutGuide.topAnchorof the scrollView. Everything laid out correctly but the constraint wouldn't show and therefore the entire content of the scrollView didn't move.
The problem is that you don't tell where the bottom of your content is. In other words you need some bottom constraints.
If you use...
scrollContentView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
...you need also to add a constraint to bind at least one view to the bottom of your UIScrollView like:
scrollContentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
... and also bind the last view in the scrollContentView to its bottomAnchor.
jobTypeDividerLine.bottomAnchor.constraint(equalTo: scrollContentView.bottomAnchor).isActive = true
This will sure fix your issue. Because this way the whole constraint sequence is linked from top to bottom.
Bottom line, the UIScrollView is not that smart that it determines its own bottom in every possible way. It is a kind of lazy. If you don't tell him enough it wouldn't simply scroll, while it is clear that your content disappears behind the bottom of your UIScrollView container.

UICollectionView scroll to bottom do not reveal the entire item

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

Resources