Parallax image scrolling is a popular concept that is being adopted by many apps these days. It’s the small attention to details like this that can really make an app great. Parallax scrolling gives you the illusion of depth by letting objects in the background scroll slower than objects in the foreground. It has been used in the past by many 2d games to make them feel more 3d. True parallax scrolling can become quite complex, but it’s not very hard to create a simple parallax image scrolling effect on iOS yourself. This post will show you how to add it to a table view using Storyboards.
NOTE: You can find all source code used by this post on GitHub ParallaxImageScrolling.
The idea here is to create a UITableView with an image header that has a parallax scrolling effect. When we scroll down the table view (i.e. swipe up), the image should scroll with half the speed of the table. And when we scroll up (i.e. swipe down), the image should become bigger so that it feels like it’s stretching while we scroll. The latter is not really a parallax scrolling effect but commonly used in combination with it. The following animation shows these effects:
But what if we want a "Pull down to Refresh" effect and need to add a UIRefreshControl? Well, then we just drop the stretch effect when scrolling up:
And as you might expect, the variation with Pull to Refresh is actually a lot easier to accomplish than the one without.
Parallax Scrolling Libraries
While you can find several objective-c or Swift libraries that provide parallax scrolling similar to the ones here, you’ll find that it’s not that hard to create these yourself. Doing it yourself has the benefit of customizing it exactly the way your want it and of course it will add to your experience. Plus it might be less code than integrating with such a library. However if you need exactly what such a library provides then using it might work better for you.
The basics
NOTE: You can find all the code of this section at the no-parallax-scrolling branch.
Let’s start with a simple example that doesn’t have any parallax scrolling effects yet.
Here we have a standard UITableViewController with a cell containing our image at the top and another cell below it with some text. Here is the code:
[code language="obj-c"]
class ImageTableViewController: UITableViewController {
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cellIdentifier = ""
switch indexPath.section {
case 0:
cellIdentifier = "ImageCell"
case 1:
cellIdentifier = "TextCell"
default: ()
}
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! UITableViewCell
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.section {
case 0:
return UITableViewAutomaticDimension
default: ()
return 50
}
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.section {
case 0:
return 200
default: ()
return 50
}
}
}
[/code]
The only thing of note here is that we’re using UITableViewAutomaticDimension for automatic cell heights determined by constraints in the cell: we have a UIImageView with constraints to use the full width and height of the cell and a fixed aspect ratio of 2:1. Because of this aspect ratio, the height of the image (and therefore of the cell) is always half of the width. In landscape it looks like this:
We’ll see later why this matters.
Parallax scrolling with Pull to Refresh
NOTE: You can find all the code of this section at the pull-to-refresh branch.
As mentioned before, creating the parallax scrolling effect is easiest when it doesn’t need to stretch. Commonly you’ll only want that if you have a Pull to Refresh. Adding the UIRefreshControl is done in the standard way so I won’t go into that.
Container view
The rest is also quite simple. With the basics from below as our starting point, what we need to do first is add a UIView around our UIImageView that acts as a container. Since our image will change it’s position while we scroll, we cannot use it anymore to calculate the height of the cell. The container view will have exactly the constraints that our image view had: use the full width and height of the cell and have an aspect ratio of 2:1. Also make sure to enable Clip Subviews on the container view to make sure the image view is clipped by it.
Align Center Y constraint
The image view, which is now inside the container view, will keep its aspect ratio constraint and use the full width of the container view. For the y position we’ll add an Align Center Y constraint to vertically center the image within the container. All that looks something like this:
Parallax scrolling using constraint
When we run this code now, it will still behave exactly as before. What we need to do is make the image view scroll with half the speed of the table view when scrolling down. We can do that by changing the constant of the Align Center Y constraint that we just created. First we need to connect it to an outlet of a custom UITableViewCell subclass:
[code language="obj-c"]
class ImageCell: UITableViewCell {
@IBOutlet weak var imageCenterYConstraint: NSLayoutConstraint!
}
[/code]
When the table view scrolls down, we need to lower the Y position of the image by half the amount that we scrolled. To do that we can use scrollViewDidScroll and the content offset of the table view. Since our UITableViewController already adheres to the UIScrollViewDelegate, overriding that method is enough:
[code language="obj-c"]
override func scrollViewDidScroll(scrollView: UIScrollView) {
imageCenterYConstraint?.constant = min(0, -scrollView.contentOffset.y / 2.0) // only when scrolling down so we never let it be higher than 0
}
[/code]
We’re left with one small problem. The imageCenterYConstraint is connected to the ImageCell that we created and the scrollViewDidScroll method is in the view controller. So what left is create a imageCenterYConstraint in the view controller and assign it when the cell is created:
[code language="obj-c"]
weak var imageCenterYConstraint: NSLayoutConstraint?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cellIdentifier = ""
switch indexPath.section {
case 0:
cellIdentifier = "ImageCell"
case 1:
cellIdentifier = "TextCell"
default: ()
}
// the new part of code:
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! UITableViewCell
if let imageCell = cell as? ImageCell {
imageCenterYConstraint = imageCell.imageCenterYConstraint
}
return cell
}
[/code]
That’s all we need to do for our first variation of the parallax image scrolling. Let’s go on with something a little more complicated.
Parallax scrolling with Pull to Refresh
NOTE: You can find all the code of this section at the no-pull-to-refresh branch.
When starting from the basics, we need to add a container view again like we did in the Container view paragraph from the previous section. The image view needs some different constraints though. Add the following constraints to the image view:
- Ass before, keep the 2:1 aspect ratio
- Add a Leading Space and Trailing Space of 0 to the Superview (our container view) and set the priority to 900. We will break these constraints when stretching the image because the image will become wider than the container view. However we still need them to determine the preferred width.
- Align Center X to the Superview. We need this one to keep the image in the center when we break the Leading and Trailing Space constraints.
- Add a Bottom Space and Top Space of 0 to the Superview. Create two outlets to the cell class ImageCell like we did in the previous section for the center Y constraint. We’ll call these bottomSpaceConstraint and topSpaceConstraint. Also assign these from the cell to the view controller like we did before so we can access them in our scrollViewDidScroll method.
The result: We now have all the constraints we need to do the effects for scrolling up and down.
Scrolling down
When we scroll down (swipe up) we want the same effect as in our previous section. Instead of having an ‘Align Center Y’ constraint that we can change, we now need to do the following:
- Set the bottom space to minus half of the content offset so it will fall below the container view.
- Set the top space to plus half of the content offset so it will be below the top of the container view.
With these two calculation we effectively delay the scrolling speed of the image view with the half of the table view scrolling speed.
[code language="obj-c"]
bottomSpaceConstraint?.constant = -scrollView.contentOffset.y / 2
topSpaceConstraint?.constant = scrollView.contentOffset.y / 2
[/code]
Scrolling up
When the table view scrolls up (swipe down) the container view is going down. What we want here is that the image view sticks to the top of the screen instead of going down as well. As well need for that is to set the constant of the topSpaceConstraint to the content offset. That means the height of the image will increase. Because of our 2:1 aspect ratio, the width of the image will grow as well. This is why we had to lower the priority of the Leading and Trailing constraint because the image no longer fits inside the container and breaks those constraints.
[code language="obj-c"]
topSpaceConstraint?.constant = scrollView.contentOffset.y
[/code]
We’re left with one problem now. When the image sticks to the top while the container view goes down, it means that the image falls outside the container view. And since we had to enable Clip Subviews for scrolling down, we now get something like this:
We can’t see the top of the image since it’s outside the container view. So what we need is to clip when scrolling down and not clip when scrolling up. We can only do that in code so we need to connect the container view to an outlet, just as we’ve done with the constraints. Then the final code in scrollViewDidScroll becomes:
[code language="obj-c"]
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 0 {
// scrolling down
containerView.clipsToBounds = true
bottomSpaceConstraint?.constant = -scrollView.contentOffset.y / 2
topSpaceConstraint?.constant = scrollView.contentOffset.y / 2
} else {
// scrolling up
topSpaceConstraint?.constant = scrollView.contentOffset.y
containerView.clipsToBounds = false
}
}
[/code]
Conclusion
So there you have it. Two variations of parallax scrolling without too much effort. As mentioned before, use a dedicated library if you have to, but don’t be afraid that it’s too complicated to do it yourself.
Additional notes
If you’ve seen the source code on GitHub you might have noted a few additional things. I didn’t want to mention in the main body of this post to prevent any distractions but it’s important to mention them anyway.
- The aspect ratio constraints need to have a priority lower than 1000. Set them 999 or 950 or something (make sure they’re higher than the Leading and Trailing Space constraints that we set to 900 in the last section). This is because of an issue related to cells with dynamic height (using UITableViewAutomaticDimension) and rotation. When the user rotates the device, the cell will get its new width while still having the previous height. The new height calculation is not yet done at the beginning of the rotation animation. At this moment, the 2:1 aspect ratio cannot exist, which is why we cannot set it to 1000 (required). Right after the new height is calculated it the aspect ratio constraint will kick back in. It seems that the state in which the aspect ratio constraint cannot exist is not even visible so don’t worry about your cell looking strange. Also leaving it at 1000 only seems to generate an error message about the constraint, after which it continues as expected.
- Instead of assigning the outlets from the ImageCell to new variables in the view controller you may also create a scrollViewDidScroll in the cell, which is then being called from the scrollViewDidScroll from your view controller. You can get the cell using cellForRowAtIndexPath. See the code on GitHub to see this done.