美文网首页swiftiOS入门开发iOS开发
iOS Apprentice中文版-从0开始学iOS开发-第七课

iOS Apprentice中文版-从0开始学iOS开发-第七课

作者: Billionfan | 来源:发表于2017-03-10 22:26 被阅读316次

    完善这个游戏

    你已经有了一个基本能玩的游戏app了。游戏的规则都执行的不错,也没有什么逻辑上的重大缺陷。我能告诉你的就是,眼下没有BUG。但是这里仍旧有许多我们要改进的地方。

    显然,目前的游戏界面看起来既不3D也不华丽,我们后面会给它整容一下。但是眼下,我们有其他一些地方需要微调一下。

    我们就从如何表现玩家的得分情况开始吧。

    如果玩家将滑条正好放到了目标值的位置,让提醒窗口显示“Perfect”,如果非常接近目标值,就显示“You almost had it”,如果偏离的比较远则显示“Not even close”,这可以给玩家得分一个比较良好的反馈。

    练习:想想实现方法。这些判断逻辑应该放在什么地方,并且你应该如何编程实现它?线索:我们在刚才好像用到了很多这样的词,“如果”。

    放置这些判断逻辑的正确位置是showAlert(),因为你在这里创建了UIAlertController的对象,用于给玩家显示一个提示窗口。你已经对message的文本做了一些处理,现在你要用类似的方法来处理title的文本。

    这里是改进后的方法的代码:

    @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 = "Your scored \(points) points"
            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)  //这里改动一下
            let action = UIAlertAction(title: "OK", style: .default, handler: nil)
            alert.addAction(action)
            present(alert,animated: true,completion: nil)
            startNewRound()
            updateLabels()
        }
    

    你创建了一个名为title的String型局部变量,用来存放在提醒窗口顶部显示的文本。最初,变量title没有任何值。

    你使用滑条和目标值的差值difference来判断应该显示哪一条文本:

    如果difference等于0,说明玩家非常牛X,于是你将“perfect”放入title。

    如果difference小于5,你将“You almost had it”放入title。

    如果difference小于10则显示“Pretty good”

    如果difference大于等于10,则认为玩家表现不佳,显示“Not even close”

    你能理解这段代码的逻辑了吗?它仅仅是一堆if语句用于判断difference变量的值,并且选一条对应的文本显示。

    你用title变量的文本替换了你在创建这个UIAlertController对象时,使用的一条固定文本(Hello World)。

    运行app,然后玩几局。你将看到title文本根据你的得分情况在不断变化。这就是if语句的作用。

    新的title文本

    练习:当玩家得到一个perfect时,给玩家额外的加100分作为奖励。并且在非常接近100分的时候,比如98,97分时也给予一定的奖励(说不定在奖励之下,有人会给你充值哦_)。

    现在对玩家挑战高难度低分时,我们有相应的激励机制了,一个perfect不仅仅是100分而是200分。并且在非常接近100时,我们也给一个50分的奖励。

    这里我如何实现这一目的的代码(注意看注释部分):

    @IBAction func showAlert() {
            let difference = abs(targetValue - currentValue)
            var points = 100 - difference //将let改为var,points从常量改变为变量
            let title: String
            if difference == 0 {
                title = "Perfect"
                points += 100   //添加这一行
            } else if difference < 5 {
                title = "You almost had it!"
                if difference == 1 {
                    points += 50
                }   //添加这个if语句
            } else if difference < 10 {
                title = "Pretty good!"
            } else {
                title = "Not even close..."
            }
            
            score += points  //这一行原来在上面,把它移到下面
    ...
    }
    

    你应该注意以下几个事情:

    在第一个if后面的花括号内,你看到了一行新的语句。当difference等于0时,你不仅使title显示为“Perfect”,而且额外的给points加了100分。

    第二个if也改了。它的内部出现了一个新的if语句。这样做并没有问题。你想要单独处理difference等于1的情况,当等于1时,额外的加50分上去,这就是这个新的if的作用。

    毕竟,当difference大于0小于5时,它当然可以为1,但并不总是1。因此你用了一个额外的if语句来检查defference是否为1,如果是,则加50分。

    因为这些新的语句添加了新的分数,所以points不能再是常量了,它现在必须是一个变量。这就是为什么我们把points前面的关键字由let更改为var。

    最后,score += points这一行必须移动到所有if语句的后面。这是必须的,因为app也许会在这些if语句的内部修改points的值,并且这些额外的得分也需要加到总分score中去。

    如果你自己写的版本和我的略有不同,也没什么关系,只要它能够提供同样的功能并且正常工作。在写程序的过程中,处理一个问题经常会有多种方法,只要它们的执行结果一致就没问题。

    运行app,并且看看刚才的改动都生效没有。

    额外的得分

    回顾一下局部变量(Local variables)

    我已经多次指出局部变量与实例变量的区别。作为你此刻应该知道的内容是,一个局部变量仅仅在它所属的方法被调期间才存在,而实力变量则在它所属的对象的视图控制器(view controler)存在期间一直存在。局部常量和实例常量也是如此。

    在showAlert()内部,有六个局部的量(常量和变量)和三个实例的量(常量和变量):

    let difference = abs(targetValue - currentValue)
    var points = 100 - difference
    let title = . . .
    score += points
    let message = . . .
    let alert = . . .
    let action = . . .
    

    练习:指出哪些是局部的,哪些是实例的,哪些是变量,哪些又是常量?

    答案:局部的非常好辨认,因为它们的名字前都有let或者var,说明它们是在方法内部被定义的。(不要误会我的意思,并不是说有let和var就是局部变量,let和var是定义常量和变量的关键字,有let或者var开头,说明它们在方法内部刚刚被定义,所以是局部的,我们一开头也说过,变量或常量的作用范围纯粹看它们被定义在哪里)。

    let difference = . . .
    var points = . . .
    let title = . . .
    let message = . . .
    let alert = . . .
    let action = . . .
    

    这些符号(let和var)用于创建新的变量(var)或者常量(let)。因为它们在方法的内部被创建,所以它们是局部的。

    这六个项目——difference, points, title, message, alert, 以及action被限制在showAlert()内部,并且在它之外并不存在。一旦showAlert()方法执行完毕,它们就被释放了。

    例如:每次玩家点击Hit Me按钮后,difference都会得到一个不同的值,即使它是常量。我们前面不是说过常量的值是不可以改变的吗?

    原因是这样的:每次showAlert()方法被调用的时候,这些局部的常量和变量都会被重新创建。旧的哪些早都被扔掉不要了。

    具体就是当showAlert()被调用时,它会创建一个全新的difference变量,之前一次的difference已经不存在了,被释放了。而这次新创建的这个difference在showAlert()运行结束后,也被扔掉了,下一次showAlert()运行时又创建了一个全新的。所以difference是个常量,但是它的值每次都不同,因为每次你看到的都是一个全新的difference(细思极恐系列_)。

    但是在showAlert()一次运行期间,difference的值,是不能发生变化的。唯一可以改变的就是points,因为它是变量(var)。

    再来看实例变量,它们被定义在任何一个方法的外面。通常都把它们放在一个文件的开头,像下面这样:

    class ViewController: UIViewController {
      var currentValue = 0
      var targetValue = 0
      var score = 0
      var round = 0
    

    你可以在任何方法内部调用这些变量或者常量,不需要重新定义一次,并且它们会长期存在。

    如果你像这样做:

    @IBAction func showAlert() {
      let difference = abs(targetValue - currentValue)
      var points = 100 - difference
      var score = score + points       // doesn’t work!
    ... }
    

    这样不会得到我们想要的结果。因为你在score前面放了一个var,这样它就是属于showAlert() 内部的一个变量了,它不会影响外面那个score的值,并且showAlert() 运行结束后,它就消失了,这样玩家的score永远不会被显示。

    很明显这不是你想要的结果,幸运的是,刚才那段代码甚至不会被编译,因为Xcode知道这样做是可疑的。

    ⚠️为了让这两种类型的变量有所区别,以便于一看就知道它们能活多久,有些程序员会在实例变量的前面加一个下划线。
    它们会将score命名为_score。这样可以减少一些麻烦,因为在变量名字前加一个下划线就不会和局部变量弄混了。这只是个人的一些习惯,Swift才不在乎你怎么给变量取名。
    还有一些程序员喜欢在前面加个m(代表member)或者加个f(代表field),有些甚至在变量名称后面加个下划线。这些方法是愚蠢的,不要去学。如果你不能心知肚明的清楚每个变量的作用范围,这些前缀或者后缀只能把你带向更深的深渊。

    等待提醒窗口离开

    在这个游戏中,仍然有些事情困扰着我。也许你已经注意到了。。。

    当你一点击Hit Me按钮,提醒窗口就弹出来了,并且与此同时滑条立即恢复到了中间位置,回合数值立即显示为加1,并且目标值也立即被一个新的随机数替换掉了。

    就是说你还木有机会观察上一回合结果的时候,新一回合的数据立马被更新到屏幕上了,这让人觉得有点怪怪的。

    你也许想知道为什么会这样,毕竟在showAlert()中你是在显示提醒窗口之后才调用的startNewRound()。

    @IBAction func showAlert() {
            . . .
            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
            let action = UIAlertAction(title: "OK", style: .default, handler: nil)
            alert.addAction(action)
            present(alert,animated: true,completion: nil)
            startNewRound()
            updateLabels()
        }
    

    和你期望的相反,present(alert. . .)并没有暂停其他方法的执行,并且等到提醒窗口消失后才执行它们。其他一些平台上的alert是被设计为如此工作的,但是在iOS上不是。

    取而代之的是present(alert. . .)将提醒窗口放到屏幕上的同时立即返回结果,然后showAlert()中的剩余方法立即被执行,新的一回合甚至在提醒窗口的弹出动画尚未结束时,就被更新到屏幕上了。

    用程序术语讲就是,alert(提醒窗口)是异步工作的。更多关于异步和同步的内容我们在下一个课程中讲,现在对你而言这件事,就是意味着,在alert执行结束前你不知道其中的进展情况。你只能赌showAlert()运行结束后一切正常。

    所以如果你无法在弹出窗口消失前在showAlert()内部等待,那么你如何等待它的关闭呢?

    答案是简单的:事件!和你之前看到的一样,大多数iOS程序都涉及等待特殊的事件发生——按钮被点击,滑条被拖动等等。这里没有什么不同,你只需要以某种事件等待alert的结束。在这段时间内,你什么都不做。

    这是它工作的原理:
    对于alert来说,每一次点击Hit Me按钮,你都必须提供一个UIAlertController的对象。这个对象告诉alert,按钮(这个按钮是指提弹出的提醒窗口上的那个按钮)上的文本是“OK”,以及这个按钮是什么样式的(这里我们使用的是默认样式):

    let action = UIAlertAction(title: "OK", style: .default, handler: nil)
    

    这里的第三个参数,handle,告诉alert当OK按钮被点击后应该发生什么。这就是你寻找的等待alert消失的事件(点击OK按钮后,alert就被解除了,并且会触发一个事件)。

    目前handle是nil,就是说什么都不做。为了做出我们需要的更改,你需要给UIAlertAction一些代码执行,当OK按钮被点击后。当玩家最终点击OK按钮后,alert将自己从屏幕上移除并且会跳转到你的代码上。你可以在这个地方打打注意。

    这种模式也被称作‘回调’,在iOS上这种模式会在多种用途中出现。之前你经常被要求创建一个新的方法用于处理事件,但是现在,你需要一点新的东西——闭包(closure)。

    将showAlert()的底部稍微改动一下:

     @IBAction func showAlert() {
            . . .
            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)
            present(alert,animated: true,completion: nil)
        }
    

    这里改变了两个地方:

    1、你移除了方法中底部的startNewRound()和updateLabels(),千万别漏掉这一点。

    2、你将这两个方法塞到UIAlertAction的参数handle的代码块中去了。

    这样的代码块就叫做闭包(closure)。你可以把它想象做一个没有名称的方法。这些代码不会被立即执行,之后点击OK按钮后才会执行。这个闭包在alert被解除后才告诉app开始新的一回合以及更新标签的内容。

    运行app并且观察一下效果。我想你的游戏效果应该比刚才好些了。

    ⚠️:self
    你也许想知道为什么在handle的代码块里你用self.startNewRound()取代了startNewRound()。
    self关键字允许view controller指向自己。这个概念对你而言不应该太陌生。当你说:“我需要一个冰淇淋”时,你用‘我’这个词指代了自己。类似的,程序中的对象也可以用一个代称来讨论它们自己。
    通常你不需要这个self向view controller传递消息。这里是个例外:在闭包中,你必须使用关键字self来指向view controller。
    这是Swift的语法规则。如果你在闭包中忘记了self,Xcode会创建app失败(亲自试试)。之所以存在这个规则是因为闭包可以‘捕获’变量,这会带来一些意外的结果,基本上不是好的结果。你会在另外的课程中学习这些内容,在本课程中,我们就讨论到这里。

    重新开始

    我不是指删掉你之前的所用东西,如果你已经这么做了,那么恭喜你多了一次复习的机会。我说的是这个游戏app中的“Start Over”按钮。这个按钮用于将你的得分和回合数重制为默认值。

    Start Over按钮的作用之一是用来和其他玩家一较高下。比如第一个玩家玩十个回合,然后重置分数让第二个玩家玩十回合,看看谁的得分更高。

    练习:试着自己完成Start Over按钮的功能。你已经见识过了按下一个按钮后,view controller是如何进行响应的,并且你应该可以实现如何改变score和round变量的值。

    你会如何做呢?如果你卡住了的话,跟着我的讲解往下做。

    首先,在ViewController.swift中添加一个新的方法,用于开始一次新的游戏。我建议你将这个方法放在startNewRound()的附近,因为这两个方法的概念差不多。

    添加新的方法:

    func startNewGame() {
            score = 0
            round = 0
            startNewRound()
        }
    

    这个方法重置了score和round的值,并且同时开始新的一个回合。

    注意一下,这里你设置round的值为0而不是1。是因为在startNewRound()中已经设置了对round加1。

    如果你将round设置为1,那么startNewRound()中再被加1,那么第一回合你看到的回合数就是2了。

    所以这里设置为0,让startNewRound()在第一局开始前对它进行加1操作。

    (这些代码比我的说明更能解释,为什么我们不用平常的语言去编程,而要用专门的编程语言)

    你同时需要一个action方法处理点击Start Over按钮后触发的动作:

    将下面的action方法添加到ViewController.swift中:

    @IBAction func startOver() {
            startNewGame()
            updateLabels()
        }
    

    这个方法放在代码中的哪个位置并不重要,但是放在其他action方法等下面是一个不错的选择。

    当Start Over按钮被点击后,startOver()这个action方法首先调用startNewGame()用于开始新的一次游戏。(看到了吗,如果你选用合适的方法名称,那么你的代码目的也就一目了然了)

    因为startNewGame()改变了score与round这两个实例变量的值,所以你需要调用updateLabels()来更新这些标签的文本。

    作为最终的完善,你应该将viewDidLoad()中的startNewRound()替换为startNewGame()。因为当app刚开始运行时分数和回合数都应该是0,这一改动不会对app的运行效果有任何影响,只是使你的代码更加合理了。

    override func viewDidLoad() {
            super.viewDidLoad()
            startNewGame()  //改动这一行
            updateLabels()
        }
    

    最后,将Start Over按钮和action方法连接起来。

    打开storyboard,按住ctrl并且拖动start over按钮到view controller。放开鼠标后在弹出的窗口上选择startOver。这样按钮的Touch Inside event就和你刚才定义的动作连接起来了。

    运行app,并且玩几局。然后点击start over按钮看看有没有生效。

    小帖士:如果你丢失了某个按钮或者标签的连接,不知道它们是连接到哪个方法,你可以右击storyboard中黄色图标的那个view controller,就可以看到你操作过的所有连接。

    view controller到所有对象的连接

    你可以在05-polish中找到本节课的相关代码。

    相关文章

      网友评论

      • ab206845a5ea:@IBAction func showAlert() { . . .
        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)
        present(alert,animated: true,completion: nil)
        }
        我做完这步,要在模拟器上点一下别的才会触发
        self.startNewRound()
        self.updateLabels()
      • 大吉__:继续点赞
      • 林水溶:赞, 一个和前端重要的区别就是: ios的alert是异步执行的.
        前端也有alert. 不过是同步执行的.

      本文标题:iOS Apprentice中文版-从0开始学iOS开发-第七课

      本文链接:https://www.haomeiwen.com/subject/hrxswttx.html