In the past years, mobile applications took the world by storm and already changed the way we use the internet for work or leisure. Various technologies emerged to create mobile apps and development processes start to consider mobile as first class citizens. But even though mobile already seems to be omnipresent, the future is just about to start. We're facing new generations of mobile devices like wearables or lots of mobile gadgets that make up the Internet of Things. We will be confronted with new types of user interfaces for displaying data as well as accepting commands. And we will recognize more and more companies going real mobile first. All this will influence the way we design, develop and test software in the coming years.
This InfoQ article is part of a series on the fast-changing world of Mobile technology. You can subscribe to notifications about new articles in the series here.
With the this year's announcement of Apple's Swift programming language and the release of 1.0 allowing iOS applications written in Swift to be submitted to the AppStore, writing applications for iOS has never been so easy. This article shows how to create a Swift iOS application from start to finish, using SceneKit to display 3D graphics.
Software Requirements
To create the code associated with this article, you will need Xcode 7.1 installed on OSX 10.9 or above. The code can be run in the simulator, but to deploy it to a hardware device, you will need an active iOS Developer Program account. (The original instructions used Xcode 6 and Swift 1.1, but the article has since been updated.)
Creating a new iOS Project
Xcode presents a Welcome dialog when starting that can be used to create a new project; alternatively the “File → New → Project” menu can be used instead.
The new dialog is shown as a sheet in a new Xcode window with a number of choices for different iOS and OSX applications. There are a number of default template types that create sample code projects, each with different behaviours.
Choosing the “iOS → Application → Game” type will create a template based on one of a number of different types:
- SceneKit
- SpriteKit
- OpenGL ES
- Metal
Create a project called MerrySwiftmas as a Swift application using SceneKit and targeting a Universal device. (Universal allows the application to be run on both an iPad and an iPhone/iPod; if the application is targeted for a single device type this can be changed here, or in the Info.plist afterwards).
Running the Project
This will create a new project, with a default project appropriate for the template. The UI is broken into 4 distinct areas; on the left is the navigator area (which can be switched between with ⌘1-9); on the top right are the inspectors (which can be switched between with ⌥⌘1-9); on the bottom right are the libraries (which can be switched between with ^.⌥⌘1-9)
In the centre of window is the editor area, which shows the file selected in the navigator area. When the top level project is selected, a set of generic information is presented (which is stored in the Info.plist file), and as other files are selected with single-click the editor area changes. Double clicking on a file will open up a new window with the file area.
At the top of the window is the run button; this will build and launch the default target and application; in this case, the simulator window with the application:
Introducing the Swift code
The ship is loaded and displayed within the GameViewController.swift file, in the viewDidLoad method.
import UIKit import SceneKit class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() ... let scnView = self.view as! SCNView scnView.scene = scene scnView.autoenablesDefaultLighting = true scnView.allowsCameraControl = true } ... }
Like other C style languages, blocks are grouped with { and } braces. type members are accessed with . and functions and constructors are called with arguments in ( and ) parentheses. Arguments can be positional or named; a colon separates the name from the value.
Although Swift is a statically compiled and typed application, it is not necessary to declare the type of a variable. It will be automatically inferred by Swift at compile time. Variables are declared with var and constants are declared with let.
The as keyword performs a type cast; this allows the self.view from the UIViewController superclass, which is declared as a UIView, to be used as a SceneKit SCNView. This provides access to SceneKit specific fields such as scene and allowsCameraControl.
Replacing the SpaceShip
The body of the viewDidLoad(), which is called when the view controller is loaded for the first time, can be replaced as above. When run this will show a black screen with no other information shown.
Graphics in a scene are represented as a hierarchy of nodes, with the rootNode being the top level element. Any node can have a child node added to it; translations and rotations on a node affect all of its child nodes uniformly.
To add a cylinder into the diagram, an SCNNode can be created containing an SCNCylinder and added to the diagram as follows:
... scnView.allowsCameraControl = true let rootNode = scene.rootNode let cylinder = SCNCylinder(radius:1,height:3) let tree = SCNNode(geometry: cylinder) rootNode.addChildNode(tree)
If the application is run now, a rather unattractive white blob can be seen:
The cylinder doesn't look like a 3D representation, but this is because it doesn't have any colour and the scene doesn't have any lighting. Both of these are easy to set up; the cylinder can have a colour set as the material as a diffuse colour, and an automatic light can be added to the scene as follows:
... rootNode.addChildNode(tree) cylinder.firstMaterial?.diffuse.contents = UIColor.brownColor() scnView.autoenablesDefaultLighting = true
Now when the application is run the cylinder will be shown as a coloured 3D item. Since the scene allows camera control, the screen can be dragged around with finger or mouse gestures:
The next stage is to add a cone on the top, which can be created in the same way.
... scnView.autoenablesDefaultLighting = true let cone = SCNNode(geometry: SCNCone(topRadius:0, bottomRadius:3, height:3)) cone.position.y = 3 cone.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor() tree.addChildNode(cone)
To move the cone up to the top of the cylinder, the starting position is moved up 3 units, but still aligned on the centre of the cylinder. The ?. is an optional accesor; if the value is nil then nothing changes; if the value is not nil then the rest of the expression is evaluated.
Running the application now looks like:
Finishing it Off
To add more cones on the top, the code could be repeated but it can also be wrapped in a for loop instead.
... scnView.autoenablesDefaultLighting = true for i in 1...3 { let cone = SCNNode(geometry: SCNCone(topRadius:0, bottomRadius:3, height:3)) cone.position.y = 2 * Float(i) + 1 cone.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor() tree.addChildNode(cone) }
The for loop uses a range, generated from 1...3 which is an open range [1,2,3]. A half-closed range [1,2] can be generated from 1..<3 instead. By stacking the cones on top of each other a tree can be generated.
The integer i can be converted to floating point using the Float cast to calculate a relative position from the base of the tree.
When the application is run, a tree will be shown:
Adding Presents
Using SCNBox it is possible to put presents under the tree. Since the cylinder (which is the base of the tree) is at 0,0,0 and extends above the zero, the box needs to be positioned slightly lower using y=-1, and with a variety of x and z positions. Explicit positions could be used, but the for loop allows the positions to be placed at (0,1), (1,0) and (1,1).
for i in 1...3 { let present = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)) present.geometry?.firstMaterial?.diffuse.contents = UIColor.blueColor() present.position.x = Float(i % 2) * 2 present.position.z = Float(i / 2) * 2 present.position.y = -1 tree.addChildNode(present) }
Refactoring to a Class
Putting everything in the view controller's load isn't good practice, because it prevents easy re-usability as well as testing. Instead, the code to create the tree can be extracted into its own class.
Using “File → New → File” create an iOS Swift file, called ChristmasTree. When created it will be empty, but it will need to import SceneKit and declare a class called ChristmasTree:
import SceneKit class ChristmasTree { }
To create a subclass of SCNNode, the parent class needs to be placed after the subclass' name with a colon. Exactly the same syntax is used if the class implements a protocol (interface) as well. In doing so, we'll need to add a couple of constructors – called init – as well:
class ChristmasTree: SCNNode { override init() { super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
Swift has a contract that one of the initializers in the object is a required or default one; in the case of many of the UIKit objects this includes adopting the NSCoder protocol which requires an init that takes a coder. This allows an object to be persisted in InterfaceBuilder by translating the field's properties into a serialized structure and back again. Xcode will suggest an error if it is missing and then insert it for you if it is missing. The net effect is that when overriding a subclass in Swift, one constructor has 'required' and the others have 'override'.
Now that the class compiles, it is time to move the content over. Move the tree creation code from the viewDidLoad method to the ChristmasTree's init method:
override init() { super.init() let cylinder = SCNCylinder(radius:1,height:3) let trunk = SCNNode(geometry: cylinder) cylinder.firstMaterial?.diffuse.contents = UIColor.brownColor() addChildNode(trunk) for i in 1...3 { let cone = SCNNode(geometry: SCNCone(topRadius:0, bottomRadius:3, height:3)) cone.position.y = 2 * Float(i) + 1 cone.geometry?.firstMaterial?.diffuse.contents = UIColor.greenColor() addChildNode(cone) } for i in 1...3 { let present = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)) present.geometry?.firstMaterial?.diffuse.contents = UIColor.blueColor() present.position.x = Float(i % 2) * 2 present.position.z = Float(i / 2) * 2 present.position.y = -1 addChildNode(present) } }
This leaves the view controller to be updated to add the tree:
override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() let scnView = self.view as! SCNView scnView.scene = scene scnView.allowsCameraControl = true let rootNode = scene.rootNode let tree = ChristmasTree() rootNode.addChildNode(tree) scnView.autoenablesDefaultLighting = true }
Planting a Forest
Why stop at one tree when you can have hundreds? Now that the tree has been extracted to a class, it is trivial to plant a hundred of them. The trees can be repositioned through the position property in a nested for loop. An integer method stride can be used to generate an iterator over numbers where the step isn't 1.
for x in 0.stride(to:100, by:10) { for z in 0.stride(to:100, by:10) { let tree = ChristmasTree() tree.position.x = Float(x) tree.position.z = Float(z) rootNode.addChildNode(tree) } }
This sets up a hundred trees, separated by 10 units, and positions them in a 2D grid.
All the trees look a little similar, partly because all the presents are neatly lined up. If we introduce rotation to the tree, we can break up the similarity a little. Rotation is specified in radians; there are 2π radians in a circle, so to get a random position we can get a random number of up to 180, divide it by 180, and then multiply by π:
tree.position.x = Float(x) tree.position.z = Float(z) tree.rotation.y = 1 tree.rotation.w = Float(M_PI) * Float(arc4random_uniform(180)) / Float(180)
Now the forest will look a little less homogenous:
Adding colour
At the moment all the presents are blue. This is fine if you happen to like blue, but it looks a little monochromatic. Instead, an array of colours can be created and then selected at random.
Although Swift supports static members for struct and enum types, it doesn't for classes as of Swift 1.1. (In fact if you try and add a static member in a Swift class, it will complain that you shouldn't use static, you should use class – and then when you use class it says that it's not currently supported.)
Fortunately variables can be defined outside of a class, which effectively makes them global, and this can be used to store a built in array of colours. They can be selected using a random modulus to get the index for the present colour:
let colors = [ UIColor.blueColor(), UIColor.cyanColor(), UIColor.magentaColor(), UIColor.orangeColor(), UIColor.purpleColor(), UIColor.redColor(), UIColor.whiteColor(), UIColor.yellowColor(), ] class ChristmasTree: SCNNode { ... let present = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)) present.geometry?.firstMaterial?.diffuse.contents = colors[random() % colors.count]
Conclusion
Swift is a very easy language to get started with, because it comes with its own REPL (swift) and creating interactive graphical applications with SceneKit can be done in only a few lines of code. I hope you have a very merry Christmas.
Please find the sourcecode for this project in Alex' GitHub space.
About the Author
Dr Alex Blewitt was introduced to object-oriented programming with Objective-C on a NeXTstation over twenty years ago and has been using the platform ever since. With the release of Swift showing the future of the OSX platform, Alex has written a book, Swift Essentials, which was published in December 2014. In his spare time and if the weather is nice, he has been known to go flying from the local Cranfield airport.
In the past years, mobile applications took the world by storm and already changed the way we use the internet for work or leisure. Various technologies emerged to create mobile apps and development processes start to consider mobile as first class citizens. But even though mobile already seems to be omnipresent, the future is just about to start. We're facing new generations of mobile devices like wearables or lots of mobile gadgets that make up the Internet of Things. We will be confronted with new types of user interfaces for displaying data as well as accepting commands. And we will recognize more and more companies going real mobile first. All this will influence the way we design, develop and test software in the coming years.
This InfoQ article is part of a series on the fast-changing world of Mobile technology. You can subscribe to notifications about new articles in the series here.