Swift🙎🏿个人喜欢,收藏Swift开发技巧

怎么样创建一个像RunKeeper一样的app(二)swift版

2015-07-29  本文已影响1167人  西木

怎么样创建一个像RunKeeper一样的APP(二)swift版

</br>
本博将不定期更新外网的iOS最新教程

简书: @西木

微博: @角落里的monster

本文翻译自raywenderlich,版权归原作者所有,转载请注明出处

原文地址为 http://www.raywenderlich.com/97945/make-app-like-runkeeper-swift-part-2

</br>
这是第二部分,也是这篇教程的最后一部分,我们将会完成badge的部分

在第一部分中,我们完成了

这个App可以很好地显示和记录你的跑步数据,但是要看到你的跑步中明显的各种变化,就不光是一个地图可以表现得,还需要做一些调整

这一部分中,你将完成MoonRunner的奖励体系的设置,它能够体现出你在运动过程中的愉悦和成就感。它能够帮助你有积极性来使用App记录你运动的历程

准备好解锁你第二部分额运动成就了吗?开始吧

Getting Started

如果你还没有看过第一部分的教程,可以查看我之前的博文

在项目文件的配置中已经包含了一个JSON文件,你可以先查看一下JSON文件,如果你好奇的话

徽章系统会从0开始记录,首先你需要完成一个马拉松,当然,很多人可能会完成更远的距离,你也可以想想是什么样的力量可以支持他们完成这些

首先,我们要把JSON数据转换成一个数组,新建一个swift文件,命名为Badge

然后,用下面的部分替换原文件的内容

import Foundation

let silverMultiplier = 1.05 // 5% speed increase
let goldMultiplier = 1.10 // 10% speed increase

class Badge {
  let name: String?
  let imageName: String?
  let information: String?
  let distance: Double?

  init(json: [String: String]) {
    name = json["name"]
    information = json["information"]
    imageName = json["imageName"]
    distance = (json["distance"] as NSString?)?.doubleValue
  }
}

如果字典中并没有包含所有的key的话,可以来这个问价查找

我们需要解析JSON数据完善你的徽章系统,仍然是这个文件,创建一个类命名为 BadgeController 并加入一下代码

class BadgeController {
  static let sharedController = BadgeController()

  lazy var badges : [Badge] = {
    var _badges = [Badge]()

    let filePath = NSBundle.mainBundle().pathForResource("badges", ofType: "json") as String!
    let jsonData = NSData.dataWithContentsOfMappedFile(filePath) as! NSData

    var error: NSError?
    if let jsonBadges = NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.AllowFragments, error: &error) as? [Dictionary<String, String>] {
      for jsonBadge in jsonBadges {
        _badges.append(Badge(json: jsonBadge))
      }
    }
    else {
      println(error)
    }

    return _badges
    }()

这里,你声明了 BadgeController 为一个单例,而且对 badges 数组做了懒加载,当第一次被调用的时候,会通过Badges.json 来初始化

Earning The Badge

你已经创建了Badge, 那么现在你需要一个对象来存储你获得的徽章奖励

这个对象需要把你的Badge 对象和Run 对象联系起来,如果有的话,还需要恩能够存储这个徽章的级别

打开 Badge.swift 在尾部添加以下代码

class BadgeEarnStatus {
  let badge: Badge
  var earnRun: Run?
  var silverRun: Run?
  var goldRun: Run?
  var bestRun: Run?

  init(badge: Badge) {
    self.badge = badge
  }
}

现在你已经可以把 Badge 和 Run 联系起来了,那么我们就需要建立它们之间的逻辑关系

添加以下代码到 Badge.swift 中

let silverMultiplier = 1.05 // 5% speed increase
let goldMultiplier = 1.10 // 10% speed increase

silverMultiplier 和 goldMultiplier是根据速度的快慢来划分的,越多的加成会获得更高级别的奖励

然后,添加以下方法在 BadgeController 类中

