其他,文章导航

「研究」游戏里的那些大咪咪(以及这货是怎么被妖魔化的)

本文由「@玄学翻译社」企划制作!
企划:宋雅文、杨雍;翻译:满月、王蛇无毒、Thest、一个兔角鹿、风语时;校对:刘嘉俊、太昊。

游戏里的那些大咪咪(以及关于大咪咪的错误)

Duang~~ Duang~~ Duang~~

历年来,许多游戏试图去模拟胸部的摇晃。乃至一个专门的名词的产生「胸部物理」(俗称“乳摇系统”,下文统称“乳摇系统”)。

若你玩过有乳摇系统的游戏,可能会发现很少有游戏中的胸部动起来和现实里一样,是受重力影响的一团脂肪。游戏中这些胸部一般都像是有自我意志的氢气球。

某些游戏对胸部表现太不真实,以至于论者认为这是性别歧视,或者游戏行业物化女性的结果。

我见过人吐槽游戏开发者是否从未接触过真实的胸部。我也见过有人暗示游戏开发者不知道怎么在游戏里去恰当的表现女性特征,以及因为做游戏的女性太少,所以才会有茫茫多奇形怪状的胸部。
(编者注:游戏人,单身狗,没朋友)

阅读全文»

Quick-Cocos2d-x初学者游戏教程13

本章将是本教程的最后一章,在这章我们将完善游戏功能,即给游戏添加粒子特效,音乐音效,和玩家的受伤动画等等。

添加受伤动画

首先,我们来添加玩家受伤动画。

玩家受伤动画是 Player 与障碍物或地面碰撞的时候播放的一个动画效果,它是一个独立的帧动画,帧序列图片如下:
hit

阅读全文»

从优秀的iPhone游戏中学习设计策略

几乎所有iPhone用户都曾使用过游戏应用,这是一个“全民游戏”的时代。iPhone应用程序中,游戏不但在下载量方面首屈一指,在设计质量上也不乏可圈可点之处。本人从大量的游戏应用中大浪淘沙,试图从中汲取营养,探索好的设计策略为我所用。

基本思路是

步骤一,提出小问题或小测试,请跟随我思考一下;

步骤二,引出其中的深层原理;

步骤三,此原理在iPhone游戏中的体现;

步骤四,给我们哪些启示?

通过以上步骤,简单地介绍肌肉记忆、长期记忆、短期记忆、识别与回忆、事物预设的特点,以及对用户界面设计的影响。文章篇幅和个人能力有限,对每一个原理不能作过多的挖掘,只愿可以抛砖引玉启发大家一些思考。

 

一、肌肉记忆

步骤一,请思考:

一天早上,你起床起晚了,十万火急地驱车去公司。来到叉路口,一个路人告诉你:这有条近道。这时候你面临着两个选择:一条道路是自己每天都走的路,非常熟悉;另一条道路你从来没有走过,有很多的不确定因素。此刻,你会选择哪一条道路?

road

大部分人会选择自己熟悉的那条路。

这条路自己太熟悉了,“闭上眼都能走回家”,脑子几乎不用思考,路即使远一点也不会觉得慢。相比之下,路人为你指的近路,可能还会有你不知所措的岔路口,一旦走错,近路就成了耽误时间的远路。

步骤二,其中的原理:肌肉记忆

人体执行某操作时的效率及准确性,很大程度上取决于是否接近该人熟悉的操作路径。如果操作被重复多次,肌肉就会形成条件反射,生成记忆效应。大脑皮层还没有做出决定,脑干和脊髓神经已经领先一步进行指挥了。如果操作处于新接触阶段,不确定性较多,此时做出决策的是大脑皮层。大脑皮层做出决策所花费的时间要比脑干和脊髓神经做出反应的时间长。

举一个例子:新手学车的时候,多用大脑皮层,执行挂档、倒桩等操作慢而且不连贯,容易出错。驾车老手执行刹车、变换档位更多的是潜意识操作,速度更快、准确性更高,是由脑干和脊椎神经指挥的。

从速度、准确性方面讲,用户熟悉的操作路径可以使操作行为更从容。操作路径如果过于新颖或者很难摸索,即使操作步骤缩短,也不见得会比用户熟知的路径更快更好。

步骤三,在手机游戏中的体现:

road1

iPhone动作游戏、街机游戏和角色扮演游戏中,一般采用横屏模式,左下角为位置控制操作区域,右下角为动作控制操作区域。简单的用户使用情景:双手托住手机,左手拇指负责操作游戏中角色的位置,右手拇指负责操作角色具体的动作。

用户对这种模式已经非常熟悉,形成了肌肉记忆。假如将它颠覆,如位置操作和动作操作都放置在右下角,用户就会觉得不适应不舒服。

步骤四,给我们哪些启示

1. 操作系统的一致性大于产品自身的一致性。

手机操作系统之间差异比较明显,执行某项操作可能会有迥异的操作路径。如Android系统较多使用长按操作,iOS系统在具体的应用程序内则较少使用长按。不同的手机操作系统,培养了用户不同的肌肉记忆;这种肌肉记忆是被无数个符合此系统特点的应用程序锻炼出来的,一旦形成,很难改变。所以产品铺平台的时候,一定要熟知和匹配该操作平台的用户界面特点,方便用户快速掌握应用程序的操作方式。

2. 应用程序要和“邻居”们和谐相处。

应用程序在用户的手机中不是孤立存在的,是众多应用程序中的一员。痛苦的是,用户并不能完全记住每一个应用程序每一个细节是如何操作的,肌肉记忆让他们尝试他们自己熟悉的那几种操作方式。因此,有必要考察一下你的“邻居”应用程序,看看他们常用的操作方式,在不影响你自己创意点发挥的前提下可以与他们保持一定的一致。

3. 应用程序小环境内,控件需要有延续性和可预见性。

肌肉记忆也会存在于应用程序小环境内。功能键有必要保持稳定的位置,在不同的页面内都能顺着视觉习惯找得到;功能键的布局需要有可预见性,出现在相关内容区附近,不要埋藏过深。

 

二、长期记忆

步骤一,请思考:

问题一:你上一个手机号是多少?

问题二:是否记得余玄定理的具体内容?

诸如此类曾让我们倒背如流的东西,时隔已久,恐怕大家已经记不起来。

这是为什么?又同时说明了什么呢?

步骤二,其中的原理:长期记忆的特点

长期记忆可以帮助人们随时提取关键信息,比如你会轻而易举地说出你的现在的手机号码、身份证号码。长期记忆,需要经过大量重复的演练而形成,可以保持几天或者几年。如果长期记忆得不到巩固,会随着时间的流逝而渐渐淡忘(艾宾浩斯曲线)。

长期记忆有2个特点:

其一,长期记忆的信息是高压缩比抽象特征的组合,图像、感觉、概念等细节将丢失。为了到达长期记忆的目的,大脑需要将复杂的信息压缩转化为简单的语言符号。

其二,提取长期记忆时,往往出现信息失真。存储信息最好的方法是与旧知识挂钩。提取时最好用熟悉的情境给予暗示。如收拾小学初中时候的东西会让你浮想联翩,旧地重游可以帮你回溯当时的人和事。

步骤三,在手机游戏中的体现:

road2

象棋游戏爱好者一般都是从现实中的象棋开始玩的。虽然现实中的象棋和手机中的象棋游戏差别很大,被省略了太多的元素和感觉,但当他们看到可以达意的棋子轮廓和棋盘格子基本就学会如何在触屏手机中操作了。

手机象棋游戏的易学性,在于源自生活实物元素的提取和临摹,快速激活了象棋爱好者的长期记忆。

步骤四,给我们哪些启示

1. 减少用户的记忆负担。

长期记忆很脆弱,提取时出现信息失真很常见。如果用户不止一遍被提示密码错误,他可能会责怪自己的记忆力为什么这么差;其实真正该责备的是我们的软件没有为用户记好密码,或是用户名和密码的特殊要求增加了用户的记忆负担。我们的软件需要更加智能更加人性化。

2. 注重一致性。

一致性可以更容易使信息被转化为长期记忆,还可以减少长期记忆提取时出现的误差。一致性越弱,用户的记忆负担就越大。不同应用不同功能在一致性上表现越好,易学性就越好,越容易上手。

3. 从相关生活中提取基因。

按照长期记忆的规律,为了使记忆的信息更多更快更牢固,需要使这些信息与以往的大脑记忆相联系。这也是联想记忆比死记硬背更持久更容易的原因。从目标用户熟悉的生活场景中提取基因应用于用户界面中,可以激发用户以往的长期记忆,不但可以提高易用性,还可以营造出温馨的产品气质。

 

三、短期记忆

步骤一,请思考:

第一组信息:74,5,7,6,87,11,0,3,20;

第二组信息:焦点、本地、国内、国际、军事、女人、娱乐、体育、社会、财经、汽车、科技、房产、教育、游戏、互联网

草草地看完上面两组信息,请问你能记住多少?

除非付出足够多的时间和注意力,记住以上信息的准确值和位置很难办到。

是否已经感受到我们的记忆力没有想象的那么强大?

步骤二,其中的原理:短期记忆的特点

短期记忆对于我们日常工作和学习非常重要,因为太多信息只需要让我们知道下一秒如何做就够了,而不需要话费太大力气去转化为长期记忆。

短期记忆不是严格意义上的信息存储,它只是信息临时存放处,是感觉和注意现象的组合。

特点之一,高度不稳定。给你的朋友汇款,汇完款基本就已经把他的银行账号忘掉了。

特点之二,短期记忆即注意的焦点。如果没有留意到的东西突然问你存在没存在过,你可能就含糊了。举个例子,你下班与某同事聊的火热,走进电梯的时候就会心里犯嘀咕:我刚才打卡了没?因为你没有将注意力放在下班打卡这件事情上,没有形成记忆。

此外,短期记忆的数量也是非常有限的。根据研究(心理学家研究的,我没那本事),有如下规律:假如信息是可以被组合的,即有一定的规律性,人们记忆的数量为7±2;假如信息没有规律性、相互之间变动过大,人们记忆的数量为4±1.

步骤三,在手机游戏中的体现:

jump

上图来源于Stylish Sprint游戏,算得上iPhone跑酷游戏中不错的产品了。游戏进行界面中,右上角区域为用户所取得的成绩和任务完成进度。每次需要采取游戏动作时候,界面都会在界面中下位置提示用户。这样做的好处是,应用程序为用户记住了核心信息,为用户节省了脑力。

还有一款类似的跑酷游戏,Darkness Rush,任务说明放置在游戏开始之前的页面中,而在游戏进行页面中没有任何提示,每次都不知道自己的任务是什么,很费解。

步骤四,给我们哪些启示

1.用户界面需要帮助用户记住核心信息。

用户使用你的应用程序是为了完成什么目标?在这个界面中,用户需要做些什么?与用户任务和用户行为紧密联系的有哪些关键信息?把这些关键信息清晰明了的展现出来,在用户迷茫的时候可以当作路灯用。

frame

Google搜索应用iPhone版,搜索框一直悬停在页面顶部。一来方便用户随时更改关键词,二来可以帮用户记住正浏览的内容的搜索关键词是什么。

2.不要高估用户的短期记忆,给予适时的提醒和反馈。

当然,不能把所有相关信息都放置在界面中,造成信息繁冗同样是需要避免的。非核心信息可以在用户需要的时候露一露面。这样的信息出现方式显得很有礼貌,不会过多的打扰用户。

frame2

布丁优惠券iPhone客户端,在每张优惠券最终展示页的上方都会用醒目的颜色提示使用方式,需要打印还是只出示即可。

3.在你认为必要的地方,适当地引导用户的注意焦点。

只有注意力到达的地方,才有可能形成短期记忆。应用程序中的新增功能点,或者是任务操作步骤,如果你觉得有必要,可以适当的引导用户的注意焦点。引导不要停留过久,出现次数也不要太多。

frame3

微信iPhone客户端为了让用户注意到群聊功能,在进入页面之初会出现气泡框提示,随即消失。

 

四. 多用识别,少用回忆

步骤一,请思考:

问题一:

people

判断4张脸你认不认识,你用了多长时间?

问题二:

frame4

记起每一个网站注册时的密码保护问题和答案,你用了多长时间?

识别人脸你认识不认识,只需要话费几分之一秒的时间;记起你曾经使用过的密码保护问题和答案,可能需要好几分钟甚至已经完全淡忘。

 

步骤二,其中的原理:行为的精确性与知识的不精确性

识别是这样一种思维模式:感觉与长期记忆的协同工作,记忆神经元“并行处理”。此时的被识别的信息是明确的、直接的,可以同时激活大脑中无数个神经元,有点类似于并联的电路,激活相同或相似的信息储备区,速度极快。

回忆是这样一种思维模式:没有明确的、具体的感觉输入,尝试性地去激活记忆神经元。速度慢,而且成功率低。

识别最常见的表现方式是选择,以精确的行为来执行。

回忆则实质上是知识的再次输出,以不精确的回答来完成。

 

步骤三,在手机游戏中的体现:

在iPhone游戏中,很少见有大量内容输入的情况出现,甚至登陆注册功能也不一定存在。游戏应用的进入、学习、关卡选择、人员配置一直到设置等步骤,几乎全部依靠点选来操作。整个操作过程顺畅简单,不会因为你遗忘了什么东西就卡壳。

fruit

步骤四,给我们哪些启示

1. 尽量减少输入,多采用选择的形式。

在手机中输入确实是一件很痛苦的事情,效率低下、战战兢兢,如果自己对所输入内容没有完全把握,出现一丝一毫的差错都足以斩断用户的去路。因此,在做应用程序的时候有必要想想,这个功能操作是否可以由选择的形式来实现?即使选择的步骤多一点,也比输入操作完成的快。

talk

时间选择器是通过选择来实现的。

2. 尽可能使用图像来表达功能。

图像更有助于识别。在iPhone 应用中,tab栏和功能控键如果使用图标或者图标与文字搭配的形式,可以使用户更容易地辨识。如下图,ZAKER板块目录页的栏目和功能键如果全部换成文字形式,易寻性和识别性就会弱一些。

homepage

五、预设:为用户提供如何操作的线索

步骤一,请思考:

key

你是怎么判断它们的使用方法的?

灯开关、车门把手、厅门把手,用自己的形态向人们讲述了自己应该如何使用。

步骤二,其中的原理:事物的属性可以通过预设来自我解释

每种事物都可以有自己的预设,书是可以翻阅的、汽车是可以载人的、笔是可以写字的,等等。

人们对事物本身的预设,在生活中已经得到了学习和验证。iPhone 上的应用程序,对于用户来说多多少少会有学习成本的,我们可以借助事物自身的预设向用户讲述应用的使用方法。

千万不要把全部希望寄托到新手引导中,用户一般不看的。

步骤三,在手机游戏中的体现:

game2

以上三款游戏分别是:愤怒的小鸟、三国赚翻天、海盗!开炮!

游戏使用了事物自身的预设为用户讲述如何操作的:弹弓可以发射、骰子可以掷、导火索可以引爆。

步骤四,给我们哪些启示

1. 巧妙地借用事物本身的预设——拟物化设计

拟物化设计是使用事物本身预设的典型例子。用户会把生活中对真实事物的使用经验直接应用于此软件中,既可以提高产品的易学性,又可以使生硬的科技产物以富生活化的形态缩短与用户心理的距离。

piano

Real Piano 是拟物化设计的典型应用,如何使用已经不言而喻。

 

总结:

该篇小文以七零八散的文字胡诌了肌肉记忆、长期记忆、短期记忆、回忆与识别、事物的预设的特点,以及它们在iPhone 游戏中的体验,并呓语这些东西对我们应用程序设计有哪些启发。有自己的思考,也有拾人牙慧之处;有貌似合乎道理的地方,也有思维不甚缜密的地方。请尽情拍砖,谢谢!

高频交易都有哪些著名的算法?

本文来至知乎,原文作者:董可人

我对“冰山算法”刚好有一些了解,可以给大家讲讲。很多人对“量化交易”的理解实在太过片面,基本上把它等同于生钱工具,我不赞同这种观点。交易首先是交易本身,有它自身的经济学意义,忽略这一点而单纯把它看成使钱增值的工具,很容易就会迷失本心。

我也不认为算法本身有什么稀奇,再好的算法也是死的,真正的核心价值一定是掌握和使用算法的人。实际上我讲的东西也都是公开的信息,但是即便了解了技术细节,能真正做好的人也寥寥无几。

希望这个回答可以让你对量化和高频交易有一个更清醒的认识。

~~~~~~~~~~~~~~~~~~

首先我相信不少人概念里的高频交易还是这个样子的:

但对高频交易来说,这种信息是非常粗糙的。所以这里先要对不熟悉背景的同学介绍一下什么叫做Order Book。现在主流的交易所一般都使用Order Book进行交易,交易所在内部的Order Book上记录所有买家和卖家的报价,比如像这样:


