Chapter 13
Big Stars
We've got the small constellation stars, let's keep going with the big ones.
It’s time to assemble all 8 layers of the Stars background and hook them up. The list of layers is:
The way we’re going to do this is like so:
This step is dead-easy. Create an array of CGFloat
values that we’ll be using throughout the rest of our setup.
Open Stars.swift
and 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]!
If you worked through the ParallaxBackground chapter, this is where things start to get a bit different.
Next, we also know that we need layers for the lines, small and big stars. So, create variables that will reference those:
1
2
3
var signLines : SignLines!
var bigStars : StarsBig!
var snapTargets : [CGFloat]!
Your class should look like this:
1
2
3
4
5
6
7
8
9
10
class Stars : CanvasController, UIScrollViewDelegate {
let speeds : [CGFloat] = [0.08,0.0,0.10,0.12,0.15,1.0,0.8,1.0]
var scrollviews : [InfiniteScrollView]!
var signLines : SignLines!
var bigStars : StarsBig!
var snapTargets : [CGFloat]!
override func setup() {
}
}
Since we’ve already built the various star background classes (all subclasses of InfiniteScrollview
), we can start working with them right away. The easiest way to add all our layers is to create and add them individually based on their speeds and images.
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.
There is one layer that we haven’t considered yet: the vignette. This is an image that sits behind most of the other layers, but over top of one, to give an added sense of depth. Since the vignette doesn’t move, we don’t need to make a class for it.
However, because all our other layers are subclasses of InfiniteScrollview
we want our vignette to be the same. Add the following function:
1
2
3
4
5
6
7
func createVignette() -> InfiniteScrollView {
let sv = InfiniteScrollView(frame: view.frame)
let img = Image("1vignette")!
img.frame = canvas.frame
sv.add(img)
return sv
}
Simple.
Here’s how we create the first five layers. Add the following to setup()
:
1
2
3
4
5
6
7
8
canvas.backgroundColor = COSMOSbkgd
scrollviews = [InfiniteScrollView]()
scrollviews.append(StarsBackground(frame: view.frame, imageName: "0Star", starCount: 20, speed: speeds[0]))
scrollviews.append(createVignette())
scrollviews.append(StarsBackground(frame: view.frame, imageName: "2Star", starCount: 20, speed: speeds[2]))
scrollviews.append(StarsBackground(frame: view.frame, imageName: "3Star", starCount: 20, speed: speeds[3]))
scrollviews.append(StarsBackground(frame: view.frame, imageName: "4Star", starCount: 20, speed: speeds[4]))
I also added a background color.
The last 3 layers are a bit different in that the 6th and 8th need to be place in variables. The reason for this is that later on we want to act on the top layer as well as the layer with the lines.
Add the following to your setup()
:
1
2
3
4
5
6
7
8
9
10
11
signLines = SignLines(frame: view.frame)
scrollviews.append(signLines)
scrollviews.append(StarsSmall(frame: view.frame, speed: speeds[6]))
bigStars = StarsBig(frame: view.frame)
scrollviews.append(bigStars)
for sv in scrollviews {
canvas.add(sv)
}
We create the signLines
variable, then append that to the scrollviews array. Then, we add the small stars, followed by creating the bigStars
variable and then append that as well.
Finally, we iterate through all the scrollviews in the array and add them to the canvas one at a time.
The next step is to start observing the top layer for when it scrolls, then have all the other layers update their positions based on that top layer.
If you want a run through of observing and contexts, etc., have a look at the ParallaxBackground chapter.
Start by adding the following variable to your class:
1
var scrollviewOffsetContext = 0
Then, after creating bigStars
add the following two lines to setup()
:
1
2
bigStars.addObserver(self, forKeyPath: "contentOffset", options: .New, context: &scrollviewOffsetContext)
bigStars.delegate = self
On its own, this won’t do anything. To see the tracking in action we’ll have to add an observeValueForKeyPath
function that has a bit of logic to determine how the layers should scroll.
1
2
3
4
5
6
7
8
9
10
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &scrollviewOffsetContext {
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)
}
}
}
Switch your project’s WorkSpace to:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import UIKit
//three colors we'll use throughout the app, so we make them project-level variables
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 = Stars()
var stars = Stars()
override func setup() {
canvas.backgroundColor = COSMOSbkgd
canvas.add(stars.canvas
}
}
Lookin’ good:
However, there are still a couple of things we want to achieve:
Jake’s design considers the following behaviour:
When a sign is on screen and the user lets go or scrolling stops, the sign should snap to the center of the screen.
To build this functionality out we need to first answer two question:
The second requires the first, so I start with the problem of figuring out if the view needs to snap based on its position.
Start by creating a list of target points by adding the following property to the class:
1
var snapTargets : [CGFloat]!
Then, I create the following:
1
2
3
4
5
6
func createSnapTargets() {
snapTargets = [CGFloat]()
for i in 0...12 {
snapTargets.append(gapBetweenSigns * CGFloat(i) * view.frame.width)
}
}
This method appends center x
position for each astrological sign.
This method needs to be called during setup
, like so:
1
2
3
4
override func setup() {
//bunch of other stuff...
createSnapTargets()
}
Next, create a method that takes an offset position as input and determines if the view needs to snap or not:
1
2
3
4
5
6
7
8
9
func snapIfNeeded(x: CGFloat, _ scrollView: UIScrollView) {
for target in snapTargets {
let dist = abs(CGFloat(target) - x)
if dist <= CGFloat(canvas.width/2.0) {
scrollView.setContentOffset(CGPointMake(target,0), animated: true)
return
}
}
}
This iterates over all the targets in snapTargets
. For each target it calculates the distance from the target to the current x
position, and if the target is less than half the width of the screen away it should snap. If it should snap then it sets the current content offset of the scrollview to the target position.
The method also has a
return
statement. The purpose of this is that it breaks out of the loop at the proper moment so that the loop doesn’t continue (i.e. if the target is the first in the list it won’t execute the remaining 12).
Now, we have to hook up the snapIfNeeded
method to the right moments, which means I now have to figure out when to execute the behaviour.
There are two basic conditions:
The UIScrollview
class has a few methods that can help us out. They are scrollViewDidEndDecelerating
for the first case and scrollViewDidEndDragging
for the second, both of which are delegate methods.
Add the first delegate method and change it so it looks as follows:
1
2
3
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
snapIfNeeded(scrollView.contentOffset.x, scrollView)
}
This method gets called implicitly when the scrollview stops moving on its own. When it gets called, the method grabs the current offset of the view and sends that to our snapIfNeeded
method.
Also add the second delegate method and change it so it looks as follows:
1
2
3
4
5
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate == false {
snapIfNeeded(scrollView.contentOffset.x, scrollView)
}
}
This one also gets called implicitly when the user stops dragging. If the user’s finger or thumb is moving slow enough then the method will have a decelerate
parameter that’s set to false
. In this case, we know we need to immediately check if the view should snap. If decelerate
is true
(when the user stops dragging after a swipe gesture) we know that the previous method will eventually be called, so we do nothing here.
To get these two methods to execute, we need to set the delegate of our top layer.
First, make the entire ParallaxBackground
class a UIScrollViewDelegate
by changing the class declaration to:
1
class ParallaxBackground : CanvasController, UIScrollViewDelegate { ... }
Second, set the delegate of the top layer in the createBigStars method right before the return statement, like so:
1
2
3
4
5
6
7
func createBigStars() {
//…
addDashesMarker(bigStars)
addSignNames(bigStars)
bigStars.delegate = self
return bigStars
}
Check it:
The final piece of polish is to animate a sign’s lines in only when that sign is centered on the screen. We’ve figured out the triggers (i.e. when to snap, etc.) now we just have to set the lines up so that we can animate them in and out.
There are 2 conditions:
The first case is easy. In snapIfNeeded()
add the following right before the return
statement:
1
2
3
delay(0.25, closure: { () -> () in
self.signLines.revealCurrentSignLines()
})
This waits a 1/4 second before animating the lines in.
Next, we’re going to have to create a method that triggers when the user starts dragging… This is also easy. Add the following method to your class:
1
2
3
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
self.signLines.hideCurrentSignLines()
}
The scrollViewWillBeginDragging
is a delegate method that gets called automatically when the user starts dragging. So, all we have to do is trigger the lines to hide.
This last step is something we’re going to do in anticipation of later hooking everything up. Add this method to your Stars class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func goto(selection: Int) {
let target = canvas.width * Double(gapBetweenSigns) * Double(selection)
let anim = ViewAnimation(duration: 3.0) { () -> Void in
self.bigStars.contentOffset = CGPoint(x: CGFloat(target),y: 0)
}
anim.curve = .EaseOut
anim.addCompletionObserver { () -> Void in
self.signLines.revealCurrentSignLines()
}
anim.animate()
signLines.currentIndex = selection
}
We’ll talk about it in a future chapter, but for now it’s a method that takes an index then animates the top layer to the proper position.
The Stars background class is good to go.
Here’s a copy of Stars.swift.
Nächste Haltestelle.