Chapter 1
Prelude To Design
An overview explaining why & how we chose to create COSMOS in the first place.
March 11th, I have an offer on the table to write a tutorial and have it published on a renown site. So, I start talking with Jake about coming up with possible concepts we can design, build and publish. Starting to get a grip on the new Swift-based C4, learning how to build animations with the new system… Thinking: lots of basic animations coming together into a nice, elegant interface. Watch a lot of concept UI videos. Brainstorm.
Jake gets an idea:
…where it starts as one filled circle in the middle, when you tap it, it shrinks a bit (90% size), and then shoots out 8 circles from that central point where all those circles create a bigger circle of “sub menus” which are just outlines of circles with different icons in each circle. Then, tapping an X in the centre will pull them all back into the original state.
Watch more videos.
Talk about the concept.
It works.
Run with it.
That’s how we roll.
The actual app is simple, there are a lot of subtle elements in the interface and background that will take some time to get just right. Also, because the app is simple – yet complex – it will make for a good set of tutorials on how to build it from end-to-end using C4.
Jake presents his concepts for a single-view app composed of a brilliant animated menu and a layered parallax background that holds all the content. I have a look at both and in my head do a tear-down of how both components will be composed.
The background is the easy one to decompose, mainly because it’s just a bunch of layers with different content moving at different speeds.
There are:
This is totally possible, so after a chat with Jake I come up with a list of things I need defined from him for this part:
My first step at this point is to test the number of layers I can get doing parallax at the same time… We need 8, so I just test with 10 to make sure it will work.
The code in this chapter represents the tests I made prior to deciding to move forward. So, when you’re done with the chapter, remember to delete everything you’ve added to the workspace file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WorkSpace: CanvasController {
//creates an empty variable array to which we'll add layers
var layers = [UIScrollView]()
override func setup() {
//loops the code while the number of layers in our array is less than 10
repeat {
//creates a layer whose frame is the same as the canvas
let layer = UIScrollView(frame: view.frame)
//sets the content size for each layer, keeping 0 for height to prevent vertical scrolling
layer.contentSize = CGSizeMake(layer.frame.size.width * 10, 0)
//add the layer to the canvas and to the array
canvas.add(layer)
layers.append(layer)
} while layers.count < 10
}
}
Pretty straightforward. I’m working from the same project as the previous chapter, and in the project’s WorkSpace, I add a repeating loop that creates a new layer and adds it to the canvas until there are 10 layers. As each is being created, I make sure to set the layer’s contentSize to something quite large (in this case 20 times the width of the canvas). Setting the size’s height value to 0 will make sure it doesn’t scroll vertically.
At this point, if I run the app I’ll see nothing, so I modify the loop to include adding labels to each layer.
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
class WorkSpace: CanvasController {
//creates an empty variable array to which we'll add layers
var layers = [InfiniteScrollView]()
override func setup() {
//loops the code while the number of layers in our array is less than 10
repeat {
//creates a layer whose frame is the same as the canvas
let layer = InfiniteScrollView(frame: view.frame)
//sets the content size for each layer, keeping 0 for height to prevent vertical scrolling
layer.contentSize = CGSizeMake(layer.frame.size.width * 10, 0)
//add the layer to the canvas and to the array
canvas.add(layer)
layers.append(layer)
//create a variable center point which will be used to position the labels
var center = Point(24,canvas.height/2.0)
//calculate the layer number (since we're adding the last layer first we start with 10 and work downwards)
let layerNumber = 10 - layers.count
//create a font whose size is based on the current layer count
let font = Font(name: "AvenirNext-DemiBold", size:Double(layers.count+1) * 8.0)!
//create a loop that runs until each layer is full of labels
repeat {
//create a label
let label = TextShape(text: "\(layerNumber)", font: font)!
//center it
label.center = center
//update the center point's position
center.x += 130.0
//add the label to the layer
layer.add(label)
} while center.x < Double(layer.contentSize.width)
} while layers.count < 10
}
}
This modifies the original setup to include a nested repeat that runs until the entire content size of the layer is filled with labels – with each label numbered based on the current layer.
Now, when the app runs there are labels. But! If I scroll the app only one layer moves…
The next step is to create an observer that looks at the top layer and moves all the rest when it is being scrolled. So, in setup just after the end of the while, I add the following:
1
2
3
4
5
6
if let top = layers.last {
//creates a variable context
var c = 0
//adds the WorkSpace as an observer of the top layer's contentOffset
top.addObserver(self, forKeyPath: "contentOffset", options: NSKeyValueObservingOptions.New, context: &c)
}
This bit sets up the WorkSpace as observer of the top layer’s contentOffset. Now, to make things pretty, I create a function that will respond to the movement of the layer and change the other ones accordingly. Like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
//iterates through all the layers, stopping 1 before the top layer
for i in 0..<layers.count-1 {
//grabs the current layer
let layer = self.layers[i]
//creates a mod value based on the layer's position (layer 0 = 0.1, layer 1 = 0.2, ...)
let mod = 0.1 * CGFloat(i+1)
//grabs the x value of the top layer's content offset
if let x = layers.last?.contentOffset.x {
//sets the content offset of the current layer by multiplying x by mod
layer.contentOffset = CGPointMake(x*mod,0)
}
}
}
Pretty. Now we know that 10 layers will definitely work… But, what about when there’s a ton of media in them?… Time to test that. Jake ballparks the number of stars per “view” per layer at 15, and gives me a small white star.
I then swap the internal repeat loop to a for that adds images rather than labels. It looks like this now:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//instead of center position, I simply add 10 * 15 stars per layer
let starCount = layers.count * 15
canvas.backgroundColor = black
//loop until there starCount stars in the layer
for _ in 0..<starCount {
//create an image for the star
let img = Image("6smallStar")!
//allow it to scale proportionately
img.constrainsProportions = true
//scale the width of the image
img.width *= 0.1 * Double(layers.count+1)
//center it at a random point in the layer
img.center = Point(Double(layer.contentSize.width)*random01(),canvas.height*random01())
//add it to the layer
layer.add(img)
}
And, the simulator looks good:
I try it on an iPhone 5 and it runs just fine. The 10-layer test works, so the only remaining bits to work out are aesthetics. And, by this point Jake has basically specified everything on the lists at the top of the Background section.
Each of the 12 astrological signs will be made up of 3 visual elements: big stars, small stars and lines connecting the shapes. This is the image that jake used to trace the positions of each kind of star:
The next thing to define is “how” the stars and lines move in the foreground layers. Jake’s idea is to have the stars shift into place, so we decide to make 3 layers one for each of the big / small stars and one for the lines. When the app snaps into place for any given sign, the current stars should line up in the right positions. Then, right as everything snaps, there’s a super short animation of the lines between stars drawing from one star to another.
Next, I get jake to define how the stars move in the background. This step is pretty simple, he thinks its something like 5%, 15% and 20% of the speed of topmost layer. He also makes some ballpark guesses on how many stars per layer.
Next, I get Jake to define how the nebula and vignette are going to look and move. This step is even easier than the previous one because the vignette simply doesn’t move, and the nebula layer moves at 10% speed.
Finally, there will be a dashed line at the bottom of entire window, with a longer dash every 20 dashes. Then, when each individual constellation is centered there will be an even longer white line positioned under a graphic symbol of the constellation and its position (in degrees).
The last step before moving on is to have a list of the different layers that I’m going to be building. In his infinite kindness, Jake sends me this:
The menu “looks” pretty straightforward, except that it isn’t. However, the only real thing that I need to figure out is how we’re going to animate the astrological signs.
Actually, animating them is the easy part, but making them is trickier because we want them to be bezier paths but constructing them is a pain because we don’t know the curve points and applications like Illustrator don’t give us access to those. Also, I don’t want to write an SVG importer because that’s overkill…
So, what do we do?
We use PaintCode to draw out trace the shapes and then export their curvatures to Core Graphics code which looks like this:
1
2
3
4
5
6
UIBezierPath* bezier2Path = UIBezierPath.bezierPath;
[bezier2Path moveToPoint: CGPointMake(250, 200)];
[bezier2Path addLineToPoint: CGPointMake(150, 200)];
[bezier2Path addCurveToPoint: CGPointMake(100, 150) controlPoint1: CGPointMake(122.4, 200) controlPoint2: CGPointMake(100, 177.6)];
...
[bezier2Path closePath];
When I translate to code that looks like this:
1
2
3
4
5
6
let bezier = Path()
bezier.moveToPoint(Point(250,200))
bezier.addLineToPoint(Point(150,200))
bezier.addCurveToPoint(Point(100,150), control1:Point(122.4,200), control2:Point(100,177.6))
...
Things start to get more clear, and much easier to work with. Now that I have a process for getting the shapes of the astrological signs into C4 code it takes little effort to animate them the way we intend.
For example, letting a shape draw itself in is as simple as specifying:
1
shape.strokeEnd = 1.0
At this point I’m ready to start building out the menu, but to do so I need redlines – specifications on position, size, etc., – for all the elements in the menu.
Jake does a lovely job of prepping this for me:
With the basic visual concepts specified, and the more complicated or questionable bits tested, it’s time to move on to some real dev work. But, before we get going I’ll sum up the problems I know I’ll have to address:
MAKE SURE TO DELETE ANY TEST CODE YOU’VE INCORPORATED INTO WorkSpace.swift… You should only have an empty setup().
Let’s go.