Bid表示买家,Offer表示卖家,这张报价单表示买卖双方发出的所有报价单(Limit Order)。这张表才是高频交易最关心的信息。任意时刻,买家的出价总是低于卖家(比如这里的98对101)。所以报价虽然一直在变化,但是只有报价是不会有任何成交的。

什么时候会产生交易呢?有两种情况,第一是任一方发出市价单(Market Order),比如一个买家发出一张单量为10的市价单,就可以买到卖方在101价格上挂的10份,这个交易成功之后,Order Book会变成这样:

第二是发出一个价格等于对方最优报价的限价单,也会产生和上述情况相同的结果。

需要强调的是,虽然真正的Order Book只存在于交易所内部,所有交易都在交易所内完成,但是交易所会把每笔报价和市价单都转发给所有人,所以所有的买家和卖家都可以自己维护一个同样的数据结构,相当于交易所Order Book的镜像。通过跟踪分析自己手里这份的镜像变化,来制定交易策略,是高频交易算法的核心思想。

~~~~~~~~~~~~~~~~~~

基础知识介绍完毕,下面为了方便大家理解,我采用一种更形象的方式来表示Order Book:

这张图对应文章开始时的那个Order Book,应该可以明白地看出,横轴表示价格,纵轴表示订单量,绿色表示买家,红四表示卖家。这样做的目的是为了引出本篇讨论的主题:冰山订单。

通过上述基本分析大家可以看出,交易所内的交易数据是完全公开的,市场上任意时刻,有谁想要买/卖多少,所有人一目了然,没有任何秘密。这样做本身是有经济学意义的,因为只有展示出买卖的需求,才会吸引潜在的商家来交易,所以在市场上一定程度的公开自己的需求是必要的。但这样同时带来一个严重的后果,一旦有某个人想要大量买/卖,他所发出的巨额限价单会直接展示给所有人。比如一个买家挂出巨额买单后,Order Book会像这样:

这对他非常不利,因为所有人都会利用这个信息来跟他做对。大家会判断,现在市场上存在大量的买压,于是会出现一大批为了赚钱而冲进来的人抢购,价格会快速上升,这样原来这个人可以在98这个价位买到的东西,很快就变得要在更高的价位上才能买到。这种情况,那些后来的人做的就是Front running,而原来的那个人则面对逆向选择风险

为了解决这个问题,交易所提供了一种针对性的工具,就是所谓的冰山订单(Iceberg Order)。这种订单可以很大,但只有一小部分是公开出来的,大部分则隐藏起来,除了交易所和发单者本人谁也看不到,真的像一座“冰山”一样。比如像这样:

灰色的部分就是冰山订单隐藏的部分。这样,只有当有对应隐藏量的交易发生时,交易所才会通知其他人,就避免了别人利用显示订单的信息来做Front running

凡事有一利必有一弊。冰山订单虽然保护了发单者的利益,但是对于其他市场参与者来说却又变成了一种不公平的规则。那些有真实的交易需求的参与者,会因为对局势的误判而损失惨重。所以接下来的问题就变成,如何发现市场上的冰山订单?

首先有一种最简单的方法。有的时候,冰山订单是挂在最优买价和卖价之间(spread),像这样:

对于这种情况,有一个非常简单的探测方法,即发一个最小额度的限价单在spread里,紧跟着取消这个订单。比如这个例子中,发出一个卖价为99的限价单然后取消。因为这个价格本身对不上显式的买价(98),如果没有冰山单的存在,一定不会成交。但有冰山单的情况下,一旦交易所收到这个卖单,会立刻成交冰山单中对应的量,而之后的取消指令就无效了。这样,以一种微小的成本,就可以发现市场中隐藏着的订单。事实上,的确有人会做这种事情,频繁的发单然后取消,在最优价差之间形成一种高频扰动,用来探测隐藏单。

为了应对这种扰动探测,大家一般都不会直接挂单在spread里。而是会像之前那样和普通的限价单挂在一起,这样发生交易之后,你就很难推测消耗掉的究竟是正常的限价单,还是冰山订单。那么应该怎么做呢?

首先有一个直接的思路。冰山订单的存在,一定程度上反映了挂单人对市场情况的解读,认为有必要使用冰山订单而做出的判断。需要强调的是,使用冰山订单并不是没有代价的,因为你隐藏了真实的需求,在屏蔽掉潜在的攻击者的同时,也屏蔽掉了真正的交易者!而且会使得成交时间显著增加--因为没人知道你想买/卖这么多,你只能慢慢等待对手盘的出现。所以当有人下决定发出冰山订单的时候,也会有对市场情况的考虑,只有合适的时机才会做这种选择。

什么是合适的时机?有一些数据应该是相关的,比如买卖价差spread,买单量对卖单量的比值等。对这些数据,你可以在历史数据上做回归分析,建立起他们和冰山订单之间的线性/非线性模型。通过历史数据训练出来的这个模型,就可以作为你在实时交易时使用的冰山订单探测器。这是 On the Dark Side of the Market: Identifying and Analyzing Hidden Order Placements 这篇论文使用的方法。

基本模型可以定义为:F(spread,bidSize/offerSize,……) = Probability(Iceberg)

如果你想玩高深的,还可以在此基础上做HMM,SVM,神经网络之类的高级模型,但基本思路是一致的:通过盘口分析计算存在冰山订单的概率。

~~~~~~~~~~~~~~~~~~

上面说的这个方法,看起来很高级,实际效果如何呢?我想大家也看出来了,这种建模不是很精确。作为事后分析手段用来说明什么情况下可能会出现冰山订单还不错,但是作为实时交易的探测器就不是很放心。因为使用的信息太模糊了,而且说到底建模的对象只是一种相关性,没有什么保证冰山订单的发送者一定是按照这个逻辑出牌的。

所以接下来介绍的,才是真正具有高频玩家神采的方法,来自 Prediction of Hidden Liquidity in the Limit Order Book of GLOBEX Futures 这篇论文。

~~~~~~~~~~~~~~~~~~

高频世界里,有一条永恒的建模准则值得铭记:先看数据再建模。如果你看了上面的介绍就开始天马行空的思考数学模型,那基本上是死路一条。我见过很多年轻人,特别有热情,一上来就开始做数学定义,然后推导偏微分方程,数学公式写满一摞纸,最后一接触数据才发现模型根本行不通,这是非常遗憾的。

而看了数据的人会怎么样呢?他很可能会发现,对于冰山订单的处理,交易所的规则是非常值得寻味的。有的交易所是这样做的:一个冰山订单包含两个参数,V表示订单总量,p表示公开显示的量。比如V=100,p=10的冰山单,实际上隐藏的量是90。如果有针对这个订单的交易发生,比如交易量20,交易所会顺序发出三条信息:

  1. 成交10
  2. Order Book的Top bid size -10
  3. 新Bid +10

这三条信息一定会连续出现,并且第三条和第一条的时差dt很小。

因此,一旦在数据中观察到这个规律,我们就可以非常有把握的判定市场中存在冰山订单,并且连p的值都可以确定!接下来的关键问题是,如何确定V的值,即判断这个冰山订单的剩余存量有多少?

这个问题从本质上说没法精确求解,因为V和p都是由下单人自己决定的,可以是任意值。但可以从两点考虑:第一,两个值都是整数;第二,人类不是完美的随机数生成器,下决定会遵循一定规律。

从这两点出发,可以对V和p建立概率模型,即计算一个给定的(V,p)值组合出现的概率是多少?这里不去深入探讨数学分析,感兴趣的朋友可以自己去看原文。简单说,可以在历史数据上通过kernel estimation技术来估算他们的概率密度函数的形状。顺带一提,如果你亲手编写过这种估算程序,就会理解我为什么一名优秀的 Quant 需要什么样的编程水平?这个答案中如此强调编程的重要性。一个可能的概率密度函数的样子是这样的:

这样,当你在实时数据中观测到一个p的值时,就可以得出对应的V值的条件概率密度函数,即上图的一个切面,比如(p = 8):

接下来显然就很容易计算V最可能是什么值了。

综上,算法的核心在于,通过在实时数据中监测短时间内连续出现的三条相关记录判断冰山订单的存在,而对冰山订单的量化则通过由历史数据训练出的概率模型来完成。

相信你也会看出,这种算法并不是什么作弊神器。它只是利用市场上的公开数据所做的一种推测。而且这个推测也仅仅是基于概率的,更多的应该是作为一种参考。它对做市商这种流动性提供者很有意义,可以使他们避免因为对局势的误判而遭受损失。但如果你想用它来作为一种攻击手段,觉得自己能发现隐藏大单而去Front run,那实在是很不明智的选择。

最后,这种算法也只是针对特定的交易所。其他的交易所也许不会采用同样的冰山订单处理方式。所以真正有价值的是这种从实际数据出发的建模思路,具体的算法并不值钱。

~~~~~~~~~~~~~~~~~~

这个小算法给你展示了高频交易领域的“冰山一角”。它看起来也许不算很复杂,但是我却很喜欢。因为它清晰地展示了什么叫做先有思路,再有量化。因为有“冰山订单”这样一个从经济学基本的供需关系出发的真实需求,通过分析实际数据找到一丝线索,最后通过数学模型刻画出定量的策略,这才是漂亮的策略研发。

如果违背这个原则,一上来就去搬各种高级的模型去套数据,指望模型自动给你生成交易信号,这在我看来无异于痴人说梦。遗憾的是,这个梦的确太过诱人,而这个世界也从来不缺少莽夫。

且行且珍惜。

iOS7人机界面指南-UI元素(下)

