I am trying to programmatically create a view controller which will lay out it subviews in a scrollable fashion. I am trying to use AutoLayout with UIScrollView and have read all the tech notes and SO answers in this regard. But I cannot get this right - it seems there is some nuance wrt the creation/usage or UIScrollView that I am missing here.
My code is below and the output that I see is also presented in pictures below.
My expectation is that I will see brown and green stripes (subviews) that would occupy the entire screen. I get that I have to specify the height of the subviews, but I do not understand why the horizontal sizing just does not work. If I do not specify the horizontal size, I would expect my subview to get stretched and occupy the width of the screen, but it does not do so.
Here's the code for my controller.
class ScrollableRowHeadersViewController : UIViewController {
var scrollView : UIScrollView!
override func loadView() {
self.view = UIView(frame: CGRectZero)
scrollView = UIScrollView()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(scrollView)
scrollView.backgroundColor = UIColor.blueColor()
self.view.addVisualConstraint("H:|-0-[scrollView]-0-|", viewsDict: ["scrollView" : scrollView])
self.view.addVisualConstraint("V:|-0-[scrollView]-0-|", viewsDict: ["scrollView" : scrollView])
self.view.contentMode = UIViewContentMode.Redraw
}
//load all the subviews after the main view and scrollview loaded.
override func viewDidLoad() {
var viewsDict = [String: UIView]()
var vertical_constraints = "V:|"
scrollView.autoresizesSubviews = true
for i in 1...100 {
var subview = UIView()
subview.setTranslatesAutoresizingMaskIntoConstraints(false)
subview.backgroundColor = (i%2 == 0 ? UIColor.brownColor() : UIColor.greenColor())
viewsDict["subview_\(i)"] = subview
self.scrollView.addSubview(subview)
vertical_constraints += "[subview_\(i)(==50)]"
self.scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[subview_\(i)]|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
}
vertical_constraints += "|"
self.scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(vertical_constraints, options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
}
}
Here is the output with horizontal constraints set to H:|[subview_(i)]|
Here is the output with horizontal constraints set to H:|[subview_(i)(==100)]|
In either case I would have expected to see alternating brown and green stripes across the entire width of the screen.
What am I missing? Thanks in advance for helping.
After much more pain and reading closely, the reason for this behavior and the answer becomes clearer. It is also very well described at this SO question. I wish the Technical Note was more English, but @rdelmar and @Rob explain very clearly that "constraints on contentView are used to set the contentSize of the scrollView".
Here is the modified code and the result (which I think is the correct solution). My view hierarchy is now such:
ViewController -> UIView (mainView) -> UIScrollVIew -> UIView (contentView) -> UIViews (subviews)
There are extra width constraints between the contentView and mainView. In addition, all subviews are added to the contentView rather than being added to scrollView directly.
class ScrollableRowHeadersViewController : UIViewController {
var scrollView : UIScrollView!
var contentView : UIView!
override func loadView() {
super.loadView()
self.view = UIView(frame: CGRectZero)
scrollView = UIScrollView(frame:CGRectZero)
scrollView.sizeToFit()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(scrollView)
scrollView.backgroundColor = UIColor.blueColor()
contentView = UIView()
contentView.setTranslatesAutoresizingMaskIntoConstraints(false)
contentView.backgroundColor = UIColor.redColor()
scrollView.addSubview(contentView)
self.view.addVisualConstraint("H:|-0-[scrollView]-0-|", viewsDict: ["scrollView" : scrollView])
self.view.addVisualConstraint("V:|-0-[scrollView]-0-|", viewsDict: ["scrollView" : scrollView])
self.view.addVisualConstraint("H:|[contentView]|", viewsDict: ["contentView" : contentView])
self.view.addVisualConstraint("V:|[contentView]|", viewsDict: ["contentView" : contentView])
//make the width of content view to be the same as that of the containing view.
self.view.addVisualConstraint("H:[contentView(==mainView)]", viewsDict: ["contentView" : contentView, "mainView" : self.view])
self.view.contentMode = UIViewContentMode.Redraw
}
//load all the subviews after the main view and scrollview loaded.
override func viewDidLoad() {
var viewsDict = [String: UIView]()
viewsDict["contentView"] = contentView
viewsDict["super"] = self.view
var vertical_constraints = "V:|"
for i in 1...50 {
var subview = UIView()
subview.setTranslatesAutoresizingMaskIntoConstraints(false)
subview.backgroundColor = (i%2 == 0 ? UIColor.brownColor() : UIColor.greenColor())
viewsDict["subview_\(i)"] = subview
contentView.addSubview(subview)
vertical_constraints += "[subview_\(i)(==50)]"
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[subview_\(i)]|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
}
vertical_constraints += "|"
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(vertical_constraints, options: NSLayoutFormatOptions.AlignAllLeft, metrics: nil, views: viewsDict))
}
}
Here is the output as I was expecting it to be:
With a scroll view, the "|" refers to the contentView, not the scroll view, so by setting your subview to 100, you're telling the contentView to be 100 points wide (likewise in your first case, because you set no width, you get a 0 width contentView). You should do something like this,
self.scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[subview_\(i)(==width)]|", options: NSLayoutFormatOptions(0), metrics:["width": self.scrollView.frame.size.width], views: viewsDict))