Chapter 9
Parallax Testing (Optional)

Estimated Time


This chapter will walk you through how I originally tested the background (speeds, layering, etc.), and is optional. If you want to see the process of how I make sure everything works then I highly recommend you go through this step by step.

Parallax Background

First, create a new file called ParallaxBackground, import UIKit (instead of Foundation), and add a class with the same name as the file (this should be a subclass of CanvasController). Your new file should look like this:

1
2
class ParallaxBackground : CanvasController {
}

Next, add a background variable to your WorkSpace so that it initializes an instance of ParallaxBackground.

And, before we move on, let’s add some color variables outside our WorkSpace so that they’re available throughout the project. Your WorkSpace should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import UIKit
import C4

let COSMOSprpl = Color(red:0.565, green: 0.075, blue: 0.996, alpha: 1.0)
let COSMOSblue = Color(red: 0.094, green: 0.271, blue: 1.0, alpha: 1.0)
let COSMOSbkgd = Color(red: 0.078, green: 0.118, blue: 0.306, alpha: 1.0)

class WorkSpace: CanvasController {
   var background = ParallaxBackground()
   
   override func setup() {
       canvas.add(background.canvas)
   }
}

The background object is a subclass of CanvasController, just like your WorkSpace, so it has a canvas (which is a view) which is why we add background.canvas

With that done, let’s outline the steps for adding the parallax layers:

  1. Create an array of speeds so we know how quickly to move each layer
  2. Create and add all the layers baseda on their speeds
  3. Add a context and observer for creating the parallax effect
  4. Test the scrolling with some labels

This should be it for now for working in the WorkSpace, from this point onwards we’re going to be working in the ParallaxBackground file

Create Speeds and Scrollviews Arrays

This step is dead-easy. Create an array of CGFloat values that we’ll be using throughout the rest of our setup.

Add the following variable to your class:

1
let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]

We have simply taken the values from the design file and added them in order such that the bottom layer’s speed, 0.08 is the first entry (i.e. [0]) in the array, and the top layer’s speed is the last entry.

We label the array : [CGFloat] because we’re going to be passing these values directly to views that are subclassed from UIScrollView and it’s just a bit cleaner to not have to cast from Double when we know we’re only going to be working with CGFloat variables.

Finally, add a variable array of InfiniteScrollview types to your class. Later on we’re going to reference this array so we’ll add it here to be a bit ahead of the game.

1
var scrollviews = [InfiniteScrollView]()

Your class should look like this:

1
2
3
4
class ParallaxBackground : CanvasController {
   let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]
   var scrollviews = [InfiniteScrollView]()
}

Create Layers Based on Speeds

Since we’ve already built the InfiniteScrollView class, we can start working with it right away. The easiest way to add all our layers is to create and add them individually, however this isn’t so efficient when we already know that their scroll speeds will be different.

The previous image shows how varying speeds will actually dictate how much of a scrollview’s contents will actually be seen. Compared to the top layer which encapsulates the full width of all our app’s contents, the first layer with a speed of 0.08 will only need a contentSize that is 8% the width of the top layer’s size. This means that we don’t need to create full-size contents for all the layers.

Dynamic Layer contentSize

We want to build our layers so that they reference their speeds and build themselves accordingly. And, the most forward-looking way to do this is through a method that takes the right parameters and builds a layer for us.

Add the following method to your class:

1
2
3
func createLayer(speed: CGFloat) -> InfiniteScrollView {
   return InfiniteScrollView()
}

This is basically a skeleton method that allows us to build dynamic layers from our setup. Let’s test it out by creating our setup() to look like this:

1
2
3
4
5
6
7
override func setup() {
   for i in 0..<speeds.count {
  let layer = createLayer(speeds[i])
       canvas.add(layer)
  scrollviews.append(layer)
   }
}

We’re adding the new layer to both the canvas and our scrollviews array. The reason for this is that a) we want to see it on screen, b) we will need to reference it later.

Run it.

We see nothing, of course because there’s no content in our things. So, let’s modify our createLayer method.

Modify createLayer

First, we know that there are going to be 12 signs, so let’s make a let signCount that we can reference.