原文转自:腾讯ISUX (http://isux.tencent.com/ios-human-interface-guidelines-ui-design-ios7-ui-2.html)

目录:

iOS7人机界面指南-UI元素(上)

iOS7人机界面指南-UI元素(下)

译者注:

本次主要翻译了iOS Human Interface Guideline的Part 4-UI Element的后半部分,包括控件(Controls)和临时视图(Temporary Views)。本次翻译与截至2013年10月20日的苹果官方文档相匹配。
如您在阅读中发现任何疏漏,恳切地希望您提出宝贵的意见和建议,感谢。

重要:这是一份针对API或其它相关技术开发而准备的预备文档。尽管文档在专业精确程度上已经过多次审查,它仍不是最终版本。文档仅供已注册苹果开发者计划的开发者使用。苹果提供这份文档的目的,是帮助开发者根据文档来规划自身应用的开发技术与界面设计。这些信息将可能发生变化,您的应用也应当根据最新的操作系统与最终文档进行相应的调整。该文档可能会由于API与相关技术的发展而更新版本。

 

控件(Controls)

活动指示器(Activity Indicator)

活动指示器表明任务或进程正在进行中,如下图所示。
QQ截图20131016154609

API提示:想要了解如何在代码中定义活动指示器,可以参考

UIActivityIndicatorView Class Reference.

活动指示器:

  • 当任务进行和加载时旋转,任务完成后自动消失
  • 不支持用户交互行为

在工具栏或主视图中使用活动指示器来告知用户任务或加载正在进行中,但并不提示该过程何时会结束。

不要使用静止的活动指示器。用户会以为该进程停滞了。

用活动指示器来让用户知道进程仍在进行中。有些时候,告诉用户进程没有停止比告诉他们何时完成更加重要。

可以的话,最好可以设计一个与应用的风格协调的活动指示器。

添加联系人按钮(Contact Add Button)

添加联系人按钮让用户将现有联系人添加到文本框或者其它文字视图中。

2

API提示:想要了解如何在代码中国定义添加联系人按钮,参考Buttons.

添加联系人按钮:

  • 展示联系人列表
  • 帮助用户将一个联系人添加到当前联系人按钮所在的视图中

使用添加联系人按钮让用户在不需要使用键盘的情况下就可以方便地访问到联系人。举个例子,在新建邮件的界面中,用户可以点击该按钮来添加收件人,而不需要用键盘输入收件人的名字。

由于添加联系人按钮属于键盘输入联系人方法的替代品,我们不推荐在不支持键盘输入的界面中使用添加联系人按钮。

日期时间选择器(Date Picker)

日期时间选择器展示关于日期和时间的组件,比如小时,分钟,天,以及年。

3

API提示:想要了解如何在代码中定义日期选择器,请参考 Date Pickers.

日期时间选择器:

  • 最多可以展示4个独立的滑轮,每一个滑轮表示一个不同的值,比如月份或小时等
  • 在每个滑轮的中央使用深色字体来表示当前选中的值
  • 日期时间选择器的大小与iPhone键盘的大小相同,并且不可更改
  • 包括四种模式,每一种模式代表了一组不同的值:

· 日期和时间。日期和时间模式(默认模式)包含日期、小时、和分钟,以及一个可选的AM/PM值。

· 时间。时间模式包括小时和分钟,以及可选的AM/PM值。

· 日期。日期模式包括月份,天以及年三个值。

· 倒计时器。倒计时器模式展示了小时和分钟值。你可以精确地设定总共的倒计时间,倒计时的最大值为23小时59分钟。

使用日期时间选择器来让用户选择时间,而不是让用户自己输入一个包含了日期、时间等多个部分的时间值。

尽量地让用户在当前内容中使用日期选择器。最好避免用户在使用日期选择器的时候要进入另外一个界面。在iPad上,日期时间选择器可能会出现在一个浮层中,或者嵌入在当前内容里。

有必要的时候,改变分钟滑轮的单位刻度。在默认情况下,分钟滑轮包含从0到59共60个值,如果你要展示一个颗粒度较大的时间,你可以让分钟滑轮的单位刻度变大,只要这个刻度可以整除60。比如说你可能会设定每15分钟为一个刻度,此时分钟滑轮就有4个值,0、15、30、45。

详情展开按钮(Detail Disclosure Button)

详情展开按钮展示了与该项相关的更多详细信息与功能描述。

5

API提示:想要了解如何在代码中定义详情展开按钮,可以参考 

UITableViewCell Class Reference

 和 Buttons.

详情展开按钮以一个单独的视图展示特定项目的更多详情信息与功能。

当详情展开按钮在表格行中出现时,点击表格行的其它区域不会激活此按钮,只会选中该行,或者触发app中其它自定义的行为。

一般来说,你会在一个表格视图中使用详情展开按钮来让用户知道更多关于这个列表项的信息。当然你也可以将这个按钮用在其它类型的视图中来为用户展示更多与特定项目相关的信息和功能。

信息按钮(Info Button)

信息按钮展示了app的配置信息,有时候它会出现在当前视图的背面。

5

API提示:想要了解如何在代码中定义信息按钮,可以参考Buttons.

iOS包含了两种信息按钮样式:适用于浅色内容上的深色按钮,以及适用于深色内容上的浅色按钮。

使用信息按钮来显示app的配置信息或选项。你可以根据自己app的UI风格来选择最为协调的信息按钮样式。

标签(Label)

标签用于放置静态文本。

6

API提示:想要了解如何在代码中定义标签,可以参考

UILabel Class Reference

.

标签可以:

  • 展示任意数量的静态文本
  • 禁止除了复制文本外的任何用户交互行为

你可以使用标签来命名或解释你的部分UI,又或者用它来给用户提供一些简单的信息。标签最适合拿来展示相对简单的文本信息。

保证你的标签清晰易读。最好支持动态文本(Dynamic Type),并使用 UIFont 中的preferredFontForTextStyle来获得标签中的展示文本。如果你要用自定义字体的话,请慎重选择字体种类,不要以牺牲清晰度为代价来换取花哨的颜色和字体效果。(想要了解关于app中字体使用的指南,可以参考 Color and Typography;想要了解更多动态文本的内容,可以参考 

Text Programming Guide for iOS 

里面 的 Text Styles 部分。)

网络活动指示器(Network Activity Indicator)

网络活动指示器在状态栏中出现,表示网络活动正在进行。

7

API提示:你可以在代码中使用 UIApplication Method networkActivityIndicatorVisible 来控制该活动指示器的可见性。

网络活动指示器:

  • 出现在状态栏中,当网络活动正在进行时它会旋转,在活动停止时它则消失
  • 不支持用户交互行为

当你的app正在链接网络,而这个连接过程将会持续好几秒的时候,你可以通过网络活动指示器来给用户以反馈。如果进程所需时间很短,则不需要用到它,因为很可能在用户注意到它之前,它就消失了。

页面控件(Page Control)

页面控件告诉用户当前共打开了多少个视图,还有他们正处在其中哪一个。

8

API提示:想要了解如何在代码中定义页面控件,可以参考 Page Controls.

页面控件:

  • 包含一系列圆点,圆点的个数代表了当前打开的视图数量(从左到右,这些圆点代表了视图打开的先后顺序)。
  • 默认情况下,使用不透明点来标识当前打开的视图,使用半透明点来表示所有其它视图。
  • 不支持用户访问不连续的视图
  • 当视图数量超过页面宽度可承载的氛围时,点的大小和间距并不会因此变小,如果需要显示的点超过一定数量,系统会把它截断。

当你的app中所有的视图都属于同级的时候,你可以使页面控件。

当你的app结构存在信息层级,请不要使用页面控件。因为页面控件不能让用户跟踪自己的访问路径,回到上一级。

将页面控件垂直居中放置于当前打开视图的底边与屏幕底边之间,这样可以保证它的可见性而又不会对内容造成干扰。避免展示太多的点,一般来说,iPhone竖屏方向最多可以容纳差不多20个点。

选择器(Picker)

选择器展示了一组值,用户可以从中选择一个。

9

API提示:想要了解如何在代码中定义选择器,请参考

UIPickerView Class Reference

.

选择器:

  • 是日期时间选择器的通用模式
  • 包括一个或多个滑轮,每个滑轮含有一组值
  • 当前选中的值在中间,以深色标识
  • 不可以自定义大小(选择器的大小与iPhone的键盘相同)

使用选择器可以让用户更容易从一系列不同的值中间进行选择。

一般来说,当用户对整组值都比较熟悉的时候,可以使用选择器。由于当滑轮静止的时候,大部分的数值会被隐藏,最好是在用户对所有数值均有预期的情况下才使用选择器。当你需要展示一大组用户并不熟悉的选项,此种选择器可能不太适合。

尽可能让让用户在当前视图中使用选择器。不要让他们在使用选择器时还要进入其它的视图。

如果你需要展示的备选项数量很多,考虑使用表格视图(Table View)而不是选择器。因为表格视图的高度较大,内容滚动起来会更快。

进度视图(Progress View)

进度视图展示了任务或进程的进度(下图是iOS默认邮件App的工具栏)。

10

API提示:想要了解更多如何在代码中定义进度视图,参考

UIProgressView Class Reference

.

进度视图:

  • 是一条轨迹,随着进程的进行从左向右进行填充
  • 不支持用户交互行为

iOS定义了两种进度视图样式:

  • 默认(Default).默认样式适合用在app的主要内容区中。
  • 进度条(Bar).此样式比默认样式细,适合用在工具栏中。

当一个任务存在明确的进程,可以使用进度条来给与用户反馈,尤其是告诉用户这个任务大约需要多少时间才能完成。

可以的话,请根据你的app的风格来设计进度条的外观。你可以自定义进度条的底色以及轨迹颜色,也可以直接使用图片。

刷新控件(Refresh Control)

刷新控件执行用户触发的内容刷新——一个典型的例子,它常在表格中出现(下图展示的是iOS默认的邮件app的mailbox列表页)。

11

API提示:想要了解如何在代码中定义刷新控件,请参考 

UIRefreshControl Class Reference

.

刷新控件:

  • 看起来类似活动指示器
  • 可以出现在标题中
  • 默认状态下不可见,当用户在表格上缘往下拖拽以刷新内容时才出现

就算你使用了刷新控件,也不要因此就不支持内容自动刷新。尽管用户喜欢在执行刷新操作时内容立刻刷新,他们也同样会喜欢内容自动刷新。如果过于一来用户自己执行所有刷新操作的话,那些不会自动刷新的用户就会疑惑,为何你app中的数据永远都不更新。一般来说,刷新控件给了用户多一个选择,让他们可以立刻获得最新的内容,但同时,你也不能奢望用户会主动获取所有的更新信息。

只有在必要的时候才加短标题。特别需要注意的是,不要使用短标题来描述刷新控件怎么使用。

圆角矩形按钮(Rounded Rectangle Button)

iOS 7已经不再使用圆角矩形按钮,而是使用了新的系统按钮——类型为UIButtonTypeSystem的UI按钮(UIButton).使用指南可参考 System Button.

分段控件(Segmented Control)

分段控件是一组分段的线性集合,每一个分段的作用类似按钮,点击之后将切换到相应的视图。

12

API提示:想要了解如何在代码中定义分段控件,请参考 Segmented Controls.

分段控件:

  • 由两个或以上的分段组成,每一个分段的宽度相同,与分段的数量成比例(分段数量越多,则宽度越小);
  • 可以包含文字或者图片

使用分段控件来提供密切相关而又互斥的选项。

保证每个分段都容易点击。为了保证每个分段的大小有至少44×44像素,请控制分段的数量。在iPhone上,1个分段控件最多包含5个分段。

尽可能地保持每个分段中的文字长度一致。因为每个分段都是等宽的,当文本长度差异很大时看上去会很不协调。

不要在同一个分段控件中混用文字和图片。每一个分段都仅可支持纯文字或纯图片。避免在同一个分段控件中,一些分段里使用纯文字,另一些分段里使用纯图。

如果你自定义了分段控件的外观,请在必要时调整分段控件中文本的对齐方式。如果你给分段控件添加了自定义底图,请确保控件里自动居中的文本依然清晰美观。你可以通过bar metrics APIs 来调整分段控件内文本的对齐方式(想要了解如何定义bar metrics,可以参考 UISegmentedControl 中关于自定义API外观(appearance-customization APIs)的描述)。

滑块(Slider)

滑块允许用户在一个限定范围内调整某个数值或进程(下图展示的是iOS设置中亮度设置的滑块,滑块的左边和右边均为自定义图形)。

13

API提示:想要了解如何在代码中定义滑块,请参考 Sliders.

  • 由一条水平的轨迹和一个Thumb(滑块中支持用户水平拖拽的圆形控件)组成
  • 左边和右边支持使用自定义图片来表述相对的最小值与最大值的含义
  • 填充轨道左边缘最小值之间到Thumb之间的部分

使用滑块来让用户精准地选择自己想要的值,或者控制当前的进程。

如果合适的话,自定义滑块的外观。比如,你可以:

  • 定义Thumb的外观,让用户一看就知道滑块当前的状态
  • 在轨迹的左右两端使用自定义图片来告诉用户滑块的最小值和最大值所代表的含义。比如说,一个图调整图片尺寸的滑块可以在最小值的左边放一张小图,在最大值的右边放一张大图。
  • 根据Thumb所在的位置和当前滑块的状态来为滑块的轨迹定义不同的颜色

步进器(Stepper)

步进器可以以常数为幅度来增减当前数值。

14

API提示:想要了解如何在代码中定义步进器,请参考 Steppers.

步进器:

  • 是一个两段控件,其中一段默认显示减号,另一端默认显示加号
  • 支持自定义图片
  • 不展示用户更改的值

当用户想要对数值进行小幅度调整时,可以使用步进器

当用户需要大幅度调整数值的时候,不要使用步进器。用户可能会在打印机里使用步进器来确定打印份数,因为这个值的变化幅度通常并不大;而当用户需要选择打印的页码范围时,使用步进器就会让操作变得繁琐,因为用户很可能要点很多下才能选定页数。

确保步进器所调整的值明显可见。步进器自身不展示任何数值,所以你需要保证让用户知道他们正在调整哪一个数值。

开关按钮(Switch)

一个开关按钮展示了两个互斥的选项或状态。

15

API提示:想要了解如何在代码中定义开关,参考 Switches.

开关按钮:

  • 显示了一个项存在二元状态
  • 仅在表格视图中可用

在表格中使用开关按钮来让用户从某一项的两个互斥状态中指定一个,比如是/否(Yes/No),开/关(On/Off)。

你可以使用开关按钮来控制视图中的其它UI元素。根据用户的选择,新的列表项可能出现或者消失,或从激活状态变为不激活状态。

系统按钮(System Button)

系统按钮执行app中定义的行为。

16

API提示:在iOS 7中,UIButtonTypeRoundedRect已经被重新定义为 UIButtonTypeSystem. 如果在iOS 6中使用了圆角矩形按钮,在连接到iOS 7的时候会自动替换为新的系统按钮。想要了解如何在代码中定义系统按钮,参考 Buttons.

系统按钮:

  • 默认状态下不含边界,也不含背景图
  • 可以是图标或者文字标题
  • 支持自定义样式,如描边或者加背景图(想要自定义按钮外观,可以使用 UIButtonTypeCustom 类型的按钮,并且提供背景图片)

使用系统按钮来执行某个动作。当你为系统按钮命名时,请遵循以下方法:

  • 使用动词或动词短语来描述按钮所代表的动作。这种命名方法告诉用户这个按钮是可交互的,也提示了用户点击之后会执行什么操作
  • 使用标题式大写(title-style capitalization,每个单词的首字母均大写)。除了冠词,并列连词以及少于4个字母的介词外,标题中每个单词的首字母均大写。
  • 标题不要太长。太长的标题会被截断,让用户难以理解其含义。

17

合适的话,为内容区域内的系统按钮描边或者加入背景。大多数情况下,你可以通过定义一个清晰的按钮名称、选择一个不一样的标题颜色或提供上下文情景提示来让用户知道这是一个按钮而非普通文本。但在某些特定的内容区域内,为按钮描边或者添加背景颜色,让用户迅速地把注意力放到按钮上,也是必要的。

以iPhone为例,给数字按键添加圆形边框强化了用户拨电话号码时的心理模型,而结束(End)和隐藏(Hide)按钮的背景色让用户拥有了更大的点击范围。

文本框(Text Field)

文本框支持用户输入单行的文本。

18

API提示:想要了解如何在代码中定义文本框,以及在文本框中支持图片和按钮,请参考 Text Fields.

文本框:

  • 高度固定,包含圆角
  • 当用户点击它时,自动唤起输入键盘
  • 可以包含系统提供的按钮,如书签按钮(Bookmarks)
  • 可以展示多种文字样式(了解更多请参考 UITextView)

使用文本框来获取用户输入的少量信息。

你可以自定义一个文本框,帮助用户更好地理解如何使用它。举个例子,你可以在文本框的左侧或者右侧加入自定义图形,或者加入系统按钮,如书签按钮等。一般来说,文本框的左侧用于表述文本框的含义,而右侧用于展示附加的功能,如书签。

合适的话,在文本框右侧加入清除按钮。轻击清除按钮变可清空当前框内输入的全部内容,无论你原本打算在这个按钮上面展示什么其它图片。

如果可以帮助用户理解的话,可以在文本框中加入提示文字。当文本框里没有任何其它提示文字时,会展示占位符文本(placeholder text),如名字、地址等。

根据输入内容的类型来指定不同的键盘类型。举例来说,你希望用户能更方便地输入网址、密码或者电话号码。iOS提供了各种不同的键盘类型,以便用户输入不同类型的文本。想要了解可用键盘类型,可以参考 

UITextInputTraits Protocol Reference

中的UIKeyboardType.想要了解如何在管理你的应用中的键盘,请参考iOS App Programming Guide中的Managing the Keyboard部分。但请注意,由于键盘的布局以及输入方法是由用户的系统语言设置决定的,这是你不能控制的。

 

临时视图(Temporary Views)

警告框(Alert)

警告框用于告知用户一些会影响到他们使用app或设备的重要信息。

19

API提示:想要了解如何在代码中定义警告,参考

UIAlertView Class Reference

.

警告框:

  • 必须包含标题,有时候会包含正文文本
  • 包含一个或多个按钮

一般来说,警告框警告出现的频率较低,也正因为如此,警告的出现通常会让用户额外重视。请严格控制你的app中警告的个数,并且保证每一个警告都能提供重要的信息,或者有用的选项。

避免出现不必要的警告框。一般来说,在以下情景中,是不需要用到警告框的:

20

当你在设计警告文案的时候,了解以下这些定义非常有用:

  • 标题式大写(Title-style capitalization)指的是除了冠词,并列连词以及少于4个字母且不处在第一个单词位置上的介词外,标题中每个单词的首字母均大写。
  • 句子式大写(Sentence-style capitalization)指的是第一个字母大写,其余除了专有名词和专有形容词外的字母均小写

简明扼要地描述当前情景,并告诉用户他们可以做什么。理想情况下,警告框中的文字应该给与用户足够的情景和上下文联想,让他们可以清楚地知道为什么警告会出现,同时帮助他们判断自己应该点哪个按钮。

保证标题足够简短,最好在一行之内。过长的标题让用户很难快速理解它的意思,还可能会被截断。

21

如果可以的话,使用句子片段而非完整的句子。一个简洁清晰的状态描述往往比一个完整的句子更容易理解。

尽可能的精炼你的标题文字,让警告框即使没有下面的正文信息也能完全让用户理解。举个例子,当你使用一个问题,或者两个短句来作为警告框标题的话,很可能你并不需要添加文本信息。

不用刻意避免在警告框中使用消极负面的文案。用户们理解大多数警告框是为了告诉他们发生的问题,或者对他们目前的状态作出警告。因此消极但清晰直接的文案优于积极但晦涩间接的文案。

尽可能地避免使用”你”,”你的”,”我”,”我的”这类字眼。有时候,这些直接指向的字眼容易引起歧义,有时候甚至会被误认为是一种冒犯。

适当地使用大写和标点符号,尤其是在以下这些场景中:

22

如果你必须为警告框添加正文文本,请使用一个完整的短句。可能的话,尽量保证句子在1到2行之间。如果句子太长,用户会需要滚动才能看完,这样的体验很糟。使用句子式大写,并在句末加上适当的标点符号。

23

 避免在文本中详细描述“该按哪个按钮”而导致文本过长。理想情况下,表意明确的警告文案和逻辑清晰的按钮文案已经足以让用户正确判断自己该按哪个按钮了。但如果你一定要在文案中描述这些内容,请遵循以下原则:

  • 确定使用轻击(tap)来描述这个选择操作,不要用触摸(touch)、点击(click)或者选择(choose)这类字眼。
  • 不要用引号,但保证大写

确保警告框在竖屏和横屏中均显示正常。横屏模式下警告框的高度会受到限制,其大小与竖屏下可能会有区别。我们推荐您限定好警告框的最大高度,保证在竖屏和横屏模式下文字均能不需要滚动便可完整地显示。

一般情况下,使用两个按钮的警告框。两个按钮的警告框是最为常见和有用的,因为它最便于用户在两个按钮中做选择。单按钮警告框不那么有用,因为它通常只是起到告知的作用,并未给予用户控制当前状态的能力。多于两个按钮的警告框太过复杂,应该尽可能地避免使用。如果你在警告框中设计了太多按钮,它也许会导致警告框被强制滚动,这也是一个非常糟糕的体验。

提示:如果你需要在警告框中给与用户超过2个选项,可以考虑使用操作列表来代替警告框。

25

正确地放置按钮。理想情况下,最容易点击也最不容易点错的按钮符合两个条件:它代表了用户最可能会选择的操作,即使用户一时不注意误点了它,也不会造成严重问题。尤其是:

  • 如果这个按钮不会造成损害性结果,又是用户最有可能会选择的操作,那么它应该放在右边,取消按钮则应该放在左边
  • 如果这个按钮会造成损害性后果,又是用户最有可能会选择的操作,那么它应该被放在左边,取消按钮应该放在右边

提示:一般来说,当警告框出现的时候,按Home键将会从该app里切回主屏幕,此时Home键的效果类似于取消按钮——当用户回到app中的时候,警告框将消失,操作也不会被执行。

为按钮设计简短而逻辑清晰的文案。好的按钮文案一般只有1到2个单词,描述用户点击按钮后的结果。设计文案时可以遵循以下指南:

  • 跟其它所有按钮一样,使用标题式大写,而且不需要标点符号
  • 尽可能的使用与警告文案直接相关的动词或动词词组,如”取消(Cancel)”,”允许(Allow)”和”回复(Reply)”等。
  • 当没有更好的选择的时候,可以使用”OK”.避免使用”是(Yes)”或”否(No)”。
  • 避免使用”你”,“你的”,“我”,“我的”这类字眼。含有这些字眼的文案可能会指代不清,还有可能造成冒犯。

操作列表(Action Sheet)

操作列表展示了与用户触发的操作直接相关的一系列选项。

26

API提示:想要了解如何在代码中定义操作列表,可以参考Action Sheets.

操作列表:

  • 由用户某个操作行为触发
  • 包含两个或以上的按钮

使用操作列表来:

提供完成一项任务的不同方法。操作列表提供一系列在当前情景下可以完成当前任务的操作,而这样的形式不会永久占用页面UI的空间。

在用户完成一项可能有风险的操作前获得用户的确认。操作列表让用户有机会停下来充分考虑当前操作可能导致的危险结果,并为他们提供了一些其它的选项,尤其是在以下这些情景下:

27

无论在哪种设备中,使用红色文字来表示可能存在破坏性的操作。在操作列表的顶部使用文字颜色为红色的按钮,因为越靠近列表顶部的操作越容易引起用户注意。在iPhone里,潜在风险的操作离列表底部越远,用户在关注Home键的时候就越不容易误点它。

28

避免让用户滚动操作列表。如果你的操作列表中存在过多按钮,用户必须要滚动才能看完所有操作。这样的体验是可能让用户不安,因为他们要花更多的时间来充分理解每个选项的区别。此外,用户在滚动的过程中将很有可能误点其它按钮。

模态视图(Modal View)

模态视图是一个以模态形式展现的视图,它为当前任务或当前工作流程提供独立的、自包含的(self-contained)功能。

29

API提示:想要了解如何在场景中定义模态视图,请参考

UIViewController Class Reference

.

模态视图:

  • 占据整个屏幕,在iPad中,它也可能占据整个父视图(parent view)的区域
  • 包含完成当前任务所需的文字和控件
  • 通常也会包含一个完成任务的按钮(点击后即可完成任务,当前模态视图也会消失),和一个取消按钮(点击后即放弃当前任务,同时当前模态视图消失)

当需要用户完成与你的app中的基础功能相关的、独立的任务的时候,可以使用模态视图。模态视图尤其适用于那些所需元素并非常驻在app主要UI中、又包含多个步骤的子任务。

在iPad上,根据当前任务的种类和你的app的整体视觉风格来选择适当的模态视图。你可以使用以下定义的任何一种模态视图样式:

30

在iPad上,不要让模态视图覆盖在弹出层之上。除了警告框外,没有任何元素应该覆盖在弹出层上面。除非极其少有的情况下,用户在弹出层内进行的操作结果必须要以模态视图的形式展现,即便是这个时候,也请先将弹出层关闭,再出现模态视图。

在iPhone上,确保你的模态视图看起来与你的app的整体视觉风格相协调。举个例子,如果一个模态视图中含有导航条和取消或完成任务的按钮,这里的导航条样式应该与你的app中导航条一样。

无论是哪种设备,合适的话,在模态视图里加入可以说明任务内容的标题。你可能还需要在模态视图里加入一些补充文字,来清楚地阐明任务内容,并提供一些任务指南。

无论是哪种设备,选择一个适当的过渡动画来展示模态视图。使用与你的app一致的过渡动画,让用户可以准确地理解当前页面内容的转变与模态视图的出现。关于这一点,你可以指定以下任意一种过渡动画:

  • 垂直出现(Vertical).模态视图从底部边缘滑入屏幕,也同样从屏幕底部滑出(默认模式)。
  • 弹出(Flip).当前视图从右往左水平滑动,露出模态视图。从视觉上看,模态视图好像原来就处于当前视图的下面,当前视图移开时,它便出现了。离开模态视图时,原先的父视图从左边滑回屏幕右边。

如果你要改变当前的过渡动画样式,请确保这种改变对于用户而言是有用而且有意义的。用户很容易便能感知到这些改变,还会认为这些改变存在特别的意义。最好能设计出一种符合逻辑并始终保持一致的过渡方式,让用户容易感知并且记忆。在没有充分理由支持的情况下,最好不要改变这些默认的过渡方式。

翻译原文下载:iOS  Human Interface Guidelines

怎么样制作一款像RunKeeper的应用:第二部分


原文地址:http://www.raywenderlich.com/74331/make-app-like-runkeeper-part-2
泰然翻译组:Stroustrup_Lee。校对:glroy。

这是教你制作一款像RunKeeper的应用的教程的第二部分,也是最后一部分的内容。这部分的内容主要完成地图着色和徽章的功能。

在教程的第一部分中,你完成了下面所列举的功能:

  • 使用Core Location记录你的路线
  • 不断的在地图上标记出你的路线,并实时的报告你的平均速度
  • 在跑步结束的时候显示出你的路线图;对地图路线进行着色,使慢的部分显示为红色,快的部分显示为绿色。
    • 这是一款非常优秀的记录数据和显示数据的应用,但是为了激发动力,我们的需求并不是仅仅的一个漂亮的地图就能满足的。

      因此,在这部分内容中,你将会完成一款有徽章系统的叫做MoonRunner 的应用,此应用更加的体现出健身是一项以娱乐和进步为基础的成就。下面就是其实现原理:

    • 一份标记着增加的距离检测点的列表来鼓励用户。
    • 在用户跑动的过程中,他们可以看到下一个徽章的缩略图和剩余距离。
    • 用户第一次达到一个距离检查点的时候,应用将奖励用户一个徽章并显示跑步的平均速度。
    • 在这之后,当用户以一个更快的速度达到这个检测点的时候,就会奖励银色或金色的徽章。
    • 地图上会以圆点的方式显示出路线上的每一个检测点,并允许用户用自定义的插图显示徽章的名称和图片。
    • 开始

      你可以在你徽章使用的主题方面随意的发挥你的创造性。但是我刚刚看完新版的电视剧:Cosmos ,因此我想把所有的徽章设计成星球的样子,如月亮或太阳系中的其他星球。那将是多么的浩瀚!

      下载MoonRunner的资源包并添加到你的项目中。注意,这个资源包包含很多的图片和一个名为badges.txt的文件。打开文件badges.txt,你会发现里面是一个很大的徽章对象的JSON数组。每一个徽章对象都有下面的属性:

    • 徽章的名称
    • 一段关于徽章的有趣的介绍
    • 取得该徽章的距离
    • 资源包中对应图片的文件名
    • 徽章都是从零开始计算--毕竟,我们总要有一个开头--到单程马拉松的距离。当然,有些人比超级马拉松走的还要远,那么你就可以认为这些有雄心的跑步的人已经进入了星际空间。

      因此,我们首先要把这个JSON文本解析成一个对象数组。选择File\New\FileiOS\Cocoa Touch\Objective-C class来创建一个继承自NSObjectBadge类。

      按照下面的内容编辑Badge.h:

      #import 
      
      
      
      @interface Badge : NSObject
      
      
      
      @property (strong, nonatomic) NSString *name;
      
      @property (strong, nonatomic) NSString *imageName;
      
      @property (strong, nonatomic) NSString *information;
      
      @property float distance;
      
      
      
      @end
      

      现在你拥有了Badge对象,是时候解析源JSON文件了。创建一个新的继承自NSObject 的类BadgeController,并按下面的方式编辑头文件:

      #import 
      
      
      
      @interface BadgeController : NSObject
      
      
      
      + (BadgeController *)defaultController;
      
      
      
      @end
      

      这个类被设计成单例模式,由defaultController创建和访问。打开文件BadgeController.m,并用下面的代码替换文件中的内容:

      #import "BadgeController.h"
      
      #import "Badge.h"
      
      
      
      @interface BadgeController ()
      
      
      
      @property (strong, nonatomic) NSArray *badges;
      
      
      
      @end
      
      
      
      @implementation BadgeController
      
      
      
      + (BadgeController *)defaultController
      
      {   
      
      static BadgeController *controller = nil;
      
      static dispatch_once_t onceToken;
      
      dispatch_once(&onceToken, ^{
      
      controller = [[BadgeController alloc] init];
      
      controller.badges = [self badgeArray];
      
      });
      
      
      
      return controller;
      
      }
      
      
      
      + (NSArray *)badgeArray
      
      {
      
      NSString *filePath = [[NSBundle mainBundle] pathForResource:@"badges" ofType:@"txt"];
      
      NSString *jsonContent = [NSString stringWithContentsOfFile:filePath usedEncoding:nil error:nil];
      
      NSData *data = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
      
      NSArray *badgeDicts = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
      
      
      
      NSMutableArray *badgeObjects = [NSMutableArray array];
      
      
      
      for (NSDictionary *badgeDict in badgeDicts) {
      
      [badgeObjects addObject:[self badgeForDictionary:badgeDict]];
      
      }
      
      
      
      return badgeObjects;
      
      }
      
      
      
      + (Badge *)badgeForDictionary:(NSDictionary *)dictionary
      
      {
      
      Badge *badge = [Badge new];
      
      badge.name = [dictionary objectForKey:@"name"];
      
      badge.information = [dictionary objectForKey:@"information"];
      
      badge.imageName = [dictionary objectForKey:@"imageName"];
      
      badge.distance = [[dictionary objectForKey:@"distance"] floatValue];
      
      return badge;
      
      }
      
      
      
      @end
      

      这里共有三个协同工作的方法:

      1.defaultController 是一个public成员,生成控制器的实例,并确保解析操作只发生一次。

      2.badgeArray 提取文本文件中的数组,并为数组中的每一个元素创建一个对象。

      3.badgeForDictionary实现JSON关键字和Badge的实际映射。

      到目前为止,我们所做的全部是为了获取徽章数据。下面我们就把这些数据添加到我们的应用中去。

      徽章Storyboard

      如果不能把这些徽章保存在一个漂亮的玻璃箱内,可以在以后的生活中感慨这些成就的话,那这些徽章还有什么用处呢?或者是你非常谦虚的说只是想把他们记录下来。:)

      无论是哪种情况,现在都应该往你的Storyboard中添加可以查看徽章的界面了。打开 Main.storyboard,通过拖动往Storyboard中添加新的表试图控制器,然后按住Ctrl键和鼠标左键把主屏上的Badges按钮拖到一个新的视图控制器中来创建一个push类型的segue:

      然后,选择你刚刚添加的表视图控制器中的表视图并打开size inspector.把表的行高增加到80.然后选中表视图中的原型单元格(prototype cell)并打开属性查看器(attributes inspector).把类型设置成自定义,标示符设置成BadgeCell

      在单元格内,把黑色设置成MoonRunner的默认背景色,并在左侧添加一个大的UIImageView 。在右侧添加两个使用金色飞船和银色飞船的资源小一点的UIImageViews,然后添加两个UILabels。可以参考下面的截图来设计你的单元格:

      一个徽章占用一个单元格,并在左侧显示徽章的图片,在右侧显示你获得这枚徽章的时间或是为了获得这枚徽章你还需要做些什么。两个金色飞船和银色飞船的小UIImageViews 是在用户达到这个等级的时候给予的奖励。

      接下来,通过拖动往你的storyboard种添加一个新的视图控制器(这次不是表视图控制器)。然后在你刚刚添加的视图控制器中按住Ctrl键并拖动表视图单元格,选择push类型的segue。那么你的新屏幕看起来就和下面的类型类似了:

      仔细观察屏幕,你会发现:

      一个大的UIImageView 来显示徽章的图片--如果是太空中的照片就最好不过了

      在UIImageView的上面有一个小的UIButton ,使用info的图片作为背景图

    • 一个描述徽章名字的UILabel
    • 一个描述徽章代表的距离的UILabel
    • 一个记录获得徽章时间的UILabel
    • 一个记录这个距离的最快的平均速度
    • 一个记录用户获得这个徽章的银色徽章的日期(或是如果他们想获得银色徽章所需要的平均速度)
    • 一个记录用户获得这个徽章的金色徽章的日期(或是如果他们想获得金色徽章所需要的平均速度)
    • 两个分别用银色徽章和金色徽章的资源表示的UIImageViews
    • 徽章的信息是非常重要的,因为它以故事或者是图表的形式记录了用户取得这个成就的背景。无论多小的徽章图片都是一种额外的经历。

      赢取徽章

      你已经创建了徽章对象,现在你需要一个存放徽章获取时间的对象。这个对象通过各种Run对象和Badge对象联系到一起,如论用户获得哪种徽章。

      点击File\New\File。选择 iOS\Cocoa Touch\Objective-C类。调用继承自NSObject的BadgeEarnStatus类并保存文件。然后打开头文件BadgeEarnStatus.h,并用下面的内容替换其中的内容:

      #import 
      
      
      
      @class Badge;
      
      @class Run;
      
      
      
      @interface BadgeEarnStatus : NSObject
      
      
      
      @property (strong, nonatomic) Badge *badge;
      
      @property (strong, nonatomic) Run *earnRun;
      
      @property (strong, nonatomic) Run *silverRun;
      
      @property (strong, nonatomic) Run *goldRun;
      
      @property (strong, nonatomic) Run *bestRun;
      
      
      
      @end
      

      然后打开BadgeEarnStatus.m并在该文件的顶部添加下面的导入内容:

      #import "Badge.h"
      
      #import "Run.h"
      

      既然你已经可以很简单的把Badge对象和Run对象联系到一起,下面就让我们来完成它们的连接逻辑。打开 BadgeController.h并在文件的顶部添加下面的常量:

      extern float const silverMultiplier;
      
      extern float const goldMultiplier;
      

      然后为接口添加下面的实例化函数:

      - (NSArray *)earnStatusesForRuns:(NSArray *)runArray;
      

      在其他的类中也可以访问常量 silverMultiplier和goldMultiplier 。它们是用户获取这些版本的徽章必须达到的速度值。

      打开BadgeController.m 并在文件的顶部添加下面的导入内容和常量定义:

      #import "BadgeEarnStatus.h"
      
      #import "Run.h"
      
      
      
      float const silverMultiplier = 1.05; // 5% speed increase
      
      float const goldMultiplier = 1.10; // 10% speed increase
      

      然后在实现部分添加下面的函数:

      - (NSArray *)earnStatusesForRuns:(NSArray *)runs {
      
      NSMutableArray *earnStatuses = [NSMutableArray array];
      
      
      
      for (Badge *badge in self.badges) {
      
      
      
      BadgeEarnStatus *earnStatus = [BadgeEarnStatus new];
      
      earnStatus.badge = badge;
      
      
      
      for (Run *run in runs) {
      
      
      
      if (run.distance.floatValue > badge.distance) {
      
      
      
      // 第一次获得该徽章
      
      if (!earnStatus.earnRun) {
      
      earnStatus.earnRun = run;
      
      }
      
      
      
      double earnRunSpeed = earnStatus.earnRun.distance.doubleValue / earnStatus.earnRun.duration.doubleValue;
      
      double runSpeed = run.distance.doubleValue / run.duration.doubleValue;
      
      
      
      // 是否可以获得银色徽章
      
      if (!earnStatus.silverRun
      
      && runSpeed > earnRunSpeed * silverMultiplier) {
      
      
      
      earnStatus.silverRun = run;
      
      }
      
      
      
      //是否可以获得金色徽章
      
      if (!earnStatus.goldRun
      
      && runSpeed > earnRunSpeed * goldMultiplier) {
      
      
      
      earnStatus.goldRun = run;
      
      }
      
      
      
      // 是否是该段距离的最好成绩
      
      if (!earnStatus.bestRun) {
      
      earnStatus.bestRun = run;
      
      
      
      } else {
      
      double bestRunSpeed = earnStatus.bestRun.distance.doubleValue / earnStatus.bestRun.duration.doubleValue;
      
      
      
      if (runSpeed > bestRunSpeed) {
      
      earnStatus.bestRun = run;
      
      }
      
      }
      
      }
      
      }
      
      
      
      [earnStatuses addObject:earnStatus];
      
      }
      
      
      
      return earnStatuses;
      
      }
      

      该函数把用户的所有跑步记录同每种徽章的距离要求作比较,建立它们之间的关系,并以数组的形式返回所有的BadgeEarnStatus 对象。

      用户第一次获得徽章的时候,会有一个earnRunSpeed的变量作为判断该用户是否有进步可以获得金色徽章或银色徽章的参考。

      按这样的方式处理的话,即使你的朋友每次都跑的特别的快,你也是有机会去赢取火星的金色徽章。只要你们两个都在提高,那么你就是赢家。

      注意:你会发现这个函数使用的是一段距离的整体的平均速度。如果想增加挑战的话,可以试着使用整个跑步过程中的一小段距离不断的计算速度。例如,你可能刚开始和快结束的时候跑的比较慢,但是中间两英里跑的特别的快。

      显示徽章

      现在,我们应该把徽章的计算逻辑和界面整合到一起呈现在用户的面前了。创建两个视图控制器和一个自定义的表单元使徽章的数据可以显示在storyboards上面。

      首先,创建一个继承自UITableViewCell的新类BadgeCell。打开BadgeCell.h并添加下面的内容:

      #import 
      
      
      
      @interface BadgeCell : UITableViewCell
      
      
      
      @property (nonatomic, weak) IBOutlet UILabel *nameLabel;
      
      @property (nonatomic, weak) IBOutlet UILabel *descLabel;
      
      @property (nonatomic, weak) IBOutlet UIImageView *badgeImageView;
      
      @property (nonatomic, weak) IBOutlet UIImageView *silverImageView;
      
      @property (nonatomic, weak) IBOutlet UIImageView *goldImageView;
      
      
      
      @end
      

      现在你前面部分添加的徽章表试图控制器中拥有了一个可以使用的自定义的单元格。

      然后,创建一个继承自UITableViewController的类BadgesTableViewController。打开 BadgesTableViewController.h并添加下面的内容:

      #import 
      
      
      
      @interface BadgesTableViewController : UITableViewController
      
      
      
      @property (strong, nonatomic) NSArray *earnStatusArray;
      
      
      
      @end
      

      earnStatusArray中存放你在前面部分添加的计算徽章状态的函数earnStatusesForRuns的返回值。

      打开BadgesTableViewController.m并在该文件的顶部添加下面的导入内容:

      #import "BadgeEarnStatus.h"
      
      #import "BadgeCell.h"
      
      #import "Badge.h"
      
      #import "MathController.h"
      
      #import "Run.h
      

      然后在该类的扩展类别中添加下面的属性:

      @interface BadgesTableViewController ()
      
      
      
      @property (strong, nonatomic) UIColor *redColor;
      
      @property (strong, nonatomic) UIColor *greenColor;
      
      @property (strong, nonatomic) NSDateFormatter *dateFormatter;
      
      @property (assign, nonatomic) CGAffineTransform transform;
      
      
      
      @end
      

      这些是在整个表视图控制器中使用的一部分属性。例如:其中颜色属性可以用来标示该徽章是否已经获得。

      在类的实现部分找到viewDidLoad ,并写成下面的样子:

      - (void)viewDidLoad
      
      {
      
      [super viewDidLoad];
      
      
      
      self.redColor = [UIColor colorWithRed:1.0f green:20/255.0 blue:44/255.0 alpha:1.0f];
      
      self.greenColor = [UIColor colorWithRed:0.0f green:146/255.0 blue:78/255.0 alpha:1.0f];
      
      self.dateFormatter = [[NSDateFormatter alloc] init];
      
      [self.dateFormatter setDateStyle:NSDateFormatterMediumStyle];
      
      self.transform = CGAffineTransformMakeRotation(M_PI/8);
      
      }
      

      该函数是用来设置你刚刚添加的属性的值的。这些属性实际上可以说是缓存,这样你就不用在每次创建一个新的单元格的时候重复的创建这些需要的属性。数据的格式化部分创建起来时非常的“昂贵的”(耗费时间和资源),因此最好的办法是创建缓存。

      然后,移除tableView:numberOfRowsInSectionnumberOfSectionsInTableView的实现部分,然后添加下面的函数:

      - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
      
      {
      
      return self.earnStatusArray.count;
      
      }
      
      
      
      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
      
      {
      
      BadgeCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BadgeCell" forIndexPath:indexPath];
      
      BadgeEarnStatus *earnStatus = [self.earnStatusArray objectAtIndex:indexPath.row];
      
      
      
      cell.silverImageView.hidden = !earnStatus.silverRun;
      
      cell.goldImageView.hidden = !earnStatus.goldRun;
      
      
      
      if (earnStatus.earnRun) {
      
      cell.nameLabel.textColor = self.greenColor;
      
      cell.nameLabel.text = earnStatus.badge.name;
      
      cell.descLabel.textColor = self.greenColor;
      
      cell.descLabel.text = [NSString stringWithFormat:@"Earned: %@", [self.dateFormatter stringFromDate:earnStatus.earnRun.timestamp]];
      
      cell.badgeImageView.image = [UIImage imageNamed:earnStatus.badge.imageName];
      
      cell.silverImageView.transform = self.transform;
      
      cell.goldImageView.transform = self.transform;
      
      cell.userInteractionEnabled = YES;
      
      } else {
      
      cell.nameLabel.textColor = self.redColor;
      
      cell.nameLabel.text = @"?????";
      
      cell.descLabel.textColor = self.redColor;
      
      cell.descLabel.text = [NSString stringWithFormat:@"Run %@ to Earn", [MathController stringifyDistance:earnStatus.badge.distance]];
      
      cell.badgeImageView.image = [UIImage imageNamed:@"question_badge.png"];
      
      cell.userInteractionEnabled = NO;
      
      }
      
      
      
      return cell;
      
      }
      

      这些函数通知表视图应该显示多少行(徽章的数量)和怎么样设置每行。正如你所看到的,每一行的显示内容都取决于用户是否获得了该徽章。另外,改行只能在已经获取该徽章之后,通过使用userInteractionEnabled来选定。

      现在你需要往徽章的表视图控制器中添加一些数据。打开HomeViewController.m然后在文件的顶部添加下面的导入内容:

      #import "BadgesTableViewController.h"
      
      #import "BadgeController.h"
      

      在类的扩展类别中添加下面的属性:

      @property (strong, nonatomic) NSArray *runArray;
      

      现在添加下面的函数:

      - (void)viewWillAppear:(BOOL)animated
      
      {
      
      [super viewWillAppear:animated];
      
      
      
      NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
      
      NSEntityDescription *entity = [NSEntityDescription
      
         entityForName:@"Run" inManagedObjectContext:self.managedObjectContext];
      
      [fetchRequest setEntity:entity];
      
      
      
      NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp" ascending:NO];
      
      [fetchRequest setSortDescriptors:@[sortDescriptor]];
      
      
      
      self.runArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
      
      }
      

      该函数会在每次显示视图控制器的时候刷新记录跑步数据的数组。该函数通过核心数据来获取按时间戳排序的所有的跑步数据来实现这个功能。

      最后,往prepareForSegue:sender添加下面的内容:

      - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
      
      {
      
      UIViewController *nextController = [segue destinationViewController];
      
      if ([nextController isKindOfClass:[NewRunViewController class]]) {
      
      ((NewRunViewController *) nextController).managedObjectContext = self.managedObjectContext;
      
      } else if ([nextController isKindOfClass:[BadgesTableViewController class]]) {
      
      ((BadgesTableViewController *) nextController).earnStatusArray = [[BadgeController defaultController] earnStatusesForRuns:self.runArray];
      
      }
      
      }
      

      这里,当徽章的表视图控制器被添加到导航栈中时,所有徽章是否获取的状态都会被计算出来,然后传递给徽章的表视图控制器。

      现在是时候把storyboard中所有的内容联系起来了。打开 Main.storyboard并完成下面的内容:

    • 设置BadgeCell和BadgesTableViewController中的所有类
    • 把BadgeCell: nameLabel, descLabel, badgeImageView, silverImageView, 和goldImageView中所有的内容联系起来
    • 点击生成并运行,然后查看你的新徽章!你应该会看到类似下面的内容:

      你现在可能只有‘Earth’徽章,但是这仅仅是一个开始!下面就让我们去获取更多的徽章吧!

      用户要做些什么才能获得一个金色的徽章呢?

      MoonRunner的最后一个视图控制器是用来徽章的详细信息的。创建一个继承自UIViewController的类BadgeDetailsViewController。打开BadgeDetailsViewController.h 并用下面的内容替换原有的内容:

      #import 
      
      
      
      @class BadgeEarnStatus;
      
      
      
      @interface BadgeDetailsViewController : UIViewController
      
      
      
      @property (strong, nonatomic) BadgeEarnStatus *earnStatus;
      
      
      
      @end
      

      然后打开BadgesTableViewController.m。在该文件的顶部添加下面的导入内容:

      #import "BadgeDetailsViewController.h"
      

      并添加下面的函数:

      - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
      
      {
      
      if ([[segue destinationViewController] isKindOfClass:[BadgeDetailsViewController class]]) {
      
      NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
      
      BadgeEarnStatus *earnStatus = [self.earnStatusArray objectAtIndex:indexPath.row];
      
      [(BadgeDetailsViewController *)[segue destinationViewController] setEarnStatus:earnStatus];
      
      }
      
      }
      

      该函数在单元格被点击的时候设置segue。它把关于视图控制器详细信息的BadgeEarnStatus 相关实例显示出来。

      打开BadgeDetailsViewController.m,并在文件的顶部添加下面的导入内容:

      #import "BadgeEarnStatus.h"
      
      #import "Badge.h"
      
      #import "MathController.h"
      
      #import "Run.h"
      
      #import "BadgeController.h"
      

      然后在类的扩展类别中添加下面的属性:

      @interface BadgeDetailsViewController ()
      
      
      
      @property (nonatomic, weak) IBOutlet UIImageView *badgeImageView;
      
      @property (nonatomic, weak) IBOutlet UILabel *nameLabel;
      
      @property (nonatomic, weak) IBOutlet UILabel *distanceLabel;
      
      @property (nonatomic, weak) IBOutlet UILabel *earnedLabel;
      
      @property (nonatomic, weak) IBOutlet UILabel *silverLabel;
      
      @property (nonatomic, weak) IBOutlet UILabel *goldLabel;
      
      @property (nonatomic, weak) IBOutlet UILabel *bestLabel;
      
      @property (nonatomic, weak) IBOutlet UIImageView *silverImageView;
      
      @property (nonatomic, weak) IBOutlet UIImageView *goldImageView;
      
      
      
      @end
      

      这些都是界面相关的IBOutlets。

      现在找到viewDidLoad,并用下面的内容改变其原有内容:

      - (void)viewDidLoad
      
      {
      
      [super viewDidLoad];
      
      
      
      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
      
      [formatter setDateStyle:NSDateFormatterMediumStyle];
      
      
      
      CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI/8);
      
      
      
      self.nameLabel.text = self.earnStatus.badge.name;
      
      self.distanceLabel.text = [MathController stringifyDistance:self.earnStatus.badge.distance];
      
      self.badgeImageView.image = [UIImage imageNamed:self.earnStatus.badge.imageName];
      
      self.earnedLabel.text = [NSString stringWithFormat:@"Reached on %@" , [formatter stringFromDate:self.earnStatus.earnRun.timestamp]];
      
      
      
      if (self.earnStatus.silverRun) {
      
      self.silverImageView.transform = transform;
      
      self.silverImageView.hidden = NO;
      
      self.silverLabel.text = [NSString stringWithFormat:@"Earned on %@" , [formatter stringFromDate:self.earnStatus.silverRun.timestamp]];
      
      
      
      } else {
      
      self.silverImageView.hidden = YES;
      
      self.silverLabel.text = [NSString stringWithFormat:@"Pace < %@ for silver!", [MathController stringifyAvgPaceFromDist:(self.earnStatus.earnRun.distance.floatValue * silverMultiplier) overTime:self.earnStatus.earnRun.duration.intValue]];
      
      }
      
      
      
      if (self.earnStatus.goldRun) {
      
      self.goldImageView.transform = transform;
      
      self.goldImageView.hidden = NO;
      
      self.goldLabel.text = [NSString stringWithFormat:@"Earned on %@" , [formatter stringFromDate:self.earnStatus.goldRun.timestamp]];
      
      
      
      } else {
      
      self.goldImageView.hidden = YES;
      
      self.goldLabel.text = [NSString stringWithFormat:@"Pace < %@ for gold!", [MathController stringifyAvgPaceFromDist:(self.earnStatus.earnRun.distance.floatValue * goldMultiplier) overTime:self.earnStatus.earnRun.duration.intValue]];
      
      }
      
      
      
      self.bestLabel.text = [NSString stringWithFormat:@"Best: %@, %@", [MathController stringifyAvgPaceFromDist:self.earnStatus.bestRun.distance.floatValue overTime:self.earnStatus.bestRun.duration.intValue], [formatter stringFromDate:self.earnStatus.bestRun.timestamp]];
      
      }
      

      这段代码设置了徽章使用的图片并把赢取这些徽章的数据添加到标签中。

      最有趣的部分是那些告诉用户为了获得梦寐以求的银色或金色徽章所需要达到的速度的提示信息。我发现只要为了达到这个速度所需要付出的努力明显是可能的时候,这些提示是非常的有激励作用的。

      最后,添加下面的函数:

      - (IBAction)infoButtonPressed:(UIButton *)sender
      
      {
      
      UIAlertView *alertView = [[UIAlertView alloc]
      
        initWithTitle:self.earnStatus.badge.name
      
        message:self.earnStatus.badge.information
      
        delegate:nil
      
        cancelButtonTitle:@"OK"
      
        otherButtonTitles:nil];
      
      [alertView show];
      
      }
      

      当图片上面的info按钮被点击的时候就会调用上面的函数。它会在一个弹出窗口上面显示这个徽章的信息。

      好极了!!!现在徽章界面这方面的代码全部搞定了。打开Main.storyboard 并把下面的内容联系起来:

    • 设置类BadgeDetailsViewController
    • 把BadgeDetailsViewController: badgeImageView, bestLabel, distanceLabel, earnedLabel, goldImageView, goldLabel, nameLabel, silverImageLabel, 和silverLabel中的内容联系起来
    • BadgeDetailsView的接收动作infoButtonPressed
    • 点击生成并运行,查看你的新徽章的详细信息:

      "胡萝卜"激励法

      随着应用的徽章部分的完成,你需要检查和更新现有应用的界面来包含徽章系统!

      打开Main.storyboard并找到New Run视图控制器。在视图中的停止按钮的周围添加一个UIImageView 和一个UILabel 。大概是下面这样:

      这些就像是棍子上的胡萝卜,在用户跑步的过程中让他们可以看到下一个徽章以及他们到下一个徽章的距离。

      在你关联你的界面之前,你需要在BadgeController 中添加两个函数来确定一段距离最合适的徽章,和接下来显示哪一个徽章。

      打开BadgeController.h,在接口中添加下面两个函数声明:

      - (Badge *)bestBadgeForDistance:(float)distance;
      
      - (Badge *)nextBadgeForDistance:(float)distance;
      

      并在导入内容和接口之间添加下面一行内容:

      @class Badge;
      

      现在打开BadgeController.m并把函数的实现部分修改成下面这样:

      - (Badge *)bestBadgeForDistance:(float)distance {
      
      Badge *bestBadge = self.badges.firstObject;
      
      for (Badge *badge in self.badges) {
      
      if (distance < badge.distance) {
      
      break;
      
      }
      
      bestBadge = badge;
      
      }
      
      return bestBadge;
      
      }
      
      
      
      - (Badge *)nextBadgeForDistance:(float)distance {
      
      Badge *nextBadge;
      
      for (Badge *badge in self.badges) {
      
      nextBadge = badge;
      
      if (distance < badge.distance) {
      
      break;
      
      }
      
      }
      
      return nextBadge;
      
      }
      

      这些函数相当的直接--他们个有一个参数,传递以米为单位的距离,然后返回下面的内容:

    • bestBadgeForDistance:上一个获取的徽章
    • nextBadgeForDistance:下一个将要获取的徽章
    • 现在打开NewRunViewController.m并在文件的顶部添加下面的内容:

      #import 
      
      #import "BadgeController.h"
      
      #import "Badge.h"
      

      和徽章相关的导入是必须的,导入的AudioToolbox 是为了在你每次获得新徽章的时候可以播放音效。

      在类的扩展类别中添加下面的三个属性:

      @property (nonatomic, strong) Badge *upcomingBadge;
      
      @property (nonatomic, weak) IBOutlet UILabel *nextBadgeLabel;
      
      @property (nonatomic, weak) IBOutlet UIImageView *nextBadgeImageView;
      

      然后找到viewWillAppear并在函数的底部添加下面的代码:

      self.nextBadgeLabel.hidden = YES;
      
      self.nextBadgeImageView.hidden = YES;
      

      和其他的视图一样,在刚开始启动的时候徽章的标签和图片是需要隐藏起来的。

      然后找到startPressed并在函数的底部添加下面的代码:

      self.nextBadgeImageView.hidden = NO;
      
      self.nextBadgeLabel.hidden = NO;
      

      这是确保徽章的标签和图片在跑步开始的时候显示出来。

      现在找到eachSecond 并在函数的底部添加下面的代码:

      self.nextBadgeLabel.text = [NSString stringWithFormat:@"%@ until %@!", [MathController stringifyDistance:(self.upcomingBadge.distance - self.distance)], self.upcomingBadge.name];
      
      [self checkNextBadge];
      

      这是确保随着跑步的进行nextBadgeLabel一直最新的。

      然后,添加下面新的函数:

      - (void)checkNextBadge
      
      {
      
      Badge *nextBadge = [[BadgeController defaultController] nextBadgeForDistance:self.distance];
      
      
      
      if (self.upcomingBadge
      
      && ![nextBadge.name isEqualToString:self.upcomingBadge.name]) {
      
      
      
      [self playSuccessSound];
      
      }
      
      
      
      self.upcomingBadge = nextBadge;
      
      self.nextBadgeImageView.image = [UIImage imageNamed:nextBadge.imageName];
      
      }
      

      该函数通过你前面添加的函数获取下一个徽章。它通过和存放在upcomingBadge属性中的下一个将要获得的徽章的名字作比较来检查该徽章是不是最新的。如果两个名字不相同,那么就播放一个成功的音效来通知用户他获取了一个新的徽章!

      你可能发现了你还没有实现playSuccessSound。添加下面的函数:

      - (void)playSuccessSound
      
      {
      
      NSString *path = [NSString stringWithFormat:@"%@%@", [[NSBundle mainBundle] resourcePath], @"/success.wav"];
      
      SystemSoundID soundID;
      
      NSURL *filePath = [NSURL fileURLWithPath:path isDirectory:NO];
      
      AudioServicesCreateSystemSoundID((CFURLRef)CFBridgingRetain(filePath), &soundID);
      
      AudioServicesPlaySystemSound(soundID);
      
      
      
      //also vibrate
      
      AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
      
      }
      

      该函数用来播放成功获取徽章的音效,同时还可以使用系统的震动ID使手机震动。它使手机震动主要是为了当用户在比较嘈杂的地方,如繁忙的街道;或者是他们在听着音乐而听不到获取徽章的音效的时候通知用户!

      打开Main.storyboard,找到New Run视图控制器。把nextBadgeLabel 的IBOutlets和nextBadgeImageView联系起来。然后点击生成并运行,并在你跑步的时候观察标签和图片的更新!当你获得新徽章的时候注意播放的音效。

      空间模式会使事情变的更好

      跑步结束之后,可以让用户查看刚刚跑步过程中获得徽章会是一个非常棒的功能。下面就让我们来添加这个功能!

      打开Main.storyboard并找到Run Details视图控制器。以和现存的MKMapView相同的框架添加一个UIImageView。然后在上面添加一个有着info图片的UIButton和一个有着解释标签的UISwitch 。界面看起来和下面差不多:

      通过在属性检查器中选择Hidden属性来隐藏你刚刚添加的图片视图。

      为能显示最新获得徽章和刚刚的跑步信息的空间模式添加一个开关将会使你的跑步信息变得更加的个性化。有没有感觉像完成一次跑步之后却发现自己跑到木星上去了。:)

      打开DetailViewController.m并在文件的顶部添加下面的导入内容:

      #import "Badge.h"
      
      #import "BadgeController.h"
      

      然后在类的扩展类别中添加下面的两个属性:

      @property (nonatomic, weak) IBOutlet UIImageView *badgeImageView;
      
      @property (nonatomic, weak) IBOutlet UIButton *infoButton;
      

      然后在configureView的底部添加下面的代码:

      Badge *badge = [[BadgeController defaultController] bestBadgeForDistance:self.run.distance.floatValue];
      
      self.badgeImageView.image = [UIImage imageNamed:badge.imageName];
      

      该函数用你刚刚获得徽章的图片来设置徽章的图片视图。它通过你在前面添加的函数获取上一个获得徽章来实现这个功能。

      现在添加下面的函数:

      - (IBAction)displayModeToggled:(UISwitch *)sender
      
      {
      
      self.badgeImageView.hidden = !sender.isOn;
      
      self.infoButton.hidden = !sender.isOn;
      
      self.mapView.hidden = sender.isOn;
      
      }
      

      当切换开关的时候将会触发该函数。开关被打开的时候,它将用图片切换掉地图,既是你在空间模式中了!

      最后,添加下面的函数:

      - (IBAction)infoButtonPressed
      
      {
      
      Badge *badge = [[BadgeController defaultController] bestBadgeForDistance:self.run.distance.floatValue];
      
      
      
      UIAlertView *alertView = [[UIAlertView alloc]
      
        initWithTitle:badge.name
      
        message:badge.information
      
        delegate:nil
      
        cancelButtonTitle:@"OK"
      
        otherButtonTitles:nil];
      
      [alertView show];
      
      }
      

      当info按钮被按下的时候将会触发该函数。它显示徽章信息的警告。

      打开Main.storyboard,找到Run Details视图控制器。把badgeImageView, infoButton, displayModeToggled:, infoButtonPresse和你刚刚添加的视图联系起来。然后点击生成并运行,在艰难的长时间的跑步之后好好的享受获取的徽章的荣耀的吧!

      为你的小镇添加太阳系的映射

      地图可以帮你记录你的跑步路线,设置指出你在那一段区域的速度比较慢。将要添加的另一个游泳的功能是可以标记处你通过徽章检测点的确切时间,因此你就可以把跑步分成几段。

      Annotations是关于地图视图如何显示点数据的。下面是你需要的移动部分:

    • 一个采用MKAnnotation 来处理注释的数据部分的类。该类提供一个标示在地图上放置注释的坐标
    • 一个继承自MKAnnotationView 的类来管理把从MKAnnotation 传递过来的数据转换成可视的形式
    • 因此,你就从把徽章数据转换成符合MKAnnotation格式的对象的数组开始。然后就使用MKMapViewDelegate 中的函数 mapView:viewForAnnotation:把数据转换成MKAnnotationViews。

      创建一个继承自MKPointAnnotation的类BadgeAnnotation。然后打开BadgeAnnotation.h并用下面的代码替换其中的内容:

      #import 
      
      
      
      @interface BadgeAnnotation : MKPointAnnotation
      
      
      
      @property (strong, nonatomic) NSString *imageName;
      
      
      
      @end
      

      然后打开BadgeController.h ,在接口中添加下面的函数声明:

      - (NSArray *)annotationsForRun:(Run *)run;
      

      在同一个文件中的导入内容的下面添加下面的一行内容:

      @class Run;
      

      接下来,打开BadgeController.m 并在文件的顶部添加下面的导入内容:

      #import 
      
      #import "Location.h"
      
      #import "MathController.h"
      
      #import "BadgeAnnotation.h"
      

      并在实现部分添加下面的函数:

      - (NSArray *)annotationsForRun:(Run *)run
      
      {
      
      NSMutableArray *annotations = [NSMutableArray array];
      
      
      
      int locationIndex = 1;
      
      float distance = 0;
      
      
      
      for (Badge *badge in self.badges) {
      
      if (badge.distance > run.distance.floatValue) {
      
      break;
      
      }
      
      
      
      while (locationIndex < run.locations.count) {
      
      
      
      Location *firstLoc = [run.locations objectAtIndex:(locationIndex-1)];
      
      Location *secondLoc = [run.locations objectAtIndex:locationIndex];
      
      
      
      CLLocation *firstLocCL = [[CLLocation alloc] initWithLatitude:firstLoc.latitude.doubleValue longitude:firstLoc.longitude.doubleValue];
      
      CLLocation *secondLocCL = [[CLLocation alloc] initWithLatitude:secondLoc.latitude.doubleValue longitude:secondLoc.longitude.doubleValue];
      
      
      
      distance += [secondLocCL distanceFromLocation:firstLocCL];
      
      locationIndex++;
      
      
      
      if (distance >= badge.distance) {
      
      BadgeAnnotation *annotation = [[BadgeAnnotation alloc] init];
      
      annotation.coordinate = secondLocCL.coordinate;
      
      annotation.title = badge.name;
      
      annotation.subtitle = [MathController stringifyDistance:badge.distance];
      
      annotation.imageName = badge.imageName;
      
      [annotations addObject:annotation];
      
      break;
      
      }
      
      }
      
      }
      
      
      
      return annotations;
      
      }
      

      该函数遍历跑步过程中的所有的位置并记录总距离。当这个总距离超过下一个徽章的检测值的时候,就创建一个BadgeAnnotation 。这个注释记录了徽章获取的坐标位置,徽章的名称,跑过的距离和徽章的图片的名字。

      现在数据已经准备好了!

      打开DetailViewController.m,并在文件的顶部添加下面的导入内容:

      #import "BadgeAnnotation.h"
      

      然后添加下面的函数:

      - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id < MKAnnotation >)annotation
      
      {
      
      BadgeAnnotation *badgeAnnotation = (BadgeAnnotation *)annotation;
      
      
      
      MKAnnotationView *annView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"checkpoint"];
      
      if (!annView) {
      
      annView=[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"checkpoint"];
      
      annView.image = [UIImage imageNamed:@"mapPin"];
      
      annView.canShowCallout = YES;
      
      }
      
      
      
      UIImageView *badgeImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 75, 50)];
      
      badgeImageView.image = [UIImage imageNamed:badgeAnnotation.imageName];
      
      badgeImageView.contentMode = UIViewContentModeScaleAspectFit;
      
      annView.leftCalloutAccessoryView = badgeImageView;
      
      
      
      return annView;
      
      }
      

      这是MKMapViewDelegate 协议的一部分。每次在地图上显示给定注释的视图时该函数都会被调用。它的功能是返回一个视图,然后地图视图就会处理把该视图显示在地图上的正确位置的逻辑。

      在该函数中,你传进去一个注释作为参数告诉地图怎么样去渲染它。注意title和subtitle属性并不显示出来,它的渲染实在地图工具包中自动完成的。

      然后找到loopMap并在调用函数addOverlays的下面添加下面的这行代码:

      [self.mapView addAnnotations:[[BadgeController defaultController] annotationsForRun:self.run]];
      

      用上面的代码,你通知地图来显示注释。

      现在,你可以在跑过一段距离之后查看地图,你会发现所有的表示你通过的检测点的圆点。点击生成并运行这款应用,完成一段跑步并点击保存。地图就会有每一个获得徽章的注意。点击其中的一个,你就可以看到它的名称,图片和距离。漂亮吧!

      至此,这款应用就算完成了!祝贺你完成了这款应用的设计。

      发展方向

      通过这两部分指南的学习,你完成了这样的一款应用:

    • 使用 Core Location来测量和记录你的跑步过程
    • 显示实时的数据,如在动态的地图上显示跑步的平均速度,
    • 在地图上用有色的折线画出你的路线并在每一个检测点添加自定义的注释
    • 为用户在距离和速度上的提高授予徽章
    • 完善这款应用的一些想法:

    • 为用户过去的跑步机路添加一个表
    • 试着使用一小段的速度来获取徽章

    • 例如:如果你在一段10km的跑步过程中在中间的5km是最快的,那么给你一个5km的银色或金色徽章怎么样?
    • 计算两个检测点之间的平均速度并把它在MKAnnotationView 的标注中显示出来
    • 添加终生成就的徽章,例如:一周跑了100km或是一直跑1000km等等
    • 用服务器同步用户的跑步数据
    • 如果你想知道这款应用完成后的样子,你可以在这里下载整个项目的代码。

      感谢你阅读这篇指南!如果你有什么问题,评论或是想要分享你的成功,请在下面的评论区写出来!快乐的在太阳系中奔跑吧!:)

