美文网首页Swift🙎🏿个人喜欢,收藏Swift开发技巧
怎么样创建一个像RunKeeper一样的app(二)swift版

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

作者: 西木 | 来源:发表于2015-07-29 00:25 被阅读1167次

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

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

    简书: @西木

    微博: @角落里的monster

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

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

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

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

    • 使用Core Location记录轨迹

    • 持续更新你的轨迹并且显示平均速度等

    • 跑步完成时显示跑步区域的地图,轨迹曲线为彩色显示,速度慢得部分为红色,速度快的部分为绿色

    这个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做下面的事情

    • 绑定 BadgeCell 和 BadgesTableViewController
    • 脱线设置 name标签、Earned标签、头像icon和奖励标识如图所示

    如果你已经用过它来跑步的话,肯定已经获得了 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做如下链接

    • 绑定BadgeDetailsViewController
    • 为BadgeDetailsVireController设置badgeImageView,bestLabel,distanceLabel,earnedLabel,goldImageView,goldLabel,nameLabel,silverImageLabel,silverLabel
    • 为info按钮设置点击事件

    Badge Motivation

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

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

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

    • Align Center X to Superview
    • Width equals 70
    • Height equals 70
    • Align top with the Start button

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

    • Align Center X to Superview
    • Top Space to:UIImageView equals 10

    新的界面长长这样

    新的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
      }
    
    

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

    • bestBadgeForDistance(_:): 你目前能获得的奖励
    • nextBadgeForDistance(_:): 你下一个能够获得的奖励

    打开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

    • 用Core Location 测量你的轨迹
    • 显示跑步过程中的实时数据
    • 在地图上用不同颜色的曲线来标记你的轨迹和位置
    • 速度和距离的个人奖励系统

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

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

    • 显示用户的跑步历史记录
    • 将奖励和速度值标注在轨迹上
    • 将奖励和注释标注在地图上

    end

    相关文章

      网友评论

        本文标题:怎么样创建一个像RunKeeper一样的app(二)swift版

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