Chapter 5
Infinite Scrollview

Estimated Time


You are going to do the following:

  1. Subclass UIScrollView (optional)
  2. Override the layoutSubviews() function
  3. Grab the currentOffset
  4. Skip to the end or beginning if needed

Subclassing UIScrollview is optional, there already exists an InfiniteScrollview.swift file for you to fill in. If you choose to skip, you can head down to the Add Some Content section. However, I have included the steps to create the subclass for those of you who haven’t done this kind of thing before.

Subclass UIScrollview

Create a new file in Xcode by choosing:

Create a new file in Xcode by choosing:

File > New > File…

or, by hitting:

CMD + N

Choose:

iOS > Source > Swift File

Name the file:

InfiniteScrollView

When the file has been created, change the following line:

import Foundation

to:

import UIKit

Then, start typing the word class and BEFORE you finish typing you should get a popup that looks like this:

Shows the subclass option for autocompleting the word: class

Choose Swift Subclass and hit return. Xcode will fill in your new class with little spaces for you to replace.

Select name and type in InfiniteScrollView

Hit Tab to move to the super class variable and type UIScrollView

Hit Tab again to move to the properties and methods variable.

Start typing layoutSubviews and when autocomplete pops up, hit return.

Next, copy the following and paste it into the code variable within the new function you just created:

1
super.layoutSubviews()

Finito. You’ve just created a new subclass called InfiniteScrollView. Your new class now works exactly like a UIScrollView. But, it doesn’t do anything special yet… So, let’s get to infinity.

PS your function should look like this:

1
2
3
override func layoutSubviews() {
   super.layoutSubviews() //calls the parent class method
}

Putting It On (Screen)

It’s time to test your new class. So, navigate to your project’s WorkSpace file and fill it in like so:

1
2
3
4
5
6
7
8
9
10
11
class WorkSpace: CanvasController {
    let infiniteScrollView = InfiniteScrollView() //create a new InfiniteScrollview object
    override func setup() {
        //creates CGRect from canvas frame
        //sets frame of infiniteScrollView to match canvas
        infiniteScrollView.frame = CGRect(canvas.frame)
        
        //add the view to the canvas
        canvas.add(infiniteScrollView)
    }
}

This creates a new InfiniteScrollView variable called infiniteScrollView and sets its frame to that of the current canvas, to which it’s then added.

You need to convert the canvas frame (a Rect) to a CGRect because InfiniteScrollView is a subclass of a UIKit object which only works with structs like CGRect and CGPoint.

Continuing on…

Hit:

Build > Run

or

CMD+R

or

the play button at the top-left of the Xcode window.

The simulator should pop up with a seemingly blank screen… There is actually a view on there, but you can’t see it because its background is clear, and you can’t scroll it yet for reasons you’ll get into shortly.

Add Some Content

In order to test your new InfiniteScrollView, you need to add a few visual indicators to it.

Copy the following and paste it into the main body of your WorkSpace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func addVisualIndicators() {
    //the max number of indicators
    let count = 20
    
    //the gap between indicators
    let gap = 150.0
    
    //initial offset because we're positioning from the center of each indicator's view
    let dx = 40.0
    
    //the calculated total width of the view's contentSize
    let width = Double(count) * gap + dx
    
    //create main indicators
    for x in 0...count {
        //create a center point for the new indicator
        let point = Point(Double(x) * gap + dx, canvas.center.y)
        //create a textshape
        let ts = TextShape(text: "\(x)")
        ts.center = point
        //add it to the canvas
        infiniteScrollView.add(ts)
    }
}

And, call that method in setup() like so:

1
2
3
4
5
override func setup() {
    infiniteScrollView.frame = CGRect(canvas.frame)
    canvas.add(infiniteScrollView)
    addVisualIndicators()
}

RUN it and you’ll see a bunch of numbers on screen now.

Content + Scrolling

Even though there’s some stuff in infiniteScrollView you still won’t be able to scroll it because the view doesn’t implicitly know that there’s content. You have to tell it how much there is by updating its contentSize variable.

Add the following immediately after the for loop:

1
infiniteScrollView.contentSize = CGSizeMake(CGFloat(width), 0)

Now that the view knows its max content size, it will scroll. You use 0 for its content height because you don’t need it to scroll vertically.

If you scroll to the end you’ll notice that the last variable hangs a bit… This is because the content size was only an estimate.

To fix this you could either:

  1. Keep track of the latest origin point
  2. Figure out where the end of the frame of the last object sits

Let’s update your code to do the latter.

Replace:

infiniteScrollView.contentSize = CGSizeMake(CGFloat(width), 0)

With:

infiniteScrollView.contentSize = CGSizeMake(CGFloat(width+gap), 0)

When you get to the last variable in the for loop, you calculate the center point of the last shape and add an extra gap to its x position. Now, you can see the end of the 20.

Next, you want to start tracking the contentOffset.

Don’t worry that the view still bounces, you’ll get to that in a bit…

Content Offset

