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.
Related
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 am trying to make a photo viewer similar to the Photo's app where the user can zoom in and out on a particular image.
The image is initially sized so fit on the screen but when zoomed can expand to cover the entire screen.
I have this happening in a collectionView cell which is the size of the screen and has paging enabled. In that cell is a scrollView with storyboard constraints set to top/bottom/leading/trailing to it's superview. The rest happens in the code below which is the custom cell.
According to the new behavior of the scrollView introduced in iOS 11, the contentView (imageView in my case) should be centered in the scrollView using:
imageView.centerXAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerYAnchor).isActive = true
However I don't see it explained anywhere and adding those two lines to my code do absolutely nothing.
The imageView inside the scrollView continues to be positioned in the top left corner.
Hopefully somebody has figured out how to do this and can help.
import UIKit
class ImageCollectionViewCell: UICollectionViewCell, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView! {
didSet {
scrollView.delegate = self
scrollView.minimumZoomScale = 0.2
scrollView.maximumZoomScale = 2.0
scrollView.contentSize = imageView.frame.size
scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: 350).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 350).isActive = true
imageView.centerXAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerYAnchor).isActive = true
}
}
var imageView = UIImageView()
var image: UIImage? {
get {
return imageView.image
}
set {
imageView.image = newValue
scrollView?.contentSize = imageView.frame.size
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imageView
}
}
I have been facing the same problem for about a day, and I finally figured it out. It is not sufficient to just add the width and height constraints to the image. For this to work using contentLayoutGuide, the image view actually has to be the same size as the scroll view. Therefore, you will have to do either of the following:
Option 1
Change the width and height constraints of the image view to be the same size as the scroll view.
imageView.widthAnchor.constraint(equalToConstant: scrollView.bounds.width).isActive = true
imageView.heightAnchor.constraint(equalToConstant: scrollView.bounds.height).isActive = true
Option 2 - Preferred
Remove the width and height constraints for the image view. Instead, add constraints pinning the top, bottom, leading, and trailing anchors of the image view to the scroll view. I tried both, and this method seemed to work the best.
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
Depending on how/where you are using this, you may need to use scrollView.layoutMarginGuide for the anchor.
I would also recommend moving this code from didSet to a private method named setup(), or something along those lines. Then, from viewWillAppear, call setup(). This is a more consistent way of doing it. Hope this helped!
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.
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.
Due to the extensive updates since iOS7, I wanted to ask this question because of my limited experience with autolayout and the new stackview, and I am wondering what is the best design practice to implement the following in Objective-C (not swift yet):
In my view, there is a container scroll view, with a child container UIView. Within this UIView, there are a number of elements. One of the elements is a stack of UIViews which differ in number once in a while.
This element is followed by a map and other views.
This is how I plan on organizing it:
Questions
Is this the correct thing to do? How would I modify the height constraint for the stackview when I remove and add elements programmatically?
How do you add a subview to the UIStackView through interface builder? When I do, the subview takes the size of the containing stackview.
Swift 4.2
If you want use code instead of story board, i create an example using auto layout that don't need to estimate size of content.
you just need to add to stack view or remove from it and scroll height modify automatically.
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
}()
}
So you might want to make the whole layout contained within the stackview. Just something to consider.
There isn't really any right way to do things. I would not set a height constraint on your UIStackView (do add a width constraint that's equal to the view's width). Only set it on the elements you add to the stack view. You will get an error, but it's just IB complaining until you add an element to your UIStackView.
To size the elements in your stackview, you have to set a horizontal constraint on them. You can then modify that single horizontal constraint in code to change the height of the object.
To add a subview you simply do:
stackView.addArrangedSubview(childVC.view)
Or in interface builder, you just drag the element into the stack view. Make sure it has that horizontal constraint or it will resize on you.