如何开发一个像runkeeper一样的应用:第一部分


原文地址:http://www.raywenderlich.com/73984/make-app-like-runkeeper-part-1
泰然翻译组:纯洁的大白纸。校对:glory。

在刚刚举办的全球开发者大会上,苹果公司发布了HealthKit API以及相应的 健康类应用。同时,健康和运动类目下的应用,变得十分受欢迎。

Runkeeper是一个利用GPS的应用程序,拥有超过2500万的用户!从这个增长的趋势,我们可以清楚的知道:

  • 健康是非常重要的。
  • 智能手机可以像一个精明能干的助手一样管理和追踪我们的健康状况。
  • 开发一个有趣的应用,能够让人们在达成目标的道路上保持动力,更重要的是能让人们保持健康。

这篇教程将向你展示如何开发一个像runkeeper一样的应用,我们会以一个能够激励用户运动的跑步检测应用为示例,如果你跟着教程学到最后,你会做出一个具有如下功能的应用:

  • 使用 Core Location 记录你的跑步路线。
  • 在你跑步的过程中,能向你展示一个实时更新的路线图。
  • 在你跑步的过程中,能持续不断的向你报告你的 平均速度
  • 不同的跑步距离,奖励不同的 成就徽章

    • 金质和银质徽章的获得,只取决于你进步的大小,和你的起始点无关。
  • 通过追踪当前位置与下一个目的地的 剩余距离 来鼓励你。
  • 完成跑步时,向你展示完整的路线图。

    • 路线图的线条是 不同颜色 的,跑的慢的部分用红色的线条表示,跑的快的部分用绿色的线条表示。

