From b1fd7f5a843144c7eda80a1cecba0aa512c1b79b Mon Sep 17 00:00:00 2001 From: Josh Cook Date: Mon, 16 Nov 2020 06:13:06 -0500 Subject: [PATCH 1/2] feat: pre-commit basic and Swift linting --- .pre-commit-config.yaml | 16 +++ README.md | 2 +- .../Base.lproj/Interface.storyboard | 4 +- .../ComplicationController.swift | 39 +++-- .../ExtensionDelegate.swift | 2 - .../Extensions/Color-extension.swift | 14 +- .../Extensions/GeometryProxy-extension.swift | 2 +- .../HostingController.swift | 2 +- .../Models/BodyPartSelections.swift | 27 ++-- .../Models/CrownVelocityCalculator.swift | 31 ++-- .../Models/Exercise enums.swift | 2 - .../Models/ExerciseInfo.swift | 2 +- .../Models/ExerciseOptions.swift | 54 +++---- .../Models/HeartRateGraphData.swift | 33 ++--- .../Models/WorkoutManager.swift | 135 ++++++++---------- .../Models/WorkoutTracker.swift | 26 ++-- .../NotificationController.swift | 5 +- .../PushNotificationPayload.apns | 4 +- .../View Models/WorkoutPickerViewModel.swift | 12 +- .../View Modifiers/ButtonStyles.swift | 7 +- .../SpinnerRotationModifier.swift | 39 +++-- .../Views/ContentView.swift | 3 +- .../Views/ExerciseFinishView.swift | 18 ++- .../Views/ExercisePicker.swift | 31 ++-- .../Views/ExerciseStartView.swift | 41 +++--- .../Views/ExerciseView.swift | 51 +++---- .../Views/General Components/Buttons.swift | 3 +- .../General Components/SectionHeader.swift | 3 +- .../GraphBackgroundSegment.swift | 43 +++--- .../HeartRateGraphPlotShape.swift | 11 +- .../HeartRateGraph/HeartRateGraphView.swift | 18 +-- .../Views/HeartRateGraph/PlotHVGridText.swift | 5 +- .../Views/HeartRateGraph/PlotHVLines.swift | 11 +- .../BodyPartSelectionListView.swift | 6 +- .../Settings Views/EditExerciseView.swift | 83 +++++------ .../Settings Views/SelectExercisesView.swift | 5 +- .../Views/Settings Views/Settings.swift | 27 ++-- .../Views/Spinner Subviews/SpinnerSlice.swift | 16 +-- .../Views/Spinner Subviews/WorkoutSlice.swift | 12 +- .../Views/WelcomeView.swift | 15 +- .../Views/WorkoutFinishView.swift | 33 ++--- .../Views/WorkoutPagingView.swift | 14 +- 42 files changed, 389 insertions(+), 518 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c2bfa8d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + args: ['--maxkb=80000'] +- repo: https://github.com/nicklockwood/SwiftFormat + rev: 0.47.3 + hooks: + - id: swiftformat + name: SwiftFormat + types: [swift] diff --git a/README.md b/README.md index 96344bc..8498fd0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **An Apple Watch App that randomly selects quick exercises.** ![Swift](https://img.shields.io/badge/Swift-Swift_Project-FA7343.svg?style=flat&logo=swift) -![iOS](https://img.shields.io/badge/watchOS-App-999999.svg?style=flat&logo=apple) +![iOS](https://img.shields.io/badge/watchOS-App-999999.svg?style=flat&logo=apple) [![jhc github](https://img.shields.io/badge/GitHub-jhrcook-181717.svg?style=flat&logo=github)](https://github.com/jhrcook) [![jhc twitter](https://img.shields.io/badge/Twitter-@JoshDoesA-00aced.svg?style=flat&logo=twitter)](https://twitter.com/JoshDoesa) [![jhc website](https://img.shields.io/badge/Website-Joshua_Cook-5087B2.svg?style=flat&logo=telegram)](https://joshuacook.netlify.com) diff --git a/Workout Spinner WatchKit App/Base.lproj/Interface.storyboard b/Workout Spinner WatchKit App/Base.lproj/Interface.storyboard index aa3bd9c..2c80e11 100644 --- a/Workout Spinner WatchKit App/Base.lproj/Interface.storyboard +++ b/Workout Spinner WatchKit App/Base.lproj/Interface.storyboard @@ -9,8 +9,8 @@ - + diff --git a/Workout Spinner WatchKit Extension/ComplicationController.swift b/Workout Spinner WatchKit Extension/ComplicationController.swift index 42ee5af..424a2d6 100644 --- a/Workout Spinner WatchKit Extension/ComplicationController.swift +++ b/Workout Spinner WatchKit Extension/ComplicationController.swift @@ -8,49 +8,46 @@ import ClockKit - class ComplicationController: NSObject, CLKComplicationDataSource { - // MARK: - Timeline Configuration - - func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) { + + func getSupportedTimeTravelDirections(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) { handler([.forward, .backward]) } - - func getTimelineStartDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { + + func getTimelineStartDate(for _: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { handler(nil) } - - func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { + + func getTimelineEndDate(for _: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { handler(nil) } - - func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) { + + func getPrivacyBehavior(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) { handler(.showOnLockScreen) } - + // MARK: - Timeline Population - - func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { + + func getCurrentTimelineEntry(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { // Call the handler with the current timeline entry handler(nil) } - - func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { + + func getTimelineEntries(for _: CLKComplication, before _: Date, limit _: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { // Call the handler with the timeline entries prior to the given date handler(nil) } - - func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { + + func getTimelineEntries(for _: CLKComplication, after _: Date, limit _: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { // Call the handler with the timeline entries after to the given date handler(nil) } - + // MARK: - Placeholder Templates - - func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { + + func getLocalizableSampleTemplate(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { // This method will be called once per supported complication, and the results will be cached handler(nil) } - } diff --git a/Workout Spinner WatchKit Extension/ExtensionDelegate.swift b/Workout Spinner WatchKit Extension/ExtensionDelegate.swift index 3375600..1ffed72 100644 --- a/Workout Spinner WatchKit Extension/ExtensionDelegate.swift +++ b/Workout Spinner WatchKit Extension/ExtensionDelegate.swift @@ -9,7 +9,6 @@ import WatchKit class ExtensionDelegate: NSObject, WKExtensionDelegate { - func applicationDidFinishLaunching() { // Perform any final initialization of your application. } @@ -52,5 +51,4 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate { } } } - } diff --git a/Workout Spinner WatchKit Extension/Extensions/Color-extension.swift b/Workout Spinner WatchKit Extension/Extensions/Color-extension.swift index f4da0dd..2592fde 100644 --- a/Workout Spinner WatchKit Extension/Extensions/Color-extension.swift +++ b/Workout Spinner WatchKit Extension/Extensions/Color-extension.swift @@ -9,38 +9,34 @@ import SwiftUI extension Color { - init(red: Int, green: Int, blue: Int) { func f(_ x: Int) -> Double { return Double(x) / 255.0 } self.init(red: f(red), green: f(green), blue: f(blue)) } - + static func randomPastelColor() -> Color { - let pastelRange = 140...255 + let pastelRange = 140 ... 255 return Color(red: Int.random(in: pastelRange), green: Int.random(in: pastelRange), blue: Int.random(in: pastelRange)) } } - struct CustomColors_Previews: PreviewProvider { - static var customColors: [Color] = [.workoutGreen, .darkGray, .deepRed, .deepRed2, .pastelDarkRed, .pastelDarkRed2] - + static var previews: some View { Group { - ForEach(0..<10) { _ in + ForEach(0 ..< 10) { _ in Color.randomPastelColor().previewLayout(.fixed(width: 50, height: 50)) } - ForEach(0.. { override var body: ContentView { diff --git a/Workout Spinner WatchKit Extension/Models/BodyPartSelections.swift b/Workout Spinner WatchKit Extension/Models/BodyPartSelections.swift index 49448d7..d3a640c 100644 --- a/Workout Spinner WatchKit Extension/Models/BodyPartSelections.swift +++ b/Workout Spinner WatchKit Extension/Models/BodyPartSelections.swift @@ -8,7 +8,6 @@ import SwiftUI - struct BodyPartSelection: Identifiable { let bodypart: ExerciseBodyPart var enabled: Bool @@ -17,46 +16,41 @@ struct BodyPartSelection: Identifiable { } } - class BodyPartSelections: ObservableObject { - @Published var bodyparts = [BodyPartSelection]() - + /// Extract the body parts from exeristing exercise information. /// - Parameter exerciseInfo: The exercise information. init(fromExerciseInfo exerciseInfo: ExerciseInfo) { - self.bodyparts = ExerciseBodyPart.allCases + bodyparts = ExerciseBodyPart.allCases .sorted { $0.rawValue < $1.rawValue } .map { BodyPartSelection(bodypart: $0, enabled: exerciseInfo.bodyParts.contains($0)) } } - - + /// Initialize with specificed body parts. init(bodyparts: [BodyPartSelection]) { self.bodyparts = bodyparts } - - + enum DefaultBodyPartsSelection { case none, all, userDefaults } - + /// Initialize with some default selection of body parts. /// - Parameter selection: The section of body parts to use. init(with selection: DefaultBodyPartsSelection) { if selection == .userDefaults { let userDefaultsData = BodyPartSelections.readUserDefaults() - self.bodyparts = ExerciseBodyPart.allCases + bodyparts = ExerciseBodyPart.allCases .sorted { $0.rawValue < $1.rawValue } - .map{ BodyPartSelection(bodypart: $0, enabled: userDefaultsData[$0.rawValue] ?? true) } + .map { BodyPartSelection(bodypart: $0, enabled: userDefaultsData[$0.rawValue] ?? true) } } else { - self.bodyparts = ExerciseBodyPart.allCases + bodyparts = ExerciseBodyPart.allCases .sorted { $0.rawValue < $1.rawValue } .map { BodyPartSelection(bodypart: $0, enabled: selection == .all) } } } - - + static func readUserDefaults() -> [String: Bool] { if let data = UserDefaults.standard.dictionary(forKey: UserDefaultsKeys.activeBodyParts.rawValue) as? [String: Bool] { return data @@ -64,8 +58,7 @@ class BodyPartSelections: ObservableObject { return [String: Bool]() } } - - + func saveDataToUserDefaults() { var data = [String: Bool]() for bp in bodyparts { diff --git a/Workout Spinner WatchKit Extension/Models/CrownVelocityCalculator.swift b/Workout Spinner WatchKit Extension/Models/CrownVelocityCalculator.swift index 1eaa13f..24f3f00 100644 --- a/Workout Spinner WatchKit Extension/Models/CrownVelocityCalculator.swift +++ b/Workout Spinner WatchKit Extension/Models/CrownVelocityCalculator.swift @@ -11,52 +11,51 @@ import Foundation class CrownVelocityCalculator { private var history = [Double]() var currentVelocity: Double = 0.0 - + var velocityThreshold: Double = 500.0 private(set) var didPassThreshold: Bool = false - + var memory: Int = 10 - + init() {} - + init(memory: Int) { self.memory = memory } - + init(velocityThreshold: Double) { self.velocityThreshold = velocityThreshold } - + init(velocityThreshold: Double, memory: Int) { self.velocityThreshold = velocityThreshold self.memory = memory } - + func update(newValue x: Double) { - self.history.append(x) + history.append(x) if history.count < memory { return } - + if history.count > memory { history = history.suffix(memory) } - + var diffs: Double = 0.0 - for i in 0..<(history.count - 1) { - diffs += history[i+1] - history[i] + for i in 0 ..< (history.count - 1) { + diffs += history[i + 1] - history[i] } - + currentVelocity = diffs / Double(history.count - 1) checkThreshold() } - + func checkThreshold() { if !didPassThreshold { didPassThreshold = abs(currentVelocity) > velocityThreshold } } - + func resetThreshold() { didPassThreshold = false } - } diff --git a/Workout Spinner WatchKit Extension/Models/Exercise enums.swift b/Workout Spinner WatchKit Extension/Models/Exercise enums.swift index 64d21c3..2f7b807 100644 --- a/Workout Spinner WatchKit Extension/Models/Exercise enums.swift +++ b/Workout Spinner WatchKit Extension/Models/Exercise enums.swift @@ -16,12 +16,10 @@ func == (lhs: ExerciseIntensity, rhs: ExerciseIntensity) -> Bool { return lhs.rawValue == rhs.rawValue } - enum ExerciseType: String, Codable, CaseIterable { case count, time } - enum ExerciseBodyPart: String, Codable, CaseIterable { case arms, core, legs, back, cardio, neck } diff --git a/Workout Spinner WatchKit Extension/Models/ExerciseInfo.swift b/Workout Spinner WatchKit Extension/Models/ExerciseInfo.swift index 7ba04dc..8d32992 100644 --- a/Workout Spinner WatchKit Extension/Models/ExerciseInfo.swift +++ b/Workout Spinner WatchKit Extension/Models/ExerciseInfo.swift @@ -15,7 +15,7 @@ struct ExerciseInfo: Identifiable, Codable, Equatable { let bodyParts: [ExerciseBodyPart] let workoutValue: [String: Float] var active: Bool = true - + static func == (lhs: ExerciseInfo, rhs: ExerciseInfo) -> Bool { return lhs.id == rhs.id } diff --git a/Workout Spinner WatchKit Extension/Models/ExerciseOptions.swift b/Workout Spinner WatchKit Extension/Models/ExerciseOptions.swift index 0d7ee54..347ae17 100644 --- a/Workout Spinner WatchKit Extension/Models/ExerciseOptions.swift +++ b/Workout Spinner WatchKit Extension/Models/ExerciseOptions.swift @@ -9,21 +9,16 @@ import Foundation class ExerciseOptions: NSObject, ObservableObject { - @Published var allExercises = [ExerciseInfo]() - + var exercises: [ExerciseInfo] { - get { - return allExercises.filter { $0.active } - } + return allExercises.filter { $0.active } } - + var exercisesBlacklistFiltered: [ExerciseInfo] { - get { - return filterBlacklistedBodyParts() - } + return filterBlacklistedBodyParts() } - + override init() { super.init() allExercises = loadExercises() @@ -31,7 +26,7 @@ class ExerciseOptions: NSObject, ObservableObject { resetExerciseOptions() } } - + /// Write exercise array to disk. func saveExercises() { let encoder = JSONEncoder() @@ -42,8 +37,7 @@ class ExerciseOptions: NSObject, ObservableObject { print("Error when encoding exercises to JSON: \(error.localizedDescription)") } } - - + /// Read in exercise array from disk. /// - Returns: Array of exercises. func loadExercises() -> [ExerciseInfo] { @@ -57,14 +51,13 @@ class ExerciseOptions: NSObject, ObservableObject { } return [] } - - + func filterBlacklistedBodyParts() -> [ExerciseInfo] { let inactiveBodyparts: [ExerciseBodyPart] = BodyPartSelections(with: .userDefaults) .bodyparts .filter { !$0.enabled } .map { $0.bodypart } - + return exercises.filter { exercise in if let _ = exercise.bodyParts.first(where: { inactiveBodyparts.contains($0) }) { return false @@ -74,16 +67,15 @@ class ExerciseOptions: NSObject, ObservableObject { } } - - // MARK: - Editing options array + extension ExerciseOptions { /// Add a new exercise. func append(_ exercise: ExerciseInfo) { allExercises.append(exercise) saveExercises() } - + /// Replace one exercise with another. func replace(_ exercise: ExerciseInfo, with newExercise: ExerciseInfo) { if let idx = allExercises.firstIndex(where: { $0 == exercise }) { @@ -91,7 +83,7 @@ extension ExerciseOptions { saveExercises() } } - + /// Remove an exercise. func remove(_ exercise: ExerciseInfo) { let startCount = allExercises.count @@ -100,7 +92,7 @@ extension ExerciseOptions { saveExercises() } } - + /// Remove multiple exercises. func remove(_ exercisesToRemove: [ExerciseInfo]) { if exercisesToRemove.count == 0 { return } @@ -112,7 +104,7 @@ extension ExerciseOptions { saveExercises() } } - + /// Update an existing exercise or append it to the end of the options. func updateOrAppend(_ exercise: ExerciseInfo) { if let idx = allExercises.firstIndex(where: { $0 == exercise }) { @@ -122,7 +114,7 @@ extension ExerciseOptions { } saveExercises() } - + /// Resest the list of exercises to default options. func resetExerciseOptions() { do { @@ -134,9 +126,8 @@ extension ExerciseOptions { } } - - // MARK: - Reading in default exercise JSON. + extension ExerciseOptions { /// Parse the JSON data to an array of workouts. /// - Parameter jsonData: JSON data as a `Data` object. @@ -149,7 +140,7 @@ extension ExerciseOptions { throw error } } - + /// Read in data from a JSON file. /// - Parameter name: Local file name. /// - Throws: Will throw an error if the file in unreachable or cannot be converted to a `Data` object. @@ -169,14 +160,14 @@ extension ExerciseOptions { throw DataReadingError.fileDoesNotExist(name) } } - + enum DataReadingError: Error, LocalizedError { case fileDoesNotExist(String) case cannotConvertToData - + var errorDescription: String? { switch self { - case .fileDoesNotExist(let fileName): + case let .fileDoesNotExist(fileName): return NSLocalizedString("No such file in bundle: '\(fileName)'", comment: "") case .cannotConvertToData: return NSLocalizedString("Cannot convert text in file to data.", comment: "") @@ -184,8 +175,3 @@ extension ExerciseOptions { } } } - - - - - diff --git a/Workout Spinner WatchKit Extension/Models/HeartRateGraphData.swift b/Workout Spinner WatchKit Extension/Models/HeartRateGraphData.swift index 34b8db9..9327291 100644 --- a/Workout Spinner WatchKit Extension/Models/HeartRateGraphData.swift +++ b/Workout Spinner WatchKit Extension/Models/HeartRateGraphData.swift @@ -15,27 +15,29 @@ struct HeartRateGraphDatum { } struct HeartRateGraphData { - var data: [HeartRateGraphDatum] - + var minX: Double { return data.map { $0.x }.min()! } + var minY: Double { return data.map { $0.y }.min()! } + var maxX: Double { return data.map { $0.x }.max()! } + var maxY: Double { return data.map { $0.y }.max()! } - + init(workoutTraker: WorkoutTracker) { - self.data = HeartRateGraphData.workoutTrackerDataToGraphData(workoutTraker) + data = HeartRateGraphData.workoutTrackerDataToGraphData(workoutTraker) // self.data = HeartRateGraphData.mockHeartRateData() } - + static func workoutTrackerDataToGraphData(_ workoutTracker: WorkoutTracker) -> [HeartRateGraphDatum] { var HRData = [HeartRateGraphDatum]() var previousEndingX = 0.0 @@ -51,35 +53,34 @@ struct HeartRateGraphData { } return HRData } - + func convertedXData(toMin: Double, toMax: Double) -> [Double] { return data.map { convert(x: $0.x, toMin: toMin, toMax: toMax) } } - + func convertedYData(toMin: Double, toMax: Double) -> [Double] { return data.map { convert(y: $0.y, toMin: toMin, toMax: toMax) } } - + func convert(x: Double, toMin: Double, toMax: Double) -> Double { - return x.rangeMap(inMin: self.minX, inMax: self.maxX, outMin: toMin, outMax: toMax) + return x.rangeMap(inMin: minX, inMax: maxX, outMin: toMin, outMax: toMax) } - + func convert(y: Double, toMin: Double, toMax: Double) -> Double { - return y.rangeMap(inMin: self.minY, inMax: self.maxY, outMin: toMin, outMax: toMax) + return y.rangeMap(inMin: minY, inMax: maxY, outMin: toMin, outMax: toMax) } } - extension HeartRateGraphData { static func mockHeartRateData() -> [HeartRateGraphDatum] { var d = [HeartRateGraphDatum]() var x = 0.0 - for i in 0..<5 { - let n = (5...25).randomElement()! + for i in 0 ..< 5 { + let n = (5 ... 25).randomElement()! var y = 50.0 - for _ in 0.. Int { - let runningTime: Int = Int(-1 * (self.start.timeIntervalSinceNow)) - return self.accumulatedTime + runningTime + let runningTime = Int(-1 * start.timeIntervalSinceNow) + return accumulatedTime + runningTime } - - + /// Request authorization to access HealthKit. func requestAuthorization() { // The quantity type to write to the health store. let typesToShare: Set = [ - HKQuantityType.workoutType() + HKQuantityType.workoutType(), ] - + // The quantity types to read from the health store. let typesToRead: Set = [ HKQuantityType.quantityType(forIdentifier: .heartRate)!, - HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! + HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!, ] - + // Request authorization for those quantity types. - healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in + healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { _, error in if let error = error { print("Error requesting data read/share authorization: \(error.localizedDescription)") return @@ -104,8 +100,7 @@ class WorkoutManager: NSObject, ObservableObject { print("Successfully requesting authoriation for data reading and sharing.") } } - - + /// Provide the workout configuration. internal func workoutConfiguration() -> HKWorkoutConfiguration { let configuration = HKWorkoutConfiguration() @@ -113,8 +108,7 @@ class WorkoutManager: NSObject, ObservableObject { configuration.locationType = .indoor return configuration } - - + /// Start the workout. internal func setupWorkout() { // Create the session and obtain the workout builder. @@ -126,16 +120,16 @@ class WorkoutManager: NSObject, ObservableObject { print("Error creating workout: \(error.localizedDescription)") return } - + // Setup session and builder. session.delegate = self builder.delegate = self - + // Set the workout builder's data source. builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: workoutConfiguration()) } - + /// Start a workout func startWorkout() { // Start the timer. @@ -146,7 +140,7 @@ class WorkoutManager: NSObject, ObservableObject { // Start the workout session and begin data collection. session.startActivity(with: Date()) } - + /// Pause a workout. func pauseWorkout() { // Pause the workout. @@ -157,7 +151,7 @@ class WorkoutManager: NSObject, ObservableObject { accumulatedTime = elapsedSeconds active = false } - + /// Resume a previously started workout. func resumeWorkout() { // Start the timer. @@ -166,7 +160,7 @@ class WorkoutManager: NSObject, ObservableObject { // Resume the workout. session.resume() } - + /// Reset all of the informational variables. func resetTrackedInformation() { accumulatedTime = 0 @@ -175,12 +169,12 @@ class WorkoutManager: NSObject, ObservableObject { activeCalories = 0 elapsedSeconds = 0 } - + /// End a workout. func endWorkout() { print("Ending workout session.") - - builder.endCollection(withEnd: Date()) { (success, error) in + + builder.endCollection(withEnd: Date()) { success, error in if let error = error { print("Data collection ended with error: \(error.localizedDescription)") } else if success { @@ -189,69 +183,66 @@ class WorkoutManager: NSObject, ObservableObject { print("Data collection did not end successfully (but without error).") } } - + session.end() active = false resetTrackedInformation() } - - + // MARK: - Update the UI + // Update the published values. internal func updateForStatistics(_ statistics: HKStatistics?) { guard let statistics = statistics else { return } - + DispatchQueue.main.async { switch statistics.quantityType { case HKQuantityType.quantityType(forIdentifier: .heartRate): let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute()) let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) - let roundedValue = Double( round( 1 * value! ) / 1 ) + let roundedValue = Double(round(1 * value!) / 1) self.allHeartRateReadings.append(HeartRateReading(heartRate: value!)) self.heartrate = roundedValue return case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned): let energyUnit = HKUnit.kilocalorie() let value = statistics.sumQuantity()?.doubleValue(for: energyUnit) - self.activeCalories = Double( round( 1 * value! ) / 1 ) + self.activeCalories = Double(round(1 * value!) / 1) return default: return } } } - + enum MockWorkoutStatistics { case heartRate, activeEnergyBurned } - + internal func updateMockStatistics(quantityType: MockWorkoutStatistics) { DispatchQueue.main.async { switch quantityType { case .heartRate: - self.heartrate += Double.random(in: self.heartrate == 0 ? 100...120 : -5...5) + self.heartrate += Double.random(in: self.heartrate == 0 ? 100 ... 120 : -5 ... 5) self.allHeartRateReadings.append(HeartRateReading(heartRate: self.heartrate)) case .activeEnergyBurned: - self.activeCalories += Double.random(in: 1...3) + self.activeCalories += Double.random(in: 1 ... 3) } } } - } - - - // MARK: - HKWorkoutSessionDelegate + extension WorkoutManager: HKWorkoutSessionDelegate { - - func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, - from fromState: HKWorkoutSessionState, date: Date) { + func workoutSession(_: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, + from fromState: HKWorkoutSessionState, date _: Date) + { print("Workout session did change state: \(workoutStateDescription(fromState)) -> \(workoutStateDescription(toState))") - + if toState == .ended { // Wait for the session to transition states before ending the builder. - builder.finishWorkout { (workout, error) in + builder.finishWorkout { _, error in // Optionally display a workout summary to the user. if let error = error { print("Builder did finish with error: \(error.localizedDescription)") @@ -260,13 +251,11 @@ extension WorkoutManager: HKWorkoutSessionDelegate { } } } - - - func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { + + func workoutSession(_: HKWorkoutSession, didFailWithError error: Error) { print("Workout session failed: \(error.localizedDescription)") } - - + /// Return a description for the workout session state. /// - Parameter state: A workout state. /// - Returns: A descriptive string. @@ -278,7 +267,7 @@ extension WorkoutManager: HKWorkoutSessionDelegate { return "not started" case .paused: return "paused" - case.prepared: + case .prepared: return "prepared" case .running: return "running" @@ -290,26 +279,22 @@ extension WorkoutManager: HKWorkoutSessionDelegate { } } - - - // MARK: - HKLiveWorkoutBuilderDelegate + extension WorkoutManager: HKLiveWorkoutBuilderDelegate { - - func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) { + func workoutBuilderDidCollectEvent(_: HKLiveWorkoutBuilder) { // Does nothing for now. } - - + func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set) { for type in collectedTypes { guard let quantityType = type as? HKQuantityType else { return // Nothing to do. } - + /// - Tag: GetStatistics let statistics = workoutBuilder.statistics(for: quantityType) - + // Update the published values. updateForStatistics(statistics) } diff --git a/Workout Spinner WatchKit Extension/Models/WorkoutTracker.swift b/Workout Spinner WatchKit Extension/Models/WorkoutTracker.swift index fb0af44..fe8f8d1 100644 --- a/Workout Spinner WatchKit Extension/Models/WorkoutTracker.swift +++ b/Workout Spinner WatchKit Extension/Models/WorkoutTracker.swift @@ -8,32 +8,30 @@ import Foundation - class WorkoutTracker: NSObject, ObservableObject { - /// Collected data of exercises. var data = [WorkoutTrackerDatum]() - + /// Number of exercises tracked. var numberOfExercises: Int { data.count } - + /// Total active calories. var totalActiveCalories: Double { return data.map { $0.activeCalories }.reduce(0, +) } - + /// Total duration. var totalDuration: Double { return data.map { $0.duration }.reduce(0, +) } - + /// Average heart rate reading. var averageHeartRate: Double? { if data.count < 1 { return nil } let HRtotal = data - .map { $0.heartRate.map({ $0.heartRate }).reduce(0, +) } + .map { $0.heartRate.map { $0.heartRate }.reduce(0, +) } .reduce(0, +) let HRcount = data .map { $0.heartRate.count } @@ -41,27 +39,26 @@ class WorkoutTracker: NSObject, ObservableObject { if HRcount == 0 { return nil } return HRtotal / Double(HRcount) } - + /// Maximum heart rate reading. var maxHeartRate: Double? { if data.count < 1 { return nil } let x = data - .map { ($0.heartRate.map({ $0.heartRate }).max()) ?? 0 } + .map { ($0.heartRate.map { $0.heartRate }.max()) ?? 0 } .max() ?? 0 return x == 0.0 ? nil : x } - + /// Minimum heart rate reading. var minHeartRate: Double? { if data.count < 1 { return nil } let x = data .filter { $0.heartRate.count > 0 } - .map { ($0.heartRate.map({ $0.heartRate }).min()) ?? 0 } + .map { ($0.heartRate.map { $0.heartRate }.min()) ?? 0 } .min() ?? 0 return x == 0.0 ? nil : x } - - + /// Add a new data point. /// - Parameters: /// - info: Exercise information object @@ -72,14 +69,13 @@ class WorkoutTracker: NSObject, ObservableObject { let newData = WorkoutTrackerDatum(exerciseInfo: info, duration: duration, activeCalories: activeCalories, heartRate: heartRate) data.append(newData) } - + /// Clear the tracked data. func clear() { data = [] } } - struct WorkoutTrackerDatum: Identifiable { let id = UUID() let exerciseInfo: ExerciseInfo diff --git a/Workout Spinner WatchKit Extension/NotificationController.swift b/Workout Spinner WatchKit Extension/NotificationController.swift index 40baa87..726caa4 100644 --- a/Workout Spinner WatchKit Extension/NotificationController.swift +++ b/Workout Spinner WatchKit Extension/NotificationController.swift @@ -6,12 +6,11 @@ // Copyright © 2020 Joshua Cook. All rights reserved. // -import WatchKit import SwiftUI import UserNotifications +import WatchKit class NotificationController: WKUserNotificationHostingController { - override var body: NotificationView { return NotificationView() } @@ -26,7 +25,7 @@ class NotificationController: WKUserNotificationHostingController some View { configuration.label .padding(padding) @@ -27,7 +25,6 @@ struct DoneButtonStyle: ButtonStyle { } } - struct ButtonStyles_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/Workout Spinner WatchKit Extension/View Modifiers/SpinnerRotationModifier.swift b/Workout Spinner WatchKit Extension/View Modifiers/SpinnerRotationModifier.swift index 2958287..49bda54 100644 --- a/Workout Spinner WatchKit Extension/View Modifiers/SpinnerRotationModifier.swift +++ b/Workout Spinner WatchKit Extension/View Modifiers/SpinnerRotationModifier.swift @@ -9,47 +9,43 @@ import SwiftUI struct SpinnerRotationModifier: AnimatableModifier { - var rotation: Angle - var onFinishedRotationAnimation: () -> () = {} + var onFinishedRotationAnimation: () -> Void = {} var completionTolerance: Double = 0.00001 - + private var targetRotation: Angle - + init(rotation: Angle) { self.rotation = rotation - self.targetRotation = rotation + targetRotation = rotation } - - init(rotation: Angle, onFinishedRotationAnimation: @escaping () -> ()) { + + init(rotation: Angle, onFinishedRotationAnimation: @escaping () -> Void) { self.rotation = rotation - self.targetRotation = rotation + targetRotation = rotation self.onFinishedRotationAnimation = onFinishedRotationAnimation } - - init(rotation: Angle, onFinishedRotationAnimation: @escaping () -> (), completionTolerance: Double) { + + init(rotation: Angle, onFinishedRotationAnimation: @escaping () -> Void, completionTolerance: Double) { self.rotation = rotation - self.targetRotation = rotation + targetRotation = rotation self.onFinishedRotationAnimation = onFinishedRotationAnimation self.completionTolerance = completionTolerance } - - + var animatableData: Double { get { rotation.degrees } - set { - rotation = .degrees(newValue) - checkIfFinished() - } + set { + rotation = .degrees(newValue) + checkIfFinished() + } } - - + func body(content: Content) -> some View { content .rotationEffect(rotation) } - - + func checkIfFinished() { if abs(rotation.degrees - targetRotation.degrees) < completionTolerance { DispatchQueue.main.async { self.onFinishedRotationAnimation() } @@ -57,7 +53,6 @@ struct SpinnerRotationModifier: AnimatableModifier { } } - struct SpinnerRotationModifier_Previews: PreviewProvider { static var previews: some View { ZStack { diff --git a/Workout Spinner WatchKit Extension/Views/ContentView.swift b/Workout Spinner WatchKit Extension/Views/ContentView.swift index 81b9a86..5f2c249 100644 --- a/Workout Spinner WatchKit Extension/Views/ContentView.swift +++ b/Workout Spinner WatchKit Extension/Views/ContentView.swift @@ -9,11 +9,10 @@ import SwiftUI struct ContentView: View { - let workoutManager = WorkoutManager() let workoutTracker = WorkoutTracker() let exerciseOptions = ExerciseOptions() - + var body: some View { WelcomeView(workoutManager: workoutManager, workoutTracker: workoutTracker, exerciseOptions: exerciseOptions) } diff --git a/Workout Spinner WatchKit Extension/Views/ExerciseFinishView.swift b/Workout Spinner WatchKit Extension/Views/ExerciseFinishView.swift index ce2ea8b..bd19269 100644 --- a/Workout Spinner WatchKit Extension/Views/ExerciseFinishView.swift +++ b/Workout Spinner WatchKit Extension/Views/ExerciseFinishView.swift @@ -12,9 +12,9 @@ struct SmallImageAndTextView: View { let imageName: String let text: String let imageColor: Color - + let font: Font = .system(size: 14) - + var body: some View { HStack { Image(systemName: imageName).font(font).foregroundColor(imageColor) @@ -24,13 +24,12 @@ struct SmallImageAndTextView: View { } struct ExerciseDataRowView: View { - let data: WorkoutTrackerDatum - + var averageHeartRate: Int { - return Int(average(data.heartRate.map({ $0.heartRate })).rounded()) + return Int(average(data.heartRate.map { $0.heartRate }).rounded()) } - + var body: some View { VStack { HStack { @@ -39,7 +38,7 @@ struct ExerciseDataRowView: View { }.padding(.bottom, 2) HStack { Spacer() - SmallImageAndTextView(imageName: "heart", text: data.heartRate.count == 0 ? "NA" : "\(averageHeartRate)", imageColor: .red) + SmallImageAndTextView(imageName: "heart", text: data.heartRate.count == 0 ? "NA" : "\(averageHeartRate)", imageColor: .red) Spacer() SmallImageAndTextView(imageName: "flame", text: "\(Int(data.activeCalories.rounded()))", imageColor: .yellow) Spacer() @@ -48,7 +47,7 @@ struct ExerciseDataRowView: View { } } } - + internal func average(_ x: [Double]) -> Double { if x.count == 0 { return 0 @@ -58,9 +57,8 @@ struct ExerciseDataRowView: View { } struct ExerciseFinishView: View { - @ObservedObject var workoutTracker: WorkoutTracker - + var body: some View { List { ForEach(workoutTracker.data) { data in diff --git a/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift b/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift index 6b82949..7e5f627 100644 --- a/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift +++ b/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift @@ -6,34 +6,33 @@ // Copyright © 2020 Joshua Cook. All rights reserved. // -import SwiftUI import Combine +import SwiftUI struct ExercisePicker: View { - @ObservedObject var workoutManager: WorkoutManager @ObservedObject var exerciseOptions: ExerciseOptions @State internal var crownRotation = 0.0 - + var numExercises: Int { return exerciseOptions.exercisesBlacklistFiltered.count } - + var crownVelocity = CrownVelocityCalculator(velocityThreshold: 50, memory: 20) - + @Binding internal var exerciseSelected: Bool @State internal var selectedExerciseIndex: Int = 0 - + var spinDirection: Double { return WKInterfaceDevice().crownOrientation == .left ? 1.0 : -1.0 } - + init(workoutManager: WorkoutManager, exerciseOptions: ExerciseOptions, exerciseSelected: Binding) { self.workoutManager = workoutManager self.exerciseOptions = exerciseOptions - self._exerciseSelected = exerciseSelected + _exerciseSelected = exerciseSelected } - + var body: some View { ZStack { VStack(spacing: 0) { @@ -41,12 +40,12 @@ struct ExercisePicker: View { GeometryReader { geo in ZStack { ZStack { - ForEach(0..) { self.workoutManager = workoutManager - self._exerciseCanceled = exerciseCanceled + _exerciseCanceled = exerciseCanceled exerciseInfo = workoutManager.exerciseInfo } - + var body: some View { VStack { - Text(exerciseInfo?.displayName ?? "(no workout selected)") .lineLimit(1) .font(.system(size: 25, weight: .regular, design: .rounded)) - + Spacer() - + Text(displayDuration) - .font(.system(size: 40, weight: .semibold , design: .rounded)) + .font(.system(size: 40, weight: .semibold, design: .rounded)) .foregroundColor(.yellow) - + Spacer() - - + HStack { Text("Starting in") Text("\(timeRemaining)") @@ -69,20 +66,18 @@ struct ExerciseStartView: View { .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) .background(Color.gray.opacity(0.3)) .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) - + Spacer() - + Text("Double tap to cancel").foregroundColor(.gray).font(.footnote) - } - .onReceive(timer) { time in + .onReceive(timer) { _ in if self.timeRemaining > 0 { self.timeRemaining -= 1 } else if self.timeRemaining <= 0 { exerciseCanceled = false self.presentationMode.wrappedValue.dismiss() } - } .edgesIgnoringSafeArea(.bottom) .onTapGesture(count: 2) { @@ -96,7 +91,6 @@ struct ExerciseStartView: View { } } - extension ExerciseStartView { static func loadExerciseIntensity() -> ExerciseIntensity { if let intensityString = UserDefaults.standard.string(forKey: UserDefaultsKeys.exerciseIntensity.rawValue) { @@ -108,10 +102,7 @@ extension ExerciseStartView { } } - - struct WorkoutStartView_Previews: PreviewProvider { - static var workouts: ExerciseOptions { let ws = ExerciseOptions() let i = ws.exercises.first { $0.type == .count }! @@ -119,7 +110,7 @@ struct WorkoutStartView_Previews: PreviewProvider { ws.allExercises = [i, j] return ws } - + static var previews: some View { Group { ForEach(workouts.exercises) { info in diff --git a/Workout Spinner WatchKit Extension/Views/ExerciseView.swift b/Workout Spinner WatchKit Extension/Views/ExerciseView.swift index 3406356..1675a62 100644 --- a/Workout Spinner WatchKit Extension/Views/ExerciseView.swift +++ b/Workout Spinner WatchKit Extension/Views/ExerciseView.swift @@ -9,23 +9,22 @@ import SwiftUI struct ExerciseView: View { - @ObservedObject var workoutManager: WorkoutManager @ObservedObject var workoutTracker: WorkoutTracker @Binding private var exerciseComplete: Bool var workoutInfo: ExerciseInfo? - + let intensity: ExerciseIntensity = ExerciseStartView.loadExerciseIntensity() - + init(workoutManager: WorkoutManager, workoutTracker: WorkoutTracker, exerciseComplete: Binding) { self.workoutManager = workoutManager self.workoutTracker = workoutTracker - self._exerciseComplete = exerciseComplete + _exerciseComplete = exerciseComplete workoutInfo = workoutManager.exerciseInfo } - + let infoFontSize: CGFloat = 18 - + /// The duration as "MM:SS". var displayDuration: String { guard let info = workoutManager.exerciseInfo else { return "" } @@ -38,12 +37,12 @@ struct ExerciseView: View { } return intensity.rawValue } - + /// The heart rate information to display. var displayHeartRate: String { workoutManager.allHeartRateReadings.count == 0 ? "--" : "\(Int(workoutManager.heartrate))" } - + var body: some View { VStack { Text(workoutInfo?.displayName ?? "(no workout)") @@ -51,44 +50,42 @@ struct ExerciseView: View { .padding(.bottom, 2) Text(displayDuration).font(.system(size: 20, weight: .regular, design: .rounded)) .padding(.bottom, 5) - + HStack { Spacer() HStack { Image(systemName: "heart").foregroundColor(.red).font(.system(size: infoFontSize)) Text(displayHeartRate).font(.system(size: infoFontSize)) } - + Spacer() - + HStack { Image(systemName: "flame").foregroundColor(.yellow).font(.system(size: infoFontSize)) Text("\(Int(workoutManager.activeCalories))").font(.system(size: infoFontSize)) } - + Spacer() } .padding(.bottom, 5) - + HStack { Image(systemName: "stopwatch").foregroundColor(.blue).font(.system(size: infoFontSize)) Text("\(convertNumberSecondsToTimeFormat(Double(workoutManager.elapsedSeconds)))").font(.system(size: infoFontSize)) } - - - + Spacer() - + Button(action: finishExercise, label: { Text("Done").foregroundColor(.green).bold() }) } } - + func convertNumberSecondsToTimeFormat(_ seconds: Double) -> String { let minutes = (seconds / 60.0).rounded(.down) let remainderSeconds = seconds.truncatingRemainder(dividingBy: 60.0).rounded() - + func doubleToPaddedString(_ x: Double, paddingLength: Int = 2) -> String { var s = "\(Int(x))" if s.count < paddingLength { @@ -96,31 +93,23 @@ struct ExerciseView: View { } return s } - + return "\(doubleToPaddedString(minutes)):\(doubleToPaddedString(remainderSeconds))" } } - extension ExerciseView { /// Called when the exercise is complete and the 'Done" button is tapped. - internal func finishExercise() { + func finishExercise() { // Add data from exercise to the workout tracker and clear the info from the workout manager. workoutTracker.addData(info: workoutManager.exerciseInfo!, duration: Double(workoutManager.elapsedSeconds), activeCalories: workoutManager.activeCalories, heartRate: workoutManager.allHeartRateReadings) workoutManager.resetTrackedInformation() - + exerciseComplete = true } } - - - - - - struct WorkoutView_Previews: PreviewProvider { - static var workoutOptions: ExerciseOptions { let ws = ExerciseOptions() let i = ws.exercises.first { $0.type == .count }! @@ -128,7 +117,7 @@ struct WorkoutView_Previews: PreviewProvider { ws.allExercises = [i, j] return ws } - + static var previews: some View { Group { ForEach(workoutOptions.exercises) { info in diff --git a/Workout Spinner WatchKit Extension/Views/General Components/Buttons.swift b/Workout Spinner WatchKit Extension/Views/General Components/Buttons.swift index ee39082..fadac63 100644 --- a/Workout Spinner WatchKit Extension/Views/General Components/Buttons.swift +++ b/Workout Spinner WatchKit Extension/Views/General Components/Buttons.swift @@ -9,10 +9,9 @@ import SwiftUI struct ListViewTextButton: View { - var label: String var action: () -> Void - + var body: some View { Button(action: action) { Text(label) diff --git a/Workout Spinner WatchKit Extension/Views/General Components/SectionHeader.swift b/Workout Spinner WatchKit Extension/Views/General Components/SectionHeader.swift index d98f173..6d4aea9 100644 --- a/Workout Spinner WatchKit Extension/Views/General Components/SectionHeader.swift +++ b/Workout Spinner WatchKit Extension/Views/General Components/SectionHeader.swift @@ -11,7 +11,7 @@ import SwiftUI struct SectionHeader: View { let imageName: String let text: String - + var body: some View { HStack { Image(systemName: imageName) @@ -20,7 +20,6 @@ struct SectionHeader: View { } } - struct SectionHeader_Previews: PreviewProvider { static var previews: some View { SectionHeader(imageName: "mustache", text: "Text") diff --git a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/GraphBackgroundSegment.swift b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/GraphBackgroundSegment.swift index ad7655e..a0d0a32 100644 --- a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/GraphBackgroundSegment.swift +++ b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/GraphBackgroundSegment.swift @@ -9,61 +9,57 @@ import SwiftUI struct GraphBackgroundSegment: View { - var graphData: HeartRateGraphData var size: CGSize var segmentData: [RectangleSegment] - + var totalSegmentWidth: Double { - return segmentData.reduce(0, { $0 + $1.width }) + return segmentData.reduce(0) { $0 + $1.width } } - + var maxSegmentHeight: Double { return segmentData.map { $0.height }.max()! } - + init(graphData: HeartRateGraphData, size: CGSize) { self.graphData = graphData self.size = size segmentData = GraphBackgroundSegment.makeSegmentData(from: graphData) } - + var body: some View { HStack(alignment: .center, spacing: 0) { - ForEach(0.. CGFloat { CGFloat(width.rangeMap(inMin: 0, inMax: totalSegmentWidth, outMin: 0, outMax: Double(size.width))) } - - + /// Scale a value to the height of the entire frame. /// - Parameter height: Height to be scaled. /// - Returns: Scaled height. func scaleHeight(_ height: Double) -> CGFloat { CGFloat(height.rangeMap(inMin: 0, inMax: maxSegmentHeight, outMin: 0, outMax: Double(size.height))) } - - + struct RectangleSegment { let width: Double let height: Double let color: Color } - - + /// Create the data for building the segments. /// - Parameter graphData: The graph data. /// - Returns: The segment data. @@ -71,7 +67,7 @@ struct GraphBackgroundSegment: View { var data = [RectangleSegment]() let groupIndices = Array(Set(graphData.data.map { $0.groupIndex })).sorted() let colors: [Color] = colorArray(numberOfColors: groupIndices.count) - + for idx in groupIndices { let xValues = graphData.data.filter { $0.groupIndex == idx }.map { $0.x } let minX = xValues.min()! @@ -80,30 +76,29 @@ struct GraphBackgroundSegment: View { let maxY = graphData.maxY data.append(RectangleSegment(width: maxX - minX, height: maxY - minY, color: colors[idx])) } - + return data } - - + /// Create an array of colors across the complete rainbow. /// - Parameter n: The number of colors to create. /// - Returns: An array of SwiftUI `Color` views. static func colorArray(numberOfColors n: Int) -> [Color] { var colors = [Color]() - + let jump = 3.0 / Double(n) var val: Double = 0 - for _ in 0.. Path { var path = Path() - + let width = Double(rect.size.width) let height = Double(rect.size.height) - + let xData = graphData.convertedXData(toMin: 0.0, toMax: width) let yData = graphData.convertedYData(toMin: height, toMax: 0.0) - + path.move(to: CGPoint(x: xData[0], y: yData[0])) for (x, y) in zip(xData, yData) { path.addLine(to: CGPoint(x: x, y: y)) } - + return path } } diff --git a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/HeartRateGraphView.swift b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/HeartRateGraphView.swift index e8cf0da..57bc8ba 100644 --- a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/HeartRateGraphView.swift +++ b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/HeartRateGraphView.swift @@ -9,29 +9,28 @@ import SwiftUI struct HeartRateGraphView: View { - var graphData: HeartRateGraphData - + let numYGridLines = 3 var yGridValues: [Double] { let min = graphData.minY.rounded(.down) let max = graphData.maxY.rounded(.up) var values = [min, max] let gap = ((max - min) / Double(numYGridLines + 1)) - for i in 1...numYGridLines { + for i in 1 ... numYGridLines { values.append(min + (gap * Double(i))) } values.sort { $0 > $1 } return values } - + var gridTextWidth: CGFloat = 25 var verticalPadding: CGFloat = 10 - - init (workoutTracker: WorkoutTracker) { + + init(workoutTracker: WorkoutTracker) { graphData = HeartRateGraphData(workoutTraker: workoutTracker) } - + var body: some View { GeometryReader { geo in HStack(alignment: .center, spacing: 0) { @@ -64,8 +63,7 @@ struct HeartRateGraphView: View { } } } - - + func reduceGeoSize(_ geo: GeometryProxy, widthBy widthAdjust: CGFloat = 0, heightBy heightAdjust: CGFloat = 0) -> CGSize { var modSize = geo.size modSize.width = modSize.width - widthAdjust @@ -74,7 +72,6 @@ struct HeartRateGraphView: View { } } - struct HeartRateGraphView_Previews: PreviewProvider { static var previews: some View { HeartRateGraphView(workoutTracker: WorkoutTracker()) @@ -82,4 +79,3 @@ struct HeartRateGraphView_Previews: PreviewProvider { .previewLayout(.fixed(width: 300, height: 200)) } } - diff --git a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVGridText.swift b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVGridText.swift index 4be2633..1cec6a4 100644 --- a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVGridText.swift +++ b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVGridText.swift @@ -9,14 +9,13 @@ import SwiftUI struct PlotHVGridText: View { - let value: Double let horizontal: Bool let graphData: HeartRateGraphData let size: CGSize - + let fontSize: CGFloat = 10 - + var position: CGPoint { var mappedValue = 0.0 if horizontal { diff --git a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVLines.swift b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVLines.swift index aace71e..9d25f5a 100644 --- a/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVLines.swift +++ b/Workout Spinner WatchKit Extension/Views/HeartRateGraph/PlotHVLines.swift @@ -9,27 +9,26 @@ import SwiftUI struct PlotHVLines: Shape { - let value: Double let horizontal: Bool let graphData: HeartRateGraphData - + func path(in rect: CGRect) -> Path { var path = Path() - + let width = Double(rect.size.width) let height = Double(rect.size.height) var mappedValue = 0.0 - + if horizontal { mappedValue = graphData.convert(y: value, toMin: height, toMax: 0) } else { mappedValue = graphData.convert(x: value, toMin: 0, toMax: width) } - + path.move(to: CGPoint(x: horizontal ? 0 : mappedValue, y: horizontal ? mappedValue : 0)) path.addLine(to: CGPoint(x: horizontal ? width : 0, y: horizontal ? mappedValue : 0)) - + return path } } diff --git a/Workout Spinner WatchKit Extension/Views/Settings Views/BodyPartSelectionListView.swift b/Workout Spinner WatchKit Extension/Views/Settings Views/BodyPartSelectionListView.swift index 2d0ed49..602b7c1 100644 --- a/Workout Spinner WatchKit Extension/Views/Settings Views/BodyPartSelectionListView.swift +++ b/Workout Spinner WatchKit Extension/Views/Settings Views/BodyPartSelectionListView.swift @@ -8,14 +8,12 @@ import SwiftUI - struct BodyPartSelectionListView: View { - @ObservedObject var bodyparts = BodyPartSelections(with: .userDefaults) - + var body: some View { List { - ForEach(0.. Int { if value == nil { return defaultValue @@ -83,8 +78,7 @@ struct EditExerciseView: View { return Int(value!) } } - - + var exerciseCountsHeader: String { switch ExerciseType.allCases[exerciseTypeIndex] { case .count: @@ -93,28 +87,27 @@ struct EditExerciseView: View { return "duration" } } - - + var body: some View { Form { Section(header: Text("Exercise name")) { TextField("Exercise name", text: $name) } - + Section { Toggle("Include exercise", isOn: $active) } - + Section(header: Text("Exercise type")) { Picker(selection: $exerciseTypeIndex, label: Text("Exercise type")) { - ForEach(0.. Int { let exerciseIntensity = UserDefaults.standard.string(forKey: UserDefaultsKeys.exerciseIntensity.rawValue) for (idx, intensity) in ExerciseIntensity.allCases.enumerated() { @@ -107,9 +101,6 @@ extension Settings { } } - - - struct Settings_Previews: PreviewProvider { static var previews: some View { Settings(exerciseOptions: ExerciseOptions()) diff --git a/Workout Spinner WatchKit Extension/Views/Spinner Subviews/SpinnerSlice.swift b/Workout Spinner WatchKit Extension/Views/Spinner Subviews/SpinnerSlice.swift index 4d6c9cb..dbc59f9 100644 --- a/Workout Spinner WatchKit Extension/Views/Spinner Subviews/SpinnerSlice.swift +++ b/Workout Spinner WatchKit Extension/Views/Spinner Subviews/SpinnerSlice.swift @@ -9,14 +9,13 @@ import SwiftUI struct SpinnerSliceShape: Shape { - let radius: CGFloat let angle: Angle - + func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.midX, y: rect.midY)) - path.addArc(center: CGPoint(x: rect.midX, y:rect.midY), + path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: angle * 0.5, endAngle: angle * (-0.5), @@ -25,21 +24,19 @@ struct SpinnerSliceShape: Shape { } } - struct SpinnerSlice: View { - let idx: Int let numberOfSlices: Int let width: CGFloat - + var sliceAngle: Angle { Angle.degrees(360.0 / Double(numberOfSlices)) } - + var rotationAngle: Angle { Angle.degrees(Double(idx) * 360.0 / Double(numberOfSlices)) } - + var body: some View { ZStack { Color.darkGray @@ -52,11 +49,10 @@ struct SpinnerSlice: View { } } - struct SpinnerSlice_Previews: PreviewProvider { static var previews: some View { VStack { - ForEach(3..<8) { i in + ForEach(3 ..< 8) { i in SpinnerSlice(idx: 1, numberOfSlices: i, width: 100) .padding() } diff --git a/Workout Spinner WatchKit Extension/Views/Spinner Subviews/WorkoutSlice.swift b/Workout Spinner WatchKit Extension/Views/Spinner Subviews/WorkoutSlice.swift index 92c7a28..efae44b 100644 --- a/Workout Spinner WatchKit Extension/Views/Spinner Subviews/WorkoutSlice.swift +++ b/Workout Spinner WatchKit Extension/Views/Spinner Subviews/WorkoutSlice.swift @@ -9,21 +9,20 @@ import SwiftUI struct WorkoutSlice: View { - let workoutInfo: ExerciseInfo let idx: Int let numberOfWorkouts: Int let size: CGFloat - + let offset: CGFloat = 8.0 var halfOffset: CGFloat { (offset / 2.0) - 2.0 } - + var rotationAngle: Angle { .degrees(Double(idx) * 360.0 / Double(numberOfWorkouts)) } - + var body: some View { Text(workoutInfo.displayName) .rotationEffect(.degrees(180)) @@ -37,14 +36,13 @@ struct WorkoutSlice: View { } } - struct WorkoutSlice_Previews: PreviewProvider { static var previews: some View { ZStack { Color.white - + VStack { - ForEach(3..<8) { i in + ForEach(3 ..< 8) { i in WorkoutSlice(workoutInfo: ExerciseOptions().exercises[0], idx: 1, numberOfWorkouts: i, size: 200) .padding() } diff --git a/Workout Spinner WatchKit Extension/Views/WelcomeView.swift b/Workout Spinner WatchKit Extension/Views/WelcomeView.swift index 94da1e2..63de664 100644 --- a/Workout Spinner WatchKit Extension/Views/WelcomeView.swift +++ b/Workout Spinner WatchKit Extension/Views/WelcomeView.swift @@ -8,7 +8,6 @@ import SwiftUI - struct StartWorkoutButtonStyle: ButtonStyle { func makeBody(configuration: Self.Configuration) -> some View { configuration.label @@ -21,19 +20,17 @@ struct StartWorkoutButtonStyle: ButtonStyle { } struct WelcomeView: View { - @ObservedObject var workoutManager: WorkoutManager @ObservedObject var workoutTracker: WorkoutTracker @ObservedObject var exerciseOptions: ExerciseOptions - + @State private var startWorkout = false @State private var presentSettingsView = false - + var body: some View { VStack { - Spacer(minLength: 0) - + NavigationLink(destination: WorkoutPagingView(workoutManager: workoutManager, workoutTracker: workoutTracker, exerciseOptions: exerciseOptions)) { Text("Start Workout") .font(.system(size: 30)) @@ -43,14 +40,14 @@ struct WelcomeView: View { } .buttonStyle(StartWorkoutButtonStyle()) .padding(.bottom, 5) - + Text("Press and hold the spinner to finish the workout.") .font(.system(size: 14)) .foregroundColor(.gray) .multilineTextAlignment(.leading) - + Spacer(minLength: 0) - + Button(action: { presentSettingsView = true }) { diff --git a/Workout Spinner WatchKit Extension/Views/WorkoutFinishView.swift b/Workout Spinner WatchKit Extension/Views/WorkoutFinishView.swift index dd36a3d..48d400d 100644 --- a/Workout Spinner WatchKit Extension/Views/WorkoutFinishView.swift +++ b/Workout Spinner WatchKit Extension/Views/WorkoutFinishView.swift @@ -8,12 +8,11 @@ import SwiftUI - struct InfoRowView: View { let title: String let titleColor: Color let value: String - + var body: some View { VStack { HStack { @@ -32,11 +31,11 @@ struct LinkedInfoRowView: View { let title: String let titleColor: Color let value: String - + var showEllipsis = true - + let action: () -> Void - + var body: some View { Button(action: action) { ZStack { @@ -53,32 +52,30 @@ struct LinkedInfoRowView: View { } } - - struct WorkoutFinishView: View { - @ObservedObject var workoutManager: WorkoutManager @ObservedObject var workoutTracker: WorkoutTracker - + @Environment(\.presentationMode) var presentationMode - + private var averageHR: String { return valueAsIntStringOrNA(workoutTracker.averageHeartRate) } + private var minHR: String { return valueAsIntStringOrNA(workoutTracker.minHeartRate) } + private var maxHR: String { return valueAsIntStringOrNA(workoutTracker.maxHeartRate) } - + @State private var showAllExercises: Bool = false @State private var showHeartRateChartView: Bool = false - + var body: some View { VStack { List { - LinkedInfoRowView(title: "Number of exercises", titleColor: .green, value: "\(workoutTracker.numberOfExercises)") { showAllExercises = true } @@ -92,9 +89,9 @@ struct WorkoutFinishView: View { } }) } - + InfoRowView(title: "Active Calories", titleColor: .yellow, value: "\(Int(workoutTracker.totalActiveCalories))") - + LinkedInfoRowView(title: "Average heart rate", titleColor: .red, value: averageHR, showEllipsis: averageHR != "NA") { if averageHR != "NA" { showHeartRateChartView = true @@ -110,9 +107,9 @@ struct WorkoutFinishView: View { } }) } - + InfoRowView(title: "Min/Max heart rate", titleColor: .red, value: "\(minHR) / \(maxHR)") - + ListViewTextButton(label: "Done") { presentationMode.wrappedValue.dismiss() workoutTracker.clear() @@ -122,7 +119,7 @@ struct WorkoutFinishView: View { } .navigationBarBackButtonHidden(true) } - + func valueAsIntStringOrNA(_ x: Double?) -> String { return x == nil ? "NA" : "\(Int(x!.rounded()))" } diff --git a/Workout Spinner WatchKit Extension/Views/WorkoutPagingView.swift b/Workout Spinner WatchKit Extension/Views/WorkoutPagingView.swift index 4cc8a9a..c13b654 100644 --- a/Workout Spinner WatchKit Extension/Views/WorkoutPagingView.swift +++ b/Workout Spinner WatchKit Extension/Views/WorkoutPagingView.swift @@ -9,19 +9,18 @@ import SwiftUI struct WorkoutPagingView: View { - @ObservedObject var workoutManager: WorkoutManager @ObservedObject var workoutTracker: WorkoutTracker @ObservedObject var exerciseOptions: ExerciseOptions - + @State private var currentPageIndex: Int = 0 @State private var exerciseSelectedByPicker = false @State private var exerciseCanceled = false @State private var exerciseComplete = false @State private var confirmFinishWorkout = false - + @Environment(\.presentationMode) var presentationMode - + var body: some View { ZStack { if currentPageIndex == 0 { @@ -71,9 +70,7 @@ struct WorkoutPagingView: View { } } - extension WorkoutPagingView { - /// Complete a single exercise. func finishExercise() { switch workoutManager.session.state { @@ -83,7 +80,7 @@ extension WorkoutPagingView { break } } - + /// Start an exercise. func startExercise() { switch workoutManager.session.state { @@ -95,7 +92,7 @@ extension WorkoutPagingView { break } } - + /// Complete the entire workout session. func finishWorkout() { workoutManager.endWorkout() @@ -107,7 +104,6 @@ extension WorkoutPagingView { } } - struct WorkoutPagingView_Previews: PreviewProvider { static var previews: some View { WorkoutPagingView(workoutManager: WorkoutManager(), workoutTracker: WorkoutTracker(), exerciseOptions: ExerciseOptions()) From fc8a40973ac95bca930a27569548a13487fa621f Mon Sep 17 00:00:00 2001 From: Josh Cook Date: Mon, 16 Nov 2020 06:15:51 -0500 Subject: [PATCH 2/2] doc: update README badges --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8498fd0..d7b8411 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,19 @@ **An Apple Watch App that randomly selects quick exercises.** -![Swift](https://img.shields.io/badge/Swift-Swift_Project-FA7343.svg?style=flat&logo=swift) -![iOS](https://img.shields.io/badge/watchOS-App-999999.svg?style=flat&logo=apple) -[![jhc github](https://img.shields.io/badge/GitHub-jhrcook-181717.svg?style=flat&logo=github)](https://github.com/jhrcook) -[![jhc twitter](https://img.shields.io/badge/Twitter-@JoshDoesA-00aced.svg?style=flat&logo=twitter)](https://twitter.com/JoshDoesa) -[![jhc website](https://img.shields.io/badge/Website-Joshua_Cook-5087B2.svg?style=flat&logo=telegram)](https://joshuacook.netlify.com) +![Swift](https://img.shields.io/badge/Swift-App-FA7343.svg?style=flat&logo=swift) +![iOS](https://img.shields.io/badge/watchOS-App-999999.svg?style=flat&logo=apple&logoColor=white) + +--- + +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![SwiftFormat](https://img.shields.io/badge/SwfitFormat-enabled-A166E6)](https://github.com/nicklockwood/SwiftFormat) +[![jhc github](https://img.shields.io/badge/GitHub-jhrcook-181717.svg?style=flat&logo=github)](https://github.com/jhrcook) +[![jhc twitter](https://img.shields.io/badge/Twitter-@JoshDoesA-00aced.svg?style=flat&logo=twitter)](https://twitter.com/JoshDoesa) +[![jhc website](https://img.shields.io/badge/Website-Joshua_Cook-5087B2.svg?style=flat&logo=telegram)](https://joshuacook.netlify.com)