func badgeEarnStatusesForRuns(runs: [Run]) -> [BadgeEarnStatus] {
    var badgeEarnStatuses = [BadgeEarnStatus]()

    for badge in badges {
      let badgeEarnStatus = BadgeEarnStatus(badge: badge)

      for run in runs {
        if run.distance.doubleValue > badge.distance {

          // This is when the badge was first earned
          if badgeEarnStatus.earnRun == nil {
            badgeEarnStatus.earnRun = run
          }

          let earnRunSpeed = badgeEarnStatus.earnRun!.distance.doubleValue / badgeEarnStatus.earnRun!.duration.doubleValue
          let runSpeed = run.distance.doubleValue / run.duration.doubleValue

          // Does it deserve silver?
          if badgeEarnStatus.silverRun == nil && runSpeed > earnRunSpeed * silverMultiplier {
            badgeEarnStatus.silverRun = run
          }

          // Does it deserve gold?
          if badgeEarnStatus.goldRun == nil && runSpeed > earnRunSpeed * goldMultiplier {
            badgeEarnStatus.goldRun = run
          }

          // Is it the best for this distance?
          if let bestRun = badgeEarnStatus.bestRun {
            let bestRunSpeed = bestRun.distance.doubleValue / bestRun.duration.doubleValue
            if runSpeed > bestRunSpeed {
              badgeEarnStatus.bestRun = run
            }
          }
          else {
            badgeEarnStatus.bestRun = run
          }
        }
      }
      
      badgeEarnStatuses.append(badgeEarnStatus)
    }
    
    return badgeEarnStatuses
  }

这个方法会吧用户的跑步距离和对应的奖励的要求做个匹配,返回一个数组,数组里包含了所有的 BadgeEarnStatus

它的作用是,每当用户获得一个Badge的时候,它会产生一个像对应的速度,来判断这个奖励的级别是 silver version 还是 gold version

比如说,虽然你的小伙伴的速度比你快,但是如果你的进步足够大得话,依然有机会获得 gold version 的奖励

Displaying the Badges

现在是时候向用户展示你所有的奖励逻辑和UI界面了

你需要创建两个控制器和一个自定义的 table cell 来显示 Badg e数据

创建一个新的swift文件命名为 BadgeCell

打开这个文件,用下面的代码替换原来的内容

import UIKit
import HealthKit

class BadgeCell: UITableViewCell {
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var descLabel: UILabel!
  @IBOutlet weak var badgeImageView: UIImageView!
  @IBOutlet weak var silverImageView: UIImageView!
  @IBOutlet weak var goldImageView: UIImageView!
}

现在,你已经用 table view controller 为 badges 自定义了一个cell

接下来,在创建一个新的swift文件命名为 BadgesTableViewController, 打开文件替换里面的内容为

import UIKit
import HealthKit

class BadgesTableViewController: UITableViewController {
  var badgeEarnStatusesArray: [BadgeEarnStatus]!
  }

在之前的 BadgeController 里调用 badgeEarnStatusesForRuns(_:) 方法的时候会返回一个 badgeEarnStatuesArray 数组

添加如下属性给刚才的类

let redColor = UIColor(red: 1, green: 20/255, blue: 44/255, alpha: 1)
  let greenColor = UIColor(red: 0, green: 146/255, blue: 78/255, alpha: 1)
  let dateFormatter: NSDateFormatter = {
    let _dateFormatter = NSDateFormatter()
    _dateFormatter.dateStyle = .MediumStyle
    return _dateFormatter
    }()
  let transform = CGAffineTransformMakeRotation(CGFloat(M_PI/8.0))

每个cell会根据奖章的不同来显示不同的颜色

这些属性会保存在缓存里,不需要每次重新创建,每次创建新的会很耗性能,所以应该尽量考虑重复使用

然后,给 UITableViewDataSource 添加如下实现

// MARK: - UITableViewDataSource
extension BadgesTableViewController {
  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return badgeEarnStatusesArray.count
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("BadgeCell") as! BadgeCell

    let badgeEarnStatus = badgeEarnStatusesArray[indexPath.row]

    cell.silverImageView.hidden = (badgeEarnStatus.silverRun != nil)
    cell.goldImageView.hidden = (badgeEarnStatus.goldRun != nil)

    if let earnRun = badgeEarnStatus.earnRun {
      cell.nameLabel.textColor = greenColor
      cell.nameLabel.text = badgeEarnStatus.badge.name!
      cell.descLabel.textColor = greenColor
      cell.descLabel.text = "Earned: " + dateFormatter.stringFromDate(earnRun.timestamp)
      cell.badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
      cell.silverImageView.transform = transform
      cell.goldImageView.transform = transform
      cell.userInteractionEnabled = true
    }
    else {
      cell.nameLabel.textColor = redColor
      cell.nameLabel.text = "?????"
      cell.descLabel.textColor = redColor
      let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: badgeEarnStatus.badge.distance!)
      cell.descLabel.text = "Run \(distanceQuantity.description) to earn"
      cell.badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)
      cell.userInteractionEnabled = false
    }

    return cell
  }
}

这个方法告诉 tableView 要显示多少行,每个cell显示什么内容,你能够看到,每个cell对应的是不同的badge,而且,因为设置了 userInteractionEnabled,只有获得奖章的 cell才能被选中

