Chapter 10
Stars Background

Estimated Time


We’re mostly going to be working in the StarsBackground.swift file. However, we’ll also use Stars.swift for testing our class.

There are 4 layers of background stars, each of whose content is on an infinite scrollview. To populate these layers we need to know 4 things:

  1. How big the frame of the view should be
  2. The name of the star image for the layer
  3. The number of stars to add to the layer
  4. The “speed” at which the layer will move

We can construct a layer once we have these 4 elements. After we’ve done so and added that layer to the app, we won’t need to access it or its elements… So, we can make our life a little easier by building everything into a single initializer.

Open StarsBackground.swift and you’ll see the following shell of an init:

1
2
3
convenience public init(frame: CGRect, imageName: String, starCount: Int, speed: CGFloat) {
    self.init(frame: frame)
}

This method takes the 4 variables necessary to build the layer. So, all we need to do from here on out is fill it in.

Gap between signs

There’s a liiiiiiittle design spec that we want to consider before we do anything… The idea is that there should be a LOT of space between the constellations so that the user has to scroll a ways to get from one to the next.1 To do this we need a variable that we can use to calculate content sizes.

Open the Stars.swift file and add the following above / outside of the class itself:

1
var gapBetweenSigns : CGFloat = 10.0

Adjusted Content Size

The first step is to calculate the content size for the layer. This is an important calculation because we want to optimize the adding of assets, and since this layer will move at a slower speed than other layers its contentSize should be smaller as well.

To do this we need 3 things:

  1. An adjusted frame size, based on the speed of the layer.
  2. A calculated size that contains all the space between the current frame and the next where the next sign starts. We’ll call this variable singleContentSize.
  3. The number of signs in our app.

Adjusted frame

This is quite simple. Add the following to your init method:

1
let adjustedFrameSize = frame.width * speed

If the speed is less than 1.0 we want the content size to be adjusted equally.

Single Content Size

The “space” between sections of the layer is defined by the frame size and the number of gaps (e.g. frame widths) that occur at the highest level between astrological sign constellations. The calculation is straightforward, add:

let singleSignContentSize = adjustedFrameSize * gapBetweenSigns

Sign count

To get the number of signs in the app we’ll access the sign provider. Add the following:

1
let count = CGFloat(AstrologicalSignProvider.sharedInstance.order.count)

Calculate the Content Size

With the three variables we’ve just made, we can now calculate the actual content size for the layer. Add the following:

1
contentSize = CGSizeMake(singleSignContentSize * count + frame.width, 1.0)

We add a frame.width to the content size because we need the layer to keep moving past its end point, and since we’re working within the space of the main app’s frame, this is the amount of space we need to add to our size.

Add some stars

We are going to iterate through our entire layer one frame at a time, dropping stars randomly in each frame and moving on until we’re done. And, only in the first frame will we make a copy of each star and add that to the end of our content so that when we scroll past the 12th we’ll end up with that nice hidden snapping effect that makes the InfiniteScrollview so nice.

We add stars via a couple of embedded loops, for each “frame” we want to run the same code a predefined number of times.

Start by adding the following shell of a loop after you’ve set the contentSize:

1
2
3
for currentFrame in 0..<Int(count) {
   //this will iterate 12 times
}

Then, add another for loop inside that one, like so:

1
2
3
4
5
for currentFrame in 0..<Int(count) {
   for _ in 0..<starCount {
       //add a star
   }
}

If we want 20 stars per frame, we’re going to get 12 (i.e. # signs) * 20 = 240 stars per layer.

We need to calculate an offset for the current frame, like so:

1
2
3
4
5
6
for currentFrame in 0..<Int(signCount) {
   let dx = Double(singleSignContentSize) * Double(currentFrame)
   for _ in 0..<starCount {
       //add a star
   }
}

We’re starting to cast to Double from Int and CGFloat. Unfortunately, this is a bit of an annoying process with Swift because it doesn’t implicitly change between different value types… So, you can’t add an Int to a Double. And, we’re working with both a UIKit and C4 object which use CGFloat and Double (respectively).

So, we know where to offset our images for each frame. Now, we need to calculate a random point within that frame.

Add the following to the inner loop:

1
2
3
let x = dx + random01() * Double(singleSignContentSize)
let y = random01() * Double(frame.size.height)
var pt = Point(x, y)

And then, add an image at that point:

1
2
3
let img = Image(imageName)!
img.center = pt
add(img)

Finally, if the currentFrame is the first frame then we want to make a copy of all the stars and add that to the 13th frame of the layer (i.e. the one that overlaps).

After adding the image, insert this bit of code:

1
2
3
4
5
6
if currentFrame == 0 {
   pt.x += Double(signCount * singleSignContentSize)
   let img = Image(imageName)!
   img.center = pt
   add(img)
}

Done. That’s it. Finito. We’re going to be able to use this simple class to create 4 of the 8 layers that are going in our app’s background.

Grab this StarsBackground.swift gist to see the final state of this class:

Test It

To test this class, add the following to your project’s WorkSpace:

1
2
3
4
override func setup() {
    canvas.backgroundColor = COSMOSbkgd
    canvas.add(StarsBackground(frame: view.frame, imageName: "6smallStar", starCount: 20, speed: 1.0))
}

You’ll notice that if you drag right automatically that the scrollview doesn’t snap.

Perrrrrfect.

  1. There are a few reasons for this… One, its a bit weird to have all the stars side by side because we’re going to apply some nice scaling that would otherwise overlap them. Two, if the gaps were really short then there’d be no need for a menu to select and animate between the layers. Three, the animations between layers look way better if there’s a big gap. Four, we’re thinking about V2 where we add some little interactive bits into those spaces so that people can explore the app more.