最后,我们把这个应用取名为 MoonRunner,因为它的徽章都是以太阳系的行星和卫星命名的。也就是说,如果你要获得所有的徽章,就相当于你在太阳系中穿越了所有的空间。

在开始学习这篇教程之前,我们列举了你应该掌握的技能和一些值得一读的建议:

  1. Storyboards:你将使用refresher构建应用的用户界面,具体使用参阅这篇教程
  2. Core Data:在个人学习的训练中,知道从哪里开始和规划进度是一项基本能力。在这个过程中,为了存储你的数据,你将使用Cocoa中被最广泛使用的数据持久性框架Core Data。具体使用参阅这篇教程
  3. 苹果公司提供的Breadcrumb的示例工程包含了许多有用的示例代码(目前不推荐使用)在跑步的过程中显示路线图。跟着我们的教程,你也会一点一点的完成同样的功能。

本教程分两部分,第一部分主要是集中在存储跑步数据和绘制由不同颜色组成的路线图,第二部分介绍徽章系统。


开始吧!

首先,创建一个新工程。
打开Xcode,选择 File\New\Project,然后选择 iOS\Application\Master-Detail Application
step1
给你的新工程取一个名字 MoonRunner,并且检查是够勾选了 Use Core Data
step2
做完上面两步,你已经有了一个由 Core Datastoryboards 构建的模板工程!


