diff --git a/Workout Spinner WatchKit Extension/Models/HapticsSettings.swift b/Workout Spinner WatchKit Extension/Models/HapticsSettings.swift new file mode 100644 index 0000000..234d613 --- /dev/null +++ b/Workout Spinner WatchKit Extension/Models/HapticsSettings.swift @@ -0,0 +1,66 @@ +// +// HapticsSettings.swift +// Workout Spinner WatchKit Extension +// +// Created by Joshua on 1/29/21. +// Copyright © 2021 Joshua Cook. All rights reserved. +// + +import SwiftUI +import WatchKit + +struct HapticsSettings { + var successfulWheelSpin: Bool = setting(for: .successfulWheelSpin) + var startExercise: Bool = setting(for: .startExercise) + var endExercise: Bool = setting(for: .endExercise) + + init() { + checkInitialRegistration() + } + + enum HapticSetting: String, CaseIterable { + case successfulWheelSpin + case startExercise + case endExercise + } + + public func saveAll() { + save(.successfulWheelSpin, as: successfulWheelSpin) + save(.startExercise, as: startExercise) + save(.endExercise, as: endExercise) + } + + public func save(_ setting: HapticSetting, as value: Bool) { + UserDefaults.standard.setValue(value, forKey: setting.rawValue) + } + + internal static func setting(for setting: HapticSetting) -> Bool { + UserDefaults.standard.bool(forKey: setting.rawValue) + } + + private func checkInitialRegistration() { + let initialCheckKey = "HapticSettingsInitialCheck" + if UserDefaults.standard.bool(forKey: initialCheckKey) { return } + for key in HapticSetting.allCases { + UserDefaults.standard.setValue(true, forKey: key.rawValue) + } + UserDefaults.standard.setValue(true, forKey: initialCheckKey) + } + + public func play(soundFor action: HapticSetting) { + switch action { + case .successfulWheelSpin: + if successfulWheelSpin { + WKInterfaceDevice.current().play(.success) + } + case .startExercise: + if startExercise { + WKInterfaceDevice.current().play(.start) + } + case .endExercise: + if endExercise { + WKInterfaceDevice.current().play(.stop) + } + } + } +} diff --git a/Workout Spinner WatchKit Extension/Models/WheelVelocityTracker.swift b/Workout Spinner WatchKit Extension/Models/WheelVelocityTracker.swift index 4520fad..1da71ce 100644 --- a/Workout Spinner WatchKit Extension/Models/WheelVelocityTracker.swift +++ b/Workout Spinner WatchKit Extension/Models/WheelVelocityTracker.swift @@ -7,25 +7,16 @@ // import Foundation +import WatchKit class WheelVelocityTracker: ObservableObject { + let velocityThreshold: Double + let memory: Int private var history = [Double]() - var velocityThreshold: Double = 50.0 private(set) var didPassThreshold: Bool = false - var memory: Int = 10 private(set) var currentVelocity = 0.0 - init() {} - - init(memory: Int) { - self.memory = memory - } - - init(velocityThreshold: Double) { - self.velocityThreshold = velocityThreshold - } - - init(velocityThreshold: Double, memory: Int) { + init(velocityThreshold: Double = 5, memory: Int = 3) { self.velocityThreshold = velocityThreshold self.memory = memory } diff --git a/Workout Spinner WatchKit Extension/View Models/WorkoutPickerViewModel.swift b/Workout Spinner WatchKit Extension/View Models/ExercisePickerViewModel.swift similarity index 96% rename from Workout Spinner WatchKit Extension/View Models/WorkoutPickerViewModel.swift rename to Workout Spinner WatchKit Extension/View Models/ExercisePickerViewModel.swift index c568619..14d49a8 100644 --- a/Workout Spinner WatchKit Extension/View Models/WorkoutPickerViewModel.swift +++ b/Workout Spinner WatchKit Extension/View Models/ExercisePickerViewModel.swift @@ -19,6 +19,7 @@ extension ExercisePicker { func rotationEffectDidFinish() { if velocityTracker.didPassThreshold { + haptics.play(soundFor: .successfulWheelSpin) exerciseSelected = true velocityTracker.resetThreshold() } diff --git a/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift b/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift index 2c788ad..7fd9a3a 100644 --- a/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift +++ b/Workout Spinner WatchKit Extension/Views/ExercisePicker.swift @@ -17,6 +17,8 @@ struct ExercisePicker: View { @ObservedObject var workoutManager: WorkoutManager @ObservedObject var exerciseOptions: ExerciseOptions + let haptics = HapticsSettings() + var numExercises: Int { exerciseOptions.exercisesBlacklistFiltered.count } @@ -29,7 +31,7 @@ struct ExercisePicker: View { // Spinning wheel constants. var spinDirection: Double { - return WKInterfaceDevice().crownOrientation == .left ? 1.0 : -1.0 + return WKInterfaceDevice.current().crownOrientation == .left ? 1.0 : -1.0 } internal var crownVelocityMultiplier = UserDefaults.readCrownVelocityMultiplier() diff --git a/Workout Spinner WatchKit Extension/Views/ExerciseStartView.swift b/Workout Spinner WatchKit Extension/Views/ExerciseStartView.swift index fc669e4..8947145 100644 --- a/Workout Spinner WatchKit Extension/Views/ExerciseStartView.swift +++ b/Workout Spinner WatchKit Extension/Views/ExerciseStartView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import WatchKit struct ExerciseStartView: View { @ObservedObject var workoutManager: WorkoutManager @@ -32,6 +33,8 @@ struct ExerciseStartView: View { return intensity.rawValue } + let haptics = HapticsSettings() + @Environment(\.presentationMode) var presentationMode init(workoutManager: WorkoutManager, exerciseCanceled: Binding) { @@ -75,6 +78,7 @@ struct ExerciseStartView: View { if self.timeRemaining > 0 { self.timeRemaining -= 1 } else if self.timeRemaining <= 0 { + haptics.play(soundFor: .startExercise) // haptic feedback exerciseCanceled = false self.presentationMode.wrappedValue.dismiss() } diff --git a/Workout Spinner WatchKit Extension/Views/ExerciseView.swift b/Workout Spinner WatchKit Extension/Views/ExerciseView.swift index 5b99129..6dee40f 100644 --- a/Workout Spinner WatchKit Extension/Views/ExerciseView.swift +++ b/Workout Spinner WatchKit Extension/Views/ExerciseView.swift @@ -7,13 +7,14 @@ // import SwiftUI +import WatchKit struct ExerciseView: View { @ObservedObject var workoutManager: WorkoutManager @ObservedObject var workoutTracker: WorkoutTracker @Binding private var exerciseComplete: Bool var workoutInfo: ExerciseInfo? - + let haptics = HapticsSettings() let intensity: ExerciseIntensity = ExerciseStartView.loadExerciseIntensity() init(workoutManager: WorkoutManager, workoutTracker: WorkoutTracker, exerciseComplete: Binding) { @@ -101,8 +102,13 @@ struct ExerciseView: View { extension ExerciseView { /// Called when the exercise is complete and the 'Done" button is tapped. func finishExercise() { + haptics.play(soundFor: .endExercise) // haptic feedback + // 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) + workoutTracker.addData(info: workoutManager.exerciseInfo!, + duration: Double(workoutManager.elapsedSeconds), + activeCalories: workoutManager.activeCalories, + heartRate: workoutManager.allHeartRateReadings) workoutManager.resetTrackedInformation() exerciseComplete = true @@ -122,7 +128,9 @@ extension ExerciseView { static var previews: some View { Group { ForEach(workoutOptions.exercises) { info in - ExerciseView(workoutManager: WorkoutManager(exerciseInfo: info), workoutTracker: WorkoutTracker(), exerciseComplete: .constant(false)) + ExerciseView(workoutManager: WorkoutManager(exerciseInfo: info), + workoutTracker: WorkoutTracker(), + exerciseComplete: .constant(false)) .previewDisplayName(info.displayName) } } diff --git a/Workout Spinner WatchKit Extension/Views/Settings Views/Settings.swift b/Workout Spinner WatchKit Extension/Views/Settings Views/Settings.swift index 01dc7cb..013f21d 100644 --- a/Workout Spinner WatchKit Extension/Views/Settings Views/Settings.swift +++ b/Workout Spinner WatchKit Extension/Views/Settings Views/Settings.swift @@ -34,6 +34,8 @@ struct Settings: View { self.exerciseOptions = exerciseOptions } + @State private var haptics = HapticsSettings() + var body: some View { Form { Section(header: SectionHeader(imageName: "figure.wave", text: "Preferences")) { @@ -72,6 +74,18 @@ struct Settings: View { PlusMinusStepper(value: $crownVelocityMultiplier, step: 1, min: 1, max: 10, label: Text("\(Int(crownVelocityMultiplier))")) } + Section(header: SectionHeader(imageName: "applewatch.radiowaves.left.and.right", text: "Haptics")) { + Toggle(isOn: $haptics.successfulWheelSpin) { + Text("Wheel spin") + } + Toggle(isOn: $haptics.startExercise) { + Text("Begin exercise") + } + Toggle(isOn: $haptics.endExercise) { + Text("End exercise") + } + } + Section(header: SectionHeader(imageName: "info.circle", text: "About")) { HStack { Text("Dev") @@ -120,6 +134,8 @@ extension Settings { UserDefaults.standard.set(crownVelocityMultiplier, forKey: UserDefaultsKeys.crownVelocityMultiplier.rawValue) + + haptics.saveAll() } static func getSavedExerciseIntensity() -> Int { diff --git a/Workout Spinner.xcodeproj/project.pbxproj b/Workout Spinner.xcodeproj/project.pbxproj index ad8ee27..98f4c62 100644 --- a/Workout Spinner.xcodeproj/project.pbxproj +++ b/Workout Spinner.xcodeproj/project.pbxproj @@ -7,9 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 8404613625C4310900BFDB80 /* HapticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8404613525C4310900BFDB80 /* HapticsSettings.swift */; }; 840D3F65254C1D6200C9E2EF /* WorkoutFinishView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D3F64254C1D6200C9E2EF /* WorkoutFinishView.swift */; }; 841D04D1250648AB00786074 /* ExerciseStartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841D04D0250648AB00786074 /* ExerciseStartView.swift */; }; - 841D04D325064A8800786074 /* WorkoutPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841D04D225064A8800786074 /* WorkoutPickerViewModel.swift */; }; + 841D04D325064A8800786074 /* ExercisePickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841D04D225064A8800786074 /* ExercisePickerViewModel.swift */; }; 841D04D525064DE600786074 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841D04D425064DE600786074 /* Settings.swift */; }; 841D04D925066CF400786074 /* UserDefaultsKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841D04D825066CF400786074 /* UserDefaultsKeys.swift */; }; 841D04DB250673DB00786074 /* BodyPartSelectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841D04DA250673DB00786074 /* BodyPartSelectionListView.swift */; }; @@ -110,9 +111,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8404613525C4310900BFDB80 /* HapticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsSettings.swift; sourceTree = ""; }; 840D3F64254C1D6200C9E2EF /* WorkoutFinishView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutFinishView.swift; sourceTree = ""; }; 841D04D0250648AB00786074 /* ExerciseStartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseStartView.swift; sourceTree = ""; }; - 841D04D225064A8800786074 /* WorkoutPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutPickerViewModel.swift; sourceTree = ""; }; + 841D04D225064A8800786074 /* ExercisePickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExercisePickerViewModel.swift; sourceTree = ""; }; 841D04D425064DE600786074 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 841D04D825066CF400786074 /* UserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsKeys.swift; sourceTree = ""; }; 841D04DA250673DB00786074 /* BodyPartSelectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyPartSelectionListView.swift; sourceTree = ""; }; @@ -348,6 +350,7 @@ 8483502825403C010081D374 /* WorkoutManager.swift */, 84C1FA7B25497CD20008CE09 /* WorkoutTracker.swift */, 84E9C1002554234D002D461B /* HeartRateGraphData.swift */, + 8404613525C4310900BFDB80 /* HapticsSettings.swift */, ); path = Models; sourceTree = ""; @@ -355,7 +358,7 @@ 84E372B424FD0AD500AFB355 /* View Models */ = { isa = PBXGroup; children = ( - 841D04D225064A8800786074 /* WorkoutPickerViewModel.swift */, + 841D04D225064A8800786074 /* ExercisePickerViewModel.swift */, ); path = "View Models"; sourceTree = ""; @@ -546,6 +549,7 @@ 84E3729624FD0A5D00AFB355 /* HostingController.swift in Sources */, 849C7325255EA64400C28F96 /* LabelWithIndicator.swift in Sources */, 849458F12503B86200C361F7 /* SpinnerRotationModifier.swift in Sources */, + 8404613625C4310900BFDB80 /* HapticsSettings.swift in Sources */, 843F33EE24FFB2E500919AD5 /* Color-extension.swift in Sources */, 84C1FA7C25497CD20008CE09 /* WorkoutTracker.swift in Sources */, 848642F52500FA3E00E3ED9B /* SpinnerSlice.swift in Sources */, @@ -564,7 +568,7 @@ 84E9C0ED25542309002D461B /* GraphBackgroundSegment.swift in Sources */, 84EFD6792508E32500806EF4 /* BodyPartSelections.swift in Sources */, 84C1FA7425497A050008CE09 /* WorkoutPagingView.swift in Sources */, - 841D04D325064A8800786074 /* WorkoutPickerViewModel.swift in Sources */, + 841D04D325064A8800786074 /* ExercisePickerViewModel.swift in Sources */, 84E9C0E8255422DE002D461B /* Double-extension.swift in Sources */, 84C10C1F2588D3B500111502 /* CannotSpinView.swift in Sources */, 848364E82565438800B357FA /* InstructionsView.swift in Sources */,