首先了解要开发的这个游戏有哪些规则和功能,游戏是如何进行的,然后才能知道怎么进行开发。也就是平时产品部门的工作。这里我们有一份简单的产品描述:
出现我们要出现一个随机整数作为目标数值,随机整数范围在1-100之间,然后用户滑动这个Slider,这个Slider数值在1到100之间,尽可能的滑动到能够代表目标数值的位置,也就是用户凭感觉来滑动了,比如App显示目标数值是80,那用户需要凭感觉将Slider滑动到代表80的位置,点击Hit Me按钮,弹出提示框,程序读取用户滑动位置的实际数值,这个位置的实际数值越接近目标数值,那么得分越高,提示框中显示用户的表现和得分,每点击一次Hit Me,游戏就进行了一局,局数加1。点击提示框中的确定按钮,提示框消失,然后得分和局数的数值就会加入刚刚完成的这局游戏的值。还有重现开始的按钮,点击这按钮,所有的得分和局数都清零。还有一个关于我们的按钮,点击会跳转到关于我们的页面。
刚刚完成了产品的工作,那么就需要设计师进行设计切图了,下面就是设计师给的最终效果图:
拿到切图文件和最终效果图后,我们就可以进行代码的逻辑设计。理顺一下自己的思路和步骤,一步步来,就能轻松完成开发。
下面这个清单是作者的写的Todo清单,作者建议不管开发什么应用,在开发之前,都要写一个Todo清单,有了这个清单,能够事半功倍。一开始写的不全没有关系,但是一定要写一个。我把作者写的清单简单翻译了一下:
- 放上按钮Hit Me,点击后出现提示框,告诉用户的表现和游戏结以及计算出来的得分。
- 放上Label得分score和局数round,根据用户的游戏情况显示相对应的得分和局数,这两个Label的文案是可以根据情况变化的。
- 放上滑动条Slider,滑动数字范围在1到100之间。
- 点击Hit Me后可以读取到Slider的数值。
- 生成一个随机数作为目标数字,显示在界面上,让用户滑动Slider尽可能接近这个目标数字。
- 对比Slider数值和目标数字之间的差距,得出得分score,同时局数round加1,显示在提示框里。。
- 放上一个Start Over按钮,点击清空之前的得分score和局数round。
- 让这个App只能横屏显示。
- 完善界面,使用设计切图达到效果图的效果,适配各个机型。
一、Storyboard中搭建界面、基础设置
拿到设计图或者产品部的线框图后,我们就可以根据线框图或设计图布局App的界面了。
1.新建工程,设置App支持的方向仅限横屏(工程详细信息中General->Deployment Info->Device Orientation)。
2.打开Storyboard,点击ViewControllerScene。
1)把Supported Device Orientations设置为横向(选中controller->Attribute Inspector->Orientation);
2)模拟器也改成横向显示;
3)不勾选Use Size Classes;
4)选中Scene设置好对应的.swift文件(Identity Inspector中的Class);
5)放入Button、Label、Slider等各种控件,修改控件的Text属性,也就是显示文字;
6)Slider的值范围设置为1-100,当前值是0;
操作完成后的样子见下图:
3.关于我们页面。
1)从Object Library库中拖入一个View Controller,这个是关于我们页面;
2)把Supported Device Orientations设置为横向(选中controller->Attribute Inspector->Orientation);
2)放入需要的控件Text View和Button,修改Text属性改变控件显示文案,Text View不勾选Editable选项;
3)新建文件,选择CocoaTouch类型->Subclass为UIViewController。
4)选中Scene设置好对应的.swift文件(Identity Inspector中的Class);
操作完成后的样子见下图:
5)Control拖拽法创建Segue,Segue:modal,Transition:Flip Horizontal
4.创建Outlet和Action连接
首先分析一下,哪些控件需要创建连接,Outlet有:目标数值,得分Score,局数Round和滑动条Slider;所有的Button控件都建立Action连接。
打开Assistant Editor,给需要建立连接的控件建立相对应的Outlet和Action连接(Control拖拽法)。
1)Outlet连接
@IBOutlet weak var targetLabel: UILabel!
@IBOutlet weak var scoreLabel: UILabel!
@IBOutlet weak var roundLabel: UILabel!
@IBOutlet weak var slider: UISlider!
2)Action连接
首页的Slider控件,Event选择Value Changed,Type选择UISlider:
@IBAction func sliderMoved(sender: UISlider) {
}
首页的Hit Me控件,Event选择Touch Up Inside,Type选择AnyObject:
@IBAction func showAlert(sender: AnyObject) {
}
首页的Start Over控件,Event选择Touch Up Inside,Type选择AnyObject:
@IBAction func startOver(sender: AnyObject) {
}
关于我们页面的Close按钮选择Touch Up Inside,选择AnyObject。
@IBAction func close(sender: AnyObject) {
}
二、写代码
通过上面的步骤,我们的布局完成了,我们需要理顺一下逻辑关系,好写代码了。
1.创建实例变量。
根据首页上的布局,有一个目标数值Label会显示在App中,有一个ScoreLabel显示分数,有一个局数Round显示分数,这三个都需要实例变量,根据游戏规则需要获取Slider中的实际数值对比目标数值,算出得分,所以还需要一个实际数值变量,所以我们需要四个实例变量(目标数值、当前实际数值、分数、局数):
1)一个代表目标值的变量,整型类型,初始值是0。
var targetValue : Int = 0
2)一个代表Slider当前的实际数值的变量,整型类型,初始值是0(要和Storyboard中当前值对应)。
var currentValue : Int = 0
3)一个能记录分数的变量,整型类型,初始值是0
var score = 0
4)一个能记录局数的变量,整型类型,初始值是0
var round = 0
2.显示随机整数+开启新游戏方法。
这个游戏的开头是现有目标数值(随机整数)然后才有后面的操作,那么,我们要保证程序启动时以及开启新的一局游戏时,这个目标数值都会变化。那么,开启新的一局游戏时,除了要更新目标数值外,还需要做什么事情呢?想一想,要把Slider的值重新调回到0的位置,Round局数也要增加1。我们把这些事情都集合到一个方法中,命名startNewRound:
func startNewRound() {
//获取新的目标数值
targetValue = 1 + Int(arc4random_uniform(100))
//Slider的值调整到0的位置
currentValue = 0
slider.value = Float(currentValue)
//新的局数要加1
round += 1
}
程序员启动后,是不是也需要做这些事情呢?那么把这个方法放在viewDidLoad中。
override func viewDidLoad() {
super.viewDidLoad()
startNewRound()
}
3.Slider的值。
上一步我们设计了目标数值,用户看到了这个目标数值,接下来就是滑动Slider,滑到某个位置。程序需要获取这个位置所代表的数值,然后和目标数值对比,方能算出得分。那么,接下来就需要我们写一个获取Slider值的方法,之前在建立Action连接时,Slider的值一有变化,就会触发事件:
@IBAction func sliderMoved(sender: UISlider) {
currentValue = lroundf(sender.value)
}
4.点击Hit Me按钮。
好了,目标数值有了,当前数值也有了,可以开始做对比了吧。用户滑动结束后,点击Hit Me,程序会进行对比、计算、显示结果,显示结果用弹出框表示。用户只会看到弹出框显示的结果,出结果之前的对比计算需要我们在代码中进行,但是不显示出来。同时我们在文案上可以设计一下,根据不同的得分段,在提示框中显示不同的话。用户看到结果后,弹出框有个OK按钮,点击OK按钮,此局游戏结束,开始新的一局,同时,App中的分数和局数以及目标数值,都需要更新,分数加上刚刚这局的得分,局数也增加1,显示新的目标数值。那么接下来的代码需要在点击Hit Me按钮的方法中编写,还好我们之前已经建立了Action连接showAlert
方法,用户每次Touch Up Inside,都会触发事件:
1)先写计算过程:
@IBAction func showAlert(sender: AnyObject) {
//对比差值
let difference = abs(targetValue - currentValue)
//计算分数,在此规则下,用户猜的再差也能得1分
let points = 100 - difference
//把这次的得分加入到总分中
score += points
}
2)再写提示框:
点击OK其实代表两件事情,此局游戏结束,开始新的一局。那么可以用到我们之前的方法startNewRound,但是这个方法只是让代码更新了,显示在Label上的内容没有变化,用户没有看到这个变化,所以我们写一个方法updateLabels,把所有的Label更新文案的事情都放这里面,这样当用户点击OK的时候,可以直接调用这个方法:
func updateLabels() {
targetLabel.text = String(targetValue)
scoreLabel.text = String(score)
roundLabel.text = String(round)
}
App启动时,也需要更新Label,不然会显示我们在搭建界面时胡乱输入的文案了,那么把这个方法放在viewDidLoad中。
override func viewDidLoad() {
super.viewDidLoad()
startNewRound()
updateLabels()
}
然后我们开始写提示框代码,注意要判断一下用户的得分属于哪个阶段,对应不同的提示语:
@IBAction func showAlert() {
let difference = abs(targetValue - currentValue)
let points = 100 - difference
score += points
//开始提示框代码
//判断得分的不同阶段,给出不同的提示语
let title: String
if difference == 0 {
title = "Perfect!"
} else if difference < 5 {
title = "You almost had it!"
} else if difference < 10 {
title = "Pretty good!"
} else {
title = "Not even close..."
}
let message = "You scored \(points) points"
let alert = UIAlertController(title: title, message: message,preferredStyle: .Alert)
//用户点击OK时,用了闭包语法,这样只有在用户点击OK后,这两个方法才会被调用
let action = UIAlertAction(title: "OK", style: .Default, handler: { action in
self.startNewRound()
self.updateLabels()
})
alert.addAction(action)
presentViewController(alert, animated: true, completion: nil)
}
点击Hit Me按钮写到这里就结束了。
5.点击Start Over按钮。
点击Start Over后,局数要清零,分数要清零,Slider的位置也要归位,各个Label上显示的内容也要清零,这些事情,都在上面两个方法startNewGame()和updateLabels()中做过了,所以我们可以直接调用这两个方法:
@IBAction func startOver() {
score = 0
round = 0
startNewGame()
updateLabels()
}
书中写到这里时,把App第一次启动后的效果,等同于点击了Start Over,完全开启新的一轮游戏。这是因为书中没有数据持久化的教程,毕竟是入门书籍。但是,如果我们已经会了数据持久化,再来优化这个App时,App启动后,就不一定是开启新的游戏了,有可能只是开启新的一局游戏而已。所以这个viewDidLoad()里用哪些方法,还要根据产品需求来决定。一般要考虑几个方面:App启动后,App进入后台后,App被强行关闭后,这三个地方一定要考虑一下如何对持久化的数据进行处理,不然就会出现用户游戏进度(用户数据)改变或者没有保存的情况。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
在AppDelegate.swift文件中,还有几种情况,也要考虑一下才好。
6.点击Close按钮。
App中目前就还剩下一个按钮的方法没有写了,那就是关于我们中的Close按钮。点击Close按钮,回到首页。鉴于Segue是Modal,所以我们使用 dismissViewControllerAnimated(),这个方法要写在关于我们的.swift文件中,不能写在首页的.swift文件里:
@IBAction func close() {
dismissViewControllerAnimated(true, completion: nil)
}
三、增加规则
1.如果用户的猜测水平在Perfect阶段,则再加100分作为奖励,如果在You almost had it阶段,则再加50分作为奖励:
@IBAction func showAlert() {
let difference = abs(targetValue - currentValue)
var points = 100 - difference
let title: String
if difference == 0 {
title = "Perfect!"
points += 100
} else if difference < 5 {
title = "You almost had it!"
if difference == 1 {
points += 50
}
} else if difference < 10 {
title = "Pretty good!"
} else {
title = "Not even close..."
}
//因为上面points还会根据不同的情况变化,所以一定要在points不再有变化后再加到score中,所以把这行放到这个位置
score += points
let message = "You scored \(points) points"
let alert = UIAlertController(title: title, message: message,preferredStyle: .Alert)
let action = UIAlertAction(title: "OK", style: .Default, handler: { action in
self.startNewRound()
self.updateLabels()
})
alert.addAction(action)
presentViewController(alert, animated: true, completion: nil)
}
四、几个疑问
1.为什么单独写一个startNewGame()方法?
2.startNewRound()和updateLabels()为啥要分成两个方法,能不能写在一方法里?
这篇文章中的步骤和一些代码并没有完全按照书中的来,比如书中还有一个startNewGame()方法,我直接把这方法写在了@IBAction func startOver(sender: AnyObject)方法里了。因为我觉得没有必要写startNewGame这个方法。至于作者为什么这么写,我暂时不理解,欢迎达人指点。
还有一处地方就是startNewRound()和updateLabels()这两个方法,为什么不写在同一个方法中?
我的理解是一个代表旧的一局游戏结束更新Label显示出结束的信息,一个代表的开始新的一局,程序内部已经准备好新的数据了。可是书中的代码显示,都是startNewRound()在前,updateLabels()在后,所以我的这个理解也就错了。
那么,真正的原因是什么呢?
还好这个问题我已经写邮件直接问作者了,感动的是,当时我是2015年12月31号晚上写的邮件, 没想到作者时隔5个小时就回复了,马上就过元旦了,还要回复读者邮件,好感动。
如果你曾经看过这本书,那么,请把你的理解告诉我,评论或者私信皆可,我会分享给你作者的回复原因。如果你没有看过这本书,那么,实在是没有必要知道原因啊,因为您都不知道我在问啥问题啊亲们!
五、完善外表
书中这部分,从106页一直写到结束150页,着实让我知道了开发App大部分的工作都用到了哪里,150页的书籍,1/3都在写完善界面的方法。看来要达到设计稿的效果,还需要程序员做出特别多的努力,花很多的时间。
需要考虑的有:设计图等设计效果,动画,自动布局AutoLayout也就是适配多个设备
看来做出一些细节优化神马的,确实比较花费时间。剩下的这部分可以略过不看了,我纯粹是整理自己的思路,没有太多干活。而且AutoLayout和AdaptiveLayout在不同的应用上差别太大。
- 横屏时去掉status bar:
Main.storyboard -> select the View Controller -> Attributes inspector -> Simulated Metrics -> Status Bar -> None.
Project Settings screen and under-> Deployment Info -> Status BarStyle -> Hide status bar.
设计切图导入Xcode
只开发iPhone端App,只需要放入@2x和@3x即可。拖宅Image View控件,放入背景图片,设置属性(width568,Height320),Editor->Arrange->Send to Back(或在outline pane中拖动)。在About View Controller中进行同样的操作
修改Label控件效果:Color+Shadow+Shadow Offset+Font+FontStyle+FontSize+Autoshrink+Size to Fit
修改Button控件效果:
1)
Hit Me按钮+关于我们页面的Close按钮:
State Config为Default时的设置:Size Inspector中的Width+Height,Attribute Inspector下的Type->Custom + Background + Font + FontSize + FontStyle + TextColor + Shadow Color
2)
Hit Me按钮+关于我们页面的Close按钮:
State Config为Highlighted时的设置:Attribute Inspector下 Background + TextColor + Shadow Color + Reverses on Highlight
3)
Start Over按钮+i按钮(关于我们按钮):
Type->Custom + 去掉text中文案 + Image + Background + Width + HeightSlider
在ViewController.swift文件中,把代码输入到viewDidLoad()方法中:
let thumbImageNormal = UIImage(named: "SliderThumb-Normal")
slider.setThumbImage(thumbImageNormal, forState: .Normal)
let thumbImageHighlighted = UIImage(named: "SliderThumb-Highlighted")
slider.setThumbImage(thumbImageHighlighted, forState: .Highlighted)
let insets = UIEdgeInsets(top: 0, left: 14, bottom: 0, right: 14)
if let trackLeftImage = UIImage(named: "SliderTrackLeft") {
let trackLeftResizable = trackLeftImage.resizableImageWithCapInsets(insets)
slider.setMinimumTrackImage(trackLeftResizable, forState: .Normal)
}
if let trackRightImage = UIImage(named: "SliderTrackRight") {
let trackRightResizable =trackRightImage.resizableImageWithCapInsets(insets)
slider.setMaximumTrackImage(trackRightResizable, forState: .Normal)
}
在输入图片名称时,可以不写@2x和.png,只写名字即可
- 关于我们页面使用web view控件展示HTML内容
1)
storyboard中删掉Text view放入web view控件然后建立Outlet连接,在Project Navigator中右键新建文件Add Files to "BullsEye",选中BullsEye.html文件点击Add完成新建。
2)
在AboutViewController.swift文件中,把代码输入到viewDidLoad()方法中:
override func viewDidLoad() {
super.viewDidLoad()
if let htmlFile = NSBundle.mainBundle().pathForResource("BullsEye",ofType: "html") {
if let htmlData = NSData(contentsOfFile: htmlFile) {
let baseURL = NSURL(fileURLWithPath:NSBundle.mainBundle().bundlePath)
webView.loadData(htmlData, MIMEType: "text/html",textEncodingName: "UTF-8", baseURL: baseURL)
}
}
}
使用Preview预览不同设备下的效果
用Auto Layout适配不同的设备
1)
主页和关于我们页面的背景图Image view控件:
Align ->( Horizontally in Container + Vertically in Container ) -> Update Frames Items of New Constraints -> Add...
2)
关于我们页面的Close按钮:
Align -> Horizontally in Container -> Add...,Pin -> Spacing to nearest neighbor -> Check Constrain to margins-> down bar 20 -> Update Frames Items of New Constraints -> Add...
3)
关于我们页面的web view控件:
Pin -> Spacing to nearest neighbor -> Uncheck Constrain to margins -> ( left bar 20 + up bar 20 + right bar 20 + down pin 20 ) -> Update Frames Items of New Constraints -> Add...
主页支持3.5-inch和4-inch:
选中控件(见下图)点击Editor->Embed In -> View。刚刚嵌入的View起名叫做container view。
给container view设置: Pin -> ( Width491 + Height285 ) -> Add...,Align ->( Horizontally in Container + Vertically in Container ) -> Update Frames Items of New Constraints -> Add...
最后设置container view的Background color属性为Clear Color
5)
支持iPhone 6和 6 Plus:
删除LaunchScreen.storyboard,到Project Settings->App Icons and Launch Images ->清空Lunch Screen File;按住Option键,点击Product -> Clean Build Folder -> Clean;在Project Navigator中右键新建文件Add Files to "BullsEye",选中Default@2x.png和Default-568h@2x.png文件点击Add完成新建。
我的小疑问:这个步骤有必要吗?这样就真可以适配iPhone 6和6 Plus吗?
- 淡入淡出效果Crossfade
在点击Start Over按钮后,增加动画效果:
在ViewController.swift文件中:
import QuartzCore
修改StartOver的方法:
@IBAction func startOver() {
startNewGame()
updateLabels()
let transition = CATransition()
transition.type = kCATransitionFade
transition.duration = 1
transition.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
view.layer.addAnimation(transition, forKey: nil)}
增加图标icon
虽然此应用没有适配iPad,但是并不能阻止iPad运行此应用,iPad上会出现iPhone大小的框,在框里运行此应用,所以之前的图片没有1x,但是icon最好能够适配iPad,也就是需要1x的icon。修改应用在手机上显示的名字
Project Navigator -> Info.plist -> Editor -> Add Item -> Bundle display name -> 输入你想要的名字在真机上运行测试
好啦,终于结束啦~
下篇文章见~