现在你需要给 BadgesTableViewController 提供一些数据,打开 HomeViewController.swift 给 prepareForSegue(_:sender): 方法添加如下代码

else if segue.destinationViewController.isKindOfClass(BadgesTableViewController) {
      let fetchRequest = NSFetchRequest(entityName: "Run")

      let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: false)
      fetchRequest.sortDescriptors = [sortDescriptor]

      let runs = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as! [Run]

      let badgesTableViewController = segue.destinationViewController as! BadgesTableViewController
      badgesTableViewController.badgeEarnStatusesArray = BadgeController.sharedController.badgeEarnStatusesForRuns(runs)
    }

这里,当 BadgesTableViewController 被压入导航栈里的时候,每一个奖励的状态都会被计算并且显示出来

链接storyboard,打开Main.storyboard做下面的事情

如果你已经用过它来跑步的话,肯定已经获得了 earth 级别的奖励,显然,奖励才刚开始

Badge Details

下一个控制器用来展示奖励的详细信息

创建一个新的swift文件命名为 BadgeDetailsViewController 并且替换内容为

import UIKit
import HealthKit

class BadgeDetailsViewController: UIViewController {
  var badgeEarnStatus: BadgeEarnStatus!

  @IBOutlet weak var badgeImageView: UIImageView!
  @IBOutlet weak var silverImageView: UIImageView!
  @IBOutlet weak var goldImageView: UIImageView!
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var distanceLabel: UILabel!
  @IBOutlet weak var earnedLabel: UILabel!
  @IBOutlet weak var silverLabel: UILabel!
  @IBOutlet weak var goldLabel: UILabel!
  @IBOutlet weak var bestLabel: UILabel!
  }

这个类用来存储你的获奖的详细状态,可以用来添加标识

添加如下的代码设置View

override func viewDidLoad() {
    super.viewDidLoad()

    let formatter = NSDateFormatter()
    formatter.dateStyle = .MediumStyle

    let transform = CGAffineTransformMakeRotation(CGFloat(M_PI/8.0))

    nameLabel.text = badgeEarnStatus.badge.name

    let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: badgeEarnStatus.badge.distance!)
    distanceLabel.text = distanceQuantity.description
    badgeImageView.image = UIImage(named: badgeEarnStatus.badge.imageName!)

    if let run = badgeEarnStatus.earnRun {
      earnedLabel.text = "Reached on " + formatter.stringFromDate(run.timestamp)
    }

    if let silverRun = badgeEarnStatus.silverRun {
      silverImageView.transform = transform
      silverImageView.hidden = false
      silverLabel.text = "Earned on " + formatter.stringFromDate(silverRun.timestamp)
    }
    else {
      silverImageView.hidden = true
      let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
      let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: badgeEarnStatus.earnRun!.duration.doubleValue / badgeEarnStatus.earnRun!.distance.doubleValue)
      silverLabel.text = "Pace < \(paceQuantity.description) for silver!"
    }

    if let goldRun = badgeEarnStatus.goldRun {
      goldImageView.transform = transform
      goldImageView.hidden = false
      goldLabel.text = "Earned on " + formatter.stringFromDate(goldRun.timestamp)
    }
    else {
      goldImageView.hidden = true
      let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
      let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: badgeEarnStatus.earnRun!.duration.doubleValue / badgeEarnStatus.earnRun!.distance.doubleValue)
      goldLabel.text = "Pace < \(paceQuantity.description) for gold!"
    }

    if let bestRun = badgeEarnStatus.bestRun {
      let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
      let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: bestRun.duration.doubleValue / bestRun.distance.doubleValue)
      bestLabel.text = "Best: \(paceQuantity.description), \(formatter.stringFromDate(bestRun.timestamp))"
    }
  }

这段代码设置了 badge image和相关的label中的数据

最有趣的部分是鼓励用户怎么样获得更高级别的奖励,这些鼓励会增加你的积极性,因为它需要更快地跑步记录

最后,添加这个方法

@IBAction func infoButtonPressed(sender: AnyObject) {
    UIAlertView(title: badgeEarnStatus.badge.name!,
      message: badgeEarnStatus.badge.information!,
      delegate: nil,
      cancelButtonTitle: "OK").show()
  }

当用户点击info按钮的时候会来到这里,将会显示badge的信息

现在详情页设置完毕了,你还需要确保在segue之前badges table view能够发送badge信息

打开BadgesTableViewController.swift 给 BadgesTableViewController添加如下方法


override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.destinationViewController.isKindOfClass(BadgeDetailsViewController) {
      let badgeDetailsViewController = segue.destinationViewController as! BadgeDetailsViewController
      let badgeEarnStatus = badgeEarnStatusesArray[tableView.indexPathForSelectedRow()!.row]
      badgeDetailsViewController.badgeEarnStatus = badgeEarnStatus
    }
  }

