diff --git a/Trainer.xcodeproj/project.pbxproj b/Trainer.xcodeproj/project.pbxproj index 89d06cb..3653932 100644 --- a/Trainer.xcodeproj/project.pbxproj +++ b/Trainer.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 87B8B8F71F6D8F4D00A6989A /* IntervalsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B8B8F61F6D8F4D00A6989A /* IntervalsViewController.swift */; }; 87B8B8F91F6D9A2400A6989A /* RepeatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B8B8F81F6D9A2400A6989A /* RepeatCell.swift */; }; 87B8B8FB1F6DDC1500A6989A /* WorkoutNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B8B8FA1F6DDC1500A6989A /* WorkoutNameCell.swift */; }; + 87D3C9ED1FACBE6D0074C7AC /* Intervals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D3C9EC1FACBE6D0074C7AC /* Intervals.swift */; }; 87FD010A1F6B32830076D6E4 /* PhasesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FD01091F6B32830076D6E4 /* PhasesViewController.swift */; }; 87FD010C1F6B34AC0076D6E4 /* PhaseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FD010B1F6B34AC0076D6E4 /* PhaseCell.swift */; }; 87FD01111F6B852E0076D6E4 /* PhaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FD010F1F6B852E0076D6E4 /* PhaseViewController.swift */; }; @@ -80,6 +81,7 @@ 87B8B8F61F6D8F4D00A6989A /* IntervalsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalsViewController.swift; sourceTree = ""; }; 87B8B8F81F6D9A2400A6989A /* RepeatCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatCell.swift; sourceTree = ""; }; 87B8B8FA1F6DDC1500A6989A /* WorkoutNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutNameCell.swift; sourceTree = ""; }; + 87D3C9EC1FACBE6D0074C7AC /* Intervals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Intervals.swift; path = Model/Intervals.swift; sourceTree = ""; }; 87FD01091F6B32830076D6E4 /* PhasesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhasesViewController.swift; sourceTree = ""; }; 87FD010B1F6B34AC0076D6E4 /* PhaseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhaseCell.swift; sourceTree = ""; }; 87FD010F1F6B852E0076D6E4 /* PhaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhaseViewController.swift; sourceTree = ""; }; @@ -149,6 +151,7 @@ 87AD14891F644852009D778A /* Trainer.xcdatamodeld */, 870193101F6722750095D847 /* TrackWorkout.swift */, 87AD14B51F64788A009D778A /* TrackPhase.swift */, + 87D3C9EC1FACBE6D0074C7AC /* Intervals.swift */, 87A4CF2F1F6AF8FE004D49D1 /* WorkoutsViewController.swift */, 87A4CF311F6B15F3004D49D1 /* WorkoutCell.swift */, 87FD01091F6B32830076D6E4 /* PhasesViewController.swift */, @@ -341,6 +344,7 @@ 87A4CF321F6B15F3004D49D1 /* WorkoutCell.swift in Sources */, 87FD010A1F6B32830076D6E4 /* PhasesViewController.swift in Sources */, 87FD011A1F6C206D0076D6E4 /* PhaseSection.swift in Sources */, + 87D3C9ED1FACBE6D0074C7AC /* Intervals.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Trainer/DataAccess.swift b/Trainer/DataAccess.swift index d41eafc..0bedf8e 100644 --- a/Trainer/DataAccess.swift +++ b/Trainer/DataAccess.swift @@ -145,106 +145,4 @@ class DataAccess { return workouts } } -extension Phase { - // Text description for a phase - public override var description:String { - get { - let time = self.duration.format() ?? "*" - //User region setting - let locale = Locale.current - let dist = Measurement(value: self.distance, unit: UnitLength.meters) - var distance = "" - let activity = self.activity?.name ?? "" - if locale.usesMetricSystem { - distance = "\(dist.converted(to: UnitLength.kilometers))" - } - else { - distance = "\(dist.converted(to: UnitLength.miles))" - } - switch EndType(rawValue: Int(self.end))! { - case EndType.Duration: - return activity + " for \(time)" - case EndType.Distance: - return activity + " for \(distance)" - case EndType.Either: - return activity + " for \(time) or \(distance)" - case EndType.Both: - return activity + " for \(time) and \(distance)" - } - } - } - // Expected time for a phase or NaN if based only on - // distance - @objc public var time:TimeInterval { - get { - if self.end == Int32(EndType.Distance.rawValue) { - return Double.nan - } - return TimeInterval(duration) - } - } -} -extension Intervals { - // Text description for intervals - public override var description:String { - get { - var text:String = "Repeat " - for object in self.phases! { - if let phase = object as? Phase { - text += "\(phase.description), " - } - } - text += "\(self.repeats) times" - return text - } - } - // Expected time for an interval or NaN if based only on - // distance - public override var time:TimeInterval { - get { - duration = 0.0 - for object in self.phases! { - if let phase = object as? Phase { - duration += phase.duration - } - } - duration *= Double(self.repeats) - return TimeInterval(duration) - } - } -} -extension Workout { - // Text description for a workout - public override var description:String { - get { - var text = "Warmup \(warmup?.activity?.name ?? "") for \(warmup?.duration.format() ?? ""), " - for object in phases! { - if let intervals = object as? Intervals { - text += intervals.description + ", " - } - else if let phase = object as? Phase { - text += "\(phase.description), " - } - } - text += "Cooldown \(cooldown?.description ?? "")" - return text - } - } - // Expected time for a workout or NaN if based only on - // distance - public var time:TimeInterval { - get { - var duration:TimeInterval = TimeInterval((warmup?.duration)!) - duration += TimeInterval((cooldown?.duration) ?? 0.0) - for object in phases! { - if let phase = object as? Phase { - duration += phase.time - if duration.isNaN { - break - } - } - } - return duration - } - } -} + diff --git a/Trainer/MainViewController.swift b/Trainer/MainViewController.swift index 9724552..296ef8d 100644 --- a/Trainer/MainViewController.swift +++ b/Trainer/MainViewController.swift @@ -111,6 +111,7 @@ class ViewController: UIViewController, CLLocationManagerDelegate, WorkoutDelega locationManager.activityType = .fitness locationManager.distanceFilter = kCLDistanceFilterNone locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.allowsBackgroundLocationUpdates = true locationManager.startUpdatingLocation() } // Let the user know that a new phase is beginning @@ -137,7 +138,6 @@ class ViewController: UIViewController, CLLocationManagerDelegate, WorkoutDelega } // Create the workout tracker if this is a different workout, then fill in the labels // for the workout - // TODO prevent changing workouts while one is in progress @objc func fillLabels() { if workoutData != ViewController.workout?.data || ViewController.workout == nil { if let data = workoutData { @@ -269,7 +269,7 @@ class ViewController: UIViewController, CLLocationManagerDelegate, WorkoutDelega else { workout.resume() } - if isGrantedNotificationAccess { + if isGrantedNotificationAccess && CLLocationManager.authorizationStatus() != .authorizedAlways { createNotifications(workout) } timer = Timer.scheduledTimer(timeInterval: 1, target: self, @@ -341,4 +341,6 @@ class ViewController: UIViewController, CLLocationManagerDelegate, WorkoutDelega self.present(alert, animated: true, completion: nil) } } - +enum WorkoutError : Error { + case workoutRunning +} diff --git a/Trainer/Model/Intervals.swift b/Trainer/Model/Intervals.swift new file mode 100644 index 0000000..6b809df --- /dev/null +++ b/Trainer/Model/Intervals.swift @@ -0,0 +1,39 @@ +// +// Intervals.swift +// Trainer +// +// Created by Deborah Engelmeyer on 11/3/17. +// Copyright © 2017 The Inquisitive Introvert. All rights reserved. +// + +import Foundation + +extension Intervals { + // Text description for intervals + public override var description:String { + get { + var text:String = "Repeat " + for object in self.phases! { + if let phase = object as? Phase { + text += "\(phase.description), " + } + } + text += "\(self.repeats) times" + return text + } + } + // Expected time for an interval or NaN if based only on + // distance + public override var time:TimeInterval { + get { + duration = 0.0 + for object in self.phases! { + if let phase = object as? Phase { + duration += phase.duration + } + } + duration *= Double(self.repeats) + return TimeInterval(duration) + } + } +} diff --git a/Trainer/TrackPhase.swift b/Trainer/TrackPhase.swift index 50cd147..e2a5df6 100644 --- a/Trainer/TrackPhase.swift +++ b/Trainer/TrackPhase.swift @@ -13,6 +13,45 @@ import Foundation public enum EndType:Int { case Duration = 0, Distance, Either, Both } +extension Phase { + // Text description for a phase + public override var description:String { + get { + let time = self.duration.format() ?? "*" + //User region setting + let locale = Locale.current + let dist = Measurement(value: self.distance, unit: UnitLength.meters) + var distance = "" + let activity = self.activity?.name ?? "" + if locale.usesMetricSystem { + distance = "\(dist.converted(to: UnitLength.kilometers))" + } + else { + distance = "\(dist.converted(to: UnitLength.miles))" + } + switch EndType(rawValue: Int(self.end))! { + case EndType.Duration: + return activity + " for \(time)" + case EndType.Distance: + return activity + " for \(distance)" + case EndType.Either: + return activity + " for \(time) or \(distance)" + case EndType.Both: + return activity + " for \(time) and \(distance)" + } + } + } + // Expected time for a phase or NaN if based only on + // distance + @objc public var time:TimeInterval { + get { + if self.end == Int32(EndType.Distance.rawValue) { + return Double.nan + } + return TimeInterval(duration) + } + } +} // Phase class to track the beginning, end, and progress of a phase public class TrackPhase { // Database record for the phase diff --git a/Trainer/TrackWorkout.swift b/Trainer/TrackWorkout.swift index 6d9d6db..8a28cbc 100644 --- a/Trainer/TrackWorkout.swift +++ b/Trainer/TrackWorkout.swift @@ -14,6 +14,41 @@ protocol WorkoutDelegate { func processPhaseChange() func processComplete() } +extension Workout { + // Text description for a workout + public override var description:String { + get { + var text = "Warmup \(warmup?.activity?.name ?? "") for \(warmup?.duration.format() ?? ""), " + for object in phases! { + if let intervals = object as? Intervals { + text += intervals.description + ", " + } + else if let phase = object as? Phase { + text += "\(phase.description), " + } + } + text += "Cooldown \(cooldown?.description ?? "")" + return text + } + } + // Expected time for a workout or NaN if based only on + // distance + public var time:TimeInterval { + get { + var duration:TimeInterval = TimeInterval((warmup?.duration)!) + duration += TimeInterval((cooldown?.duration) ?? 0.0) + for object in phases! { + if let phase = object as? Phase { + duration += phase.time + if duration.isNaN { + break + } + } + } + return duration + } + } +} // Workout class to track the progress of a workout class TrackWorkout { public var description:String { @@ -59,6 +94,7 @@ class TrackWorkout { while one is in progress. */ fileprivate func createPhases(_ data: Workout) { + phases = [] let warmup = TrackPhase(data: (data.warmup)!) phases.append(warmup) for object in data.phases! { @@ -80,12 +116,6 @@ class TrackWorkout { let cooldown = TrackPhase(data: data.cooldown!) phases.append(cooldown) currentPhase = phases[0] - if let remaining = endTime?.timeIntervalSinceNow { - if remaining > 0.0 { - start(at: endTime! - duration) - update(distance: 0.0) - } - } } public init(data:Workout, delegate:WorkoutDelegate) { @@ -114,6 +144,11 @@ class TrackWorkout { endTime = startTime! + duration data.last = endTime timeStamp = Date() + if let remaining = endTime?.timeIntervalSinceNow { + if remaining > 0.0 { + update(distance: 0.0) + } + } } // Calculate a new end time func updateEndTime() { @@ -181,6 +216,7 @@ class TrackWorkout { // stop the workout public func stop() { endTime = nil + running = false } public var ttg:TimeInterval { get {