版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.18 星期四 |
前言
很多的app都有定位功能,比如说滴滴,美团等,他们都需要获取客户所在的位置,并且根据位置推送不同的模块数据以及服务,可以说,定位方便了我们的生活,接下来这几篇我们就说一下定位框架
CoreLocation
。感兴趣的可以看我写的上面几篇。
1. CoreLocation框架详细解析 —— 基本概览(一)
2. CoreLocation框架详细解析 —— 选择定位服务的授权级别(二)
3. CoreLocation框架详细解析 —— 确定定位服务的可用性(三)
4. CoreLocation框架详细解析 —— 获取用户位置(四)
5. CoreLocation框架详细解析 —— 监控用户与地理区域的距离(五)
6. CoreLocation框架详细解析 —— 确定接近iBeacon(六)
7. CoreLocation框架详细解析 —— 将iOS设备转换为iBeacon(七)
8. CoreLocation框架详细解析 —— 获取指向和路线信息(八)
9. CoreLocation框架详细解析 —— 在坐标和用户友好的地名之间转换(九)
10. CoreLocation框架详细解析(十) —— 跟踪访问位置简单示例(一)
11. CoreLocation框架详细解析(十一) —— 跟踪访问位置简单示例(二)
12. CoreLocation框架详细解析(十二) —— 仿Runkeeper的简单实现(一)
13. CoreLocation框架详细解析(十三) —— 仿Runkeeper的简单实现(二)
14. CoreLocation框架详细解析(十四) —— 仿Runkeeper的简单实现(三)
Everything Is Better When it Has a "Space Mode"
跑步结束后,为您的用户提供查看他们获得的最后一个徽章的能力会很不错。
打开Main.storyboard
并找到Run Details View Controller Scene
。 在地图视图的顶部拖动UIImageView
。 按住control键从图像视图控制拖动到地图视图。 在弹出窗口中,按住Shift键并选择Top, Bottom, Leading and Trailing
。 单击Add Constraints
以将Image View
的边缘固定到Map View
的边缘。
Xcode将添加约束,每个约束的值为0,这正是您想要的。 但是,目前,图像视图并未完全覆盖地图视图,因此您可以看到橙色警告线。 单击Update Frames
按钮(下面的红色部分)以调整图像视图的大小。
将UIButton
拖到Image View的顶部。 删除Button的Title
并将其Image值设置为info
。
按住Ctrl键并从按钮拖动到Image View
。 在弹出窗口中,按住Shift键并选择Bottom and Trailing
。 单击Add Constraints
将按钮固定到图像视图的右下角。
在Size Inspector
中,Edit
每个约束并将其值设置为-8
。
再次单击Update Frames
按钮以固定Button的大小和位置。
选择Image View并将其Content Mode
设置为Aspect Fit
,将其Alpha
设置为0。
选择Button并将其Alpha设置为0。
注意:您正在使用Alpha属性而不是Hidden属性隐藏这些视图,因为您要将它们动画到视图中以获得更流畅的用户体验。
将UISwitch
和UILabel
拖动到视图的右下角。
选择Switch
并按Add New Contraints
按钮(“Tie Fighter”按钮)。 使用值8为Right,Bottom和Left添加约束。确保Left约束相对于Label。 选择Add 3 Constraints
。
将Switch的Value设置为Off。
按住ctrl键从Switch到Label。 在弹出的结果中,选择Center Vertically
。
选择Label,将其Title设置为SPACE MODE
,将其Color设置为White Color
。
在文档大纲中,按住Control键从Switch到Stack View
。 从弹出的结果中选择Vertical Spacing
。
在Switch的Size Inspector
中,编辑约束Top Space to: Stack View
。 将其关系设置为≥
,将其值设置为8
。
在完成所有布局工作后,您应该获得徽章!
在Assistant Editor
中打开RunDetailsViewController.swift
,并按如下方式连接Image View
和 Info Button
的outlets :
@IBOutlet weak var badgeImageView: UIImageView!
@IBOutlet weak var badgeInfoButton: UIButton!
为Switch添加以下action并连接它:
@IBAction func displayModeToggled(_ sender: UISwitch) {
UIView.animate(withDuration: 0.2) {
self.badgeImageView.alpha = sender.isOn ? 1 : 0
self.badgeInfoButton.alpha = sender.isOn ? 1 : 0
self.mapView.alpha = sender.isOn ? 0 : 1
}
}
当切换值更改时,您可以通过更改其Alpha值来动画设置Image View
, Info Button
和 Map View
的可见性。
现在为Info Button
添加action
并连接它:
@IBAction func infoButtonTapped() {
let badge = Badge.best(for: run.distance)
let alert = UIAlertController(title: badge.name,
message: badge.information,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true)
}
这与您在BadgeDetailsViewController.swift
中实现的按钮处理程序完全相同。
最后一步是将以下内容添加到configureView()
的末尾:
let badge = Badge.best(for: run.distance)
badgeImageView.image = UIImage(named: badge.imageName)
您可以找到用户在跑步中获得的最后一个徽章并将其设置为显示。
Build并运行。 在跑步中发送模拟器,保存详细信息并尝试新的“Space Mode”
!
Mapping the Solar System In Your Town
跑步后的地图已经帮助您记住您的路线,甚至可以确定速度较低的特定区域。 现在,您将添加一项功能,准确显示每个徽章的获取位置。
MapKit使用annotations
来显示诸如此类的点数据。 要创建annotations
,您需要:
- 遵循
MKAnnotation
的类,它提供描述annotations
位置的坐标。 -
MKAnnotationView
的子类,显示与annotations
关联的信息。
要实现这一点,您将:
- 创建遵循
MKAnnotation
的类BadgeAnnotation
。 - 创建一个
BadgeAnnotation
对象数组并将它们添加到地图中。 - 实现
mapView(_:viewFor :)
以创建MKAnnotationViews
。
将新的Swift文件添加到项目中并将其命名为BadgeAnnotation.swift
。 将其内容替换为:
import MapKit
class BadgeAnnotation: MKPointAnnotation {
let imageName: String
init(imageName: String) {
self.imageName = imageName
super.init()
}
}
MKPointAnnotation
符合MKAnnotation
,因此您只需要将图像名称传递给渲染系统。
打开RunDetailsViewController.swift
并添加这个新方法:
private func annotations() -> [BadgeAnnotation] {
var annotations: [BadgeAnnotation] = []
let badgesEarned = Badge.allBadges.filter { $0.distance < run.distance }
var badgeIterator = badgesEarned.makeIterator()
var nextBadge = badgeIterator.next()
let locations = run.locations?.array as! [Location]
var distance = 0.0
for (first, second) in zip(locations, locations.dropFirst()) {
guard let badge = nextBadge else { break }
let start = CLLocation(latitude: first.latitude, longitude: first.longitude)
let end = CLLocation(latitude: second.latitude, longitude: second.longitude)
distance += end.distance(from: start)
if distance >= badge.distance {
let badgeAnnotation = BadgeAnnotation(imageName: badge.imageName)
badgeAnnotation.coordinate = end.coordinate
badgeAnnotation.title = badge.name
badgeAnnotation.subtitle = FormatDisplay.distance(badge.distance)
annotations.append(badgeAnnotation)
nextBadge = badgeIterator.next()
}
}
return annotations
}
这将创建一个BadgeAnnotation
对象数组,每个对象用于跑步时获得的每个徽章。
在loadMap()
的末尾添加以下内容:
mapView.addAnnotations(annotations())
这会将annotations
放在地图上。
最后,将此方法添加到MKMapViewDelegate
扩展:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? BadgeAnnotation else { return nil }
let reuseID = "checkpoint"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseID)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseID)
annotationView?.image = #imageLiteral(resourceName: "mapPin")
annotationView?.canShowCallout = true
}
annotationView?.annotation = annotation
let badgeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
badgeImageView.image = UIImage(named: annotation.imageName)
badgeImageView.contentMode = .scaleAspectFit
annotationView?.leftCalloutAccessoryView = badgeImageView
return annotationView
}
在这里,您为每个annotation
创建一个MKAnnotationView
,并将其配置为显示徽章的图像。
Build并运行。 在跑步中发送模拟器并在结束时保存跑步。 现在,地图将为每个获得的徽章添加annotation
。 单击一个,您可以看到它的名称,图片和距离。
源码
1. Swift
首先看一下组织结构
看一下sb的内容
下面看一下源代码
1. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UINavigationBar.appearance().tintColor = .white
UINavigationBar.appearance().barTintColor = .black
let locationManager = LocationManager.shared
locationManager.requestWhenInUseAuthorization()
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
CoreDataStack.saveContext()
}
func applicationWillTerminate(_ application: UIApplication) {
CoreDataStack.saveContext()
}
}
2. HomeViewController.swift
import UIKit
class HomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
3. NewRunViewController.swift
import UIKit
import CoreLocation
import MapKit
import AVFoundation
class NewRunViewController: UIViewController {
@IBOutlet weak var launchPromptStackView: UIStackView!
@IBOutlet weak var dataStackView: UIStackView!
@IBOutlet weak var startButton: UIButton!
@IBOutlet weak var stopButton: UIButton!
@IBOutlet weak var distanceLabel: UILabel!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var paceLabel: UILabel!
@IBOutlet weak var mapContainerView: UIView!
@IBOutlet weak var mapView: MKMapView!
@IBOutlet weak var badgeStackView: UIStackView!
@IBOutlet weak var badgeImageView: UIImageView!
@IBOutlet weak var badgeInfoLabel: UILabel!
private var run: Run?
private let locationManager = LocationManager.shared
private var seconds = 0
private var timer: Timer?
private var distance = Measurement(value: 0, unit: UnitLength.meters)
private var locationList: [CLLocation] = []
private var upcomingBadge: Badge!
private let successSound: AVAudioPlayer = {
guard let successSound = NSDataAsset(name: "success") else {
return AVAudioPlayer()
}
return try! AVAudioPlayer(data: successSound.data)
}()
override func viewDidLoad() {
super.viewDidLoad()
dataStackView.isHidden = true // required to work around behavior change in Xcode 9 beta 1
badgeStackView.isHidden = true // required to work around behavior change in Xcode 9 beta 1
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
locationManager.stopUpdatingLocation()
}
@IBAction func startTapped() {
startRun()
}
@IBAction func stopTapped() {
let alertController = UIAlertController(title: "End run?",
message: "Do you wish to end your run?",
preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertController.addAction(UIAlertAction(title: "Save", style: .default) { _ in
self.stopRun()
self.saveRun()
self.performSegue(withIdentifier: .details, sender: nil)
})
alertController.addAction(UIAlertAction(title: "Discard", style: .destructive) { _ in
self.stopRun()
_ = self.navigationController?.popToRootViewController(animated: true)
})
present(alertController, animated: true)
}
private func startRun() {
launchPromptStackView.isHidden = true
dataStackView.isHidden = false
startButton.isHidden = true
stopButton.isHidden = false
mapContainerView.isHidden = false
mapView.removeOverlays(mapView.overlays)
seconds = 0
distance = Measurement(value: 0, unit: UnitLength.meters)
locationList.removeAll()
badgeStackView.isHidden = false
upcomingBadge = Badge.next(for: 0)
badgeImageView.image = UIImage(named: upcomingBadge.imageName)
updateDisplay()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.eachSecond()
}
startLocationUpdates()
}
private func stopRun() {
launchPromptStackView.isHidden = false
dataStackView.isHidden = true
startButton.isHidden = false
stopButton.isHidden = true
mapContainerView.isHidden = true
badgeStackView.isHidden = true
locationManager.stopUpdatingLocation()
}
func eachSecond() {
seconds += 1
checkNextBadge()
updateDisplay()
}
private func updateDisplay() {
let formattedDistance = FormatDisplay.distance(distance)
let formattedTime = FormatDisplay.time(seconds)
let formattedPace = FormatDisplay.pace(distance: distance,
seconds: seconds,
outputUnit: UnitSpeed.minutesPerMile)
distanceLabel.text = "Distance: \(formattedDistance)"
timeLabel.text = "Time: \(formattedTime)"
paceLabel.text = "Pace: \(formattedPace)"
let distanceRemaining = upcomingBadge.distance - distance.value
let formattedDistanceRemaining = FormatDisplay.distance(distanceRemaining)
badgeInfoLabel.text = "\(formattedDistanceRemaining) until \(upcomingBadge.name)"
}
private func startLocationUpdates() {
locationManager.delegate = self
locationManager.activityType = .fitness
locationManager.distanceFilter = 10
locationManager.startUpdatingLocation()
}
private func saveRun() {
let newRun = Run(context: CoreDataStack.context)
newRun.distance = distance.value
newRun.duration = Int16(seconds)
newRun.timestamp = Date()
for location in locationList {
let locationObject = Location(context: CoreDataStack.context)
locationObject.timestamp = location.timestamp
locationObject.latitude = location.coordinate.latitude
locationObject.longitude = location.coordinate.longitude
newRun.addToLocations(locationObject)
}
CoreDataStack.saveContext()
run = newRun
}
private func checkNextBadge() {
let nextBadge = Badge.next(for: distance.value)
if upcomingBadge != nextBadge {
badgeImageView.image = UIImage(named: nextBadge.imageName)
upcomingBadge = nextBadge
successSound.play()
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
}
}
}
// MARK: - Navigation
extension NewRunViewController: SegueHandlerType {
enum SegueIdentifier: String {
case details = "RunDetailsViewController"
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segueIdentifier(for: segue) {
case .details:
let destination = segue.destination as! RunDetailsViewController
destination.run = run
}
}
}
// MARK: - Location Manager Delegate
extension NewRunViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for newLocation in locations {
let howRecent = newLocation.timestamp.timeIntervalSinceNow
guard newLocation.horizontalAccuracy < 20 && abs(howRecent) < 10 else { continue }
if let lastLocation = locationList.last {
let delta = newLocation.distance(from: lastLocation)
distance = distance + Measurement(value: delta, unit: UnitLength.meters)
let coordinates = [lastLocation.coordinate, newLocation.coordinate]
mapView.add(MKPolyline(coordinates: coordinates, count: 2))
let region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 500, 500)
mapView.setRegion(region, animated: true)
}
locationList.append(newLocation)
}
}
}
// MARK: - Map View Delegate
extension NewRunViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard let polyline = overlay as? MKPolyline else {
return MKOverlayRenderer(overlay: overlay)
}
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .blue
renderer.lineWidth = 3
return renderer
}
}
4. RunDetailsViewController.swift
import UIKit
import MapKit
class RunDetailsViewController: UIViewController {
@IBOutlet weak var mapView: MKMapView!
@IBOutlet weak var distanceLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var paceLabel: UILabel!
@IBOutlet weak var badgeImageView: UIImageView!
@IBOutlet weak var badgeInfoButton: UIButton!
var run: Run!
override func viewDidLoad() {
super.viewDidLoad()
configureView()
}
@IBAction func displayModeToggled(_ sender: UISwitch) {
UIView.animate(withDuration: 0.2) {
self.badgeImageView.alpha = sender.isOn ? 1 : 0
self.badgeInfoButton.alpha = sender.isOn ? 1 : 0
self.mapView.alpha = sender.isOn ? 0 : 1
}
}
@IBAction func infoButtonTapped() {
let badge = Badge.best(for: run.distance)
let alert = UIAlertController(title: badge.name,
message: badge.information,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true)
}
private func configureView() {
let distance = Measurement(value: run.distance, unit: UnitLength.meters)
let seconds = Int(run.duration)
let formattedDistance = FormatDisplay.distance(distance)
let formattedDate = FormatDisplay.date(run.timestamp)
let formattedTime = FormatDisplay.time(seconds)
let formattedPace = FormatDisplay.pace(distance: distance,
seconds: seconds,
outputUnit: UnitSpeed.minutesPerMile)
distanceLabel.text = "Distance: \(formattedDistance)"
dateLabel.text = formattedDate
timeLabel.text = "Time: \(formattedTime)"
paceLabel.text = "Pace: \(formattedPace)"
loadMap()
let badge = Badge.best(for: run.distance)
badgeImageView.image = UIImage(named: badge.imageName)
}
private func mapRegion() -> MKCoordinateRegion? {
guard
let locations = run.locations,
locations.count > 0
else {
return nil
}
let latitudes = locations.map { location -> Double in
let location = location as! Location
return location.latitude
}
let longitudes = locations.map { location -> Double in
let location = location as! Location
return location.longitude
}
let maxLat = latitudes.max()!
let minLat = latitudes.min()!
let maxLong = longitudes.max()!
let minLong = longitudes.min()!
let center = CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2,
longitude: (minLong + maxLong) / 2)
let span = MKCoordinateSpan(latitudeDelta: (maxLat - minLat) * 1.3,
longitudeDelta: (maxLong - minLong) * 1.3)
return MKCoordinateRegion(center: center, span: span)
}
private func polyLine() -> [MulticolorPolyline] {
// 1
let locations = run.locations?.array as! [Location]
var coordinates: [(CLLocation, CLLocation)] = []
var speeds: [Double] = []
var minSpeed = Double.greatestFiniteMagnitude
var maxSpeed = 0.0
// 2
for (first, second) in zip(locations, locations.dropFirst()) {
let start = CLLocation(latitude: first.latitude, longitude: first.longitude)
let end = CLLocation(latitude: second.latitude, longitude: second.longitude)
coordinates.append((start, end))
//3
let distance = end.distance(from: start)
let time = second.timestamp!.timeIntervalSince(first.timestamp! as Date)
let speed = time > 0 ? distance / time : 0
speeds.append(speed)
minSpeed = min(minSpeed, speed)
maxSpeed = max(maxSpeed, speed)
}
//4
let midSpeed = speeds.reduce(0, +) / Double(speeds.count)
//5
var segments: [MulticolorPolyline] = []
for ((start, end), speed) in zip(coordinates, speeds) {
let coords = [start.coordinate, end.coordinate]
let segment = MulticolorPolyline(coordinates: coords, count: 2)
segment.color = segmentColor(speed: speed,
midSpeed: midSpeed,
slowestSpeed: minSpeed,
fastestSpeed: maxSpeed)
segments.append(segment)
}
return segments
}
private func loadMap() {
guard
let locations = run.locations,
locations.count > 0,
let region = mapRegion()
else {
let alert = UIAlertController(title: "Error",
message: "Sorry, this run has no locations saved",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true)
return
}
mapView.setRegion(region, animated: true)
mapView.addOverlays(polyLine())
mapView.addAnnotations(annotations())
}
private func segmentColor(speed: Double, midSpeed: Double, slowestSpeed: Double, fastestSpeed: Double) -> UIColor {
enum BaseColors {
static let r_red: CGFloat = 1
static let r_green: CGFloat = 20 / 255
static let r_blue: CGFloat = 44 / 255
static let y_red: CGFloat = 1
static let y_green: CGFloat = 215 / 255
static let y_blue: CGFloat = 0
static let g_red: CGFloat = 0
static let g_green: CGFloat = 146 / 255
static let g_blue: CGFloat = 78 / 255
}
let red, green, blue: CGFloat
if speed < midSpeed {
let ratio = CGFloat((speed - slowestSpeed) / (midSpeed - slowestSpeed))
red = BaseColors.r_red + ratio * (BaseColors.y_red - BaseColors.r_red)
green = BaseColors.r_green + ratio * (BaseColors.y_green - BaseColors.r_green)
blue = BaseColors.r_blue + ratio * (BaseColors.y_blue - BaseColors.r_blue)
} else {
let ratio = CGFloat((speed - midSpeed) / (fastestSpeed - midSpeed))
red = BaseColors.y_red + ratio * (BaseColors.g_red - BaseColors.y_red)
green = BaseColors.y_green + ratio * (BaseColors.g_green - BaseColors.y_green)
blue = BaseColors.y_blue + ratio * (BaseColors.g_blue - BaseColors.y_blue)
}
return UIColor(red: red, green: green, blue: blue, alpha: 1)
}
private func annotations() -> [BadgeAnnotation] {
var annotations: [BadgeAnnotation] = []
let badgesEarned = Badge.allBadges.filter { $0.distance < run.distance }
var badgeIterator = badgesEarned.makeIterator()
var nextBadge = badgeIterator.next()
let locations = run.locations?.array as! [Location]
var distance = 0.0
for (first, second) in zip(locations, locations.dropFirst()) {
guard let badge = nextBadge else { break }
let start = CLLocation(latitude: first.latitude, longitude: first.longitude)
let end = CLLocation(latitude: second.latitude, longitude: second.longitude)
distance += end.distance(from: start)
if distance >= badge.distance {
let badgeAnnotation = BadgeAnnotation(imageName: badge.imageName)
badgeAnnotation.coordinate = end.coordinate
badgeAnnotation.title = badge.name
badgeAnnotation.subtitle = FormatDisplay.distance(badge.distance)
annotations.append(badgeAnnotation)
nextBadge = badgeIterator.next()
}
}
return annotations
}
}
// MARK: - Map View Delegate
extension RunDetailsViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard let polyline = overlay as? MulticolorPolyline else {
return MKOverlayRenderer(overlay: overlay)
}
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = polyline.color
renderer.lineWidth = 3
return renderer
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? BadgeAnnotation else { return nil }
let reuseID = "checkpoint"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseID)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseID)
annotationView?.image = #imageLiteral(resourceName: "mapPin")
annotationView?.canShowCallout = true
}
annotationView?.annotation = annotation
let badgeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
badgeImageView.image = UIImage(named: annotation.imageName)
badgeImageView.contentMode = .scaleAspectFit
annotationView?.leftCalloutAccessoryView = badgeImageView
return annotationView
}
}
5. BadgesTableViewController.swift
import UIKit
import CoreData
class BadgesTableViewController: UITableViewController {
var statusList: [BadgeStatus]!
override func viewDidLoad() {
super.viewDidLoad()
statusList = BadgeStatus.badgesEarned(runs: getRuns())
}
private func getRuns() -> [Run] {
let fetchRequest: NSFetchRequest<Run> = Run.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: #keyPath(Run.timestamp), ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
return try CoreDataStack.context.fetch(fetchRequest)
} catch {
return []
}
}
}
// MARK: - Table View Data Source
extension BadgesTableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return statusList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: BadgeCell = tableView.dequeueReusableCell(for: indexPath)
cell.status = statusList[indexPath.row]
return cell
}
}
// MARK: - Navigation
extension BadgesTableViewController: SegueHandlerType {
enum SegueIdentifier: String {
case details = "BadgeDetailsViewController"
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segueIdentifier(for: segue) {
case .details:
let destination = segue.destination as! BadgeDetailsViewController
let indexPath = tableView.indexPathForSelectedRow!
destination.status = statusList[indexPath.row]
}
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
guard let segue = SegueIdentifier(rawValue: identifier) else { return false }
switch segue {
case .details:
guard let cell = sender as? UITableViewCell else { return false }
return cell.accessoryType == .disclosureIndicator
}
}
}
6. BadgeDetailsViewController.swift
import UIKit
class BadgeDetailsViewController: UIViewController {
@IBOutlet weak var badgeImageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var distanceLabel: UILabel!
@IBOutlet weak var earnedLabel: UILabel!
@IBOutlet weak var bestLabel: UILabel!
@IBOutlet weak var silverLabel: UILabel!
@IBOutlet weak var goldLabel: UILabel!
@IBOutlet weak var silverImageView: UIImageView!
@IBOutlet weak var goldImageView: UIImageView!
var status: BadgeStatus!
override func viewDidLoad() {
super.viewDidLoad()
let badgeRotation = CGAffineTransform(rotationAngle: .pi / 8)
badgeImageView.image = UIImage(named: status.badge.imageName)
nameLabel.text = status.badge.name
distanceLabel.text = FormatDisplay.distance(status.badge.distance)
let earnedDate = FormatDisplay.date(status.earned?.timestamp)
earnedLabel.text = "Reached on \(earnedDate)"
let bestDistance = Measurement(value: status.best!.distance, unit: UnitLength.meters)
let bestPace = FormatDisplay.pace(distance: bestDistance,
seconds: Int(status.best!.duration),
outputUnit: UnitSpeed.minutesPerMile)
let bestDate = FormatDisplay.date(status.earned?.timestamp)
bestLabel.text = "Best: \(bestPace), \(bestDate)"
let earnedDistance = Measurement(value: status.earned!.distance, unit: UnitLength.meters)
let earnedDuration = Int(status.earned!.duration)
if let silver = status.silver {
silverImageView.transform = badgeRotation
silverImageView.alpha = 1
let silverDate = FormatDisplay.date(silver.timestamp)
silverLabel.text = "Earned on \(silverDate)"
} else {
silverImageView.alpha = 0
let silverDistance = earnedDistance * BadgeStatus.silverMultiplier
let pace = FormatDisplay.pace(distance: silverDistance,
seconds: earnedDuration,
outputUnit: UnitSpeed.minutesPerMile)
silverLabel.text = "Pace < \(pace) for silver!"
}
if let gold = status.gold {
goldImageView.transform = badgeRotation
goldImageView.alpha = 1
let goldDate = FormatDisplay.date(gold.timestamp)
goldLabel.text = "Earned on \(goldDate)"
} else {
goldImageView.alpha = 0
let goldDistance = earnedDistance * BadgeStatus.goldMultiplier
let pace = FormatDisplay.pace(distance: goldDistance,
seconds: earnedDuration,
outputUnit: UnitSpeed.minutesPerMile)
goldLabel.text = "Pace < \(pace) for gold!"
}
}
@IBAction func infoButtonTapped() {
let alert = UIAlertController(title: status.badge.name,
message: status.badge.information,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true)
}
}
7. CoreDataStack.swift
import CoreData
class CoreDataStack {
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MoonRunner")
container.loadPersistentStores { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
static var context: NSManagedObjectContext { return persistentContainer.viewContext }
class func saveContext () {
let context = persistentContainer.viewContext
guard context.hasChanges else {
return
}
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
8. Badge.swift
import Foundation
struct Badge {
let name: String
let imageName: String
let information: String
let distance: Double
init?(from dictionary: [String: String]) {
guard
let name = dictionary["name"],
let imageName = dictionary["imageName"],
let information = dictionary["information"],
let distanceString = dictionary["distance"],
let distance = Double(distanceString)
else {
return nil
}
self.name = name
self.imageName = imageName
self.information = information
self.distance = distance
}
static let allBadges: [Badge] = {
guard let fileURL = Bundle.main.url(forResource: "badges", withExtension: "txt") else {
fatalError("No badges.txt file found")
}
do {
let jsonData = try Data(contentsOf: fileURL, options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: jsonData) as! [[String: String]]
return jsonResult.flatMap(Badge.init)
} catch {
fatalError("Cannot decode badges.txt")
}
}()
static func best(for distance: Double) -> Badge {
return allBadges.filter { $0.distance < distance }.last ?? allBadges.first!
}
static func next(for distance: Double) -> Badge {
return allBadges.filter { distance < $0.distance }.first ?? allBadges.last!
}
}
// MARK: - Equatable
extension Badge: Equatable {
static func ==(lhs: Badge, rhs: Badge) -> Bool {
return lhs.name == rhs.name
}
}
9. BadgeStatus.swift
import Foundation
struct BadgeStatus {
let badge: Badge
let earned: Run?
let silver: Run?
let gold: Run?
let best: Run?
static let silverMultiplier = 1.05
static let goldMultiplier = 1.1
static func badgesEarned(runs: [Run]) -> [BadgeStatus] {
return Badge.allBadges.map { badge in
var earned: Run?
var silver: Run?
var gold: Run?
var best: Run?
for run in runs where run.distance > badge.distance {
if earned == nil {
earned = run
}
let earnedSpeed = earned!.distance / Double(earned!.duration)
let runSpeed = run.distance / Double(run.duration)
if silver == nil && runSpeed > earnedSpeed * silverMultiplier {
silver = run
}
if gold == nil && runSpeed > earnedSpeed * goldMultiplier {
gold = run
}
if let existingBest = best {
let bestSpeed = existingBest.distance / Double(existingBest.duration)
if runSpeed > bestSpeed {
best = run
}
} else {
best = run
}
}
return BadgeStatus(badge: badge, earned: earned, silver: silver, gold: gold, best: best)
}
}
}
10. BadgeCell.swift
import UIKit
class BadgeCell: UITableViewCell {
@IBOutlet weak var badgeImageView: UIImageView!
@IBOutlet weak var silverImageView: UIImageView!
@IBOutlet weak var goldImageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var earnedLabel: UILabel!
var status: BadgeStatus! {
didSet {
configure()
}
}
private let redLabel = #colorLiteral(red: 1, green: 0.07843137255, blue: 0.1725490196, alpha: 1)
private let greenLabel = #colorLiteral(red: 0, green: 0.5725490196, blue: 0.3058823529, alpha: 1)
private let badgeRotation = CGAffineTransform(rotationAngle: .pi / 8)
private func configure() {
silverImageView.isHidden = status.silver == nil
goldImageView.isHidden = status.gold == nil
if let earned = status.earned {
nameLabel.text = status.badge.name
nameLabel.textColor = greenLabel
let dateEarned = FormatDisplay.date(earned.timestamp)
earnedLabel.text = "Earned: \(dateEarned)"
earnedLabel.textColor = greenLabel
badgeImageView.image = UIImage(named: status.badge.imageName)
silverImageView.transform = badgeRotation
goldImageView.transform = badgeRotation
isUserInteractionEnabled = true
accessoryType = .disclosureIndicator
} else {
nameLabel.text = "?????"
nameLabel.textColor = redLabel
let formattedDistance = FormatDisplay.distance(status.badge.distance)
earnedLabel.text = "Run \(formattedDistance) to earn"
earnedLabel.textColor = redLabel
badgeImageView.image = nil
isUserInteractionEnabled = false
accessoryType = .none
selectionStyle = .none
}
}
}
11. BadgeAnnotation.swift
import MapKit
class BadgeAnnotation: MKPointAnnotation {
let imageName: String
init(imageName: String) {
self.imageName = imageName
super.init()
}
}
12. StoryboardSupport.swift
import UIKit
protocol StoryboardIdentifiable {
static var storyboardIdentifier: String { get }
}
extension StoryboardIdentifiable where Self: UIViewController {
static var storyboardIdentifier: String {
return String(describing: self)
}
}
extension StoryboardIdentifiable where Self: UITableViewCell {
static var storyboardIdentifier: String {
return String(describing: self)
}
}
extension UIViewController: StoryboardIdentifiable { }
extension UITableViewCell: StoryboardIdentifiable { }
extension UITableView {
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withIdentifier: T.storyboardIdentifier, for: indexPath) as? T else {
fatalError("Could not find table view cell with identifier \(T.storyboardIdentifier)")
}
return cell
}
func cellForRow<T: UITableViewCell>(at indexPath: IndexPath) -> T {
guard let cell = cellForRow(at: indexPath) as? T else {
fatalError("Could not get cell as type \(T.self)")
}
return cell
}
}
/// Use in view controllers:
///
/// 1) Have view controller conform to SegueHandlerType
/// 2) Add `enum SegueIdentifier: String { }` to conformance
/// 3) Manual segues are trigged by `performSegue(with:sender:)`
/// 4) `prepare(for:sender:)` does a `switch segueIdentifier(for: segue)` to select the appropriate segue case
protocol SegueHandlerType {
associatedtype SegueIdentifier: RawRepresentable
}
extension SegueHandlerType where Self: UIViewController, SegueIdentifier.RawValue == String {
func performSegue(withIdentifier identifier: SegueIdentifier, sender: Any?) {
performSegue(withIdentifier: identifier.rawValue, sender: sender)
}
func segueIdentifier(for segue: UIStoryboardSegue) -> SegueIdentifier {
guard
let identifier = segue.identifier,
let segueIdentifier = SegueIdentifier(rawValue: identifier)
else {
fatalError("Invalid segue identifier: \(String(describing: segue.identifier))")
}
return segueIdentifier
}
}
13. UnitExtensions.swift
import Foundation
class UnitConverterPace: UnitConverter {
private let coefficient: Double
init(coefficient: Double) {
self.coefficient = coefficient
}
override func baseUnitValue(fromValue value: Double) -> Double {
return reciprocal(value * coefficient)
}
override func value(fromBaseUnitValue baseUnitValue: Double) -> Double {
return reciprocal(baseUnitValue * coefficient)
}
private func reciprocal(_ value: Double) -> Double {
guard value != 0 else { return 0 }
return 1.0 / value
}
}
extension UnitSpeed {
class var secondsPerMeter: UnitSpeed {
return UnitSpeed(symbol: "sec/m", converter: UnitConverterPace(coefficient: 1))
}
class var minutesPerKilometer: UnitSpeed {
return UnitSpeed(symbol: "min/km", converter: UnitConverterPace(coefficient: 60.0 / 1000.0))
}
class var minutesPerMile: UnitSpeed {
return UnitSpeed(symbol: "min/mi", converter: UnitConverterPace(coefficient: 60.0 / 1609.34))
}
}
14. FormatDisplay.swift
import Foundation
struct FormatDisplay {
static func distance(_ distance: Double) -> String {
let distanceMeasurement = Measurement(value: distance, unit: UnitLength.meters)
return FormatDisplay.distance(distanceMeasurement)
}
static func distance(_ distance: Measurement<UnitLength>) -> String {
let formatter = MeasurementFormatter()
return formatter.string(from: distance)
}
static func time(_ seconds: Int) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
return formatter.string(from: TimeInterval(seconds))!
}
static func pace(distance: Measurement<UnitLength>, seconds: Int, outputUnit: UnitSpeed) -> String {
let formatter = MeasurementFormatter()
formatter.unitOptions = [.providedUnit] // 1
let speedMagnitude = seconds != 0 ? distance.value / Double(seconds) : 0
let speed = Measurement(value: speedMagnitude, unit: UnitSpeed.metersPerSecond)
return formatter.string(from: speed.converted(to: outputUnit))
}
static func date(_ timestamp: Date?) -> String {
guard let timestamp = timestamp as Date? else { return "" }
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: timestamp)
}
}
15. LocationManager.swift
import CoreLocation
class LocationManager {
static let shared = CLLocationManager()
private init() { }
}
16. MulticolorPolyline.swift
import UIKit
import MapKit
class MulticolorPolyline: MKPolyline {
var color = UIColor.black
}
看一个简单的效果
后记
本篇主要讲述了仿Runkeeper的简单实现,感兴趣的给个赞或者关注~~~