模块:跑和位置

在这个应用中,对 Core Data 的使用很少,只用到了两个entities:RunsLocations

打开 MoonRunner.xcdatamodeld,删除 Event。然后添加两个新entities,名为RunLocation。按下图所示设置 Run
step3
一个 Run 有三个属性:durationdistancetimestamp。它也有一个名为 locations 的 relationship,用来关联其他对象。

现在,按照下图设置 Location
step4
一个 locations 包含像 timestamp 一样的 latitudelongitude,此外,LocationRun 关联。
确保将 locations 的relationship设置成 to-manyordered
step5
下一步,Xcode已经生成了模型类。单击 File\New\File, 然后选择 Core Data\NSManagedObject subclass
step6
在接下来的设置中,确保勾选了 MoonRunnerRunLocation
step7

step8
Alright!,你已经完成了这个应用的Core Data部分。
这个教程需要使用 MapKit, 所以我们需要连接它。单击顶部工程导航栏里的工程,打开target里的 Build Phases,将 MapKit 添加到 Link Binary With Libraries列表中。
step9


设置 Storyboards

到了设计用户界面的时间了,首先设计storyboard,如果接下来讲到的东西你没有接触过或者暂时忘记了,请参考step10
打开 Main.storyboard.
开始阶段,MoonRunner只有一些简单的界面:

  • Home 界面,提供一些应用程序的导航选项
  • New Run界面,显示用户的起点并记录一个新的跑步活动
  • Run Details界面,展示包含不同颜色线条组成的路线图在内的跑步细节

这些界面都没有使用 UITableViewController,所以单击Master View Controller下面的黑框选中它,并按下键盘的 delete
step11

然后,从对象库中拖拽出两个view controler,将其中一个命名为‘Home’,然后在attributes inspector中设置标题。将另一个命名为‘New Run’。

Navigation ControllerHome 通过 Control-drag 相关联,将relationship设置为‘root view’。
step12

然后,将 New Run 下方黑框中的黄色图标和 Detail 界面相关联,并将segue设置为 push
step13

单击刚刚添加的segue,然后在 Attributes Inspector中,将名字指定为‘RunDetails’。
step14

Look!你的Storyboards:
step15

现在,你需要去单独设计每一个界面。


丰富你的Storyboards

请下载step16,解压并将所有的文件拖拽进工程,请确保你选择了‘Copy items into destination group’s folder (if needed)’。

打开 Main.storyboard ,找到‘Home’ view controler。

‘Home’ view controller是你应用的主菜单,拖拽一个 UILabel 放在这个View的顶部,并添加一句问候语‘Welcome To MoonRunner!’。

然后拖拽进三个 UIButtons,分别命名为 ‘New Run’, ‘Past Runs’ 和 ‘My Badges’。

在starter pack中有green-btn 和 blue-btn,我使用黑色的北京白色文字,现在界面看起来像下面:
step17

然后,用control-drag将‘New Run’ 按钮和 ‘New Run’ 界面关联起来,并且将segue设置为push.
step18

New Run 界面有两个模块:pre-runduring-run。View Controller处理每一个模块如何显示。现在,拖拽三个UILabels,用来显示实时更新的 distance , timepace

添加一个UILabels作为提示(比如: “Ready To Launch?”),添加两个 UIButtons控制开始和结束。

我使用黑色背景和 green-btnred-btn,如果你和我一样,你的storyboard将会是这样的:
step19

最后,Run Details 界面显示用户指定的一个跑步活动的线路图。

在storyboard中找到‘Detail View Controller’,然后删除已存在的label,添加一个 MKMapView 和 四个 UILabels, 内容分别为 distance, date, time 和 pace.

你的storyboard看起来应该是这样:
step20

好了,你已经为界面做了一些简单的连接,是时候用Controllers去做一些控制了。


完善Basic App Flow

Xcode在Master-Detail模板中做了大量的工作,但是你并不需要其中的MasterViewController,所以,删除 MasterViewController.hMasterViewController.m

然后使用 HomeViewController代替,选择 File\New\File,再选择 iOS\Cocoa Touch\Objective-C class,命名为HomeViewController,并选择 UIViewController 作为子类。

你需要给View Controller声明一个NSManagedObjectContext,所以,在头文件HomeViewController.h添加一个NSManagedObjectContext声明,看起来应该和下面一样:

#import 

@interface HomeViewController : UIViewController

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@end

暂时回到AppDelegate.m中,在上面的步骤删除了MasterViewController之后出现了一个错误,这只是一个小问题,在第一行的导入头文件中,使用HomeViewController.h代替MasterViewController.h

