UIScrollView and Autolayout


ios

UIScrollView and Autolayout


Daniel Shin - May 11, 2015

In this post, I will try to expound upon the things that really tripped me up last night while I was working on my iOS project.

In the app, I had to implement custom profile modal view with scrolling, more or less like Facebook profile page on mobile.

Since it needs to be custom view, I could not use UITableView. So I thought to myself, how bad can it be to implement my own scroll view? I mean Apple made something that is just for that, UIScrollView.

To my defense, I am not the only one who doubted one’s intelligence with the complexity that UIScrollView coupled with Autolayout brings to the table. You will see numerous threads and questions on stackoverflow etc.

Basically, the problem is that UIScrollView is not just another UIView. It behaves differently when put in the context of Autolayout.

First of all, you should read this technical note by Apple.

Good?

Okay. Depending on your level of intelligence, you might have or might not have got it. I certainly didn’t. So I will share my knowledge from last night.

I will use Mixed Approach here since it feels more natural to me and less of a hassle.

Also in my case, I didn’t use horizontal scrolling, so I’ll disregard gracefully.

The gist of Mixed Approach is to simply put a ‘container’ UIView inside UIScrollView and do all your Storyboard mundanity inside that container UIView as usual.

Henceforth, we call this container UIView as scrollContentView (not sure if such term exists already, but you get the idea).

UIScrollView wont automatically enable scrolling when you insert it in Storyboard.

It enables scrolling only when it needs to, that is, when scrollView.contentSize is wider/longer than scrollView.frame.

Get that through your head first.

Also scrollView.contentSize doesn’t equal the size of its subview, at least not automatically. (In our case, this subview is scrollContentView)

These two points are essential to understand some of the weird behaviors that may baffle you when experimenting with UIScrollView.

You will need to either (1) Manually set scrollView.contentSize (Mixed Approach) or (2) Set all the constraints in a way that nib renderer can extract the contentSize automagically (Pure Approach). I won’t go into Pure Approach.

scrollContentView is just a normal UIView and is no special other than being the only subview of UIScrollView, and thus the only way for UIScrollView to calculate its self.contentSize at runtime.

This makes our life easier.

In IB, you will set scrollContentView’s height to an arbitrary height. This doesn’t need to be accurate but needs to be set, otherwise IB will complain something like,

Height is not set.

Let’s assume that we set its height as an equal height of its superview, which is UIScrollView. (**Note that I don’t mean equal height as in the constraint Equal Height. I mean to just set it manually to more or less equal height of what UIScrollView has in IB.**).

Then you might ask,

If we do that, wont UIScrollView simply not enable scrolling since its self.contentSize is equal to its self.frame?

Exactly. You will face dreadful ‘I know there is more text but I cannot scroll’.

This is why we set scrollView.contentSize = scrollContentView.bounds.size in viewDidLayoutSubviews. (You will need to export each UIScrollView and container UIView to your code. I named scrollView and scrollContentView respectively)

All is well for now. But then again you might ask,

But we set the height of scrollContentView explicitly in IB. Its height cannot change? Right?

Actually it can. And that’s the power of Autolayout. What you need to do inside scrollContentView is to set all its subviews’ constraints in a way that the size increases in subviews will eventually force the increase in the size of scrollContentView.

Let me give you an example. In my project, I had a UILabel inside scrollContentView, which not unlike most UILabel has dynamic height that adjusts with its content. (you accomplish this by setting Lines to 0).

By setting top & bottom space constraints of UILabel to be pinned to top and bottom margin of scrollContentView, as UILabel grows in size, it will try to maintain that same top & bottom space constraints given, and thus will force the size of scrollContentView to increase.

At first, I thought it might cause conflicting constraint warning since the height of scrollContentView was also set explicitly, but it worked without setting priority explicitly. I’m assuming Apple chose height constraint to be of less priority by default from the get-go.

Also one more caveat regarding setting top & bottom space constraints, you must have at most one UIView that pins to the top and also at most one UIView that pins to the bottom of scrollContentView. Any subviews that are positioned in the middle of these top and bottom UIView’s (that is, if any) should pin themselves relatively to either these top or bottom UIView’s.

They should be chained to one another so that they all line up linearly (at least in constraints-wise). Middle views must not pin themselves to scrollContentView directly. This is because scrollContentView actually uses these constraints to re-caculate its height and having multiple constraints pinning to it certainly won’t help.

That’s all there is to it.

I hope you got something from this rambling post. Things aren’t yet organized in my head either since I just figured these stuffs out too. But since these are some of the more complex topics in iOS that trip many programmers, I wanted to share my experience to give you a bit of headstart.

Cheers!