The way that a UIScrollView works is essentially like this: a scrollview has a ton more content than the size of its frame, at any given time the user is looking at a “content view” which is somewhere within the bounds of the scrollview’s content.

What you now want to find out is the location of that content view when a user is scrolling.

Go back to your InfiniteScrollView.swift file and update layoutSubviews() to look like this:

1
2
3
4
5
override func layoutSubviews() {
   //calls UIScrollView's layout method
   super.layoutSubviews()
   print(contentOffset.x)
}

UIScrollview has a contentOffset property, so what you’ve just done is told your app to print that variable’s x coordinate to the console whenever layoutSubviews() gets called.

RUN the app and when you scroll it in the simulator you’ll see a bunch of numbers pop up in Xcode’s console.

If you can’t see Xcode’s console, then just hit CMD+SHIFT+C

Or, tap the icon in the top-right of the Xcode window, like this:

You are now tracking the x position of your scrollview’s contentOffset.

You may have noticed that when you bounce towards the beginning of the content the value of contentOffset.x goes negative… You’re going to use this to your advantage.

Infinite Rules

You now want to apply the rules (above) to your view so that it scrolls infinitely.

Update your layoutSubviews() function to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public override func layoutSubviews() {
    super.layoutSubviews()

    //grab the current content offset (top-left corner)
    var curr = contentOffset
    //if the x value is less than zero
    if curr.x < 0 {
        //update x to the end of the scrollview
        curr.x = contentSize.width - frame.width
        //set the content offset for the view
        contentOffset = curr
    }
        //if the x value is greater than the width - frame width
        //(i.e. when the top-right point goes beyond contentSize.width)
    else if curr.x >= contentSize.width - frame.width {
        //update x to the beginning of the scrollview
        curr.x = 0
        //set the content offset for the view
        contentOffset = curr
    }
}

Whenever you go past the edges of your content, in either direction, the view should immediately skip so that it can continue scrolling.

RUN it and try it out for yourself.

Overlap Snap

One thing you should have noticed is that when the view overlaps (i.e. goes from beginning to end, or vice versa), its content makes a very awkward jump…

The solution to this is to attach a copy of the first content view’s contents to the end of your scrollview so that when it snaps it looks seamless.

You'll add a copy of the first frame to the end of your content view.

However, this has absolutely NOTHING to do with the scrollview itself. This trick relies entirely on how you add content to your scrollview.

So, head back over to your WorkSpace.

Proper Setup

Add a new method called createIndicator() that you’ll use to create and add text shapes to your scrollview.

Add the following to your WorkSpace:

1
2
3
4
5
6
7
8
func createIndicator(text: String, at point: Point) {
    //create a textshape
    let ts = TextShape(text: text)
    //center the shape
    ts.center = point
    //add it to the canvas
    infiniteScrollView.add(ts)
}

This method takes a String and a Point, creates a new text shape indicator, positions it and adds it to the scrollview.

Simple.

Now, replace the contents of addVisualIndicators with the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func addVisualIndicators() {
    //the max number of indicators
    let count = 20
    
    //the gap between indicators
    let gap = 150.0
    
    //initial offset because we're positioning from the center of each indicator's view
    let dx = 40.0
    
    //the calculated total width of the view's contentSize
    let width = Double(count + 1) * gap + dx
    
    //create main indicators
    for x in 0...count {
        //create a center point for the new indicator
        let point = Point(Double(x) * gap + dx, canvas.center.y)
        //create a new indicator
        createIndicator("\(x)", at: point)
    }
    
    //create additional indicators
    var x : Int = 0
    
    //create an offset variable
    var offset = dx
    
    //The total width (including the last "view" of the infiniteScrollView is based on the width + screen width
    //So, the total width and count of how many "extra" indicators to add is somewhat arbitrary
    //This is why we use a while loop
    
    //while the offset is less than the view's width
    while offset < Double(infiniteScrollView.frame.size.width) {
        //create a center point whose x value starts is the total width + the current offset
        let point = Point(width + offset, canvas.center.y)
        //create the width
        createIndicator("\(x)", at: point)
        //increase the offset for the next point
        offset += gap
        //increate x to be used as the variable for the next indicator's number
        x++
    }
    
    //update infiniteScrollView contentSize
    infiniteScrollView.contentSize = CGSizeMake(CGFloat(width) + infiniteScrollView.frame.size.width, 0)
}

Now, everything is nice and smooth.

The view now smoothly loops when you go past the end or the beginning.

Wrapping Up.

You’ve added infinite scrolling to UIKit’s UIScrollView class, which was the easy part. The magic in getting the effect of infinite scrolling actually depends on how you create and add content to the view itself.

You’ve also learned how to add an extra bit of content on to the end of your view so that the effect is seamless when the view snaps into position as it scrolls in any direction.

Getting the extra content positioned properly is usually where the most of your work will take place in setting up an infinite scrollview. Like in this example, if you’re laying out code programmatically, you’ll probably have to make a bunch of tweaks until everything is in the right place.

Code

Here’s a final copy of the InfiniteScrollview class:

InfiniteScrollview Code