当cell被点击的时候,BadgesDetailsViewController能展示相关的BadgeEarnStatus

现在,UI部分设置完毕了,打开Main.storyb做如下链接

Badge Motivation

作为徽章奖励系统新的一部分,你需要回到UI部分,把它纳入之前的徽章体系中

打开Main.storyboard,找到new Run场景,在stop按钮的上方添加一个UIImageView和一个UILabel

为UIImageView,使用自动布局设置约束

为UILabel,使用自动布局设置约束

新的界面长长这样

新的view在start按钮的地方会有部分重叠,但是在开始跑步以后start按钮会隐藏起来只显示另外两个控件

在跑步时会使用“carrot-on-a-stick”方式激励用户,会显示一个山峰的样子来描述你离下一个级别的奖励还有都少差距

在显示UI之前,你需要添加两个方法给 BadgeController 来决定你最好在在这次可以拿到某个奖励然后再下一次就可以拿到另外一个奖励

打开 Badge.swift 给 BadgeController添加以下方法

func bestBadgeForDistance(distance: Double) -> Badge {
    var bestBadge = badges.first as Badge!
    for badge in badges {
      if distance < badge.distance {
        break
      }
      bestBadge = badge
    }
    return bestBadge
  }

  func nextBadgeForDistance(distance: Double) -> Badge {
    var nextBadge = badges.first as Badge!
    for badge in badges {
      nextBadge = badge
      if distance < badge.distance {
        break
      }
    }
    return nextBadge
  }

这个很简单,只要你输入距离,就会返回

打开NewRunViewController.swift在顶部导入

import AudioToolbox

导入AudioToolbox之后你就能在用户每次获得新奖励的时候播放音效

接下来,为NewRunViewController 添加以下属性

var upcomingBadge : Badge?
  @IBOutlet weak var nextBadgeLabel: UILabel!
  @IBOutlet weak var nextBadgeImageView: UIImageView!

在viewWillAppear(_:)方法结尾处添加

nextBadgeLabel.hidden = true
nextBadgeImageView.hidden = true

badge label和badge image 一开始是需要隐藏的

给 startPressed(_:)方法结尾处添加

nextBadgeLabel.hidden = false
nextBadgeImageView.hidden = false

让 badge label 和 badge image 在跑步开始后显示

添加下面两个方法

func playSuccessSound() {
    let soundURL = NSBundle.mainBundle().URLForResource("success", withExtension: "wav")
    var soundID : SystemSoundID = 0
    AudioServicesCreateSystemSoundID(soundURL, &soundID)
    AudioServicesPlaySystemSound(soundID)

    //also vibrate
    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate));
  }

  func checkNextBadge() {
    let nextBadge = BadgeController.sharedController.nextBadgeForDistance(distance)

    if let upcomingBadge = upcomingBadge {
      if upcomingBadge.name! != nextBadge.name! {
        playSuccessSound()
      }
    }
    
    upcomingBadge = nextBadge
  }

第一个方法播放音效的时候也会产生震动,以便在嘈杂的环境里通知用户或者防止播放音乐的过程中无法听到音效

当用户满足获得一个奖励的条件的时候会调用第二个方法检测这次获得的奖励是不是上一次获奖时记录的下一次即将获得的那个奖励,如果是,允许播放音效,并且把下一次即将要获得的奖励保存下来

为 eachSecond(_:)方法添加

checkNextBadge()
    if let upcomingBadge = upcomingBadge {
      let nextBadgeDistanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: upcomingBadge.distance! - distance)
      nextBadgeLabel.text = "\(nextBadgeDistanceQuantity.description) until \(upcomingBadge.name!)"
      nextBadgeImageView.image = UIImage(named: upcomingBadge.imageName!)
    }

这段代码可以让 nextBadgeLabel 和 nextBadgeImageView 在跑步的过程中持续更新

编译运行,start a new run

你可以看到label和image在不断更新

Where to go From Here

恭喜你!

完成了一个可以在跑步过程中实时记录运行轨迹并且有成就激励系统的App

你可以在这里下载完整代码

http://cdn3.raywenderlich.com/wp-content/uploads/2015/05/MoonRunner-Part2-Final.zip

根据这两篇教程,你做了一个app

这个app只是完成了这类app的基础功能,要让跟多的人使用你的app你还需要做更多地完善,成就奖励是一个“游戏化”app很好的方式

如果你想让你的app有进一步的提升,你还需要做

end

上一篇下一篇

猜你喜欢

热点阅读