import "HomeViewController.h"

接下来,实现*application:didFinishLaunchingWithOptions*:

(BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions
{
  UINavigationController navigationController = (UINavigationController )self.window.rootViewController;
  HomeViewController controller = (HomeViewController )navigationController.topViewController;
  controller.managedObjectContext = self.managedObjectContext;
  return YES;
}

现在,再添加一个文件,选择File\New\File,再选择 iOS\Cocoa Touch\Objective-C class,命名为NewRunViewController,继承UIViewController
像在HomeViewController.h中做的一样,在NewRunViewController.h中声明一个NSManagedObjectContext

import 

@interface NewRunViewController : UIViewController

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@end

打开 **NewRunViewController.m**,在顶部添加如下代码:

import "DetailViewController.h"

import "Run.h"

static NSString * const detailSegueName = @"RunDetails";

@interface NewRunViewController () 

@property (nonatomic, strong) Run *run;

@property (nonatomic, weak) IBOutlet UILabel promptLabel;
@property (nonatomic, weak) IBOutlet UILabel timeLabel;
@property (nonatomic, weak) IBOutlet UILabel distLabel;
@property (nonatomic, weak) IBOutlet UILabel paceLabel;
@property (nonatomic, weak) IBOutlet UIButton startButton;
@property (nonatomic, weak) IBOutlet UIButton stopButton;
@end

请注意,我们还没有添加 UIActionSheetDelegate的实现,Let's do it.

下一步是定义UI的初始状态和按钮的关联动作。
将下面的方法添加到的

NewRunViewController

中的主函数:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

self.startButton.hidden = NO;
self.promptLabel.hidden = NO;

self.timeLabel.text = @"";
self.timeLabel.hidden = YES;
self.distLabel.hidden = YES;
self.paceLabel.hidden = YES;
self.stopButton.hidden = YES;
}

开始时,你只能看到开始按钮和提示。下一步,添加下面的方法:

-(IBAction)startPressed:(id)sender
{
    // 隐藏开始界面
    self.startButton.hidden = YES;
    self.promptLabel.hidden = YES;

// 显示跑步界面
self.timeLabel.hidden = NO;
self.distLabel.hidden = NO;
self.paceLabel.hidden = NO;
self.stopButton.hidden = NO;

}
(IBAction)stopPressed:(id)sender
{
  UIActionSheet actionSheet = [[UIActionSheet alloc] initWithTitle:@"" delegate:self
          cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
          otherButtonTitles:@"Save", @"Discard", nil];
  actionSheet.actionSheetStyle = UIActionSheetStyleDefault;
  [actionSheet showInView:self.view];
}

这个两个方法实现了两个按钮的两个动作。按下开始按钮时,将UI切换到 “during-run” ,按下停止按钮时,会显示一个 UIActionSheet*,这样用户可以选择是否存储保存跑步数据。

现在,你需要做一些事情去响应用户在UIActionSheet中的选择,添加如下方法:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    // 保存
    if (buttonIndex == 0) {
        [self performSegueWithIdentifier:detailSegueName sender:nil];

// 放弃保存
} else if (buttonIndex == 1) {
    [self.navigationController popToRootViewControllerAnimated:YES];
}

}


这个方法能够使得用户按下 ‘Save’ 时继续显示跑步详情,按下‘Discard’ 时返回主菜单。
最后,添加如下方法:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    [[segue destinationViewController] setRun:self.run];
}


这个方法使得当segue被触发时,DetailViewController和当前的跑步活动联系起来。

现在,跑步活动是空的,那是因为你还没有开始一个跑步活动,或者结束了应用。你可以看到,这个应用会根据从跑步中读取到的位置信息构建这一个跑步活动。

现在,开始实现NewRunViewController.m
打开 DetailViewController.h,按如下代码所示:


import 

@class Run;

@interface DetailViewController : UIViewController

@property (strong, nonatomic) Run *run;

@end


DetailViewController方法中添加一个Run属性声明,这会跑步过程中显示详情。

然后打开 DetailViewController.m,用如下代码代替整个文件。

import "DetailViewController.h"

import 

@interface DetailViewController () 

@property (nonatomic, weak) IBOutlet MKMapView mapView;
@property (nonatomic, weak) IBOutlet UILabel distanceLabel;
@property (nonatomic, weak) IBOutlet UILabel dateLabel;
@property (nonatomic, weak) IBOutlet UILabel timeLabel;
@property (nonatomic, weak) IBOutlet UILabel *paceLabel;

@end

@implementation DetailViewController

pragma mark - Managing the detail item

(void)setRun:(Run *)run
{
  if (run != run) {
      run = run;
      [self configureView];
  }
}
(void)configureView
{
}
(void)viewDidLoad
{
  [super viewDidLoad];
  [self configureView];
}

@end


这里导入了MapKit,所以你就可以使用 MKMapView,它也为UI里的属性添加了私有方法。其他方法也添加了基本的实现,当界面被加载和一个新的跑步活动开始时,调用这个方法配置这个界面。

在返回Storyboards和连接私有方法之前,还有最后一步,打开HomeViewController.m,添加如下语句:

import "NewRunViewController.h"


然后添加如下的方法实现:

(void)prepareForSegue:(UIStoryboardSegue)segue sender:(id)sender
{
UIViewController nextController = [segue destinationViewController];
if ([nextController isKindOfClass:[NewRunViewController class]]) {
    ((NewRunViewController *) nextController).managedObjectContext = self.managedObjectContext;
}
}