Add the following to your class:

1
let signCount : CGFloat = 12.0 

Again, we only need this value for laying out a UIKit object, so we specify that it’s a CGFloat. Also, I like to keep all my variables at the top of my class so I put this one under the speeds array

Since we’re testing our dynamic layout we can simply modify the createLayer method to add a bunch of numbers to our layers.

Start by grabbing the frame of our canvas view and use that to initialize a layer, like so:

1
2
3
4
5
func createLayer(speed: CGFloat) -> InfiniteScrollView {
   let frame = view.bounds
   let layer = InfiniteScrollView(frame: frame)
   return layer
}

Instead of simply returning an InfiniteScrollView, breaking up the steps like this (i.e. making a layer then returning the layer) gives us the opportunity to modify the object before returning it.

Next, we’re going to modify the size of our layer’s contents:

1
2
3
4
5
6
7
func createLayer(speed: CGFloat) -> InfiniteScrollView {
   let frame = view.bounds
   let layer = InfiniteScrollView(frame: frame)
   let contentSize = CGSizeMake(frame.size.width * 2.0 * signCount, frame.size.height)
   layer.contentSize = contentSize
   return layer
}

Now, let’s add some labels so that we can see what we’re actually putting on screen. Before we return the layer let’s add a repeat that takes the current number of elements in our scrollviews array and adds a bunch of labels for the entire content size of our layer:

1
2
3
4
5
6
7
8
let dx = Double(layer.contentSize.width) / 100.0
var center = Point(dx, canvas.center.y)
repeat {
   let label = TextShape(text: "\(scrollviews.count)")
   label.center = center
   layer.add(label)
   center.x += dx
} while center.x < Double(layer.contentSize.width)

This step creates a displacement variable, dx, that will give us 20 labels based on the width of the layer’s contentSize. Then, it creates a variable center point whose x position will get modified for every label we add. Finally, it runs a repeat that loops until the center position is outside the layer’s contents. During that loop, we create a new label based on the current number of elements in the scrollviews array (so the first layer should be 0).

And, when you run it your app should look like this:

See how they’re all on top of one another? This is because the contentSize for each layer is identical. Let’s put the speed variable to good use now.

Modify the calculation of the content size to this:

1
let contentSize = CGSizeMake(frame.size.width * 2.0 * signCount * speed, frame.size.height)

Your whole ParallaxBackground file should look like:

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
import UIKit

class ParallaxBackground : CanvasController {
    let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]
    var scrollviews : [InfiniteScrollView]!
    let signCount : CGFloat = 12.0
    
    override func setup() {
        scrollviews = [InfiniteScrollView]()
        for i in 0..<speeds.count {
            let layer = createLayer(speeds[i])
            canvas.add(layer)
            scrollviews.append(layer)
        }
    }
    
    func createLayer(speed: CGFloat) -> InfiniteScrollView {
        let frame = view.bounds
        let layer = InfiniteScrollView(frame: frame)
        let contentSize = CGSizeMake(frame.size.width * 2.0 * signCount * speed, frame.size.height)
        layer.contentSize = contentSize

        let dx = Double(layer.contentSize.width) / 100.0
        var center = Point(dx, canvas.center.y)
        repeat {
            let label = TextShape(text: "\(scrollviews.count)")!
            label.center = center
            layer.add(label)
            center.x += dx
        } while center.x < Double(layer.contentSize.width)
        
        return layer
    }
}

And your app should look like

Not pretty. So, let’s offset those labels based on the layer number and the content size:

1
2
3
4
5
6
7
8
9
let dx = Double(layer.contentSize.width) / 100.0
let dy = Double(layer.contentSize.height) / Double(speeds.count+1)
var center = Point(dx, Double(scrollviews.count + 1) * dy)
let fontSize = Double(scrollviews.count + 1) * 6.0
let font = Font(name:"AvenirNext-DemiBold", size: fontSize)!
repeat {
    let label = TextShape(text: "\(scrollviews.count)", font: font)!
    //...
}

Now it’s pretty.

See that 1 that’s strangely achored to the left? That’s because the speed for that layer is 0.0… Basically, it will be a non-moving layer, so its content size is zero.

