From f5af8cf6177ad8d695792f30ab06f73d94b2f386 Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Sun, 5 Aug 2018 07:54:08 -0600 Subject: [PATCH 1/8] After reading through the steps for writing Route Data to HealthKit (https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/creating_a_workout_route), I've added filtering to location updates to filter out any points that have a horizontal accuracy outside of 50 meters. --- GeoTrackKit/Core/GeoTrackManager.swift | 46 +++++++++++++++++++++----- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/GeoTrackKit/Core/GeoTrackManager.swift b/GeoTrackKit/Core/GeoTrackManager.swift index d6d43f9..2f78632 100644 --- a/GeoTrackKit/Core/GeoTrackManager.swift +++ b/GeoTrackKit/Core/GeoTrackManager.swift @@ -19,22 +19,34 @@ public class GeoTrackManager: NSObject { public static let shared: GeoTrackService = GeoTrackManager() // GeoTrackService stuff - internal var trackingState: GeoTrackState = .notTracking + var trackingState: GeoTrackState = .notTracking /// Your app's name - internal var appName: String = "No Application Name" + var appName: String = "No Application Name" // Other stuff - internal var locationManager: CLLocationManager? + var locationManager: CLLocationManager? + /// The last Geo Point to be tracked fileprivate(set) public var lastPoint: CLLocation? + /// Are we authorized for location tracking? fileprivate(set) public var authorized: Bool = false + /// The Track fileprivate(set) public var track: GeoTrack? /// When we startup, if we find points to be older than this threshold, we toss them away. /// Defaults to 5 seconds, but you can adjust this as you see fit. - static var oldPointThreshold: TimeInterval = 5 + public static var oldPointThreshold: TimeInterval = 5 + + /// Sets the locationManager instance and then configures it to the needs + /// of GeoTrackKit. + /// + /// - Parameter locationManager: The locationManager instance to set. + public func setLocationManager(_ locationManager: CLLocationManager?) { + self.locationManager = locationManager + configureLocationManager() + } } // MARK: - API @@ -133,7 +145,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { // Ensure that the first point is recent (not old points which we often get when tracking begins): if lastPoint == nil { - locations.forEach { (location) in + locations.filter({ $0.isAccurateEnough }).forEach { location in guard abs(location.timestamp.timeIntervalSinceNow) < GeoTrackManager.oldPointThreshold else { return } @@ -143,7 +155,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { return } } else { - recentLocations = locations + recentLocations = locations.filter { $0.isAccurateEnough } } GTDebug(message: "New Locations: \(recentLocations)") @@ -212,7 +224,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { // MARK: - Helpers -fileprivate extension GeoTrackManager { +private extension GeoTrackManager { /// Initializes the location manager and sets the preferences func initializeLocationManager() { @@ -221,6 +233,14 @@ fileprivate extension GeoTrackManager { } let locationManager = CLLocationManager() + setLocationManager(locationManager) + } + + /// Configures the locationManager + func configureLocationManager() { + guard let locationManager = locationManager else { + return + } locationManager.activityType = .fitness locationManager.desiredAccuracy = kCLLocationAccuracyBest @@ -231,8 +251,6 @@ fileprivate extension GeoTrackManager { locationManager.distanceFilter = 10 locationManager.allowsBackgroundLocationUpdates = true locationManager.delegate = self - - self.locationManager = locationManager } /// Handles requesting always authorization from location services @@ -265,6 +283,16 @@ fileprivate extension GeoTrackManager { } +// MARK: - CLLocation + +extension CLLocation { + + /// Is the accuracy of the point within the acceptable range? + var isAccurateEnough: Bool { + return horizontalAccuracy <= 50 + } +} + // MARK: - Notifications public extension Notification.Name { From a9e661f4c957473f07a5d92d56d3fa43263ad7c8 Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Sun, 5 Aug 2018 09:45:15 -0600 Subject: [PATCH 2/8] First pass at writing the data to HealthKit. It works, but it doesn't seem to be giving me everything I'd expect in the Activity App. --- .../Core/Map/UIModels/UIGeoTrack.swift | 30 ++++ GeoTrackKit/HealthKit/ActivityService.swift | 131 ++++++++++++++++-- .../project.pbxproj | 4 + .../GeoTrackKitExample/Info.plist | 2 + .../Views/ReferenceTrack/SaveTrackCell.swift | 22 +++ .../TrackMapViewController.swift | 2 + .../TrackOverviewTableViewController.swift | 40 +++++- .../Views/ReferenceTrack/TrackView.storyboard | 27 +++- .../TrackImportTableViewController.swift | 4 + 9 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/SaveTrackCell.swift diff --git a/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift b/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift index d8f74f9..528c9a3 100644 --- a/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift +++ b/GeoTrackKit/Core/Map/UIModels/UIGeoTrack.swift @@ -6,6 +6,7 @@ // Copyright © 2017 Eric Internicola. All rights reserved. // +import CoreLocation import Foundation /// A UI Model for a track. It keeps track of a Track (`GeoTrack`), a Track Analyzer (`GeoTrackAnalyzer`) and a collection of Legs (ascents, descents) that are currently visible @@ -86,6 +87,35 @@ public extension UIGeoTrack { return analyzer.legs } + /// Gets you an array of points for the provided leg + /// + /// - Parameter index: The index of the leg you want to get the points for. + /// - Returns: An array of CLLocation objects which are the points for the leg. + func getPoints(forLeg index: Int) -> [CLLocation]? { + guard index < allLegs.count else { + return nil + } + let leg = allLegs[index] + return getPoints(for: leg) + } + + /// Gets you an array of points for the provided leg + /// + /// - Parameter leg: The leg that you want the points for. + /// - Returns: An array of CLLocation objects which are teh points for the leg. + func getPoints(for leg: Leg) -> [CLLocation]? { + let range = leg.index...leg.endIndex + + return Array(track.points[range]) + } + + var startDate: Date? { + return track.points.first?.timestamp + } + + var endDate: Date? { + return track.points.last?.timestamp + } } // MARK: - Helpers diff --git a/GeoTrackKit/HealthKit/ActivityService.swift b/GeoTrackKit/HealthKit/ActivityService.swift index b66d828..ece10b4 100644 --- a/GeoTrackKit/HealthKit/ActivityService.swift +++ b/GeoTrackKit/HealthKit/ActivityService.swift @@ -29,12 +29,34 @@ public typealias TrackCallback = ([CLLocation]?, Error?) -> Void /// `HKWorkoutRoute` is a `HKSeriesSample` /// /// A typical workflow goes like this: -/// 1. Ensure we are authorized: `ActivityService.shared.authorize { //...` -/// 2. Query for the workouts: `ActivityService.shared.queryWorkouts { //...` -/// 3. Filter down to workouts that have Routes: `ActivityService.shared.queryRoute(from: workout) { // ...` -/// 4. Get the Track (Route) for a workout: `ActivityService.shared.queryTrack(from: workout) { // ...` +/// 1. Set Access Type: `` +/// 2. Ensure we are authorized: `ActivityService.shared.authorize { //...` +/// 3. Query for the workouts: `ActivityService.shared.queryWorkouts { //...` +/// 4. Filter down to workouts that have Routes: `ActivityService.shared.queryRoute(from: workout) { // ...` +/// 5. Get the Track (Route) for a workout: `ActivityService.shared.queryTrack(from: workout) { // ...` public class ActivityService { + /// Access modes for HealthKit data + /// + /// - readonly: I only wish to read data from health kit. + /// - writeonly: I only wish to write data to health kit. + /// - readwrite: I wish to read and write data to health kit. + public enum AccessType { + case readonly + case writeonly + case readwrite + + /// Does this access type have read access? + var readAccess: Bool { + return [AccessType.readonly, AccessType.readwrite].contains(self) + } + + /// Does this access type have write access? + var writeAccess: Bool { + return [AccessType.writeonly, AccessType.readwrite].contains(self) + } + } + /// Shared (singleton) instance of the `ActivityService` public static let shared = ActivityService() @@ -46,13 +68,15 @@ public class ActivityService { /// The HealthKitStore (if available) private var store = HKHealthStore() + /// The way in which you wish to access data with HealthKit. + public var accessType = AccessType.readwrite + } // MARK: - Authorization API extension ActivityService { - /// Request authorization for health kit data. /// /// - Parameter callback: the callback that will hand back a boolean (indicating success or failure) @@ -67,13 +91,15 @@ extension ActivityService { return callback(false, GeoTrackKitError.iOS11Required) } - let allTypes = Set([ + let workoutsWithRoutes = Set([ HKObjectType.workoutType(), - HKObjectType.activitySummaryType(), HKObjectType.seriesType(forIdentifier: HKWorkoutRouteTypeIdentifier)! ]) - store.requestAuthorization(toShare: nil, read: allTypes) { (success, error) in + let shareMode = accessType.writeAccess ? workoutsWithRoutes : nil + let readMode = accessType.readAccess ? workoutsWithRoutes : nil + + store.requestAuthorization(toShare: shareMode, read: readMode) { (success, error) in if let error = error { callback(success, error) return GTError(message: "Could not get health store authorization: \(error.localizedDescription)") @@ -90,7 +116,94 @@ extension ActivityService { } -// MARK: - Workout Queries +// MARK: - Write APIs + +extension ActivityService { + + /// Saves the provided track to HealthKit + /// + /// - Parameter model: the model that contains the points, legs, etc to be + /// saved in HealthKit. + public func saveTrack(_ model: UIGeoTrack, for activity: HKWorkoutActivityType) { + guard let startDate = model.startDate, let endDate = model.endDate, let distance = model.analyzer.stats?.totalDistance else { + return assertionFailure("No start / end date") + } + let elapsed = endDate.timeIntervalSince1970 - startDate.timeIntervalSince1970 + let workout = HKWorkout(activityType: activity, start: startDate, + end: endDate, + duration: elapsed, + totalEnergyBurned: nil, + totalDistance: HKQuantity(unit: HKUnit.meter(), doubleValue: distance), metadata: nil) + + store.save(workout) { (success, error) in + if let error = error { + return GTError(message: error.localizedDescription) + } + self.helpSaveTrack(model, workout: workout) + } + } + + private func helpSaveTrack(_ model: UIGeoTrack, workout: HKWorkout) { + var savedLegs = 0 + var failure = false + for leg in model.allLegs { + guard let points = model.getPoints(for: leg) else { + savedLegs += 1 + failure = true + continue + } + let builder = HKWorkoutRouteBuilder(healthStore: store, device: nil) + builder.insertRouteData(points) { (success, error) in + if let error = error { + savedLegs += 1 + failure = true + return GTError(message: error.localizedDescription) + } + + builder.finishRoute(with: workout, metadata: model.metadata(for: leg), completion: { (route, error) in + savedLegs += 1 + if let error = error { + failure = true + return GTError(message: error.localizedDescription) + } + + }) + } + } + } + +} + +// MARK: - UIGeoTrack extensions + +extension UIGeoTrack { + + func averageSpeed(for leg: Leg) -> CLLocationSpeed { + guard let points = getPoints(for: leg) else { + return 0 + } + + return points.reduce(0, { $0 + $1.speed }) / CLLocationSpeed(points.count) + } + + func metadata(for leg: Leg) -> [String: Any]? { + var metamap = [String: Any]() + + if #available(iOS 11.2, *) { + if leg.direction == .downward { + metamap[HKMetadataKeyElevationDescended] = abs(leg.altitudeChange) + } else if leg.direction == .upward { + metamap[HKMetadataKeyElevationAscended] = abs(leg.altitudeChange) + } + metamap[HKMetadataKeyMaximumSpeed] = leg.stat.maximumSpeed + metamap[HKMetadataKeyAverageSpeed] = averageSpeed(for: leg) + } + + return metamap + } +} + +// MARK: - Read APIs extension ActivityService { diff --git a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj index f8d4fb4..a8b272a 100644 --- a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj +++ b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 36A7C6E52104CC690073407A /* TrackOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A7C6E42104CC690073407A /* TrackOverviewTableViewController.swift */; }; 36A7C6E72104CD120073407A /* TrackView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36A7C6E62104CD120073407A /* TrackView.storyboard */; }; 36A7C6E92104D8870073407A /* LiveTrackingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A7C6E82104D8870073407A /* LiveTrackingViewController.swift */; }; + 36B5A61C21173EBA008D8E5D /* SaveTrackCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B5A61B21173EBA008D8E5D /* SaveTrackCell.swift */; }; 36C45E451DCE2D3500E87710 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C45E441DCE2D3500E87710 /* AppDelegate.swift */; }; 36C45E4A1DCE2D3500E87710 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36C45E481DCE2D3500E87710 /* Main.storyboard */; }; 36C45E4C1DCE2D3500E87710 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 36C45E4B1DCE2D3500E87710 /* Assets.xcassets */; }; @@ -64,6 +65,7 @@ 36A7C6E42104CC690073407A /* TrackOverviewTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackOverviewTableViewController.swift; sourceTree = ""; }; 36A7C6E62104CD120073407A /* TrackView.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = TrackView.storyboard; sourceTree = ""; }; 36A7C6E82104D8870073407A /* LiveTrackingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTrackingViewController.swift; sourceTree = ""; }; + 36B5A61B21173EBA008D8E5D /* SaveTrackCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveTrackCell.swift; sourceTree = ""; }; 36C45E411DCE2D3500E87710 /* GeoTrackKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeoTrackKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36C45E441DCE2D3500E87710 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36C45E491DCE2D3500E87710 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -179,6 +181,7 @@ 36A7C6E42104CC690073407A /* TrackOverviewTableViewController.swift */, 36A7C6DE2104CC0D0073407A /* LegSwitchCell.swift */, 36A7C6DF2104CC0D0073407A /* TrackMapViewController.swift */, + 36B5A61B21173EBA008D8E5D /* SaveTrackCell.swift */, ); path = ReferenceTrack; sourceTree = ""; @@ -499,6 +502,7 @@ 36A7C6E92104D8870073407A /* LiveTrackingViewController.swift in Sources */, 36C45E451DCE2D3500E87710 /* AppDelegate.swift in Sources */, 36A7C6C72100B0980073407A /* EventLogAppender.swift in Sources */, + 36B5A61C21173EBA008D8E5D /* SaveTrackCell.swift in Sources */, 36A7C6D02103ECB40073407A /* ConsoleLogAppender.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/GeoTrackKitExample/GeoTrackKitExample/Info.plist b/GeoTrackKitExample/GeoTrackKitExample/Info.plist index bc7728b..a853cab 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Info.plist +++ b/GeoTrackKitExample/GeoTrackKitExample/Info.plist @@ -2,6 +2,8 @@ + NSHealthUpdateUsageDescription + Write workout routes to your health data. CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/SaveTrackCell.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/SaveTrackCell.swift new file mode 100644 index 0000000..b705e59 --- /dev/null +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/SaveTrackCell.swift @@ -0,0 +1,22 @@ +// +// SaveTrackCell.swift +// GeoTrackKitExample +// +// Created by Eric Internicola on 8/5/18. +// Copyright © 2018 Eric Internicola. All rights reserved. +// + +import UIKit + +class SaveTrackCell: UITableViewCell { + + struct Constants { + static let saveTrackNotification = Notification.Name(rawValue: "tapped.save.track") + } + + @IBAction + func tappedSaveTrack(_ sender: Any) { + NotificationCenter.default.post(name: Constants.saveTrackNotification, object: nil) + } + +} diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift index ad851d3..513895f 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackMapViewController.swift @@ -21,6 +21,7 @@ class TrackMapViewController: UIViewController { } var useDemoTrack = true + var legVisibleByDefault: Bool { return !useDemoTrack } @@ -108,6 +109,7 @@ extension TrackMapViewController { } tableVC = destinationVC tableVC?.model = model + tableVC?.showSaveTrackCell = useDemoTrack } } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift index b617eda..b4779d2 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackOverviewTableViewController.swift @@ -7,10 +7,15 @@ // import GeoTrackKit +import HealthKit import UIKit class TrackOverviewTableViewController: UITableViewController { + /// Should the "Save Track Cell" be visible? (For Demo Track) + var showSaveTrackCell = false + + /// Sets the Track Model var model: UIGeoTrack? { didSet { tableView.reloadData() @@ -25,6 +30,23 @@ class TrackOverviewTableViewController: UITableViewController { super.viewDidLoad() tableView.estimatedRowHeight = 45 tableView.rowHeight = UITableViewAutomaticDimension + + NotificationCenter.default.addObserver(self, selector: #selector(saveTrack(_:)), name: SaveTrackCell.Constants.saveTrackNotification, object: nil) + } + +} + +// MARK: - Notification Handlers + +extension TrackOverviewTableViewController { + + @objc + func saveTrack(_ notification: NSNotification) { + print("You tapped Save Track") + guard let model = model else { + return + } + ActivityService.shared.saveTrack(model, for: HKWorkoutActivityType.downhillSkiing) } } @@ -34,17 +56,27 @@ class TrackOverviewTableViewController: UITableViewController { extension TrackOverviewTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { - return 1 + return 2 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - guard let analyzer = analyzer else { - return 0 + switch section { + case 0: + return showSaveTrackCell ? 1 : 0 + + default: + guard let analyzer = analyzer else { + return 0 + } + return analyzer.legs.count } - return analyzer.legs.count + } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard indexPath.section == 1 else { + return tableView.dequeueReusableCell(withIdentifier: "SaveTrackCell", for: indexPath) + } let cell = tableView.dequeueReusableCell(withIdentifier: "LegSwitchCell", for: indexPath) guard let model = model, let legCell = cell as? LegSwitchCell else { diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackView.storyboard b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackView.storyboard index 2eea949..0e3be1d 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackView.storyboard +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/ReferenceTrack/TrackView.storyboard @@ -4,7 +4,6 @@ - @@ -68,9 +67,33 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackImport/TrackImportTableViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackImport/TrackImportTableViewController.swift index 069e289..c2c5e11 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackImport/TrackImportTableViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackImport/TrackImportTableViewController.swift @@ -18,7 +18,10 @@ class TrackImportTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) loadTracksFromWorkouts() } @@ -74,6 +77,7 @@ extension TrackImportTableViewController { /// Loads the tracks from the workouts for you. func loadTracksFromWorkouts() { + workouts.removeAll() ActivityService.shared.authorize { (success, _) in guard success else { print("We won't be querying activities, no authorization") From 7ec2c4989f18e212a04425e17f53fc93a9118713 Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Sun, 5 Aug 2018 12:47:30 -0600 Subject: [PATCH 3/8] I've added the ability to save a track to the documents folder (and list the tracks in the documents folder, but I don't have a view for those tracks yet). --- GeoTrackKit/Core/GeoTrackManager.swift | 31 +++++-- GeoTrackKit/Core/GeoTrackService.swift | 3 + .../project.pbxproj | 4 + .../GeoTrackKitExample/AppDelegate.swift | 3 + .../Services/TrackFileService.swift | 89 +++++++++++++++++++ .../LiveTrackingViewController.swift | 2 +- .../TrackConsoleViewController.swift | 3 +- README.md | 5 +- 8 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift diff --git a/GeoTrackKit/Core/GeoTrackManager.swift b/GeoTrackKit/Core/GeoTrackManager.swift index 2f78632..7cb1832 100644 --- a/GeoTrackKit/Core/GeoTrackManager.swift +++ b/GeoTrackKit/Core/GeoTrackManager.swift @@ -73,6 +73,16 @@ extension GeoTrackManager: GeoTrackService { return trackingState == .awaitingFix } + /// Resets the current track + public func reset() { + guard trackingState == .notTracking else { + assertionFailure("reset() cannot be called when tracking") + return GTError(message: "reset() cannot be called when tracking") + } + lastPoint = nil + track = nil + } + /// Attempts to start tracking (if we're not already). public func startTracking() { GTInfo(message: "User requested Start Tracking") @@ -84,6 +94,7 @@ extension GeoTrackManager: GeoTrackService { initializeLocationManager() beginLocationUpdates() trackingState = .awaitingFix + NotificationCenter.default.post(name: Notification.GeoTrackKit.trackingStarted, object: nil) } /// Stops tracking @@ -92,6 +103,7 @@ extension GeoTrackManager: GeoTrackService { endLocationUpdates() trackingState = .notTracking + NotificationCenter.default.post(name: Notification.GeoTrackKit.trackingStopped, object: nil) } } @@ -170,7 +182,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { return } track.add(locations: recentLocations) - NotificationCenter.default.post(name: Notification.Name.GeoTrackKit.didUpdateLocations, object: recentLocations) + NotificationCenter.default.post(name: Notification.GeoTrackKit.didUpdateLocations, object: recentLocations) } /// Handles location tracking pauses @@ -179,7 +191,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { GTDebug(message: "Paused Location Updates") track?.pauseTracking(message: "locationManagerDidPauseLocationUpdates event") - NotificationCenter.default.post(name: Notification.Name.GeoTrackKit.didPauseLocationUpdates, object: nil) + NotificationCenter.default.post(name: Notification.GeoTrackKit.didPauseLocationUpdates, object: nil) } /// Handles location tracking resuming. @@ -188,7 +200,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { public func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { GTDebug(message: "Resumed Location Updates") track?.startTracking(message: "locationManagerDidResumeLocationUpdates event") - NotificationCenter.default.post(name: Notification.Name.GeoTrackKit.didResumeLocationUpdates, object: nil) + NotificationCenter.default.post(name: Notification.GeoTrackKit.didResumeLocationUpdates, object: nil) } /// Handles location tracking errors @@ -199,7 +211,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { GTError(message: "Failed to perform location tracking: \(error.localizedDescription), \(error)") track?.error(error: error) - NotificationCenter.default.post(name: Notification.Name.GeoTrackKit.didFailWithError, object: error) + NotificationCenter.default.post(name: Notification.GeoTrackKit.didFailWithError, object: error) } /// Handles deferred update errors. @@ -217,7 +229,7 @@ extension GeoTrackManager: CLLocationManagerDelegate { } else { track?.error(message: "locationManager:didFinishDeferredUpdatesWithError: nil error") } - NotificationCenter.default.post(name: Notification.Name.GeoTrackKit.didFinishDeferredUpdatesWithError, object: error) + NotificationCenter.default.post(name: Notification.GeoTrackKit.didFinishDeferredUpdatesWithError, object: error) } } @@ -295,10 +307,17 @@ extension CLLocation { // MARK: - Notifications -public extension Notification.Name { +public extension Notification { /// GeoTrackKit notification constants public struct GeoTrackKit { + + /// Notification that the user has started tracking + public static let trackingStarted = Notification.Name(rawValue: "com.geotrackkit.user.started.tracking") + + /// Notification that the user has stopped tracking + public static let trackingStopped = Notification.Name(rawValue: "com.geotrackkit.user.stopped.tracking") + /// Notofication that the location was updated public static let didUpdateLocations = Notification.Name(rawValue: "com.geotrackkit.did.update.locations") diff --git a/GeoTrackKit/Core/GeoTrackService.swift b/GeoTrackKit/Core/GeoTrackService.swift index e672ed1..80159c8 100644 --- a/GeoTrackKit/Core/GeoTrackService.swift +++ b/GeoTrackKit/Core/GeoTrackService.swift @@ -32,6 +32,9 @@ public protocol GeoTrackService { /// The most recently tracked point var lastPoint: CLLocation? { get } + /// Resets the current track + func reset() + /// Starts tracking func startTracking() diff --git a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj index a8b272a..5bd2fdc 100644 --- a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj +++ b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0E1E09A3899897CDAE589A5B /* Pods_GeoTrackKitExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A51639AB7B6B4BAEF27C313 /* Pods_GeoTrackKitExample.framework */; }; 1B2451A1B46FD1E813C13A25 /* Pods_GeoTrackKitExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6E72B23B9554498B66030FE /* Pods_GeoTrackKitExampleTests.framework */; }; 36302DB9210E17B400834A1D /* GeoTrackKitErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36302DB8210E17B400834A1D /* GeoTrackKitErrorTests.swift */; }; + 3665F7D521176FA200B58BBA /* TrackFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3665F7D421176FA200B58BBA /* TrackFileService.swift */; }; 366CE43C1DFCAC360090BD42 /* GeoTrackSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366CE43B1DFCAC360090BD42 /* GeoTrackSerializationTests.swift */; }; 368A87AE1E6332C1003D115A /* reference-track-1.json in Resources */ = {isa = PBXBuildFile; fileRef = 368A87AD1E6332C1003D115A /* reference-track-1.json */; }; 368A87B11E63332E003D115A /* TrackReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 368A87B01E63332E003D115A /* TrackReader.swift */; }; @@ -51,6 +52,7 @@ /* Begin PBXFileReference section */ 36302DB8210E17B400834A1D /* GeoTrackKitErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoTrackKitErrorTests.swift; sourceTree = ""; }; + 3665F7D421176FA200B58BBA /* TrackFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackFileService.swift; sourceTree = ""; }; 366CE43B1DFCAC360090BD42 /* GeoTrackSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoTrackSerializationTests.swift; sourceTree = ""; }; 368A87AD1E6332C1003D115A /* reference-track-1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reference-track-1.json"; sourceTree = ""; }; 368A87B01E63332E003D115A /* TrackReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackReader.swift; sourceTree = ""; }; @@ -150,6 +152,7 @@ children = ( 36A7C6C62100B0980073407A /* EventLogAppender.swift */, 36A7C6CF2103ECB40073407A /* ConsoleLogAppender.swift */, + 3665F7D421176FA200B58BBA /* TrackFileService.swift */, ); path = Services; sourceTree = ""; @@ -496,6 +499,7 @@ files = ( 36A7C6E02104CC0D0073407A /* LegSwitchCell.swift in Sources */, 36A7C6E52104CC690073407A /* TrackOverviewTableViewController.swift in Sources */, + 3665F7D521176FA200B58BBA /* TrackFileService.swift in Sources */, 36A7C6E12104CC0D0073407A /* TrackMapViewController.swift in Sources */, 36A7C6DD2104CBFE0073407A /* TrackConsoleViewController.swift in Sources */, 36D099B7210B6A2C00C8C841 /* TrackImportTableViewController.swift in Sources */, diff --git a/GeoTrackKitExample/GeoTrackKitExample/AppDelegate.swift b/GeoTrackKitExample/GeoTrackKitExample/AppDelegate.swift index 45e7fea..50f862c 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/AppDelegate.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/AppDelegate.swift @@ -22,6 +22,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { GeoTrackEventLog.shared.add(appender: ConsoleLogAppender.shared) #endif + // Bootstrap the TrackFileService + TrackFileService.shared + return true } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift b/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift new file mode 100644 index 0000000..9b93e98 --- /dev/null +++ b/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift @@ -0,0 +1,89 @@ +// +// TrackFileService.swift +// GeoTrackKitExample +// +// Created by Eric Internicola on 8/5/18. +// Copyright © 2018 Eric Internicola. All rights reserved. +// + +import Foundation +import GeoTrackKit + +class TrackFileService { + static let shared = TrackFileService() + + /// Gets you the track files in the folder + /// + /// - Returns: a list of track files in the folder + var trackFiles: [String] { + // Full path to documents directory + let docs = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].path + + // List all contents of directory and return as [String] OR nil if failed + return (try? fileManager.contentsOfDirectory(atPath: docs).filter({ $0.hasSuffix(Constants.trackSuffix) })) ?? [] + } + + /// Saves the provided track to a file. + /// + /// - Parameter track: The track to save. + func save(track: UIGeoTrack) { + guard let startDate = track.startDate else { + return assertionFailure("Failed to get start date from track") + } + let fileName = Constants.formatter.string(from: startDate) + Constants.trackSuffix + let fullPath = "\(documents)/\(fileName)" + + guard let jsonData = try? JSONSerialization.data(withJSONObject: track.track.map, options: []) else { + return assertionFailure("Failed to create JSON Data for track") + } + + let fileUrl = URL(fileURLWithPath: fullPath) + do { + try jsonData.write(to: fileUrl) + print("Wrote new track file: \(fileUrl.absoluteString)") + } catch { + print("ERROR: \(error.localizedDescription)") + } + } + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(stoppedTracking(_:)), name: Notification.GeoTrackKit.trackingStopped, object: nil) + } +} + +// MARK: - Event Handlers + +extension TrackFileService { + + @objc + func stoppedTracking(_ notification: NSNotification) { + guard let track = GeoTrackManager.shared.track else { + return assertionFailure("Failed to locate a track to save") + } + + self.save(track: UIGeoTrack(with: track)) + } +} + +// MARK: - Implementation + +private extension TrackFileService { + + struct Constants { + static let trackSuffix = "-track.json" + static let formatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy_MMM_dd_HH_mm_ss" + return dateFormatter + }() + } + + var fileManager: FileManager { + return FileManager.default + } + + var documents: String { + return fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].path + } + +} diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/LiveTracking/LiveTrackingViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/LiveTracking/LiveTrackingViewController.swift index 427717b..0c82443 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/LiveTracking/LiveTrackingViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/LiveTracking/LiveTrackingViewController.swift @@ -20,7 +20,7 @@ class LiveTrackingViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(trackUpdated), name: Notification.Name.GeoTrackKit.didUpdateLocations, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(trackUpdated), name: Notification.GeoTrackKit.didUpdateLocations, object: nil) } } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackConsole/TrackConsoleViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackConsole/TrackConsoleViewController.swift index 9122b89..820abf5 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackConsole/TrackConsoleViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackConsole/TrackConsoleViewController.swift @@ -28,7 +28,7 @@ class TrackConsoleViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - NotificationCenter.default.addObserver(self, selector: #selector(locationDidUpdate(_:)), name: Notification.Name.GeoTrackKit.didUpdateLocations, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(locationDidUpdate(_:)), name: Notification.GeoTrackKit.didUpdateLocations, object: nil) } @IBAction @@ -58,6 +58,7 @@ fileprivate extension TrackConsoleViewController { if GeoTrackManager.shared.isTracking { GeoTrackManager.shared.stopTracking() } else { + GeoTrackManager.shared.reset() GeoTrackManager.shared.startTracking() } updateButtonText() diff --git a/README.md b/README.md index 50cc4e5..9d793e3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ - Pull tracks in from HealthKit (Activity App) - `NOTE:` This is an iOS 11+ only feature and requires a physical device to test - This capability is in a subspec: `HealthKit` +- Write tracks to HealthKit + - `NOTE:` This is an iOS 11+ only feature and requires a physical device to test + - This capability is in a subspec: `HealthKit` - Example App to demonstrate capabilities @@ -35,7 +38,7 @@ This project is currently a work in progress. ### Example App TODO: -- [ ] Save tracks to disk +- [x] Save tracks to disk - [ ] Provide a track list - [x] Pull tracks in from HealthKit From 4012694457996e5f5e926d1e371f2a5ba36a42c1 Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Sun, 5 Aug 2018 12:55:06 -0600 Subject: [PATCH 4/8] Updated the README and added some icons for a track list tab icon. --- .../track_list.imageset/Contents.json | 23 ++++++++++++++++++ .../track_list.imageset/track_list.png | Bin 0 -> 658 bytes .../track_list.imageset/track_list@2x.png | Bin 0 -> 1304 bytes .../track_list.imageset/track_list@3x.png | Bin 0 -> 2160 bytes README.md | 4 +++ 5 files changed, 27 insertions(+) create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/Contents.json create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list.png create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@2x.png create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@3x.png diff --git a/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/Contents.json b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/Contents.json new file mode 100644 index 0000000..e4e5f77 --- /dev/null +++ b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "track_list.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "track_list@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "track_list@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list.png b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list.png new file mode 100644 index 0000000000000000000000000000000000000000..56265bc90f7be220512074f2527c2257f423282f GIT binary patch literal 658 zcmV;D0&V??P)Px%Oi4sRR9Fe^nK4KlQ4oetBZz?%DuSd<5wuVnu~bNEjdr4tB4U#WmX=$afEG&n z3n8U~y=Y-$B>@FPBnA-#F(y?&LH$Pe!DDalc6RUW?On11zccUcdo%z3EN^ztrb3~x z4|+kP*g+FaZ^1U$0`+bq5F8egLfVs{0nCHrZF2(fsa75mu2p)MUirYHRE8=U;7xp2yXmw z8~!us@W<6Sd8K$8l|ZAGbj%$;(KY@Q{P4$_IE~Um>MdQb(h-YnVOsNd;x21_r*+M&N$TZGzmj=eRJU=tdr?(X{sYc|u?vEJ z;7Vzex2)GZ9Y}&gqc}!LXt^=`dB&n9G$; sL)EaH4f!8iAuT@#^oHG0oz5NH0bzV~;B%Me#sB~S07*qoM6N<$f@Ov+lmGw# literal 0 HcmV?d00001 diff --git a/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@2x.png b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f54d9dad425695b8c69a7b924d91a6a227a6f23e GIT binary patch literal 1304 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 s3!+_b0-FE~qixDGjq}Q9%)NeW5BI|Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY - [Map Location free icon](https://www.flaticon.com/free-icon/map-location_149985#term=gps&page=1&position=24) -
Icons made by Smashicons from www.flaticon.com is licensed by CC 3.0 BY
+- [List Icon](https://www.flaticon.com/free-icon/list_149367#term=listing&page=1&position=2) + -
Icons made by Smashicons from www.flaticon.com is licensed by CC 3.0 BY
+ +I've taken the above images and converted them into less than 50x50 icons to use for tabs From cfb169caacee649db733ffeb30785d12e016b18d Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Sun, 5 Aug 2018 19:32:15 -0600 Subject: [PATCH 5/8] Added a file list that will also show the track you've selected. --- .../project.pbxproj | 16 +++++ .../Base.lproj/Main.storyboard | 12 +++- .../Services/TrackFileService.swift | 11 ++-- .../Views/TrackList/TrackList.storyboard | 63 +++++++++++++++++++ .../TrackListTableViewController.swift | 57 +++++++++++++++++ 5 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackList.storyboard create mode 100644 GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift diff --git a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj index 5bd2fdc..515cff5 100644 --- a/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj +++ b/GeoTrackKitExample/GeoTrackKitExample.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 1B2451A1B46FD1E813C13A25 /* Pods_GeoTrackKitExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6E72B23B9554498B66030FE /* Pods_GeoTrackKitExampleTests.framework */; }; 36302DB9210E17B400834A1D /* GeoTrackKitErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36302DB8210E17B400834A1D /* GeoTrackKitErrorTests.swift */; }; 3665F7D521176FA200B58BBA /* TrackFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3665F7D421176FA200B58BBA /* TrackFileService.swift */; }; + 36682DFF2117BC0C0076A6A8 /* TrackListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36682DFE2117BC0C0076A6A8 /* TrackListTableViewController.swift */; }; 366CE43C1DFCAC360090BD42 /* GeoTrackSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366CE43B1DFCAC360090BD42 /* GeoTrackSerializationTests.swift */; }; 368A87AE1E6332C1003D115A /* reference-track-1.json in Resources */ = {isa = PBXBuildFile; fileRef = 368A87AD1E6332C1003D115A /* reference-track-1.json */; }; 368A87B11E63332E003D115A /* TrackReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 368A87B01E63332E003D115A /* TrackReader.swift */; }; @@ -26,6 +27,7 @@ 36A7C6E72104CD120073407A /* TrackView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36A7C6E62104CD120073407A /* TrackView.storyboard */; }; 36A7C6E92104D8870073407A /* LiveTrackingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A7C6E82104D8870073407A /* LiveTrackingViewController.swift */; }; 36B5A61C21173EBA008D8E5D /* SaveTrackCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B5A61B21173EBA008D8E5D /* SaveTrackCell.swift */; }; + 36B5A61F2117BA60008D8E5D /* TrackList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36B5A61E2117BA60008D8E5D /* TrackList.storyboard */; }; 36C45E451DCE2D3500E87710 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C45E441DCE2D3500E87710 /* AppDelegate.swift */; }; 36C45E4A1DCE2D3500E87710 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36C45E481DCE2D3500E87710 /* Main.storyboard */; }; 36C45E4C1DCE2D3500E87710 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 36C45E4B1DCE2D3500E87710 /* Assets.xcassets */; }; @@ -53,6 +55,7 @@ /* Begin PBXFileReference section */ 36302DB8210E17B400834A1D /* GeoTrackKitErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoTrackKitErrorTests.swift; sourceTree = ""; }; 3665F7D421176FA200B58BBA /* TrackFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackFileService.swift; sourceTree = ""; }; + 36682DFE2117BC0C0076A6A8 /* TrackListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackListTableViewController.swift; sourceTree = ""; }; 366CE43B1DFCAC360090BD42 /* GeoTrackSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoTrackSerializationTests.swift; sourceTree = ""; }; 368A87AD1E6332C1003D115A /* reference-track-1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reference-track-1.json"; sourceTree = ""; }; 368A87B01E63332E003D115A /* TrackReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackReader.swift; sourceTree = ""; }; @@ -68,6 +71,7 @@ 36A7C6E62104CD120073407A /* TrackView.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = TrackView.storyboard; sourceTree = ""; }; 36A7C6E82104D8870073407A /* LiveTrackingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTrackingViewController.swift; sourceTree = ""; }; 36B5A61B21173EBA008D8E5D /* SaveTrackCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveTrackCell.swift; sourceTree = ""; }; + 36B5A61E2117BA60008D8E5D /* TrackList.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TrackList.storyboard; sourceTree = ""; }; 36C45E411DCE2D3500E87710 /* GeoTrackKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeoTrackKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36C45E441DCE2D3500E87710 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36C45E491DCE2D3500E87710 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -160,6 +164,7 @@ 36A7C6D52104C7480073407A /* Views */ = { isa = PBXGroup; children = ( + 36B5A61D2117BA2D008D8E5D /* TrackList */, 36D099B3210B64D900C8C841 /* TrackImport */, 36A7C6D72104C7480073407A /* TrackConsole */, 36A7C6D82104C7480073407A /* ReferenceTrack */, @@ -198,6 +203,15 @@ path = LiveTracking; sourceTree = ""; }; + 36B5A61D2117BA2D008D8E5D /* TrackList */ = { + isa = PBXGroup; + children = ( + 36B5A61E2117BA60008D8E5D /* TrackList.storyboard */, + 36682DFE2117BC0C0076A6A8 /* TrackListTableViewController.swift */, + ); + path = TrackList; + sourceTree = ""; + }; 36C45E381DCE2D3500E87710 = { isa = PBXGroup; children = ( @@ -403,6 +417,7 @@ 36A7C6DB2104CBF30073407A /* TrackConsole.storyboard in Resources */, 36C45E4F1DCE2D3500E87710 /* LaunchScreen.storyboard in Resources */, 36E3C43D1F4A0817005738DB /* reference-track-1.json in Resources */, + 36B5A61F2117BA60008D8E5D /* TrackList.storyboard in Resources */, 36A7C6E72104CD120073407A /* TrackView.storyboard in Resources */, 36C45E4C1DCE2D3500E87710 /* Assets.xcassets in Resources */, 36D099B5210B64E200C8C841 /* TrackImport.storyboard in Resources */, @@ -502,6 +517,7 @@ 3665F7D521176FA200B58BBA /* TrackFileService.swift in Sources */, 36A7C6E12104CC0D0073407A /* TrackMapViewController.swift in Sources */, 36A7C6DD2104CBFE0073407A /* TrackConsoleViewController.swift in Sources */, + 36682DFF2117BC0C0076A6A8 /* TrackListTableViewController.swift in Sources */, 36D099B7210B6A2C00C8C841 /* TrackImportTableViewController.swift in Sources */, 36A7C6E92104D8870073407A /* LiveTrackingViewController.swift in Sources */, 36C45E451DCE2D3500E87710 /* AppDelegate.swift in Sources */, diff --git a/GeoTrackKitExample/GeoTrackKitExample/Base.lproj/Main.storyboard b/GeoTrackKitExample/GeoTrackKitExample/Base.lproj/Main.storyboard index 57a0c63..58350cf 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Base.lproj/Main.storyboard +++ b/GeoTrackKitExample/GeoTrackKitExample/Base.lproj/Main.storyboard @@ -4,7 +4,6 @@ - @@ -19,6 +18,7 @@ + @@ -49,6 +49,16 @@ + + + + + + + + + + diff --git a/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift b/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift index 9b93e98..d71bea9 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Services/TrackFileService.swift @@ -12,6 +12,10 @@ import GeoTrackKit class TrackFileService { static let shared = TrackFileService() + var documents: String { + return fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].path + } + /// Gets you the track files in the folder /// /// - Returns: a list of track files in the folder @@ -20,7 +24,8 @@ class TrackFileService { let docs = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].path // List all contents of directory and return as [String] OR nil if failed - return (try? fileManager.contentsOfDirectory(atPath: docs).filter({ $0.hasSuffix(Constants.trackSuffix) })) ?? [] + let fileList = (try? fileManager.contentsOfDirectory(atPath: docs).filter({ $0.hasSuffix(Constants.trackSuffix) })) ?? [] + return fileList.sorted(by: { $0 > $1 }) } /// Saves the provided track to a file. @@ -82,8 +87,4 @@ private extension TrackFileService { return FileManager.default } - var documents: String { - return fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].path - } - } diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackList.storyboard b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackList.storyboard new file mode 100644 index 0000000..5397813 --- /dev/null +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackList.storyboard @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift new file mode 100644 index 0000000..b8acb0b --- /dev/null +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift @@ -0,0 +1,57 @@ +// +// TrackListTableViewController.swift +// GeoTrackKitExample +// +// Created by Eric Internicola on 8/5/18. +// Copyright © 2018 Eric Internicola. All rights reserved. +// + +import GeoTrackKit +import UIKit + +class TrackListTableViewController: UITableViewController { + + var trackList = [String]() + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + trackList = TrackFileService.shared.trackFiles + tableView.reloadData() + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return trackList.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "TrackCell", for: indexPath) + cell.textLabel?.text = trackList[indexPath.row] + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let filePath = "\(TrackFileService.shared.documents)/\(trackList[indexPath.row])" + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return assertionFailure("Failed to open file: \(trackList[indexPath.row])") + } + guard let jsonData = try? JSONSerialization.jsonObject(with: data, options: []) else { + return assertionFailure("Failed to read json content from: \(filePath)") + } + guard let jsonMap = jsonData as? [String: Any], let track = GeoTrack.fromMap(map: jsonMap) else { + return assertionFailure("Wrong data type") + } + + let map = TrackMapViewController.loadFromStoryboard() + map.model = UIGeoTrack(with: track) + map.useDemoTrack = false + + navigationController?.pushViewController(map, animated: true) + } + +} From a4db5232d78a38f51abd3296691b6c8cd3e27eba Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Mon, 6 Aug 2018 07:55:53 -0600 Subject: [PATCH 6/8] Some minor UX / UI cleanup. --- .../track_list.imageset/track_list.png | Bin 658 -> 557 bytes .../track_list.imageset/track_list@2x.png | Bin 1304 -> 1177 bytes .../track_list.imageset/track_list@3x.png | Bin 2160 -> 1971 bytes .../TrackListTableViewController.swift | 13 +++++++++++++ 4 files changed, 13 insertions(+) diff --git a/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list.png b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list.png index 56265bc90f7be220512074f2527c2257f423282f..f2736cd4ea64668f45c49e293f855e26a5d471ee 100644 GIT binary patch delta 520 zcmV+j0{8ur1+4^-Fnj1VbljJ#X>~MC?_i*LHVC;2P1+7c`=~g_oWkO+6>&lgEOXIaNUFX)Nb@X^ zWG=dkPc^eg@qbhq6(V$oL0$V;wGDkjAJ83C zi52k(pj`cUg$|%ftcdRbJ%ffI<~D*}5&0uz(CulH`PuwwMZ8ZK=`0_RN(s&EQ3|Lm z8lj~dMdli(2LU9XrHkZC9a{svj**sANAuK~=gnmSOn+<83iJxiLx<2cBbvc>S~z&CsUzjdi7bxi;O002ov KPDHLkU;%=;JoHTf delta 621 zcmV-z0+Ri$1d;`iFnfsa75mu2p)MUirY zHRE8=U;7xp2yXmw8~!us@W<6Sd8K$8l|ZAGbj%$;(KY@Q{P4$_IE~Um>!r zvM<0icn(g0oYzHQx*}oiOhfu7bf!R$PfMDM+<+l0AIEdRMH@dP&leDBnh)DavdB8Y zW*}ob<6r(YvnHwk8ps_SdQwNDv}}9^R=^&318low;D178J7>jMOIqIQ5&P+u%bEXFjdl+%au+;)v%lm`5#*$Ek6hJhTTz}&K=wVVSIJqbC>4E00000NkvXX Hu0mjf9|0*D diff --git a/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@2x.png b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@2x.png index f54d9dad425695b8c69a7b924d91a6a227a6f23e..0cfe7bb4b1024cc6c97bc60229f3c04a9285e819 100644 GIT binary patch delta 1143 zcmV--1c>{X3YiIzF@IA@L_t(|0qvYkh)z)$$H&)%h!jeb&E$Jhip7+eSXfYFr%X{4 zr6dc<(q@uuESa*fBp)eDvY3*T5{X6@OlXRTiHxuRU+<~+-0r#W$2iaX&bj}m{&Vkn z?z!jO^SjS`=Dz2-GaVf=5fA|p5CIVo0TF05fk9lr59p875Pt(QBRkKgc_qKV6K44? zmFgrgfqI?Qb&EhVl4+yR{eOG&#OWa0kU%3yn55Uk_yoX z2y`^Ge!~Lo^N3aw=xAvDh6UQ^5v?T9(a`!03$)K8T1lXzq4gUUXrD*4l0Zj8>o;r# zdLFv1`@6q?iGOL7tU#XtuH9)MYi<-Xn>($cvlgKCG3XU^qoFess%#O_$H9xRH$eRf zv1@23F(R5!&qM2=6%fnLZenK)8iBaReG>W&C2uhz`ZK6qht@;YkR8k=*x?Ph43z>h z2Oa}7+r0(xm*005GWH(jKIlot?}|g%`3boNbn9(y&VSEcT#T(7;AAo^4|9MKWXPDK z9EV)rt=hg0U0@A=FvAjKj&|3=Ps`d$fm4M0^z^t2Ia?+*2cSLBjGW^ZE=Kevi;i3$ zE}EK+=x#Jbz*nKw&=zPI;vAl;wJf4Hg6N|P)#uo5hIT^yruY>qjm4ZgMgkOX2mDF9 z-MpFfAb;CoZ~|>~7X(_MjR+WZqlgx0BLYUPkUqE-9vus<7h&~LcOChe_Z7myMH{KW#&GgkNXeGpy)hfst z7nHb)#|5Xy&{>E}sin5btVbULIRbQTWVc)*?SF$(v*3Dktf}K72p)j)qV)^>JD__| zo(;jNoM9fB0^b81%SQ8sg8IrC7QiVw#1vN6)?^)3kDY#q?|3-lg&c%cG0-lEi~J$Giv#oK7DYC zkW~n{xs`XR8C!&tI&)6sqaC9?MGHoB%q;lF0Lf1Qf70&Mo@3kgW(l;>tr2K}HgF+%kTpht%*akA0wN#+A}~b=`~}p(*dvon(T@NC002ov JPDHLkV1nC49x?y` delta 1271 zcmVd3OYtduunk?!i-5N=olTrJ_RiZGbW{=V{`=j6tp1Bn3RH!(Gl!Z(1I{y zQVKdoN3c&p3&M;^Dd-p-!M>EB`vk|?6;kd4f2j!(@(?`xbWlpaDmorpuOT#p3(%qN zej&Z8%6M$OhJV0}7DB^pjXg!ZS3t9>5|(XthozGT|MNj%Gkkf|*P>hoH}(*fM{q)? z--LgK*TD$dnx$?d?Nz4W@Y`@#CsaS(o^<7pwj3l@G-*tRUxjaHBVeZxKNjd?n7RCI z=;XJG`O?PA-xc`t0>T7s%~H3K&MMqVAB4Mlrhcx4T{ZHH|`u_tM^db0UwK(4m-%A6}!9(3Qa@)A2^|Hf%CR* zAN(bpcO7*!kFD1zKw}Ydcje^s+I=u5UWbrnD|OdV2WXwlk)RtU`Nz;`YMDboLFYh8 z5mL}8sDI{YD(D<2DMAW51=So)1)T#WMMy!XpqitppmU(42r1|kRC6>HbPkjhp_HKa zr5JFPHSL2B!AWd`)^|Af6YFdqTdyHRlze@n)%z%I z4S&^AZ!0VRU|u4q_X2(f{;}DbPYxp7f#HP+HlzbKJakE{w!ei?- z3XqtA+^K>5d4YJhhb>!u-5~9cnxGpd{m0PgYMCLRpff;HH5GI!vKd_kodJ@nsi0Gl z&FCuV43Jb!1)Yj)Mpr>+fTU_F=u~7gx_=5f10+>5CFo7523=KM``|-x0{ps)k4*WX zsm|uH^%_D{Za{~+$A$E&D&w*B8Uiy~0}Z439`)%0npKrB9R5l?Cgtk}N8l^q{0|3x z@LsqvZgTQ_YJToJ1U~_<-G4Wbc(H{L33Dqv&n6?d3ceD486JY%jUU}(y8q2C?|=WH zj%wVRceUI)H#&DmRJ;D8d(Pbj5@9i-u4@@RqkXhRcGrWJxOBJ-L$`CGxXI=B_DbB$ote zt`Ee!J#0Zs+(&mitRnMkTrwz-@^GP2kpXsHu7WPl1`&5 zT0xgYprq62idN7i5h&?2x}p_yNd!tdP4=NuNvEyfX?W@2ex6!d0>gYQfS;^!&(3{C hAnj@ErOt(X`X9AEQfvsu?gIb-002ovPDHLkV1l1dW##|? diff --git a/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@3x.png b/GeoTrackKitExample/GeoTrackKitExample/Assets.xcassets/track_list.imageset/track_list@3x.png index 6ab68843e2fb9d2cfd666fb817deb18e808c213a..797a86365c94b55c7dbcbccc28a81577214dee82 100644 GIT binary patch literal 1971 zcmbtVc{Cg77EcgT8r!p#$RcW~cIFvHW2rS0TT#)9r9+bvDs7)=O6*jLQL(grsEV~z zi=wtxTC|n~gW9H1HI~G_w5X}Hn6&Tx_1-`4&D?Xo@1EcN&OPVebAP|@rnov|A(E<+ z0001DZ)fW!^vPcjSX_9U%{zR30EW6@tpU~j&_w`1;*!0sm3ugF^&BS^enu8%sC;;> zS?y3e-%lE5%hG9<>$GW8wDPeGl{Nc0Tux?ELF}|Gi}rpAUYM|eLBiY>4DD=7PSZbE zH%A!~_0xME`Fe!4E>&1zz#(`yl8kS;ofE?mKfFCG+ z`NR5rMyzh0q-aIONaHx;K{f1!j&uLXs9e1~K)h&kl6u23JD(P8Y;L zRaoJ%rPd(xJQFDe-sP3doLI|;>Aw*3FP#uMwagvy>^4*f?VhA}MU@dPMt9|0$qno4 zMK!M4RH|m~_0n?;#KGEqXjvw&9WZF{LXVp9t)o8&Hph-f52< zwmODKWb0)#A<4UPfnEc$!-Dzdt7vjeB7XneW|&`SPh9wMPfXj4sbqFpzmrz>`91TF zuJ?14rW4#xQ}u8WrMEF-0bw{&SCiBC@ljH>AuC^wt)PMOLBA7*k@S!Hf@g^f-h)v_cI(#l?0(C+Ybb!gKNoAB~k{f{C9^U>*it4AZaXm`T55oJk+!&ySMZ1 z!sWP{G^}OFSj=rKwRl&D#P|@5-&flZ;rY-T_4LF4rcnzY!fcnw4w_sn=+3>x*~GW9 zUvoAtgE_T;LfDbRH@43VFBs>~l#)-k3?G3}?hxJ+?iw2tj(UHP}Ei1br(fl^(#PG>1d8N^4=4*7Btly;rfwV&{4|zw%YM@|G%A zu#V^sd*k{}=HGMobX`VV^niFB?K$vS3K`L}CvuaIB%({)8n5R>0d!rst*JXCz82#Z z_;?kG9yC2NW;A)D2#T%QYnj_HNy4~1e2kfTkQCS1)TqrG8_7+HFrACFA|Jf62iop= zmn5X4(%WgKpOBGL9x0I6?r7UM>WB+?D0TLOxx|IoqI5r9kWhGWR#HioZt99DB3Lk z#t;3=IqSy3zv5B-5=hsXqq7%#ug<^OiA{=py`JC$%>2y(78eD{EQ>r#`eF$QNKwR(t>Vfg|J3n2M$maHguBb0 zOCPT0YHkK}(IHD4nT2vWHso(jDb=%QCfYH~8tk$Ua5AekY4RHAjS-D=TAJ8rSv-3> zkBv+HACh{U7~a5PPg)^C3no&OY-2VHT(%yULApR{?%NyxOWI*;juc5}!k#!9^=9nKw|y7z}s#^Iyc;ZjeIIJqC zR^`6Dhwxh}yJE*TO~b4P>XgLhG0Rt!kzBkoVVs@wd;4Tp?WQ`4I2fe}Obu4db_!!` zi0xXiw=r~9dOexXq~y;+Y!MyxnLE!VBU3izOQSM-FZc03AAD^NKCkw*EhFljPu_&? zchswt47t#nhB`@&a|}+ zh+`$)EX3a(ch7+G;{HOAtOkyc&I&A4yX*gBTo|Kolsh$1%sWxjPW$1D068^gcU#dU$})>@m)^)z*Fqe*k2Ol@kB} literal 2160 zcmb`J?LQL=8^^aigk}??$vhJ>#j&(`SR-P}qJ)f0j`9>6;-nqTQ=ZmH2t{>>B=VT2 z#@2?Skcwp3Od_T&XA!F7`~mly`@UaXzw5>C^L_XG{H`n09q+8Duv-BD04U;I98YZR z^nXWAc1vwPx&CVaV@^0b0O|(e3jl!JUYw)7=N~|!e|W~JQ!ve@wEb7mC+fPdy!lv%1Kf?zxF>_@X0s;c2^>`}00HTli5bz?_4pB19 zZ3hsFVI=kUII_D5x1U2DV46FzsvY;p9x*4)W)yaH)rL%r-1B#noFl4_EIzLEVaQ+b z)QpWt297e^b~qQg5O7B^|4kl#Ou@hnt!hF-9;#=)&V z!wc?nb~@dR~=?f<>XLqbS6OvJdDRYZS?<7^7 zt-m4c;fuHO?NWuW8BPI=@>gbb;BaRfPK=27Qg?8YQQ#j*w)MIfu1K%){197|nS_se zdeJmrg*2Ht|#1?Z+ zN=3^8?qQ7Io}gVK|IbC}o1G!C7F2cAYG;9%+c5d#G5_E}9W{kyZ=+TC#Sgz!+9#En z&JV(+G(W7lTGY1L|4dD(0iMBug9rGckN0lh9*XKqEcG@nN!=`q`zl`^314ClK@c;so9-eKQzq*FFyz00j?TJL_>4& zvT8ppiH0P3Y~ZkLk_Oe3YDP8HgZH1X%oX&D5qT61-k0UlB|GF9c|Vzu7B(Ja7&f3w zA1#=1H~D(K6QM7Kt}LJ}gv~3-8qk%V(7wCEdlj_u>Nq=}6_V@}{Bp6-GxTsJ~}r2s`!wKee1)K4`F>QYW` zNKUUHWT7h4q|K(0pnN^Ln@fB`N&bPX{ZfV*DbpI7fMtoIZ(=CScq=p|)flRuZr~l$@0uTbdTAG*d>*a|!T8(@;!?pt`_H^W0gTcdbL{!D1%mZc7Km`&&Dd zB$S3nNW}pj#M;lLOUrnH#9_h`NJ;ImddR(B12eF)wxL zG48BPty3WXJA&ZbE0?Gzk51AKx!WXFc+))(Rzx;ftCn`ZAl1^#;eGhT!9M-COK*CO z9wqc!CzrHcg?ocr=bxe(FrRIt+WMQ+(GJZW_4@~&WA~At=G@*+{mATlwb}jbn-x)) z88Gg*lkZ+wk_3O#FKh6f1oxQc75LUZ<})4|DVy ztcUu4X_WumTLpUHM16lR*j7}66j0-!mnJlg6@I=Bz0*DI4Yg&i#sday@3By)JPM6| z7FBk~19qsA{lbiH!%AEX4(%HL`@upOw2*=)iyn$y(jZ;S@d@|c(u-j(;;?N_B((Vu zi?TE%vsN5_gTy?5zDk``6%}072A+11T^q^G?nULi+I6F>hQ_CVY8o2b)BYs=(Pa>l z^!bTzMO!op+r_0QwQ0+>jJ*ythGhKYP1jYGtZCnx$nfi z)>G0~@CPtYp!lHD8t@(R_@l_vQbv&iV~OGdTaK7=KJGCyr$!>m5(Gh=CumcyjzmIk)Puc^lF!y6%P(7spL$uH<5 z16Q~cF&XT554F0X20L@XHIdQp0w$IodcVVXp!Ipms&41r@18&QYA@DLfM%iFwL`=? z7bvja4W%qTmh*XvZ)kM71J`$IspMP5g;orGyy2MX$yoA>NG z%Jt!qzy=q1nrcM_$$*~s(8C9z>>Ma#Vgk=BuLy&xWpg}y>|GU{XN;?Nr}gOlPZ&QQ z`JG`uw&wLtt<`>G56tH(Vq@}{O(y$?-^NvSUh@e?G_3M)wG9Y#5}txjiR-~;C$Tc$ s3!+_b0-FE~qixDGjq}Q9%)NeW5BI| Int { + guard trackList.count > 0 else { + return 1 + } return trackList.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard trackList.count > 0 else { + let cell = UITableViewCell() + cell.textLabel?.text = "No tracks yet" + cell.textLabel?.textAlignment = .center + return cell + } let cell = tableView.dequeueReusableCell(withIdentifier: "TrackCell", for: indexPath) cell.textLabel?.text = trackList[indexPath.row] return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard trackList.count > 0 else { + return + } let filePath = "\(TrackFileService.shared.documents)/\(trackList[indexPath.row])" guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { return assertionFailure("Failed to open file: \(trackList[indexPath.row])") From c0c88ec3d29c6fbe5865fb676b0abd21ac06de3f Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Fri, 10 Aug 2018 11:29:53 -0600 Subject: [PATCH 7/8] I've added the ability to share a track file from the example app. --- .../TrackListTableViewController.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift index f3d0c7f..c262e3f 100644 --- a/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift +++ b/GeoTrackKitExample/GeoTrackKitExample/Views/TrackList/TrackListTableViewController.swift @@ -45,6 +45,21 @@ class TrackListTableViewController: UITableViewController { return cell } + override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let actionConfig = UISwipeActionsConfiguration(actions: [ + UIContextualAction(style: .normal, title: "Share") { _, _, _ in + self.shareTrack(indexPath) + tableView.setEditing(false, animated: true) + } + ]) + + return actionConfig + } + + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + return nil + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard trackList.count > 0 else { return @@ -68,3 +83,27 @@ class TrackListTableViewController: UITableViewController { } } + +// MARK: - Implementation + +extension TrackListTableViewController { + + func shareTrack(_ indexPath: IndexPath) { + guard let filename = filename(forIndex: indexPath) else { + return print("No filename") + } + let fileURL = URL(fileURLWithPath: filename) + print("User wants to share file: \(filename)") + let activityVC = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil) + + self.present(activityVC, animated: true, completion: nil) + + } + + func filename(forIndex indexPath: IndexPath) -> String? { + guard indexPath.row < trackList.count else { + return nil + } + return "\(TrackFileService.shared.documents)/\(trackList[indexPath.row])" + } +} From 9eaccc7e3da71fcfa00d5c0e66c85b4bbe4db24e Mon Sep 17 00:00:00 2001 From: Eric Internicola Date: Sat, 8 Dec 2018 09:16:13 -0700 Subject: [PATCH 8/8] Updated travis config. --- .travis.yml | 2 +- Gemfile.lock | 68 +++++++++++++++++++++++++--------------------------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/.travis.yml b/.travis.yml index dff2ce6..b725302 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode9.4 +osx_image: xcode10.1 script: - bundle exec fastlane test diff --git a/Gemfile.lock b/Gemfile.lock index 8af2b79..66bd206 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,14 +2,14 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.0) - activesupport (4.2.10) + activesupport (4.2.11) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) - atomos (0.1.2) + atomos (0.1.3) babosa (1.0.2) claide (1.0.2) cocoapods (1.5.3) @@ -36,12 +36,12 @@ GEM fuzzy_match (~> 2.0.4) nap (~> 1.0) cocoapods-deintegrate (1.0.2) - cocoapods-downloader (1.2.1) + cocoapods-downloader (1.2.2) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) cocoapods-stats (1.0.0) - cocoapods-trunk (1.3.0) + cocoapods-trunk (1.3.1) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.1.0) @@ -49,7 +49,7 @@ GEM colored2 (3.1.2) commander-fastlane (4.4.6) highline (~> 1.7.2) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.3) declarative (0.0.10) declarative-option (0.1.0) domain_name (0.5.20180417) @@ -58,15 +58,15 @@ GEM emoji_regex (0.1.1) escape (0.0.4) excon (0.62.0) - faraday (0.15.2) + faraday (0.15.4) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) http-cookie (~> 1.0.0) faraday_middleware (0.12.2) faraday (>= 0.7.4, < 1.0) - fastimage (2.1.3) - fastlane (2.100.1) + fastimage (2.1.5) + fastlane (2.109.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -81,7 +81,7 @@ GEM faraday_middleware (~> 0.9) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.21.2, < 0.22.0) + google-api-client (>= 0.21.2, < 0.24.0) highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) mini_magick (~> 4.5.1) @@ -90,7 +90,7 @@ GEM multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) public_suffix (~> 2.0.0) - rubyzip (>= 1.2.1, < 2.0.0) + rubyzip (>= 1.2.2, < 2.0.0) security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) @@ -99,27 +99,27 @@ GEM tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) - xcodeproj (>= 1.5.7, < 2.0.0) - xcpretty (~> 0.2.8) + xcodeproj (>= 1.6.0, < 2.0.0) + xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) ffi (1.9.25) fourflusher (2.0.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-api-client (0.21.2) + google-api-client (0.23.9) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.5, < 0.7.0) httpclient (>= 2.8.1, < 3.0) mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - googleauth (0.6.2) + signet (~> 0.9) + googleauth (0.6.7) faraday (~> 0.12) jwt (>= 1.4, < 3.0) - logging (~> 2.0) - memoist (~> 0.12) + memoist (~> 0.16) multi_json (~> 1.11) - os (~> 0.9) + os (>= 0.9, < 2.0) signet (~> 0.7) highline (1.7.10) http-cookie (1.0.3) @@ -127,7 +127,7 @@ GEM httpclient (2.8.3) i18n (0.9.5) concurrent-ruby (~> 1.0) - jazzy (0.9.3) + jazzy (0.9.4) cocoapods (~> 1.0) mustache (~> 0.99) open4 @@ -139,17 +139,13 @@ GEM json (2.1.0) jwt (2.1.0) liferaft (0.0.6) - little-plugger (1.1.4) - logging (2.2.2) - little-plugger (~> 1.1) - multi_json (~> 1.10) memoist (0.16.0) - mime-types (3.1) + mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types-data (3.2018.0812) mini_magick (4.5.1) minitest (5.11.3) - molinillo (0.6.5) + molinillo (0.6.6) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -159,7 +155,7 @@ GEM naturally (2.2.0) netrc (0.11.0) open4 (1.3.4) - os (0.9.6) + os (1.0.0) plist (3.4.0) public_suffix (2.0.5) rb-fsevent (0.10.3) @@ -172,15 +168,15 @@ GEM uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) - ruby-macho (1.2.0) - rubyzip (1.2.1) - sass (3.5.7) + ruby-macho (1.3.1) + rubyzip (1.2.2) + sass (3.7.2) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) security (0.1.3) - signet (0.8.1) + signet (0.11.0) addressable (~> 2.3) faraday (~> 0.9) jwt (>= 1.5, < 3.0) @@ -196,8 +192,8 @@ GEM thread_safe (0.3.6) tty-cursor (0.6.0) tty-screen (0.6.5) - tty-spinner (0.8.0) - tty-cursor (>= 0.5.0) + tty-spinner (0.9.0) + tty-cursor (~> 0.6.0) tzinfo (1.2.5) thread_safe (~> 0.1) uber (0.1.0) @@ -208,13 +204,13 @@ GEM word_wrap (1.0.0) xcinvoke (0.3.0) liferaft (~> 0.0.6) - xcodeproj (1.5.9) + xcodeproj (1.7.0) CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.2) + atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.5) - xcpretty (0.2.8) + nanaimo (~> 0.2.6) + xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.0) xcpretty (~> 0.2, >= 0.0.7)