最后,找到storyboard,然后做如下设置:
  • HomeViewController设置成Home View Controller。

  • NewRunViewController设置成New Run View Controller。

  • NewRunViewController 和 *DetailViewController**中的私有方法联系起来。

  • NewRunViewController中接收到的两个动作(startPressed:stopPressed:)联系起来。

  • MKMapView 设置成DetailViewController的delegate。




  • 做完这些之后,你已经到达了第一个目的地。Build and Run!
    step21

    你的应用现在有一个非常基础的UI:你应该有一个导航栏能在三个界面和开始结束界面之间切换。






    Math and Units



    注意你创建的一系列界面和依附在storyboard中显示状态和时间的界面。Core Location都会以公制的数据显示,一个很好的用户体验设计是让英制和公制都可以使用。

    单击File\New\File,选择iOS\Cocoa Touch\Objective-C class,调用并创建MathController,继承NSObject,然后打开MathController.h,加入如下所示的代码:

    import 
    
    @interface MathController : NSObject
    
    (NSString *)stringifyDistance:(float)meters;
    (NSString *)stringifySecondCount:(int)seconds usingLongFormat:(BOOL)longFormat;
    (NSString *)stringifyAvgPaceFromDist:(float)meters overTime:(int)seconds;
    @end
    


    打开**MathController.m**,然后在文件头部添加如下代码:

    static bool const isMetric = YES;
    static float const metersInKM = 1000;
    static float const metersInMile = 1609.344;
    


    在美国,可以将*isMetric*的值修改成*NO*。
    下一步,添加如下代码:


    + (NSString )stringifyDistance:(float)meters {
    float unitDivider; NSString
    unitName;



    // 公制
    if (isMetric) {
    unitName = @"km";
    // to get from meters to kilometers divide by this
    unitDivider = metersInKM;
    // 美国
    } else {
    unitName = @"mi";
    // to get from meters to miles divide by this
    unitDivider = metersInMile;
    }

    return [NSString stringWithFormat:@"%.2f %@", (meters / unitDivider), unitName];

    }

    (NSString *)stringifySecondCount:(int)seconds usingLongFormat:(BOOL)longFormat
    {
    int remainingSeconds = seconds;
    int hours = remainingSeconds / 3600;
    remainingSeconds = remainingSeconds - hours * 3600;
    int minutes = remainingSeconds / 60;
    remainingSeconds = remainingSeconds - minutes * 60;



    if (longFormat) {
    if (hours > 0) {
    return [NSString stringWithFormat:@"%ihr %imin %isec", hours, minutes, remainingSeconds];
    } else if (minutes > 0) {
    return [NSString stringWithFormat:@"%imin %isec", minutes, remainingSeconds];
    } else {
    return [NSString stringWithFormat:@"%isec", remainingSeconds];
    }
    } else {
    if (hours > 0) {
    return [NSString stringWithFormat:@"%02i:%02i:%02i", hours, minutes, remainingSeconds];
    } else if (minutes > 0) {
    return [NSString stringWithFormat:@"%02i:%02i", minutes, remainingSeconds];
    } else {
    return [NSString stringWithFormat:@"00:%02i", remainingSeconds];
    }
    }
    }
    (NSString *)stringifyAvgPaceFromDist:(float)meters overTime:(int)seconds
    {
    if (seconds == 0 || meters == 0) {
    return @"0";
    }

    float avgPaceSecMeters = seconds / meters;

    float unitMultiplier;
    NSString *unitName;

    // 公制
    if (isMetric) {
    unitName = @"min/km";
    unitMultiplier = metersInKM;
    // 美国
    } else {
    unitName = @"min/mi";
    unitMultiplier = metersInMile;
    }

    int paceMin = (int) ((avgPaceSecMeters * unitMultiplier) / 60);
    int paceSec = (int) (avgPaceSecMeters * unitMultiplier - (paceMin*60));

    return [NSString stringWithFormat:@"%i:%02i %@", paceMin, paceSec, unitName];
    }


    这些方法能够将 distances, time 和 speeds转换成字符串。这里不做太多细节的解释,详情可以参考这篇教程








    开始一个跑步活动



    你是否记得有人说过:“即使是马拉松也是用一个CLLocationManager update开始的。”也许没人说过,但是现在我们就来证明这句话。

    下一步,你将开始记录用户在跑步中的位置信息。

    你需要对你的工程做一个非常重要的改变,点击工程导航栏中的工程,选择Capabilities,然后打开Background Modes,在展开的右边部分,勾选Location Updates,这样,即使用户返回主菜单或者接听电话或者上网或者查看附近的星巴克的时候,你的应用也能更新位置信息。
    step23



    你知道吗?如果你的应用上架到APP > > Store,你需要为你的应用添加一份免责声明:在后台继续使用GPS会很容易的减少电池的使用寿命。



    现在,回到代码中来,打开NewRunViewController.m,将下列语句添加到文件头部:

    import 
    
    import "MathController.h"
    
    import "Location.h"
    


    下一步,添加 [CLLocationManagerDelegate][26] 和一些新属性。

    @interface NewRunViewController () 
    
    @property int seconds;
    @property float distance;
    @property (nonatomic, strong) CLLocationManager locationManager;
    @property (nonatomic, strong) NSMutableArray locations;
    @property (nonatomic, strong) NSTimer *timer;
    
    ...
    


    • seconds:监测跑步的持续时间,以秒为单位。
    • distance:记录跑步中持续增长的距离,以米为单位。
    • locationManager:用来接收你的停止和开始读取用户位置信息的命令。
    • locations:一个数组,用来存储不断存入的位置信息。
    • timer:fire每秒并更新UI。


    现在,添加如下方法:

    - (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];
        [self.timer invalidate];
    }
    


    这个方法中,当用户导航在视图中不可见的时候,停止timer。

    添加如下方法:

    - (void)eachSecond
    {
        self.seconds++;
        self.timeLabel.text = [NSString stringWithFormat:@"Time: %@",  [MathController stringifySecondCount:self.seconds usingLongFormat:NO]];
        self.distLabel.text = [NSString stringWithFormat:@"Distance: %@", [MathController stringifyDistance:self.distance]];
        self.paceLabel.text = [NSString stringWithFormat:@"Pace: %@",  [MathController stringifyAvgPaceFromDist:self.distance overTime:self.seconds]];
    }
    


    通过使用NSTimer,这个方法每一秒都会被调用,每一次调用这个方法,增加计时并更新对应的标签信息。

    添加如下所示的另一个方法:

    - (void)startLocationUpdates
    {
        // 如果没有位置管理器,创建一个。
        if (self.locationManager == nil) {
            self.locationManager = [[CLLocationManager alloc] init];
        }
    
        self.locationManager.delegate = self;
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
        self.locationManager.activityType = CLActivityTypeFitness;
    
        // 时间的入口
        self.locationManager.distanceFilter = 10; // 米
    
        [self.locationManager startUpdatingLocation];
    }
    


    如果需要的话,你需要一个 CLLocationManager,将它的入口设置为这一个类,这样它就知道要把更新的位置信息发送到哪里。

    之后,你需要提供kCLLocationAccuracyBestdesiredAccuracy ,你可能觉得这么做很傻 ,为什么不做到最好哪?

    手机可以从GPS硬件获取精确地读数,但是它加剧了电池的使用。无节制的使用已经打开的无线通讯功能,比如 Wi-Fi or cell-tower 也会加剧电池的使用。

    是的,GPS会消耗大量的电量,这里有一些粗略读取数据的使用场景,比如:当你只需要确定用户的大致位置时。无论如何,这个应用追踪你的真实跑步路径,所以你需要尽可能的精确。

    activityType参数正是用来给这样的应用使用。它能很智能的在用户的整个跑步过程中节省电量。

    最后,你要设置一个10米的distanceFilter,和activityType不同的是,它不对电池使用产生影响。activityType在读取数据时使用,distanceFilter是在报告读取的数据时使用。

    在一个跑步之后,你会看到,读取的位置路线和直线有一些不同。

    高精度的distanceFilter的能尽量减少和真实路线的区别,给你一个更真实的路线。
    不幸的是,太高精度的过滤器会使获得的数据失真,这就是为什么10米是一个很好地选择。

    最后,你要告诉管理器去开始获得位置信息。

    开始一个跑步活动,添加startPressed文件末尾添加如下信息:

    self.seconds = 0;
    self.distance = 0;
    self.locations = [NSMutableArray array];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:(1.0) target:self
            selector:@selector(eachSecond) userInfo:nil repeats:YES];
    [self startLocationUpdates];
    


    这段代码实现了,即使你重置了一个跑步活动,所有的变量也能继续更新。



    记录跑步活动



    你已经创建了CLLocationManager,现在,你需要通过它更新数据。我们可以通过它的入口实现。打开NewRunController.m,添加如下代码:

    (void)locationManager:(CLLocationManager )manager
       didUpdateLocations:(NSArray )locations
    {
      for (CLLocation *newLocation in locations) {
          if (newLocation.horizontalAccuracy < 20) {
    
          // 更新距离
          if (self.locations.count > 0) {
              self.distance += [newLocation distanceFromLocation:self.locations.lastObject];
          }
    
          [self.locations addObject:newLocation];
      }
      }
    }
    


    每一次位置更新的时候,这个方法都会被调用。

    它通常使用一个*CLLocations*数组更新数据,通常数组里只有一个对象, 但是如果有多个对象的时候,只保存最新位置的信息。

    *CLLocation*包含了许多有用的信息。比如随着读取的时间的不同,保存不同的经纬度。

    但是在盲目的接收读取的数据之前,需要一个*horizontalAccuracy*检查,当手机位置不确定时,它读取的用户位置数据有一个20米左右的误差,最好从你的数据集合中移除这个数据。

    > 注意:这个检查在开始跑步活动是非常重要,在这个阶段,他可能一些不确定的信息更新。

    如果CLLocation通过检查,这个位置和最近的一个点之间的位置就会被添加到增长的距离中, [distanceFromLocation][28]:这个方法很方便,描述了一些包括地球曲面曲度在内的复杂的数学问题。

    最后,位置对象被添加进位置数组中。

    > 注意:*CLLocation*对象使用*verticalAccuracy*包含高度信息。所有的跑步者都知道,山丘是跑步过程中的一个转折点,高度能够影响氧气密度。对你的挑战是,想出一个办法将这个信息包含进你的应用中。

    ---
    ## 保存跑步信息

    在一些点上,尽管身体里有个声音在激励你继续下去,但是到了跑步结束的时间。你应经在UI中做了接收输入的措施,现在是时候去处理数据了。

    将下列代码添加进**NewRunViewController.m**:

    (void)saveRun
    {
    Run *newRun = [NSEntityDescription insertNewObjectForEntityForName:@"Run"
    inManagedObjectContext:self.managedObjectContext];

    newRun.distance = [NSNumber numberWithFloat:self.distance];
    newRun.duration = [NSNumber numberWithInt:self.seconds];
    newRun.timestamp = [NSDate date];



    NSMutableArray locationArray = [NSMutableArray array]; for (CLLocation location in self.locations) {
    Location *locationObject = [NSEntityDescription insertNewObjectForEntityForName:@"Location"
    inManagedObjectContext:self.managedObjectContext];

    locationObject.timestamp = location.timestamp;
    locationObject.latitude = [NSNumber numberWithDouble:location.coordinate.latitude];
    locationObject.longitude = [NSNumber numberWithDouble:location.coordinate.longitude];
    [locationArray addObject:locationObject];

    }

    newRun.locations = [NSOrderedSet orderedSetWithArray:locationArray];
    self.run = newRun;

    // Save the context.
    NSError error = nil; if (![self.managedObjectContext save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } }
    发生了什么?如果之前,你对Core Data做了简单的处理,你现在应该感觉到存储对象有点熟悉。你创建了一个新的run对象,并且给它设置了一个持续增长的距离和跑步的持续时间。 跑步过程中记录的每一个CLLocation都被裁剪进一个新的CLLocation*的对象并且保存,位置信息和跑步连接,接下来去实现这个功能。





    最后,编辑actionSheet:clickedButtonAtIndex,在执行segue之前,停止读取位置信息,保存跑步活动。代码如下所示:

    - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        [self.locationManager stopUpdatingLocation];
    
    // 保存
    if (buttonIndex == 0) {
        [self saveRun]; ///< 添加这一行
        [self performSegueWithIdentifier:detailSegueName sender:nil];
    
    // 放弃
    } else if (buttonIndex == 1) {
        [self.navigationController popToRootViewControllerAnimated:YES];
    }
    }
    


    运行模拟器



    正如我希望的,这篇教程和你跟随着做的应用应该给你健康运动一定的鼓励。当你开发这个应用的时候,不需要逐步执行Build and Run。

    Build & runin模拟器,点击Debug\Location\City Run开启模拟器生产虚拟数据。
    step27

    当然,这样做比真实的去跑步测试容易的多。

    无论如何,我都建议你最终做一个真实的测试,这么做给你机会去调整位置管理器的参数,评估你获得的位置数据的质量。



    展示路线图



    现在是时候显示路线图了。

    打开DetailViewController.m,添加如下代码:

    import "MathController.h"
    
    import "Run.h"
    
    import "Location.h"
    


    然后,如下修改**configureView**:

    (void)configureView
    {
      self.distanceLabel.text = [MathController stringifyDistance:self.run.distance.floatValue];
    
      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
      [formatter setDateStyle:NSDateFormatterMediumStyle];
      self.dateLabel.text = [formatter stringFromDate:self.run.timestamp];
    
      self.timeLabel.text = [NSString stringWithFormat:@"Time: %@",  [MathController stringifySecondCount:self.run.duration.intValue usingLongFormat:YES]];
    
      self.paceLabel.text = [NSString stringWithFormat:@"Pace: %@",  [MathController stringifyAvgPaceFromDist:self.run.distance.floatValue overTime:self.run.duration.intValue]];
    }
    


    这段代码实现了将跑步的细节在标签变量上显示出来。

    传递路线图还需要一些小细节。这里有三个基本的步骤实现。第一,区域要设置,只能获取跑步路线的信息不能获取全世界。



    添加如下代码:

    - (MKCoordinateRegion)mapRegion
    {
        MKCoordinateRegion region;
        Location *initialLoc = self.run.locations.firstObject;
    
    float minLat = initialLoc.latitude.floatValue;
    float minLng = initialLoc.longitude.floatValue;
    float maxLat = initialLoc.latitude.floatValue;
    float maxLng = initialLoc.longitude.floatValue;
    
    for (Location *location in self.run.locations) {
        if (location.latitude.floatValue < minLat) {
            minLat = location.latitude.floatValue;
        }
        if (location.longitude.floatValue < minLng) {
            minLng = location.longitude.floatValue;
        }
        if (location.latitude.floatValue > maxLat) {
            maxLat = location.latitude.floatValue;
        }
        if (location.longitude.floatValue > maxLng) {
            maxLng = location.longitude.floatValue;
        }
    }
    
    region.center.latitude = (minLat + maxLat) / 2.0f;
    region.center.longitude = (minLng + maxLng) / 2.0f;
    
    region.span.latitudeDelta = (maxLat - minLat) * 1.1f; // 10% padding
    region.span.longitudeDelta = (maxLng - minLng) * 1.1f; // 10% padding
    
    return region;
    }
    


    MKCoordinateRegion用来显示区域。你要用一个中心点和区域去定义它。

    例如,我慢跑可能相当与放大我的短跑路线周围区域。而我更喜欢跑步的朋友将显示更大区域的缩略图。

    一点填充也是很重要的,它让你的路线和路线图的边缘不至于那么拥挤。



    下一步,添加如下代码:

    - (MKOverlayRenderer )mapView:(MKMapView )mapView rendererForOverlay:(id < MKOverlay >)overlay
    {
        if ([overlay isKindOfClass:[MKPolyline class]]) {
            MKPolyline polyLine = (MKPolyline )overlay;
            MKPolylineRenderer *aRenderer = [[MKPolylineRenderer alloc] initWithPolyline:polyLine];
            aRenderer.strokeColor = [UIColor blackColor];
            aRenderer.lineWidth = 3;
            return aRenderer;
        }
    
    return nil;
    }
    


    这个方法的功能是:无论路线图是否遇到了一个添加overlay的请求,它都应该去检查是否是一个MKPolyline,如果是,它应该使用一个画黑线的渲染器。overlay是画在路线图头部的。

    最后,你需要给折线定义坐标,添加如下代码:

    - (MKPolyline *)polyLine {
    
    CLLocationCoordinate2D coords[self.run.locations.count];
    
    for (int i = 0; i < self.run.locations.count; i++) {
        Location *location = [self.run.locations objectAtIndex:i];
        coords[i] = CLLocationCoordinate2DMake(location.latitude.doubleValue, location.longitude.doubleValue);
    }
    
    return [MKPolyline polylineWithCoordinates:coords count:self.run.locations.count];
    
    }
    


    在这里,你将从Location对象获取的数据推进CLLocationCoordinate2D(折线需要的格式)数组中。

    现在,把这三步合在一起,添加如下方法:

    - (void)loadMap
    {
        if (self.run.locations.count > 0) {
    
        self.mapView.hidden = NO;
    
        // set the map bounds
        [self.mapView setRegion:[self mapRegion]];
    
        // make the line(s!) on the map
        [self.mapView addOverlay:[self polyLine]];
    
    } else {
    
        // no locations were found!
        self.mapView.hidden = YES;
    
        UIAlertView *alertView = [[UIAlertView alloc]
                                  initWithTitle:@"Error"
                                  message:@"Sorry, this run has no locations saved."
                                  delegate:nil
                                  cancelButtonTitle:@"OK"
                                  otherButtonTitles:nil];
        [alertView show];
    }
    }
    


    添加如下代码到*configureView*的底部:

    [self loadMap];
    


    现在执行build & run,你应该在模拟器上看到路线图:

    step28






    找到正确的颜色



    这个应用现在已经很好了,有一个方法可以帮你的用户将应用训练的更聪明,那就是让应用知道你在每一次跑步中胳膊的快慢。

    点击File\New\File,选择iOS\Cocoa Touch\Objective-C class,调用MulticolorPolylineSegment,继承MKPolyline并创建它,然后打开MulticolorPolylineSegment.h,修改成如下:

    import 
    
    @interface MulticolorPolylineSegment : MKPolyline
    
    @property (strong, nonatomic) UIColor *color;
    
    @end
    


    这种特殊的,自定义的折线将用来转换跑步活动中的每一部分。颜色代表着速度折线上每一部分的颜色都是存储在这里。
    下一步,你需要计算出怎么样给折线的每一部分指定颜色。听起来是数学问题。打开**MathController.h**,添加如下代码:

    + (NSArray )colorSegmentsForLocations:(NSArray )locations;
    


    然后打开**MathController.m**,并添加如下代码:

    import "Location.h"
    
    import "MulticolorPolylineSegment.h"
    


    然后添加如下方法实现:

    (NSArray )colorSegmentsForLocations:(NSArray )locations
    {
      // make array of all speeds, find slowest+fastest
      NSMutableArray *speeds = [NSMutableArray array];
      double slowestSpeed = DBL_MAX;
      double fastestSpeed = 0.0;
    
      for (int i = 1; i < locations.count; i++) {
          Location firstLoc = [locations objectAtIndex:(i-1)];
          Location secondLoc = [locations objectAtIndex:i];
    
      CLLocation *firstLocCL = [[CLLocation alloc] initWithLatitude:firstLoc.latitude.doubleValue longitude:firstLoc.longitude.doubleValue];
      CLLocation *secondLocCL = [[CLLocation alloc] initWithLatitude:secondLoc.latitude.doubleValue longitude:secondLoc.longitude.doubleValue];
    
      double distance = [secondLocCL distanceFromLocation:firstLocCL];
      double time = [secondLoc.timestamp timeIntervalSinceDate:firstLoc.timestamp];
      double speed = distance/time;
    
      slowestSpeed = speed < slowestSpeed ? speed : slowestSpeed;
      fastestSpeed = speed > fastestSpeed ? speed : fastestSpeed;
    
      [speeds addObject:@(speed)];
    
      }
    
      return speeds;
    }
    


    这个方法为位置的每一个顺序对返回一个速度数组。
    你看到的第一件事是 循环遍历了输入的所有位置,你需要将每一个*Location*转换成*CLLocation*,所以你需要使用*distanceFromLocation*。
    不要忘了物理知识:距离等于速度乘以时间,第一个位置之后的每一个位置都会和它的前一个位置比较,循环结束后,你会得到一个完整速度变化的集合。
    下一步,添加如下代码:

    //知道最快和最慢速度,求meanSpeed
    double meanSpeed = (slowestSpeed + fastestSpeed)/2;
    
    // 慢的用红色
    CGFloat r_red = 1.0f;
    CGFloat r_green = 20/255.0f;
    CGFloat r_blue = 44/255.0f;
    
    // 不快不慢的用黄色
    CGFloat y_red = 1.0f;
    CGFloat y_green = 215/255.0f;
    CGFloat y_blue = 0.0f;
    
    // 快的用绿色
    CGFloat g_red = 0.0f;
    CGFloat g_green = 146/255.0f;
    CGFloat g_blue = 78/255.0f;
    


    在这里定义了三种颜色分别代表折现中的三种不同的速度。

    每一种颜色,都有它的RGB值,最慢的使用纯红色,介于最慢和最快中间的使用黄色,最快的使用绿色。其他的就是它两边的颜色的混合。所以最后的结果可能是五彩缤纷的。
    step29



    最后,移除原本的返回值并且添加如下代码:

    NSMutableArray *colorSegments = [NSMutableArray array];
    
    for (int i = 1; i < locations.count; i++) {
      Location firstLoc = [locations objectAtIndex:(i-1)];
      Location secondLoc = [locations objectAtIndex:i];
    
      CLLocationCoordinate2D coords2;
      coords[0].latitude = firstLoc.latitude.doubleValue;
      coords[0].longitude = firstLoc.longitude.doubleValue;
    
      coords1.latitude = secondLoc.latitude.doubleValue;
      coords1.longitude = secondLoc.longitude.doubleValue;
    
      NSNumber speed = [speeds objectAtIndex:(i-1)];
      UIColor color = [UIColor blackColor];
    
      // between red and yellow
      if (speed.doubleValue < meanSpeed) {
        double ratio = (speed.doubleValue - slowestSpeed) / (meanSpeed - slowestSpeed);
        CGFloat red = r_red + ratio * (y_red - r_red);
        CGFloat green = r_green + ratio * (y_green - r_green);
        CGFloat blue = r_blue + ratio * (y_blue - r_blue);
        color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
    
    // between yellow and green
      } else {
        double ratio = (speed.doubleValue - meanSpeed) / (fastestSpeed - meanSpeed);
        CGFloat red = y_red + ratio * (g_red - y_red);
        CGFloat green = y_green + ratio * (g_green - y_green);
        CGFloat blue = y_blue + ratio * (g_blue - y_blue);
        color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
      }
    
      MulticolorPolylineSegment *segment = [MulticolorPolylineSegment polylineWithCoordinates:coords count:2];
      segment.color = color;
    
      [colorSegments addObject:segment];
    }
    
    return colorSegments;
    


    在这个循环中,你决定了每一个与计算的速度的值。这个比率在后面决定了UIColor应用到的部分。

    下一步,你使用两个坐标和混合的颜色构建一个MulticolorPolylineSegment

    最后,你获得了所有的颜色混合的部分,你已经做好了转换的准备。






    应用多种颜色



    使用多颜色的折线重构你的detail view controller是很简单的事,打开DetailViewController.m,添加如下所示的头文件:

    import "MulticolorPolylineSegment.h"
    


    现在,找到*loadMap*,用这一行:

    NSArray *colorSegmentArray = [MathController colorSegmentsForLocations:self.run.locations.array];
    [self.mapView addOverlays:colorSegmentArray];
    


    替换下面的行:

    [self.mapView addOverlay:[self polyLine]];
    


    它使用math controller创建了一个segments数组,并将所有的overlays添加进路线图。

    最后,你需要准备你的折线转换器去注意每一部分的特殊颜色,所以,使用下列代码替换mapView:rendererForOverlay的实现:

    - (MKOverlayRenderer )mapView:(MKMapView )mapView rendererForOverlay:(id < MKOverlay >)overlay
    {
        if ([overlay isKindOfClass:[MulticolorPolylineSegment class]]) {
            MulticolorPolylineSegment polyLine = (MulticolorPolylineSegment )overlay;
            MKPolylineRenderer *aRenderer = [[MKPolylineRenderer alloc] initWithPolyline:polyLine];
            aRenderer.strokeColor = polyLine.color;
            aRenderer.lineWidth = 3;
            return aRenderer;
        }
    
    return nil;
    }
    


    这和之前做的很相似,但是现在每一段特定的颜色都是单独呈现。
    好了,现在启动模拟器运行。
    step30






    Leaving a Trail Of Breadcrumbs



    那个跑步后的地图是惊人的,但是它是怎么运行的那?Breadcrumb sample project有它的功能。但是在这篇文章里面,包含了一些在iOS7中不推荐使用的方法。

    打开Main.storyboard,找到‘New Run’ view controller,拖拽进一个新的MKMapView
    step32



    然后打开NewRunViewController.m,并且添加如下导入语句:

    import 


    添加MKMapViewDelegate声明:

    @interface NewRunViewController () 
    


    接下来,为路线图添加IBOutlet到扩展类:

    @property (nonatomic, weak) IBOutlet MKMapView *mapView;
    


    然后在viewWillAppear后添加如下一行:

    self.mapView.hidden = YES;
    


    现在在startPressed后添加下面一行:

    self.mapView.hidden = NO;
    


    它实现了当跑步开始时,显示路线图。

    足迹将会开始一个新的折线,所以是时候去添加你的老朋友mapView:rendererForOverlay了,添加如下所示的代码:

    - (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id < MKOverlay >)overlay
    {
        if ([overlay isKindOfClass:[MKPolyline class]]) {
            MKPolyline *polyLine = (MKPolyline *)overlay;
            MKPolylineRenderer *aRenderer = [[MKPolylineRenderer alloc] initWithPolyline:polyLine];
            aRenderer.strokeColor = [UIColor blueColor];
            aRenderer.lineWidth = 3;
            return aRenderer;
        }
        return nil;
    }
    


    这个版本和跑步细节视图是相似的,除了这里的strokeColor总是蓝色的。
    接下来,你需要写一些代码去更新你的地图区域,并且在每一个通过验证的位置信息被获取之后,画折线。找到locationManager:didUpdateLocations,并用下面代码更新它:

    - (void)locationManager:(CLLocationManager )manager
         didUpdateLocations:(NSArray )locations
    {
        for (CLLocation *newLocation in locations) {
    
        NSDate *eventDate = newLocation.timestamp;
    
        NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
    
        if (abs(howRecent) < 10.0 && newLocation.horizontalAccuracy < 20) {
    
            // update distance
            if (self.locations.count > 0) {
                self.distance += [newLocation distanceFromLocation:self.locations.lastObject];
    
                CLLocationCoordinate2D coords[2];
                coords[0] = ((CLLocation *)self.locations.lastObject).coordinate;
                coords[1] = newLocation.coordinate;
    
                MKCoordinateRegion region =
                MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 500, 500);
                [self.mapView setRegion:region animated:YES];
    
                [self.mapView addOverlay:[MKPolyline polylineWithCoordinates:coords count:2]];
            }
    
            [self.locations addObject:newLocation];
        }
    }
    }
    


    现在,地图总是集中在最近的位置,不断增加的蓝色小折线显示用户的轨迹。
    打开Main.storyboard,找到‘New Run’ view controller,连接mapView 的出口到地图视图。然后将mapView的入口设置为view controller。
    Build & run,开始一个新的跑步活动,你会看到实时更新的地图。
    step33


    还能做些什么?

    干得好,这里有一些不错的想法去尝试:

    • 利用从NewRunController获得的高度信息计算路面的曲度。

    • 如果你寻求一种纯数学方面的挑战,你可以尝试去使用平均速度更平滑的混合颜色。

    敬请期待本篇教程的第二部分,第二部分将介绍徽章系统。
    一如即让,欢迎发表意见和评论。

    ?>