And, when you scroll it:

Let’s hook those other ones up.

Hook’em UuuuP

We’ve done it before, we’re doing it again, we’re going to add a context and observer for creating the parallax effect based on the speeds of our layers.

First, add a new property to your class:

1
var scrollviewOffsetContext = 0

This is a variable that identifies a context for observing the offset of our layers.

Start typing out observeValueForKeyPath, and by the time you get to the b Xcode should look like:

Hit return to choose that method.

Now, where it says the word code add the following:

1
2
3
if context == &scrollviewOffsetContext {
   print("top layer is scrolling")
}

Then, hook that up to our top layer by adding the following in setup(), after the for loop that creates the layers:

1
2
3
if let topLayer = scrollviews.last {
    topLayer.addObserver(self, forKeyPath: "contentOffset", options: .New, context: &scrollviewOffsetContext)
}

Your class should basically look like this:

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
class ParallaxBackground : CanvasController {
   var speeds ...
   var scrollviews ...
   let signCount ...
    
   var scrollviewOffsetContext = 0
    
   override func setup() {
       for i in 0..<speeds.count {
      ...
       }   
       if let topLayer = scrollviews.last {
           topLayer.addObserver(self, forKeyPath: "contentOffset", options: .New, context: &scrollviewOffsetContext)
       }
   }
    
   func createLayer(speed: CGFloat) -> InfiniteScrollView {
   }

   override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
       if context == &scrollviewOffsetContext {
           print("top layer is scrolling")
       }
   }
}

Run it. Scroll it and the Xcode console should be spitting out a bunch of stuff…

Now, let’s modify the guts of our observe method to hook up the rest of the layers.

Replace:

1
print("top layer is scrolling")

With:

1
2
3
4
5
6
7
let sv = object as! InfiniteScrollView
let offset = sv.contentOffset

for i in 0..<scrollviews.count-1 {
   let layer = scrollviews[i]
   layer.contentOffset = CGPointMake(offset.x * speeds[i], 0.0)
}

This bit of code grabs the object that is being observed, casts it to an InfiniteScrollview (we can do this with a ! because we know that we added the observer to this kind of object). Then, we grab the offset and use that to help calculate the rest of the offsets for the other layers.

Finally, we create a loop that runs through all the layers and adjusts their contentOffset based on the layer’s speed. the top layer’s current offset value.

Now we get this:

There’s a bug in our logic. Why? It seems like we’re doing the right thing, right? We’re basically saying: “If the speed is 80% of the main layer’s speed, then we only need to see a canvas that is 80% of the width.” Which is totally true.

But, when the main layer is scrolled far enough the end of the smaller 80% canvas will be “on” the screen. At this point the view will stop scrolling because a view will only scroll if there is content hanging off of the end of its frame.

We need to extend each canvas by 20%

Basically, a canvas that is 80% of the main canvas will only scroll until 80% - visibleFrameWidth. So, we need to add the width of the frame to anything that is less than 1.0 speed.

To fix this bug we need to go back into the createLayer() method and add a condition between creating the content size and setting that value to the layer:

1
2
3
let contentSize = ...
contentSize.width += speed < 1.0 ? view.frame.width : 0
layer.contentSize = contentSize

Make sure you change the let contentSize = ... to a var

We’re using a nice shorthand trick here that allows us to conditionally set a variable in a single line of code.

This:

1
speed < 1.0 ?

Basically means: “Is the speed less than one?”

And this:

1
view.frame.width : 0

Is the answer to the question where if the answer is true then we return view.frame.width otherwise 0.

Now, this is lovely. Everything scrolls the way we expect it to, like so:

Here’s a copy of the ParallaxBackground.swift file.

Recap

This is how I first tested the parallax effect, to see how the various speeds would affect the content / scrolling / etc. And it did its job well: we know how the content of each layer will scroll and that all the layers will respond to the top’s movement.

Even though this approach works, we’re going to be taking another approach in building the app, but we’ll see how that differs when we get into the next few chapters.

For now, you can delete this entire class from your project if you want, we won’t use it anymore.

Andiamo!