过去几年中,移动应用像风暴一样席卷世界,改变了我们的网上工作、娱乐方式。很多移动应用开发技术应运而生,而移动也开始得到开发过程重视。尽管移动已经看似无所不在,但未来才刚刚开始。新一代的移动设备,好比可穿戴设备,好比物联网的许许多多移动构件,就在我们面前。我们会发现:用于数据展示和命令接收的用户界面不断地推陈出新;越来越多的公司站在真正的移动浪潮之巅。这会在未来几年中影响软件的设计、开发、测试方式。
InfoQ的这篇文章是快速变革的移动技术系列的一部分。你可以在这里订阅该系列的新文章通知。
今年,随着Swift编程语言及其1.0版的发布,苹果允许向AppStore提交用Swift开发的iOS应用,为iOS编写应用从未如此简单。这篇文章展示了用Swift创建iOS应用的全过程,以及如何通过SceneKit展示3D图形。
本文中代码的Swift版本为1.0版,仅供学习使用。
软件需求
创建本文相关代码之前,先要在OSX 10.9或更高版本中安装Xcode 6.1。文中的代码可以直接在模拟器中运行,想要部署到硬件设备上的话,需要激活iOS开发者账号。
iOS新项目创建
创建新项目,可以通过Xcode启动时的欢迎对话框,或是“文件→新建→项目”菜单。
出现在Xcode新窗口菜单中的新对话框包含了多种iOS或OS X应用选项。有很多缺省模版类型,用以创建不同行为的示例代码项目。
选择“iOS→应用→游戏”可以从众多不同类型中选择一个来创建模版:
- SceneKit
- SpriteKit
- OpenGL ES
- Metal
用SceneKit创建一个面向通用设备的名为MerrySwiftmas的Swift应用。(通用应用意味着该应用可以运行在iPad和iPhone/iPod上;如果应用只面向一种设备,可以在此处或接下来的info.plist中进行更改)。
运行项目
这样就根据模版创建了一个新项目。项目的用户界面可以被划分为四个独立区域:左边是导航区(可以通过快捷键⌘1-9进行切换);右上部是各种检视器(可以通过快捷键⌥⌘1-9进行切换);右下方是各个类库(可以通过快捷键^⌥⌘1-9进行切换)。
窗口中间是编辑区,该区域会显示导航区所选中的文件。选中最顶层项目时,编辑区会展示一系列通用信息(这些信息存储在info.plist文件中);单击选中其他文件时,编辑区会发生变化,双击文件时,则会弹出含有该文件的新窗口。
窗口顶部是运行按钮;运行意味着构建并启动缺省目标设备及应用;本例中,是一个加载应用的模拟器窗口:
Swift代码简介
飞船的加载、显示是靠GameViewController.swift文件中的viewDidLoad函数。
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 } ... }
正如其他C类型语言,块集成用花括号{},类成员访问用.,函数和构造器传参用括号()。参数可以是定位参数或命名参数,冒号隔开名称和值。
虽说Swift是静态编译的强类型应用,却不必声明变量类型。Swift会在编译时自动推导。Var用于声明变量,let用于声明常量。
关键字as用于类型转换,有了它,源于UIViewController超类、被声明为UIView的self.view就可以当SceneKit SCNView使用,进而对诸如scene或是allowsCameraControl这类SceneKit特有的字段进行访问。
替换宇宙飞船
视图控制器首次加载时,viewDidLoad()的函数体会被调用,函数体可以用上述代码替换。运行替换后的代码会得到不含其他信息的黑色屏幕。
场景图像通过结点结构进行展示,并以rootNode为顶层结点。任何结点都能添加子结点;结点的翻转变换会统一影响其所有子结点。
向图中添加柱体,可以像下面这样,创建一个含有SCNCylinder的SCNNode并添加到图中:
... scnView.allowsCameraControl = true let rootNode = scene.rootNode let cylinder = SCNCylinder(radius:1,height:3) let tree = SCNNode(geometry: cylinder) rootNode.addChildNode(tree)
如果现在运行应用,你会看到一个相当无趣的白块:
柱体看上去并不具备3D效果,这只是因为柱体尚未上色、场景尚未投灯光。这些都很容易搞定:可以像下面这样,将柱体的漫反射材质上色,场景投自动灯光:
... rootNode.addChildNode(tree) cylinder.firstMaterial?.diffuse.contents = UIColor.brownColor() scnView.autoenablesDefaultLighting = true
现在再运行应用,柱体就会显示出着色后的的3D效果。因为场景允许摄像控制(采用上帝视角观察),所以可以通过手指或鼠标手势拖动场景:
下一步是在柱体之上添加锥体,方法与添加柱体类似。
... 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)
为了把锥体放在柱体之上,初始位置上移了3个单元,但仍保持和柱体中心对称。?.是可选访问器:如果值为空,那么一切都不变;如果值非空,那么计算表达式的剩余部分。
现在应用的运行结果如下:
完成树
由于要在顶部添加更多锥体,锥体的创建代码会被重复多次。也可以用含有这部分代码的for循环替代。
... 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) }
for循环中,用1...3代表闭区间[1,2,3]。半闭合区间[1,2]可以用1..<3表示。通过堆叠锥体来生成树。
整型i可以通过Float强制转换成浮点型以便根据树根计算相对位置。
应用运行时,会显示一棵树:
添加礼物
用SCNBox的话,可以把礼物放在树下。柱体(即树的底部)的位置在0,0,0及其之上,盒子的位置要略低一点:y=-1。X和z方向随意。可以显式指定位置,但for循环所允许的位置只有(0,1),(1,0)以及(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) }
重构为类
把这些一股脑儿堆放在视图控制器加载的时候,并不是好的实践方案。这种情况下,代码复用和测试都会变得相对困难。实际上,创建树的相关代码可以单独成类。
用“文件→新建→文件”创建一个名为ChristmasTree的iOS Swift文件。文件创建时为空,需要导入SceneKit并声明一个名为ChristmasTree的类。
import SceneKit class ChristmasTree { }
想创建SCNNode子类的话,要把父类名称放在子类名称之后,并用冒号隔开。实现协议(接口)用的也是同样的语法。这样做时,还要添加一系列调用init的构造器:
class ChristmasTree: SCNNode { override init() { super.init() } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
Swift约定:对象要有一个主要(或缺省)初始化函数;在很多UIKit对象中,这意味着要遵循NSCoder协议,也就是说,要有一个编码器相关的init函数。通过把字段属性转换成序列化结构并还原,NSCoder能将Interface Builder中的对象持久化。如果你忘了写,Xcode会报错并帮你添上。这样做的实际效果就是,用Swift重写子类时,“需要”一个构造器,“重写”其他构造器。
现在类框架写好了,也该补充内容了。把创建树的代码从viewDidLoad方法移动到ChristmasTree的init方法中来:
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) } }
这样就把树的添加工作留到视图控制器更新时再做:
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 }
种植森林
可以拥有上千棵树,为何满足于只有一棵?现在树被抽象为一个类。种一百棵树很简单。用嵌套for循环的position属性安排每棵树的位置。在步长不为1的情况下,可以通过内置函数stride生成数字迭代器。
for x in stride(from:0, to:100, by:10) { for z in stride(from:0, to:100, by:10) { let tree = ChristmasTree() tree.position.x = Float(x) tree.position.z = Float(z) rootNode.addChildNode(tree) } }
这样,一百棵树就种好了,间隔10个单位,分布在2D网格中。
所有树看上去都很相似,部分原因在于所有礼物的整齐排列。加入树的旋转后,会不再那么相似。旋转用弧度衡量:圆的弧度为2π,因此,想得到随机位置,可以选一个180以内的随机数,除以180,再乘以π:
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)
现在森林看上去有那么一点不一样了:
添加颜色
目前所有礼物都是蓝色。如果你恰巧喜欢蓝色,就很不错,只是看上去会有一点单调。还有一种选择,你可以创建颜色序列,并随机选取。
尽管Swift支持struct和enum类型的static成员,但直到Swift1.1,尚不支持static类成员。(实际上,要是你试着向Swift类中添加static成员,编译器会抱怨你不该用static,而应该用class,可当你用了class,编译器又会表示尚不支持)
值得庆幸的是,类之外也能声明变量,这样声明的全局变量正好能存储构建好的颜色序列。通过随机系数就可以选取礼物的颜色。
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]
结论
Swift是一种很容易上手的语言,因为它有自己的REPL(swift,简单交互式编程环境,源于Lisp,译者注);只通过几行代码就可以用SceneKit创建交互图形应用。最后,祝你们圣诞快乐!
在Alex的GitHub空间可以找到该项目的源代码。
关于作者
二十年前,Dr Alex Blewitt第一次在NeXTstation上接触到Objective-C面向对象编程并一直使用至今。Swift的发布预示着OS X平台的未来,Alex为此写过一本书,Swift Essentials,该书定于下月出版发行。有空时,如果天气很好,Alex会从本地的Crafield机场试飞。
过去几年中,移动应用像风暴一样席卷世界,改变了我们的网上工作、娱乐方式。很多移动应用开发技术应运而生,而移动也开始得到开发过程重视。尽管移动已经看似无所不在,但未来才刚刚开始。新一代的移动设备,好比可穿戴设备,好比物联网的许许多多移动构件,就在我们面前。我们会发现:用于数据展示和命令接收的用户界面不断推陈出新;越来越多的公司站在真正的移动浪潮之巅。这会在未来几年中影响软件的设计、开发、测试方式。
InfoQ的这篇文章是快速变革的移动技术系列的一部分。你可以在这里订阅该系列的新文章通知。
查看英文原文:Merry Swiftmas from InfoQ