add apple tv app

This commit is contained in:
Trey t
2024-06-18 12:03:56 -05:00
parent addeca4ead
commit 7d2b6b3e6e
134 changed files with 869 additions and 37 deletions

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
<rect key="frame" x="0.0" y="832" width="393" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon" translatesAutoresizingMaskIntoConstraints="NO" id="TO7-SX-zZe">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" red="0.61568627450980395" green="0.54117647058823526" blue="1" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="trailing" secondItem="TO7-SX-zZe" secondAttribute="trailing" id="1uw-Tg-zq5"/>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
<constraint firstItem="TO7-SX-zZe" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="5vV-DO-kiV"/>
<constraint firstItem="TO7-SX-zZe" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="RIk-Do-dx8"/>
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="SfN-ll-jLj"/>
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
<constraint firstAttribute="bottom" secondItem="TO7-SX-zZe" secondAttribute="bottom" id="qmU-rS-Ali"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="icon" width="246" height="246"/>
</resources>
</document>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array/>
<key>WKBackgroundModes</key>
<array>
<string>workout-processing</string>
</array>
</dict>
</plist>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
BuildableName = "Werkout_ios.app"
BlueprintName = "Werkout_ios"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
BuildableName = "Werkout_ios.app"
BlueprintName = "Werkout_ios"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
BuildableName = "Werkout_ios.app"
BlueprintName = "Werkout_ios"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A922A452D270042FFBD"
BuildableName = "Werkout_watch Watch App.app"
BlueprintName = "Werkout_watch Watch App"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A212A3972840042FFBD"
BuildableName = "Werkout_ios.app"
BlueprintName = "Werkout_ios"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A922A452D270042FFBD"
BuildableName = "Werkout_watch Watch App.app"
BlueprintName = "Werkout_watch Watch App"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1CF65A922A452D270042FFBD"
BuildableName = "Werkout_watch Watch App.app"
BlueprintName = "Werkout_watch Watch App"
ReferencedContainer = "container:Werkout_ios.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,40 @@
//
// AudioQueue.swift
// Werkout_ios
//
// Created by Trey Tartt on 8/12/23.
//
import Foundation
enum AudioType {
case shortBeep
case finishBeep
case remoteURL(URL)
}
struct AudioQueue: Codable, Identifiable, Equatable {
var id = UUID()
let playAt: Int
let audioURL: String
enum CodingKeys: String, CodingKey {
case playAt = "play_at"
case audioURL = "audio_url"
}
var audioType: AudioType {
if audioURL == "short_beep" {
return .shortBeep
} else if audioURL == "long_beep" {
return .finishBeep
} else {
if let url = URL(string: BaseURLs.currentBaseURL + audioURL) {
return .remoteURL(url)
} else {
return .shortBeep
}
}
}
}

View File

@@ -0,0 +1,50 @@
//
// CompletedWorkout.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/21/23.
//
import Foundation
struct CompletedWorkout: Codable {
let id: Int
let workout: Workout
let createdAt, updatedAt: String
let difficulty, totalTime: Int?
let workoutStartTime: String
let notes: String?
let totalCalories: Float?
enum CodingKeys: String, CodingKey {
case id, workout
case createdAt = "created_at"
case updatedAt = "updated_at"
case difficulty
case totalTime = "total_time"
case workoutStartTime = "workout_start_time"
case notes
case totalCalories = "total_calories"
}
}
struct CompletedWorkoutPOSTReturn: Codable {
let id: Int
let workout: Int
let createdAt, updatedAt: String
let difficulty, totalTime: Int?
let workoutStartTime: String
let notes: String?
let totalCalories: Int?
enum CodingKeys: String, CodingKey {
case id, workout
case createdAt = "created_at"
case updatedAt = "updated_at"
case difficulty
case totalTime = "total_time"
case workoutStartTime = "workout_start_time"
case notes
case totalCalories = "total_calories"
}
}

View File

@@ -0,0 +1,24 @@
//
// Equipment.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/15/23.
//
import Foundation
struct Equipment: Codable, Identifiable, Equatable {
let id: Int
let equipment: Int?
let name, createdAt, updatedAt: String
let is_weight: Bool?
let category: String?
enum CodingKeys: String, CodingKey {
case id, name, equipment
case createdAt = "created_at"
case updatedAt = "updated_at"
case is_weight
case category
}
}

View File

@@ -0,0 +1,97 @@
//
// Exercise.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import Foundation
struct SupersetExercise: Identifiable, Codable, Equatable, Hashable {
var id: Int?
let workout: Int?
let exercise: Exercise
let weight: Int?
let reps: Int?
let duration: Int?
let durationAudio: String?
let weightAudio: String?
let createdAt: String?
let order, superset: Int?
let uniqueID: String?
let description: String?
let audioQueues: [AudioQueue]?
enum CodingKeys: String, CodingKey {
case workout, exercise, weight, reps, duration, order, superset, id, description
case durationAudio = "duration_audio"
case weightAudio = "weight_audio"
case createdAt = "created_at"
case uniqueID = "unique_id"
case audioQueues = "audio_queues"
}
public func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
}
struct Exercise: Identifiable, Codable, Equatable, Hashable {
static func == (lhs: Exercise, rhs: Exercise) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
let id: Int
let equipment: [Equipment]
let muscles: [Muscle]
let audioURL, videoURL, createdAt, updatedAt: String
let name, description: String
let side, jointsUsed, movementPatterns: String?
let isTwoDumbbells, isTrackableDistance, isAlternating, isWeight: Bool
let isDistance, isDuration, isReps: Bool
let equipmentRequired, muscleGroups: String?
let synonyms: String?
enum CodingKeys: String, CodingKey {
case id, muscles, equipment
case audioURL = "audio_url"
case videoURL = "video_url"
case createdAt = "created_at"
case updatedAt = "updated_at"
case name, description, side
case isTwoDumbbells = "is_two_dumbbells"
case isTrackableDistance = "is_trackable_distance"
case isAlternating = "is_alternating"
case isWeight = "is_weight"
case isDistance = "is_distance"
case isDuration = "is_duration"
case isReps = "is_reps"
case jointsUsed = "joints_used"
case movementPatterns = "movement_patterns"
case equipmentRequired = "equipment_required"
case muscleGroups = "muscle_groups"
case synonyms
}
var extName: String {
if side != nil && side!.count > 0 {
var returnString = name + " - " + side!
returnString = returnString.replacingOccurrences(of: "_", with: " ")
return returnString.capitalized
}
return name
}
var spacedMuscleGroups: String {
return muscleGroups?.replacingOccurrences(of: ",", with: ", ") ?? ""
}
var spacedEquipmentRequired: String {
return equipmentRequired?.replacingOccurrences(of: ",", with: ", ") ?? ""
}
}

View File

@@ -0,0 +1,20 @@
//
// Muscle.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/15/23.
//
import Foundation
struct Muscle: Codable, Identifiable, Equatable {
let id: Int
let muscle: Int?
let name, createdAt, updatedAt: String
enum CodingKeys: String, CodingKey {
case id, name, muscle
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}

View File

@@ -0,0 +1,17 @@
//
// NSFWVideo.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/29/23.
//
import Foundation
struct NSFWVideo: Codable {
let videoFile, genderValue: String
enum CodingKeys: String, CodingKey {
case videoFile = "video_file"
case genderValue = "gender_value"
}
}

View File

@@ -0,0 +1,29 @@
//
// PlannedWorkout.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/1/23.
//
import Foundation
struct PlannedWorkout: Codable {
let id: Int
let createdAt, updatedAt, onDate: String
let workout: Workout
enum CodingKeys: String, CodingKey {
case id
case createdAt = "created_at"
case updatedAt = "updated_at"
case onDate = "on_date"
case workout
}
var date: Date? {
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd"
df.locale = Locale(identifier: "en_US_POSIX")
return df.date(from: self.onDate)
}
}

View File

@@ -0,0 +1,31 @@
//
// RegisteredUser.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import Foundation
struct RegisteredUser: Codable, Hashable {
let id: Int
let firstName, lastName, image: String?
let nickName: String?
let token: String?
let email: String?
let hasNSFWToggle: Bool?
enum CodingKeys: String, CodingKey {
case id
case firstName = "first_name"
case lastName = "last_name"
case image, token
case email = "email_address"
case nickName = "nick_name"
case hasNSFWToggle = "has_nsfw_toggle"
}
var NSFWValue: Bool {
return hasNSFWToggle ?? false
}
}

View File

@@ -0,0 +1,28 @@
//
// Superset.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/17/23.
//
import Foundation
struct Superset: Codable, Identifiable, Hashable {
let id: Int?
let exercises: [SupersetExercise]
let createdAt, updatedAt, name: String?
let rounds, order, workout: Int
let estimatedTime: Double?
enum CodingKeys: String, CodingKey {
case id, exercises
case createdAt = "created_at"
case updatedAt = "updated_at"
case name, rounds, order, workout
case estimatedTime = "estimated_time"
}
public func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
}

View File

@@ -0,0 +1,63 @@
//
// Workout.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import Foundation
struct Workout: Codable, Identifiable, Equatable {
static func == (lhs: Workout, rhs: Workout) -> Bool {
lhs.id == rhs.id
}
let id: Int
let name: String
let description: String?
let supersets: [Superset]?
let registeredUser: RegisteredUser?
let muscles: [String]?
let equipment: [String]?
let exercise_count: Int?
let createdAt: Date?
let estimatedTime: Double?
let allSupersetExecercise: [SupersetExercise]?
enum CodingKeys: String, CodingKey {
case id, name, description, supersets, exercise_count, muscles, equipment
case registeredUser = "registered_user"
case createdAt = "created_at"
case estimatedTime = "estimated_time"
case allSupersetExecercise = "all_superset_exercise"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "NA"
self.description = try container.decodeIfPresent(String.self, forKey: .description)
self.registeredUser = try container.decodeIfPresent(RegisteredUser.self, forKey: .registeredUser)
self.id = try container.decode(Int.self, forKey: .id)
self.supersets = try container.decodeIfPresent([Superset].self, forKey: .supersets)
self.equipment = try container.decodeIfPresent([String].self, forKey: .equipment)
self.muscles = try container.decodeIfPresent([String].self, forKey: .muscles)
self.exercise_count = try container.decodeIfPresent(Int.self, forKey: .exercise_count)
let createdAtStr = try container.decodeIfPresent(String.self, forKey: .createdAt)
if let createdAtStr = createdAtStr {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date = formatter.date(from: createdAtStr)
self.createdAt = date
} else {
self.createdAt = nil
}
self.estimatedTime = try container.decodeIfPresent(Double.self, forKey: .estimatedTime)
allSupersetExecercise = try container.decodeIfPresent([SupersetExercise].self, forKey: .allSupersetExecercise)
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"images" : [
{
"filename" : "icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "icon 1.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,28 @@
{
"colors" : [
{
"color" : {
"platform" : "universal",
"reference" : "systemPurpleColor"
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"platform" : "universal",
"reference" : "systemPurpleColor"
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "icon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

Binary file not shown.

View File

@@ -0,0 +1,17 @@
//
// BaseURLs.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/10/23.
//
import Foundation
enum BaseURLs: String {
case local = "http://127.0.0.1:8000"
case dev = "https://dev.werkout.fitness"
static var currentBaseURL: String {
return BaseURLs.dev.rawValue
}
}

View File

@@ -0,0 +1,386 @@
//
// TimerModule.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import Foundation
import WatchConnectivity
import AVFoundation
import HealthKit
enum WatchActions: Codable {
case nextExercise
case restartExercise
case previousExercise
case stopWorkout
case pauseWorkout
case workoutComplete(Data)
}
enum PhoneToWatchActions: Codable {
case inExercise(WatchPackageModel)
case reset
case endWorkout
}
class BridgeModule: NSObject, ObservableObject {
private let kMessageKey = "message"
static let shared = BridgeModule()
@Published var isShowingOnExternalDisplay = false
@Published var isInWorkout = false
var completedWorkout: (() -> Void)?
@Published var currentWorkoutRunTimeInSeconds: Int = -1
private var currentWorkoutRunTimer: Timer?
public private(set) var workoutStartDate: Date?
private var currentExerciseTimer: Timer?
@Published public private(set) var currentExerciseInfo = CurrentWorkoutInfo()
@Published var previewWorkout: Workout?
@Published var currentExerciseTimeLeft: Int = 0
private var isWatchConnected = false
// workoutEndDate fills out WatchPackageModel.workoutEndDate which
// tells the watch app to stop the workout
public private(set) var workoutEndDate: Date?
@Published public private(set) var healthKitUUID: UUID?
@Published var isPaused = false
var audioPlayer: AVAudioPlayer?
var avPlayer: AVPlayer?
func start(workout: Workout) {
currentExerciseInfo.complete = {
self.completeWorkout()
}
currentExerciseInfo.start(workout: workout)
currentWorkoutRunTimeInSeconds = 0
currentWorkoutRunTimer?.invalidate()
currentWorkoutRunTimer = nil
isPaused = false
if let superetExercise = currentExerciseInfo.currentExercise {
updateCurrent(exercise: superetExercise)
startWorkoutTimer()
workoutStartDate = Date()
isInWorkout = true
if WCSession.isSupported() {
WCSession.default.delegate = self
WCSession.default.activate()
}
}
}
func goToExerciseAt(section: Int, row: Int) {
if let superetExercise = currentExerciseInfo.goToWorkoutAt(supersetIndex: section,
exerciseIndex: row) {
updateCurrent(exercise: superetExercise)
}
}
var nextExerciseObject: SupersetExercise? {
currentExerciseInfo.goToNextExercise
}
func resetCurrentWorkout() {
DispatchQueue.main.async {
self.currentWorkoutRunTimeInSeconds = 0
self.currentWorkoutRunTimer?.invalidate()
self.currentWorkoutRunTimer = nil
self.currentExerciseTimer?.invalidate()
self.currentExerciseTimer = nil
self.currentWorkoutRunTimeInSeconds = -1
self.currentExerciseInfo.reset()
self.isInWorkout = false
self.workoutStartDate = nil
self.workoutEndDate = nil
}
}
private func startWorkoutTimer() {
currentWorkoutRunTimer?.invalidate()
currentWorkoutRunTimer = nil
currentWorkoutRunTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(addOneToWorkoutRunTime),
userInfo: nil,
repeats: true)
currentWorkoutRunTimer?.fire()
}
private func startExerciseTimerWith(duration: Int) {
DispatchQueue.main.async {
self.currentExerciseTimer?.invalidate()
self.currentExerciseTimer = nil
self.currentExerciseTimeLeft = duration
self.currentExerciseTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(self.updateCurrentExerciseTimer),
userInfo: nil,
repeats: true)
self.currentExerciseTimer?.fire()
}
}
@objc func updateCurrentExerciseTimer() {
if currentExerciseTimeLeft > 1 {
currentExerciseTimeLeft -= 1
if let currentExercise = currentExerciseInfo.allSupersetExecercise, let audioQueues = currentExercise.audioQueues {
if let audioQueue = audioQueues.first(where: {
$0.playAt == currentExerciseTimeLeft
}) {
switch audioQueue.audioType {
case .shortBeep:
playBeep()
case .finishBeep:
playFinished()
case .remoteURL(let url):
playRemoteAudio(fromURL: url)
}
}
}
sendCurrentExerciseToWatch()
} else {
nextExercise()
}
}
func pauseWorkout() {
if let _ = currentExerciseTimer {
currentExerciseTimer?.invalidate()
currentExerciseTimer = nil
isPaused = true
} else {
isPaused = false
startExerciseTimerWith(duration: currentExerciseTimeLeft)
}
}
func nextExercise() {
if let nextSupersetExercise = currentExerciseInfo.goToNextExercise {
updateCurrent(exercise: nextSupersetExercise)
} else {
completeWorkout()
}
}
func previousExercise() {
if let nextSupersetExercise = currentExerciseInfo.previousExercise {
updateCurrent(exercise: nextSupersetExercise)
} else {
completeWorkout()
}
}
func restartExercise() {
if let currentExercise = currentExerciseInfo.currentExercise {
updateCurrent(exercise: currentExercise)
}
}
@objc func addOneToWorkoutRunTime() {
currentWorkoutRunTimeInSeconds += 1
}
func updateCurrent(exercise: SupersetExercise) {
DispatchQueue.main.async {
self.currentExerciseTimer?.invalidate()
self.currentExerciseTimer = nil
if let duration = exercise.duration, duration > 0 {
self.startExerciseTimerWith(duration: duration)
}
self.sendCurrentExerciseToWatch()
}
}
func completeWorkout() {
self.currentExerciseTimer?.invalidate()
self.currentExerciseTimer = nil
self.isInWorkout = false
workoutEndDate = Date()
if let completedWorkout = completedWorkout {
completedWorkout()
self.completedWorkout = nil
}
}
func playRemoteAudio(fromURL url: URL) {
#if os(iOS)
let playerItem = AVPlayerItem(url: url)
do {
try AVAudioSession.sharedInstance().setCategory(.playback,
mode: .default,
options: [.mixWithOthers])
try AVAudioSession.sharedInstance().setActive(true)
avPlayer = AVPlayer(playerItem: playerItem)
avPlayer?.play()
} catch {
print("ERROR")
}
#endif
}
func playBeep() {
#if os(iOS)
if let path = Bundle.main.path(forResource: "short_beep", ofType: "m4a") {
do {
try AVAudioSession.sharedInstance().setCategory(.playback,
mode: .default,
options: [.mixWithOthers])
try AVAudioSession.sharedInstance().setActive(true)
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
audioPlayer?.play()
} catch {
print("ERROR")
}
}
#endif
}
func playFinished() {
#if os(iOS)
if let path = Bundle.main.path(forResource: "long_beep", ofType: "m4a") {
do {
try AVAudioSession.sharedInstance().setCategory(.playback,
mode: .default,
options: [.mixWithOthers])
try AVAudioSession.sharedInstance().setActive(true)
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
audioPlayer?.play()
} catch {
print("ERROR")
}
}
#endif
}
}
extension BridgeModule: WCSessionDelegate {
func sendResetToWatch() {
let watchModel = PhoneToWatchActions.reset
let data = try! JSONEncoder().encode(watchModel)
send(data)
}
func sendWorkoutCompleteToWatch() {
if WCSession.default.isReachable {
let model = PhoneToWatchActions.endWorkout
let data = try! JSONEncoder().encode(model)
send(data)
}
}
func sendCurrentExerciseToWatch() {
if let currentExercise = currentExerciseInfo.currentExercise,
let duration = currentExercise.duration ,
duration > 0 {
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date())
let model = PhoneToWatchActions.inExercise(watchModel)
let data = try! JSONEncoder().encode(model)
send(data)
} else {
if let currentExercise = currentExerciseInfo.currentExercise,
let reps = currentExercise.reps,
reps > 0 {
// if not a timer we need to set the watch display with number of reps
// if timer it will set when timer updates
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: reps, workoutStartDate: self.workoutStartDate ?? Date())
let model = PhoneToWatchActions.inExercise(watchModel)
let data = try! JSONEncoder().encode(model)
self.send(data)
}
}
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) {
switch model {
case .nextExercise:
nextExercise()
playFinished()
case .workoutComplete(let data):
DispatchQueue.main.async {
let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data)
self.healthKitUUID = model.healthKitUUID
}
case .restartExercise:
restartExercise()
case .previousExercise:
previousExercise()
case .stopWorkout:
completeWorkout()
case .pauseWorkout:
pauseWorkout()
}
}
}
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {
switch activationState {
case .notActivated:
print("notActivated")
case .inactive:
print("inactive")
case .activated:
print("activated")
#if os(iOS)
let workoutConfiguration = HKWorkoutConfiguration()
workoutConfiguration.activityType = .functionalStrengthTraining
workoutConfiguration.locationType = .indoor
if WCSession.isSupported(), WCSession.default.activationState == .activated, WCSession.default.isWatchAppInstalled {
HKHealthStore().startWatchApp(with: workoutConfiguration, completion: { (success, error) in
print(error.debugDescription)
})
}
#endif
}
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
#endif
func send(_ data: Data) {
guard WCSession.default.activationState == .activated else {
return
}
#if os(iOS)
guard WCSession.default.isWatchAppInstalled else {
return
}
#else
guard WCSession.default.isCompanionAppInstalled else {
return
}
#endif
WCSession.default.sendMessageData(data, replyHandler: nil) { error in
print("Cannot send message: \(String(describing: error))")
}
}
}

View File

@@ -0,0 +1,157 @@
//
// CurrentWorkoutInfo.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/17/23.
//
import Foundation
class CurrentWorkoutInfo {
var supersetIndex: Int = 0
var exerciseIndex: Int = -1
var workout: Workout?
var complete: (() -> Void)?
var currentRound = 1
var allSupersetExecerciseIndex = 0
var superset: [Superset] {
return workout?.supersets?.sorted(by: { $0.order < $1.order }) ?? [Superset]()
}
var numberOfRoundsInCurrentSuperSet: Int {
guard let workout = workout else { return -1 }
guard let supersets = workout.supersets else { return -1 }
if supersetIndex >= supersets.count {
return -1
}
let currentSuperSet = supersets[supersetIndex]
if exerciseIndex >= currentSuperSet.exercises.count {
return -1
}
return currentSuperSet.rounds
}
var currentExercise: SupersetExercise? {
guard let supersets = workout?.supersets else { return nil }
if supersetIndex >= supersets.count { return nil }
let superset = supersets[supersetIndex]
// will be -1 for a moment while going to previous workout / superset
if exerciseIndex < 0 { return nil }
if exerciseIndex > superset.exercises.count { return nil }
let exercise = superset.exercises[exerciseIndex]
return exercise
}
var allSupersetExecercise: SupersetExercise? {
let obj = workout?.allSupersetExecercise?[allSupersetExecerciseIndex]
return obj
}
// this is setting nothing so we can half ass it
var nextExerciseInfo: SupersetExercise? {
guard let workout = workout else { return nil }
let _exerciseIndex = allSupersetExecerciseIndex + 1
if _exerciseIndex >= workout.allSupersetExecercise?.count ?? -1 {
return nil
}
return workout.allSupersetExecercise?[_exerciseIndex]
}
// this needs to set stuff for iphone
var goToNextExercise: SupersetExercise? {
guard let workout = workout else { return nil }
guard let supersets = workout.supersets else { return nil }
exerciseIndex += 1
let currentSuperSet = supersets[supersetIndex]
if exerciseIndex >= currentSuperSet.exercises.count {
currentRound += 1
if currentRound > currentSuperSet.rounds {
supersetIndex += 1
currentRound = 1
if supersetIndex >= supersets.count {
return nil
}
}
exerciseIndex = 0
}
let superset = supersets[supersetIndex]
let exercise = superset.exercises[exerciseIndex]
allSupersetExecerciseIndex += 1
return exercise
}
var previousExercise: SupersetExercise? {
guard let workout = workout else { return nil }
guard let supersets = workout.supersets else { return nil }
exerciseIndex -= 1
if exerciseIndex < 0 {
if currentRound > 1 {
currentRound -= 1
let superset = supersets[supersetIndex]
exerciseIndex = superset.exercises.count-1
} else {
if supersetIndex > 0 {
supersetIndex -= 1
let superset = supersets[supersetIndex]
currentRound = superset.rounds
exerciseIndex = superset.exercises.count-1
} else {
exerciseIndex = 0
}
}
}
let superset = supersets[supersetIndex]
let exercise = superset.exercises[exerciseIndex]
allSupersetExecerciseIndex -= 1
if allSupersetExecerciseIndex < 0 {
allSupersetExecerciseIndex = 0
}
return exercise
}
func start(workout: Workout) {
reset()
self.workout = workout
}
func reset() {
supersetIndex = 0
exerciseIndex = 0
currentRound = 1
allSupersetExecerciseIndex = 0
self.workout = nil
}
func goToWorkoutAt(supersetIndex: Int, exerciseIndex: Int) -> SupersetExercise? {
self.supersetIndex = supersetIndex
self.exerciseIndex = exerciseIndex
self.currentRound = 1
let currentExercise = currentExercise
if let firstIdx = workout?.allSupersetExecercise?.firstIndex(where: {
$0.exercise.id == currentExercise?.exercise.id
}) {
allSupersetExecerciseIndex = firstIdx
}
return currentExercise
}
}

View File

@@ -0,0 +1,132 @@
//
// DataStore.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/19/23.
//
import Foundation
import SwiftUI
class DataStore: ObservableObject {
enum DataStoreStatus {
case loading
case idle
}
static let shared = DataStore()
public private(set) var allWorkouts: [Workout]?
public private(set) var allMuscles: [Muscle]?
public private(set) var allEquipment: [Equipment]?
public private(set) var allExercise: [Exercise]?
public private(set) var allNSFWVideos: [NSFWVideo]?
@Published public private(set) var status = DataStoreStatus.idle
private let fetchAllDataQueue = DispatchGroup()
public func randomVideoFor(gender: String) -> String? {
return allNSFWVideos?.filter({
$0.genderValue.lowercased() == gender.lowercased()
}).randomElement()?.videoFile ?? nil
}
public var nsfwGenderOptions: [String]? {
let values = self.allNSFWVideos?.map({
$0.genderValue
})
if let values = values {
return Array(Set(values))
}
return nil
}
public var workoutsUniqueUsers: [RegisteredUser]? {
guard let workouts = allWorkouts else {
return nil
}
let users = workouts.compactMap({ $0.registeredUser })
return Array(Set(users))
}
public func fetchAllData(completion: @escaping (() -> Void)) {
status = .loading
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.enter()
fetchAllDataQueue.notify(queue: .main) {
self.status = .idle
completion()
}
AllWorkoutFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allWorkouts = model
case .failure(let error):
print(error)
}
self.fetchAllDataQueue.leave()
})
AllMusclesFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allMuscles = model.sorted(by: {
$0.name < $1.name
})
case .failure(let error):
print(error)
}
self.fetchAllDataQueue.leave()
})
AllEquipmentFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allEquipment = model.sorted(by: {
$0.name < $1.name
})
case .failure(let error):
print(error)
}
self.fetchAllDataQueue.leave()
})
AllExerciseFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allExercise = model.sorted(by: {
$0.name < $1.name
})
case .failure(let error):
print(error)
}
self.fetchAllDataQueue.leave()
})
AllNSFWVideosFetchable().fetch(completion: { result in
switch result {
case .success(let model):
self.allNSFWVideos = model
case .failure(let error):
print(error)
}
self.fetchAllDataQueue.leave()
})
}
func setupFakeData() {
allWorkouts = PreviewData.allWorkouts()
allMuscles = PreviewData.parseMuscle()
allEquipment = PreviewData.parseEquipment()
allExercise = PreviewData.parseExercises()
}
}

View File

@@ -0,0 +1,141 @@
//
// Extensions.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/20/23.
//
import Foundation
import UIKit
import SwiftUI
extension Dictionary {
func percentEncoded() -> Data? {
map { key, value in
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
return escapedKey + "=" + escapedValue
}
.joined(separator: "&")
.data(using: .utf8)
}
}
extension CharacterSet {
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed: CharacterSet = .urlQueryAllowed
allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
return allowed
}()
}
extension Date {
var timeFormatForUpload: String {
let isoFormatter = DateFormatter()
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
return isoFormatter.string(from: self)
}
}
extension String {
var dateFromServerDate: Date? {
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
df.locale = Locale(identifier: "en_US_POSIX")
return df.date(from: self)
}
var plannedDate: Date? {
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd"
df.locale = Locale(identifier: "en_US_POSIX")
return df.date(from: self)
}
}
extension Date {
func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents {
return calendar.dateComponents(Set(components), from: self)
}
func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int {
return calendar.component(component, from: self)
}
var formatForPlannedWorkout: String {
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd"
df.locale = Locale(identifier: "en_US_POSIX")
return df.string(from: self)
}
var weekDay: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEE"
let weekDay = dateFormatter.string(from: self)
return weekDay
}
var monthString: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM"
let weekDay = dateFormatter.string(from: self)
return weekDay
}
var dateString: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "d"
let weekDay = dateFormatter.string(from: self)
return weekDay
}
}
extension Bundle {
public var icon: UIImage? {
if let icons = infoDictionary?["CFBundleIcons"] as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let lastIcon = iconFiles.last {
return UIImage(named: lastIcon)
}
return nil
}
}
extension Double {
/*
10000.asString(style: .positional) // 2:46:40
10000.asString(style: .abbreviated) // 2h 46m 40s
10000.asString(style: .short) // 2 hr, 46 min, 40 sec
10000.asString(style: .full) // 2 hours, 46 minutes, 40 seconds
10000.asString(style: .spellOut) // two hours, forty-six minutes, forty seconds
10000.asString(style: .brief) // 2hr 46min 40sec
*/
func asString(style: DateComponentsFormatter.UnitsStyle) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second, .nanosecond]
formatter.unitsStyle = style
return formatter.string(from: self) ?? ""
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}

View File

@@ -0,0 +1,128 @@
//
// HealthKitHelper.swift
// Werkout_ios
//
// Created by Trey Tartt on 8/13/23.
//
import Foundation
import HealthKit
struct HealthKitWorkoutData {
var caloriesBurned: Double?
var minHeartRate: Double?
var maxHeartRate: Double?
var avgHeartRate: Double?
}
class HealthKitHelper {
// this is dirty and i dont care
var returnCount = 0
let healthStore = HKHealthStore()
var healthKitWorkoutData = HealthKitWorkoutData(
caloriesBurned: nil,
minHeartRate: nil,
maxHeartRate: nil,
avgHeartRate: nil)
var completion: ((HealthKitWorkoutData) -> Void)?
func getDetails(forHealthKitUUID uuid: UUID, completion: @escaping ((HealthKitWorkoutData) -> Void)) {
self.completion = completion
self.returnCount = 0
print("get details \(uuid.uuidString)")
let predicate = HKQuery.predicateForObject(with: uuid)
let query = HKSampleQuery(sampleType: HKWorkoutType.workoutType(),
predicate: predicate,
limit: 0,
sortDescriptors: nil)
{ (sampleQuery, results, error ) -> Void in
if let queryError = error {
print( "There was an error while reading the samples: \(queryError.localizedDescription)")
} else {
for samples: HKSample in results! {
let workout: HKWorkout = (samples as! HKWorkout)
self.getTotalBurned(forWorkout: workout)
self.getHeartRateStuff(forWorkout: workout)
print("got workout")
}
}
}
healthStore.execute(query)
}
func getHeartRateStuff(forWorkout workout: HKWorkout) {
print("get heart")
let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate)
let heartPredicate: NSPredicate? = HKQuery.predicateForSamples(withStart: workout.startDate,
end: workout.endDate,
options: HKQueryOptions.strictEndDate)
let heartQuery = HKStatisticsQuery(quantityType: heartType!,
quantitySamplePredicate: heartPredicate,
options: [.discreteAverage, .discreteMin, .discreteMax],
completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
if let result = result,
let minValue = result.minimumQuantity(),
let maxValue = result.maximumQuantity(),
let avgValue = result.averageQuantity() {
let _minHeartRate = minValue.doubleValue(
for: HKUnit(from: "count/min")
)
let _maxHeartRate = maxValue.doubleValue(
for: HKUnit(from: "count/min")
)
let _avgHeartRate = avgValue.doubleValue(
for: HKUnit(from: "count/min")
)
self.healthKitWorkoutData.avgHeartRate = _avgHeartRate
self.healthKitWorkoutData.minHeartRate = _minHeartRate
self.healthKitWorkoutData.maxHeartRate = _maxHeartRate
print("got heart")
DispatchQueue.main.async {
self.shitReturned()
}
}
})
healthStore.execute(heartQuery)
}
func getTotalBurned(forWorkout workout: HKWorkout) {
print("get total burned")
let calType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)
let calPredicate: NSPredicate? = HKQuery.predicateForSamples(withStart: workout.startDate,
end: workout.endDate,
options: HKQueryOptions.strictEndDate)
let calQuery = HKStatisticsQuery(quantityType: calType!,
quantitySamplePredicate: calPredicate,
options: [.cumulativeSum],
completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
if let result = result {
self.healthKitWorkoutData.caloriesBurned = result.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()) ?? -1
print("got total burned")
DispatchQueue.main.async {
self.shitReturned()
}
}
})
healthStore.execute(calQuery)
}
func shitReturned() {
DispatchQueue.main.async {
self.returnCount += 1
print("\(self.returnCount)")
if self.returnCount == 2 {
self.completion!(self.healthKitWorkoutData)
}
}
}
}

View File

@@ -0,0 +1,230 @@
[
{
"id": 1,
"created_at": "2023-06-13T01:58:13.212264Z",
"updated_at": "2023-06-13T01:58:13.212311Z",
"name": "deltoids"
},
{
"id": 2,
"created_at": "2023-06-13T01:58:13.214487Z",
"updated_at": "2023-06-13T01:58:13.214520Z",
"name": "core"
},
{
"id": 3,
"created_at": "2023-06-13T01:58:13.215255Z",
"updated_at": "2023-06-13T01:58:13.215277Z",
"name": "obliques"
},
{
"id": 4,
"created_at": "2023-06-13T01:58:13.216495Z",
"updated_at": "2023-06-13T01:58:13.216522Z",
"name": "glutes"
},
{
"id": 5,
"created_at": "2023-06-13T01:58:13.217347Z",
"updated_at": "2023-06-13T01:58:13.217371Z",
"name": "quads"
},
{
"id": 6,
"created_at": "2023-06-13T01:58:13.218407Z",
"updated_at": "2023-06-13T01:58:13.218434Z",
"name": "hamstrings"
},
{
"id": 7,
"created_at": "2023-06-13T01:58:13.219494Z",
"updated_at": "2023-06-13T01:58:13.219521Z",
"name": "hip abductors"
},
{
"id": 8,
"created_at": "2023-06-13T01:58:13.220702Z",
"updated_at": "2023-06-13T01:58:13.220737Z",
"name": "triceps"
},
{
"id": 9,
"created_at": "2023-06-13T01:58:13.221543Z",
"updated_at": "2023-06-13T01:58:13.221564Z",
"name": "upper back"
},
{
"id": 10,
"created_at": "2023-06-13T01:58:13.222148Z",
"updated_at": "2023-06-13T01:58:13.222166Z",
"name": "lats"
},
{
"id": 11,
"created_at": "2023-06-13T01:58:13.222710Z",
"updated_at": "2023-06-13T01:58:13.222729Z",
"name": ""
},
{
"id": 12,
"created_at": "2023-06-13T01:58:13.223248Z",
"updated_at": "2023-06-13T01:58:13.223267Z",
"name": "Abs"
},
{
"id": 13,
"created_at": "2023-06-13T01:58:13.223787Z",
"updated_at": "2023-06-13T01:58:13.223805Z",
"name": "middle back"
},
{
"id": 14,
"created_at": "2023-06-13T01:58:13.224313Z",
"updated_at": "2023-06-13T01:58:13.224330Z",
"name": "biceps"
},
{
"id": 15,
"created_at": "2023-06-13T01:58:13.224843Z",
"updated_at": "2023-06-13T01:58:13.224860Z",
"name": "rotator cuff"
},
{
"id": 16,
"created_at": "2023-06-13T01:58:13.225377Z",
"updated_at": "2023-06-13T01:58:13.225394Z",
"name": "hip flexor"
},
{
"id": 17,
"created_at": "2023-06-13T01:58:13.226340Z",
"updated_at": "2023-06-13T01:58:13.226370Z",
"name": "Quads"
},
{
"id": 18,
"created_at": "2023-06-13T01:58:13.227071Z",
"updated_at": "2023-06-13T01:58:13.227094Z",
"name": "Glutes"
},
{
"id": 19,
"created_at": "2023-06-13T01:58:13.227668Z",
"updated_at": "2023-06-13T01:58:13.227687Z",
"name": "lower back"
},
{
"id": 20,
"created_at": "2023-06-13T01:58:13.228176Z",
"updated_at": "2023-06-13T01:58:13.228193Z",
"name": "chest"
},
{
"id": 21,
"created_at": "2023-06-13T01:58:13.228647Z",
"updated_at": "2023-06-13T01:58:13.228663Z",
"name": "Shoulders"
},
{
"id": 22,
"created_at": "2023-06-13T01:58:13.229110Z",
"updated_at": "2023-06-13T01:58:13.229126Z",
"name": "calves"
},
{
"id": 23,
"created_at": "2023-06-13T01:58:13.229566Z",
"updated_at": "2023-06-13T01:58:13.229581Z",
"name": "Obliques"
},
{
"id": 24,
"created_at": "2023-06-13T01:58:13.230014Z",
"updated_at": "2023-06-13T01:58:13.230031Z",
"name": "Hip Flexor"
},
{
"id": 25,
"created_at": "2023-06-13T01:58:13.230457Z",
"updated_at": "2023-06-13T01:58:13.230472Z",
"name": "Middle Back"
},
{
"id": 26,
"created_at": "2023-06-13T01:58:13.230905Z",
"updated_at": "2023-06-13T01:58:13.230919Z",
"name": "Lats"
},
{
"id": 27,
"created_at": "2023-06-13T01:58:13.231350Z",
"updated_at": "2023-06-13T01:58:13.231364Z",
"name": "hip adductors"
},
{
"id": 28,
"created_at": "2023-06-13T01:58:13.231849Z",
"updated_at": "2023-06-13T01:58:13.231864Z",
"name": "traps"
},
{
"id": 29,
"created_at": "2023-06-13T01:58:13.232295Z",
"updated_at": "2023-06-13T01:58:13.232310Z",
"name": "forearms"
},
{
"id": 30,
"created_at": "2023-06-13T01:58:13.232733Z",
"updated_at": "2023-06-13T01:58:13.232748Z",
"name": "abs"
},
{
"id": 31,
"created_at": "2023-06-13T01:58:13.233175Z",
"updated_at": "2023-06-13T01:58:13.233192Z",
"name": "Biceps"
},
{
"id": 32,
"created_at": "2023-06-13T01:58:13.233616Z",
"updated_at": "2023-06-13T01:58:13.233631Z",
"name": "Triceps"
},
{
"id": 33,
"created_at": "2023-06-13T01:58:13.234054Z",
"updated_at": "2023-06-13T01:58:13.234068Z",
"name": "Core"
},
{
"id": 34,
"created_at": "2023-06-13T01:58:13.234489Z",
"updated_at": "2023-06-13T01:58:13.234503Z",
"name": "intercostals"
},
{
"id": 35,
"created_at": "2023-06-13T01:58:13.234927Z",
"updated_at": "2023-06-13T01:58:13.234941Z",
"name": "Chest"
},
{
"id": 36,
"created_at": "2023-06-13T01:58:13.235368Z",
"updated_at": "2023-06-13T01:58:13.235383Z",
"name": "feet"
},
{
"id": 37,
"created_at": "2023-06-13T01:58:13.236126Z",
"updated_at": "2023-06-13T01:58:13.236153Z",
"name": "arms"
},
{
"id": 38,
"created_at": "2023-06-13T01:58:13.236718Z",
"updated_at": "2023-06-13T01:58:13.236734Z",
"name": "it band"
}
]

View File

@@ -0,0 +1,436 @@
[
{
"id": 3,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"quads",
"lats",
"lower back",
"upper back",
"biceps"
],
"equipment": [
"Rower"
],
"exercise_count": 14,
"created_at": "2023-07-03T17:00:01.350058Z",
"updated_at": "2023-07-04T16:51:07.231366Z",
"name": "Rowwwwwwer",
"description": "row until you pass out"
},
{
"id": 4,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"quads",
"lats",
"lower back",
"upper back",
"biceps"
],
"equipment": [
"Rower"
],
"exercise_count": 30,
"created_at": "2023-07-04T17:36:56.902038Z",
"updated_at": "2023-07-04T18:02:18.798549Z",
"name": "Row 2",
"description": "Lets pass out"
},
{
"id": 6,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"calves",
"quads",
"core",
"hip flexor",
"lats",
"lower back",
"upper back",
"biceps"
],
"equipment": [
"Rower",
"Yoga Mat"
],
"exercise_count": 60,
"created_at": "2023-07-05T16:16:57.374041Z",
"updated_at": "2023-07-05T16:16:57.378248Z",
"name": "Cardio Mix",
"description": "Mix of cardio stuff"
},
{
"id": 7,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"calves",
"hamstrings",
"glutes",
"core",
"quads",
"hip flexor",
"middle back",
"lats",
"lower back",
"upper back",
"biceps"
],
"equipment": [
"Rower",
"Yoga Mat"
],
"exercise_count": 56,
"created_at": "2023-07-05T17:15:16.596202Z",
"updated_at": "2023-07-05T17:15:16.597621Z",
"name": "Cardio mix #2",
"description": ""
},
{
"id": 8,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"glutes",
"quads",
"core",
"triceps",
"deltoids",
"hip flexor",
"biceps"
],
"equipment": [
"Bike",
"Battle Ropes"
],
"exercise_count": 24,
"created_at": "2023-07-06T14:47:57.669190Z",
"updated_at": "2023-07-06T14:47:57.673138Z",
"name": "HiiT #1",
"description": "Upper body tabata"
},
{
"id": 9,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"glutes",
"quads",
"feet"
],
"equipment": [],
"exercise_count": 24,
"created_at": "2023-07-06T14:49:24.336730Z",
"updated_at": "2023-07-06T14:49:24.338220Z",
"name": "HiiT #2",
"description": "Lower body tabata"
},
{
"id": 10,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"rotator cuff",
"chest",
"core",
"hamstrings",
"glutes",
"triceps",
"quads",
"abs",
"middle back",
"forearms",
"lats",
"hip abductors",
"upper back",
"biceps"
],
"equipment": [
"Pull-Up Bar",
"Kettlebell"
],
"exercise_count": 20,
"created_at": "2023-07-06T14:51:00.277200Z",
"updated_at": "2023-07-06T14:51:00.278874Z",
"name": "HiiT #3",
"description": "Total body"
},
{
"id": 11,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"chest",
"biceps",
"forearms",
"triceps"
],
"equipment": [
"Dumbbell",
"Barbell",
"Plates",
"Bench"
],
"exercise_count": 24,
"created_at": "2023-07-06T14:56:03.908583Z",
"updated_at": "2023-07-06T14:56:03.910350Z",
"name": "Arms and HiiT",
"description": ""
},
{
"id": 12,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"hamstrings",
"traps",
"core",
"quads",
"glutes",
"middle back",
"forearms",
"lats",
"lower back",
"biceps"
],
"equipment": [
"Dumbbell",
"Barbell",
"Plates"
],
"exercise_count": 21,
"created_at": "2023-07-06T15:00:42.677119Z",
"updated_at": "2023-07-06T15:00:42.678641Z",
"name": "Upper Pull",
"description": ""
},
{
"id": 13,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"core",
"middle back",
"forearms",
"lats",
"upper back",
"biceps"
],
"equipment": [
"Barbell",
"Dumbbell",
"Plates",
"Chest Supported Row Machine",
"Bench"
],
"exercise_count": 32,
"created_at": "2023-07-06T15:06:57.002749Z",
"updated_at": "2023-07-06T15:06:57.004148Z",
"name": "Upper Pull",
"description": ""
},
{
"id": 14,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"deltoids",
"chest",
"core",
"triceps"
],
"equipment": [
"Dumbbell",
"Barbell",
"Box",
"Bench"
],
"exercise_count": 29,
"created_at": "2023-07-06T15:11:52.085325Z",
"updated_at": "2023-07-06T15:11:52.086738Z",
"name": "Upper push",
"description": ""
},
{
"id": 15,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"rotator cuff",
"chest",
"core",
"traps",
"quads",
"triceps",
"deltoids",
"abs",
"hamstrings",
"glutes",
"upper back"
],
"equipment": [
"Barbell",
"Dumbbell",
"Plates",
"Bench",
"Seated Chest Press Machine"
],
"exercise_count": 31,
"created_at": "2023-07-06T15:23:51.228664Z",
"updated_at": "2023-07-06T15:23:51.230103Z",
"name": "Upper push",
"description": ""
},
{
"id": 17,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"glutes",
"hamstrings",
"quads",
"feet",
"hip flexor",
"hip abductors"
],
"equipment": [
"Barbell",
"Dumbbell",
"Box",
"Bench",
"Bike"
],
"exercise_count": 33,
"created_at": "2023-07-06T15:39:08.326899Z",
"updated_at": "2023-07-06T17:53:45.996684Z",
"name": "Lower #1",
"description": null
},
{
"id": 20,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"glutes",
"quads",
"feet",
"upper back"
],
"equipment": [
"Dumbbell",
"Barbell",
"Bench"
],
"exercise_count": 33,
"created_at": "2023-07-06T18:20:27.100936Z",
"updated_at": "2023-07-06T18:20:27.102968Z",
"name": "Lower 2",
"description": "3"
},
{
"id": 21,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"rotator cuff",
"chest",
"core",
"hamstrings",
"glutes",
"triceps",
"quads",
"abs",
"deltoids",
"biceps"
],
"equipment": [
"Bench",
"Kettlebell",
"Battle Ropes"
],
"exercise_count": 50,
"created_at": "2023-07-07T17:18:47.385158Z",
"updated_at": "2023-07-07T17:18:47.387277Z",
"name": "Cardio",
"description": "Ropes, bw squat, kb swings, push up"
}
]

View File

@@ -0,0 +1,158 @@
[
{
"id": 4,
"workout": {
"id": 3,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"quads",
"lats",
"lower back",
"upper back",
"biceps"
],
"equipment": [
"Rower"
],
"exercise_count": 14,
"created_at": "2023-07-03T17:00:01.350058Z",
"updated_at": "2023-07-04T16:51:07.231366Z",
"name": "Rowwwwwwer",
"description": "row until you pass out"
},
"created_at": "2023-07-03T18:05:58.978336Z",
"updated_at": "2023-07-03T18:05:58.985053Z",
"difficulty": 3,
"total_time": 529,
"workout_start_time": "2023-07-03T17:57:01Z",
"notes": "",
"total_calories": -1.0
},
{
"id": 5,
"workout": {
"id": 7,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"calves",
"hamstrings",
"glutes",
"core",
"quads",
"hip flexor",
"middle back",
"lats",
"lower back",
"upper back",
"biceps"
],
"equipment": [
"Rower",
"Yoga Mat"
],
"exercise_count": 56,
"created_at": "2023-07-05T17:15:16.596202Z",
"updated_at": "2023-07-05T17:15:16.597621Z",
"name": "Cardio mix #2",
"description": ""
},
"created_at": "2023-07-05T18:00:26.462986Z",
"updated_at": "2023-07-05T18:00:26.465537Z",
"difficulty": 1,
"total_time": 1145,
"workout_start_time": "2023-07-05T17:41:05Z",
"notes": "",
"total_calories": -1.0
},
{
"id": 6,
"workout": {
"id": 21,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"rotator cuff",
"chest",
"core",
"hamstrings",
"glutes",
"triceps",
"quads",
"abs",
"deltoids",
"biceps"
],
"equipment": [
"Bench",
"Kettlebell",
"Battle Ropes"
],
"exercise_count": 50,
"created_at": "2023-07-07T17:18:47.385158Z",
"updated_at": "2023-07-07T17:18:47.387277Z",
"name": "Cardio",
"description": "Ropes, bw squat, kb swings, push up"
},
"created_at": "2023-07-07T17:53:34.629016Z",
"updated_at": "2023-07-07T17:53:34.631101Z",
"difficulty": 4,
"total_time": 1688,
"workout_start_time": "2023-07-07T17:25:19Z",
"notes": "",
"total_calories": -1.0
},
{
"id": 7,
"workout": {
"id": 11,
"registered_user": {
"id": 1,
"first_name": "User1",
"last_name": "user1",
"image": "",
"nick_name": "test user1"
},
"muscles": [
"chest",
"biceps",
"forearms",
"triceps"
],
"equipment": [
"Dumbbell",
"Barbell",
"Plates",
"Bench"
],
"exercise_count": 24,
"created_at": "2023-07-06T14:56:03.908583Z",
"updated_at": "2023-07-06T14:56:03.910350Z",
"name": "Arms and HiiT",
"description": ""
},
"created_at": "2023-07-10T19:50:36.188887Z",
"updated_at": "2023-07-10T19:50:36.192789Z",
"difficulty": 2,
"total_time": 1937,
"workout_start_time": "2023-07-10T19:18:12Z",
"notes": "",
"total_calories": 176.9303436279297
}
]

View File

@@ -0,0 +1,418 @@
[
{
"id": 1088,
"created_at": "2023-06-11T23:00:16.734003Z",
"updated_at": "2023-06-11T23:00:16.734088Z",
"is_weight": true,
"category": "Weights",
"name": "Kettlebell"
},
{
"id": 1089,
"created_at": "2023-06-11T23:00:16.736417Z",
"updated_at": "2023-06-11T23:00:16.736445Z",
"is_weight": false,
"category": "Surfaces",
"name": "Yoga Mat"
},
{
"id": 1090,
"created_at": "2023-06-11T23:00:16.737296Z",
"updated_at": "2023-06-11T23:00:16.737319Z",
"is_weight": false,
"category": "Other",
"name": "Suspension Trainer"
},
{
"id": 1091,
"created_at": "2023-06-11T23:00:16.738300Z",
"updated_at": "2023-06-11T23:00:16.738326Z",
"is_weight": true,
"category": "Weights",
"name": "Dumbbell"
},
{
"id": 1092,
"created_at": "2023-06-11T23:00:16.739434Z",
"updated_at": "2023-06-11T23:00:16.739463Z",
"is_weight": false,
"category": "Surfaces",
"name": "Box"
},
{
"id": 1093,
"created_at": "2023-06-11T23:00:16.743650Z",
"updated_at": "2023-06-11T23:00:16.743687Z",
"is_weight": false,
"category": "Mobility",
"name": "Lacrosse Ball"
},
{
"id": 1094,
"created_at": "2023-06-11T23:00:16.744876Z",
"updated_at": "2023-06-11T23:00:16.744905Z",
"is_weight": false,
"category": "Surfaces",
"name": "Bench"
},
{
"id": 1095,
"created_at": "2023-06-11T23:00:16.746362Z",
"updated_at": "2023-06-11T23:00:16.746391Z",
"is_weight": true,
"category": "Weights",
"name": "Barbell"
},
{
"id": 1096,
"created_at": "2023-06-11T23:00:16.747581Z",
"updated_at": "2023-06-11T23:00:16.747618Z",
"is_weight": false,
"category": "Other",
"name": "Pull-Up Bar"
},
{
"id": 1097,
"created_at": "2023-06-11T23:00:16.748547Z",
"updated_at": "2023-06-11T23:00:16.748572Z",
"is_weight": true,
"category": "",
"name": "Plate"
},
{
"id": 1098,
"created_at": "2023-06-11T23:00:16.749551Z",
"updated_at": "2023-06-11T23:00:16.749584Z",
"is_weight": true,
"category": "Bands",
"name": "Resistance Band"
},
{
"id": 1099,
"created_at": "2023-06-11T23:00:16.750466Z",
"updated_at": "2023-06-11T23:00:16.750493Z",
"is_weight": true,
"category": "Weights",
"name": "Medicine Ball"
},
{
"id": 1100,
"created_at": "2023-06-11T23:00:16.751132Z",
"updated_at": "2023-06-11T23:00:16.751148Z",
"is_weight": true,
"category": "Machine",
"name": "Horizontal Leg Press Machine"
},
{
"id": 1101,
"created_at": "2023-06-11T23:00:16.751708Z",
"updated_at": "2023-06-11T23:00:16.751725Z",
"is_weight": false,
"category": "Other",
"name": "Battle Ropes"
},
{
"id": 1102,
"created_at": "2023-06-11T23:00:16.752239Z",
"updated_at": "2023-06-11T23:00:16.752254Z",
"is_weight": true,
"category": "Machine",
"name": "Leg Press Machine"
},
{
"id": 1103,
"created_at": "2023-06-11T23:00:16.752770Z",
"updated_at": "2023-06-11T23:00:16.752785Z",
"is_weight": true,
"category": "Other",
"name": "Cable Resistance Machine"
},
{
"id": 1104,
"created_at": "2023-06-11T23:00:16.753276Z",
"updated_at": "2023-06-11T23:00:16.753291Z",
"is_weight": false,
"category": "Other",
"name": "Partner"
},
{
"id": 1105,
"created_at": "2023-06-11T23:00:16.753777Z",
"updated_at": "2023-06-11T23:00:16.753791Z",
"is_weight": true,
"category": "Machine",
"name": "Leg Extension Machine"
},
{
"id": 1106,
"created_at": "2023-06-11T23:00:16.754288Z",
"updated_at": "2023-06-11T23:00:16.754302Z",
"is_weight": false,
"category": "Surfaces",
"name": "Wall"
},
{
"id": 1107,
"created_at": "2023-06-11T23:00:16.754782Z",
"updated_at": "2023-06-11T23:00:16.754797Z",
"is_weight": true,
"category": "Weights",
"name": "Plates"
},
{
"id": 1108,
"created_at": "2023-06-11T23:00:16.755276Z",
"updated_at": "2023-06-11T23:00:16.755291Z",
"is_weight": true,
"category": "Bands",
"name": "Miniband"
},
{
"id": 1109,
"created_at": "2023-06-11T23:00:16.755769Z",
"updated_at": "2023-06-11T23:00:16.755784Z",
"is_weight": false,
"category": "Other",
"name": "Agility Ladder"
},
{
"id": 1110,
"created_at": "2023-06-11T23:00:16.756257Z",
"updated_at": "2023-06-11T23:00:16.756272Z",
"is_weight": true,
"category": "Machine",
"name": "Pec Deck / Reverse Fly Machine"
},
{
"id": 1111,
"created_at": "2023-06-11T23:00:16.756790Z",
"updated_at": "2023-06-11T23:00:16.756807Z",
"is_weight": true,
"category": "Machine",
"name": "Chest Supported Row Machine"
},
{
"id": 1112,
"created_at": "2023-06-11T23:00:16.757376Z",
"updated_at": "2023-06-11T23:00:16.757393Z",
"is_weight": true,
"category": "Machine",
"name": "Lat Pull Down Machine"
},
{
"id": 1113,
"created_at": "2023-06-11T23:00:16.757953Z",
"updated_at": "2023-06-11T23:00:16.757969Z",
"is_weight": true,
"category": "Weights",
"name": "EZ Bar"
},
{
"id": 1114,
"created_at": "2023-06-11T23:00:16.758520Z",
"updated_at": "2023-06-11T23:00:16.758536Z",
"is_weight": false,
"category": "Surfaces",
"name": "Stability Ball"
},
{
"id": 1115,
"created_at": "2023-06-11T23:00:16.759018Z",
"updated_at": "2023-06-11T23:00:16.759033Z",
"is_weight": false,
"category": "Mobility",
"name": "Dowel"
},
{
"id": 1116,
"created_at": "2023-06-11T23:00:16.760471Z",
"updated_at": "2023-06-11T23:00:16.760489Z",
"is_weight": false,
"category": "Mobility",
"name": "PVC Pipe"
},
{
"id": 1117,
"created_at": "2023-06-11T23:00:16.761033Z",
"updated_at": "2023-06-11T23:00:16.761047Z",
"is_weight": false,
"category": "Mobility",
"name": "Foam Roll"
},
{
"id": 1118,
"created_at": "2023-06-11T23:00:16.761537Z",
"updated_at": "2023-06-11T23:00:16.761552Z",
"is_weight": true,
"category": "Machine",
"name": "Seated Chest Press Machine"
},
{
"id": 1119,
"created_at": "2023-06-11T23:00:16.762040Z",
"updated_at": "2023-06-11T23:00:16.762055Z",
"is_weight": false,
"category": "Other",
"name": "Tricep Rope"
},
{
"id": 1120,
"created_at": "2023-06-11T23:00:16.762531Z",
"updated_at": "2023-06-11T23:00:16.762544Z",
"is_weight": false,
"category": "Surfaces",
"name": "Chair"
},
{
"id": 1121,
"created_at": "2023-06-11T23:00:16.763016Z",
"updated_at": "2023-06-11T23:00:16.763031Z",
"is_weight": true,
"category": "Machine",
"name": "Seated Shoulder Press Machine"
},
{
"id": 1122,
"created_at": "2023-06-11T23:00:16.763508Z",
"updated_at": "2023-06-11T23:00:16.763522Z",
"is_weight": true,
"category": "Machine",
"name": "Leg Curl Seated Machine"
},
{
"id": 1123,
"created_at": "2023-06-11T23:00:16.763995Z",
"updated_at": "2023-06-11T23:00:16.764010Z",
"is_weight": true,
"category": "Machine",
"name": "Seated Calf Raise Machine"
},
{
"id": 1124,
"created_at": "2023-06-11T23:00:16.764475Z",
"updated_at": "2023-06-11T23:00:16.764488Z",
"is_weight": true,
"category": "Weights",
"name": "Trap Bar"
},
{
"id": 1125,
"created_at": "2023-06-11T23:00:16.764897Z",
"updated_at": "2023-06-11T23:00:16.764910Z",
"is_weight": false,
"category": "Other",
"name": "Ab Wheel"
},
{
"id": 1126,
"created_at": "2023-06-11T23:00:16.765319Z",
"updated_at": "2023-06-11T23:00:16.765332Z",
"is_weight": true,
"category": "Machine",
"name": "Leg Curl Prone Machine"
},
{
"id": 1127,
"created_at": "2023-06-11T23:00:16.765741Z",
"updated_at": "2023-06-11T23:00:16.765753Z",
"is_weight": true,
"category": "Weights",
"name": "Weighted Vest"
},
{
"id": 1128,
"created_at": "2023-06-11T23:00:16.766161Z",
"updated_at": "2023-06-11T23:00:16.766173Z",
"is_weight": false,
"category": "Other",
"name": "Slider"
},
{
"id": 1129,
"created_at": "2023-06-11T23:00:16.766577Z",
"updated_at": "2023-06-11T23:00:16.766590Z",
"is_weight": false,
"category": "Cardio",
"name": "Treadmill"
},
{
"id": 1130,
"created_at": "2023-06-11T23:00:16.766998Z",
"updated_at": "2023-06-11T23:00:16.767010Z",
"is_weight": false,
"category": "Other",
"name": "Machine"
},
{
"id": 1131,
"created_at": "2023-06-11T23:00:16.767415Z",
"updated_at": "2023-06-11T23:00:16.767428Z",
"is_weight": false,
"category": "Cardio",
"name": "Stair Climber"
},
{
"id": 1132,
"created_at": "2023-06-11T23:00:16.767834Z",
"updated_at": "2023-06-11T23:00:16.767845Z",
"is_weight": true,
"category": "Machine",
"name": "Assisted Chin up / Dips Machine"
},
{
"id": 1133,
"created_at": "2023-06-11T23:00:16.768250Z",
"updated_at": "2023-06-11T23:00:16.768261Z",
"is_weight": true,
"category": "Machine",
"name": "Bicep Preacher Curl Machine"
},
{
"id": 1134,
"created_at": "2023-06-11T23:00:16.768666Z",
"updated_at": "2023-06-11T23:00:16.768678Z",
"is_weight": true,
"category": "Machine",
"name": "Hack Squat Machine"
},
{
"id": 1135,
"created_at": "2023-06-11T23:00:16.769086Z",
"updated_at": "2023-06-11T23:00:16.769098Z",
"is_weight": false,
"category": "Cardio",
"name": "Jump Rope"
},
{
"id": 1136,
"created_at": "2023-06-11T23:00:16.769515Z",
"updated_at": "2023-06-11T23:00:16.769527Z",
"is_weight": true,
"category": "Machine",
"name": "Hack Squat 45 Degree Machine"
},
{
"id": 1137,
"created_at": "2023-06-11T23:00:16.770084Z",
"updated_at": "2023-06-11T23:00:16.770096Z",
"is_weight": false,
"category": "Cardio",
"name": "Bike"
},
{
"id": 1138,
"created_at": "2023-06-11T23:00:16.770500Z",
"updated_at": "2023-06-11T23:00:16.770512Z",
"is_weight": false,
"category": "Cardio",
"name": "Rower"
},
{
"id": 1139,
"created_at": "2023-06-11T23:00:16.770915Z",
"updated_at": "2023-06-11T23:00:16.770928Z",
"is_weight": false,
"category": "Cardio",
"name": "Elliptical"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
[
{
"id": 1,
"created_at": "2023-07-02T04:16:34.262272Z",
"updated_at": "2023-07-02T04:16:34.262315Z",
"on_date": "2023-07-02",
"workout": {
"id": 22,
"created_at": "2023-06-26T02:53:56.101972Z",
"updated_at": "2023-06-26T02:53:56.106136Z",
"name": "Aaaa",
"description": "description",
"registered_user": 1
}
}
]

View File

@@ -0,0 +1,107 @@
//
// PreviewData.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import Foundation
class PreviewData {
class func workout() -> Workout {
let filepath = Bundle.main.path(forResource: "WorkoutDetail", ofType: "json")!
let data = try! Data(NSData(contentsOfFile: filepath))
let workout = try! JSONDecoder().decode(Workout.self, from: data)
return workout
}
class func allWorkouts() -> [Workout] {
if let filepath = Bundle.main.path(forResource: "AllWorkouts", ofType: "json") {
do {
let data = try Data(NSData(contentsOfFile: filepath))
let workout = try JSONDecoder().decode([Workout].self, from: data)
return workout
} catch {
print(error)
fatalError()
}
} else {
fatalError()
}
}
class func parseExercises() -> [Exercise] {
if let filepath = Bundle.main.path(forResource: "Exercises", ofType: "json") {
do {
let data = try Data(NSData(contentsOfFile: filepath))
let exercises = try JSONDecoder().decode([Exercise].self, from: data)
return exercises
} catch {
print(error)
fatalError()
}
} else {
fatalError()
}
}
class func parseEquipment() -> [Equipment] {
if let filepath = Bundle.main.path(forResource: "Equipment", ofType: "json") {
do {
let data = try Data(NSData(contentsOfFile: filepath))
let equipment = try JSONDecoder().decode([Equipment].self, from: data)
return equipment
} catch {
print(error)
fatalError()
}
} else {
fatalError()
}
}
class func parseMuscle() -> [Muscle] {
if let filepath = Bundle.main.path(forResource: "AllMuscles", ofType: "json") {
do {
let data = try Data(NSData(contentsOfFile: filepath))
let muscles = try JSONDecoder().decode([Muscle].self, from: data)
return muscles
} catch {
print(error)
fatalError()
}
} else {
fatalError()
}
}
class func parseRegisterdUser() -> RegisteredUser {
if let filepath = Bundle.main.path(forResource: "RegisteredUser", ofType: "json") {
do {
let data = try Data(NSData(contentsOfFile: filepath))
let muscles = try JSONDecoder().decode(RegisteredUser.self, from: data)
return muscles
} catch {
print(error)
fatalError()
}
} else {
fatalError()
}
}
class func parseCompletedWorkouts() -> [CompletedWorkout] {
if let filepath = Bundle.main.path(forResource: "CompletedWorkouts", ofType: "json") {
do {
let data = try Data(NSData(contentsOfFile: filepath))
let muscles = try JSONDecoder().decode([CompletedWorkout].self, from: data)
return muscles
} catch {
print(error)
fatalError()
}
} else {
fatalError()
}
}
}

View File

@@ -0,0 +1,11 @@
{
"id": 1,
"email_address": "user1@user1.com",
"created_at": "2023-06-11T22:09:52.419314Z",
"updated_at": "2023-06-11T22:09:52.419342Z",
"first_name": "test1_fist",
"last_name": "test1_last",
"image": "",
"nick_name": "NickkkkName",
"token": "8f10a5b8c7532f7f8602193767b46a2625a85c52"
}

View File

@@ -0,0 +1,221 @@
{
"id": 21,
"name": "Ipad",
"description": "description",
"supersets": [
{
"id": 1,
"exercises": [
{
"id": 1,
"exercise": {
"id": 520,
"muscles": [
{
"id": 7264,
"name": "hip flexor",
"created_at": "2023-06-14T17:05:39.760515Z",
"updated_at": "2023-06-14T17:05:39.761372Z",
"exercise": 520,
"muscle": 16
},
{
"id": 7265,
"name": "glutes",
"created_at": "2023-06-14T17:05:39.762342Z",
"updated_at": "2023-06-14T17:05:39.762814Z",
"exercise": 520,
"muscle": 4
}
],
"equipment": [
{
"id": 941,
"name": "Wall",
"created_at": "2023-06-13T02:28:04.289213Z",
"updated_at": "2023-06-13T02:28:04.290354Z",
"exercise": 520,
"equipment": 1106
}
],
"audio_url": "exercise_audio/1-Step_Wall_March.m4a",
"video_url": "exercise_videos/1-Step_Wall_March.mp4",
"created_at": "2023-06-11T22:50:19.020826Z",
"updated_at": "2023-06-11T22:50:19.020834Z",
"name": "1-Step Wall March",
"description": "Keeping a tall posture, lean forward with both arms straight ahead, palms on the wall. While pushing your toes back through the floor, drive one knee up towards your ribs. Pause, then quickly repeat the same movement with the opposite leg. ",
"side": "",
"is_two_dumbbells": false,
"is_trackable_distance": false,
"is_alternating": true,
"is_weight": true,
"is_distance": false,
"is_duration": true,
"is_reps": true,
"joints_used": "ankle,knee,hip,wrist,shoulder,elbow",
"movement_patterns": "plyometric",
"equipment_required": "Wall",
"muscle_groups": "hip flexor,glutes",
"synonyms": ""
},
"created_at": "2023-07-17T18:56:36.984049Z",
"updated_at": "2023-07-17T19:06:00.534838Z",
"weight": null,
"reps": null,
"duration": 30,
"order": 1,
"superset": 1
},
{
"id": 2,
"exercise": {
"id": 992,
"muscles": [
{
"id": 7270,
"name": "hamstrings",
"created_at": "2023-06-14T17:05:39.769351Z",
"updated_at": "2023-06-14T17:05:39.769758Z",
"exercise": 992,
"muscle": 6
},
{
"id": 7271,
"name": "glutes",
"created_at": "2023-06-14T17:05:39.770480Z",
"updated_at": "2023-06-14T17:05:39.771111Z",
"exercise": 992,
"muscle": 4
}
],
"equipment": [
{
"id": 944,
"name": "Dumbbell",
"created_at": "2023-06-13T02:28:04.294180Z",
"updated_at": "2023-06-13T02:28:04.294658Z",
"exercise": 992,
"equipment": 1091
}
],
"audio_url": "exercise_audio/2_Dumbbell_Single-Leg_Deadlift.m4a",
"video_url": "exercise_videos/2_Dumbbell_Single-Leg_Deadlift.mp4",
"created_at": "2023-06-11T22:50:19.197099Z",
"updated_at": "2023-06-11T22:50:19.197105Z",
"name": "2 Dumbbell Single-Leg Deadlift",
"description": "Holding a dumbbell in each hand, with your right leg on the ground, hinge at your hips and let your body see-saw down until you are parallel with the ground. Snap back to a standing position.",
"side": "right_leg",
"is_two_dumbbells": true,
"is_trackable_distance": false,
"is_alternating": false,
"is_weight": true,
"is_distance": false,
"is_duration": true,
"is_reps": true,
"joints_used": "ankle,lumbar spine,hip,knee,wrist",
"movement_patterns": "lower pull,lower pull - hip hinge",
"equipment_required": "Dumbbell",
"muscle_groups": "hamstrings,glutes",
"synonyms": "2 Dumbbell Single Leg Deadlift"
},
"created_at": "2023-07-17T18:56:36.984523Z",
"updated_at": "2023-07-17T19:06:00.535334Z",
"weight": 30,
"reps": null,
"duration": null,
"order": 2,
"superset": 1
}
],
"created_at": "2023-07-17T18:56:36.983159Z",
"updated_at": "2023-07-17T19:06:00.534027Z",
"name": "test superste",
"rounds": 3,
"order": 1,
"workout": 21
},
{
"id": 2,
"exercises": [
{
"id": 3,
"exercise": {
"id": 992,
"muscles": [
{
"id": 7270,
"name": "hamstrings",
"created_at": "2023-06-14T17:05:39.769351Z",
"updated_at": "2023-06-14T17:05:39.769758Z",
"exercise": 992,
"muscle": 6
},
{
"id": 7271,
"name": "glutes",
"created_at": "2023-06-14T17:05:39.770480Z",
"updated_at": "2023-06-14T17:05:39.771111Z",
"exercise": 992,
"muscle": 4
}
],
"equipment": [
{
"id": 944,
"name": "Dumbbell",
"created_at": "2023-06-13T02:28:04.294180Z",
"updated_at": "2023-06-13T02:28:04.294658Z",
"exercise": 992,
"equipment": 1091
}
],
"audio_url": "exercise_audio/2_Dumbbell_Single-Leg_Deadlift.m4a",
"video_url": "exercise_videos/2_Dumbbell_Single-Leg_Deadlift.mp4",
"created_at": "2023-06-11T22:50:19.197099Z",
"updated_at": "2023-06-11T22:50:19.197105Z",
"name": "2 Dumbbell Single-Leg Deadlift",
"description": "Holding a dumbbell in each hand, with your right leg on the ground, hinge at your hips and let your body see-saw down until you are parallel with the ground. Snap back to a standing position.",
"side": "right_leg",
"is_two_dumbbells": true,
"is_trackable_distance": false,
"is_alternating": false,
"is_weight": true,
"is_distance": false,
"is_duration": true,
"is_reps": true,
"joints_used": "ankle,lumbar spine,hip,knee,wrist",
"movement_patterns": "lower pull,lower pull - hip hinge",
"equipment_required": "Dumbbell",
"muscle_groups": "hamstrings,glutes",
"synonyms": "2 Dumbbell Single Leg Deadlift"
},
"created_at": "2023-07-17T18:58:35.585418Z",
"updated_at": "2023-07-17T18:58:35.585435Z",
"weight": 11,
"reps": null,
"duration": null,
"order": 1,
"superset": 2
}
],
"created_at": "2023-07-17T18:58:35.584036Z",
"updated_at": "2023-07-17T19:03:57.175639Z",
"name": "two",
"rounds": 3,
"order": 2,
"workout": 21
}
],
"registered_user": {
"id": 2,
"first_name": "test2_first",
"last_name": "test2_last",
"image": "",
"nick_name": null
},
"male_videos": [],
"female_videos": [
"videos/Recover_8.mp4"
],
"both_videos": []
}

View File

@@ -0,0 +1,172 @@
//
// Keychain.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/5/23.
//
import Foundation
class KeychainInterface {
enum KeychainError: Error {
// Attempted read for an item that does not exist.
case itemNotFound
// Attempted save to override an existing item.
// Use update instead of save to update existing items
case duplicateItem
// A read of an item in any format other than Data
case invalidItemFormat
// Any operation result status than errSecSuccess
case unexpectedStatus(OSStatus)
}
static let serviceID = "werkout.fitness"
static func save(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to save in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecAttrSynchronizable as String: kCFBooleanTrue,
// kSecValueData is the item value to save
kSecValueData as String: password as AnyObject
]
// SecItemAdd attempts to add the item identified by
// the query to keychain
let status = SecItemAdd(
query as CFDictionary,
nil
)
// errSecDuplicateItem is a special case where the
// item identified by the query already exists. Throw
// duplicateItem so the client can determine whether
// or not to handle this as an error
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
// Any status other than errSecSuccess indicates the
// save operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
static func update(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to update in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecAttrSynchronizable as String: kCFBooleanTrue,
kSecClass as String: kSecClassGenericPassword
]
// attributes is passed to SecItemUpdate with
// kSecValueData as the updated item value
let attributes: [String: AnyObject] = [
kSecValueData as String: password as AnyObject
]
// SecItemUpdate attempts to update the item identified
// by query, overriding the previous value
let status = SecItemUpdate(
query as CFDictionary,
attributes as CFDictionary
)
// errSecItemNotFound is a special status indicating the
// item to update does not exist. Throw itemNotFound so
// the client can determine whether or not to handle
// this as an error
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
// Any status other than errSecSuccess indicates the
// update operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
static func readPassword(service: String = KeychainInterface.serviceID, account: String) throws -> Data {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to read in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
kSecAttrSynchronizable as String: kCFBooleanTrue,
// kSecMatchLimitOne indicates keychain should read
// only the most recent item matching this query
kSecMatchLimit as String: kSecMatchLimitOne,
// kSecReturnData is set to kCFBooleanTrue in order
// to retrieve the data for the item
kSecReturnData as String: kCFBooleanTrue
]
// SecItemCopyMatching will attempt to copy the item
// identified by query to the reference itemCopy
var itemCopy: AnyObject?
let status = SecItemCopyMatching(
query as CFDictionary,
&itemCopy
)
// errSecItemNotFound is a special status indicating the
// read item does not exist. Throw itemNotFound so the
// client can determine whether or not to handle
// this case
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
// Any status other than errSecSuccess indicates the
// read operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
// This implementation of KeychainInterface requires all
// items to be saved and read as Data. Otherwise,
// invalidItemFormat is thrown
guard let password = itemCopy as? Data else {
throw KeychainError.invalidItemFormat
}
return password
}
static func deletePassword(service: String = KeychainInterface.serviceID, account: String) throws {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to delete in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecAttrSynchronizable as String: kCFBooleanTrue,
kSecClass as String: kSecClassGenericPassword
]
// SecItemDelete attempts to perform a delete operation
// for the item identified by query. The status indicates
// if the operation succeeded or failed.
let status = SecItemDelete(query as CFDictionary)
// Any status other than errSecSuccess indicates the
// delete operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
}

View File

@@ -0,0 +1,109 @@
//
// AllWorkoutFetchable.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/19/23.
//
import Foundation
class AllWorkoutFetchable: Fetchable {
typealias Response = [Workout]
var endPoint: String = "/workout/all/"
}
class WorkoutDetailFetchable: Fetchable {
typealias Response = Workout
var endPoint: String
init(workoutID: Int) {
self.endPoint = "/workout/"+String(workoutID)+"/details/"
}
}
class AllMusclesFetchable: Fetchable {
typealias Response = [Muscle]
var endPoint: String = "/muscle/all/"
}
class AllEquipmentFetchable: Fetchable {
typealias Response = [Equipment]
var endPoint: String = "/equipment/all/"
}
class AllExerciseFetchable: Fetchable {
typealias Response = [Exercise]
var endPoint: String = "/exercise/all/"
}
class CompletedWorkoutFetchable: Fetchable {
typealias Response = [CompletedWorkout]
var endPoint: String = "/workout/completed/"
}
class PlannedWorkoutFetchable: Fetchable {
typealias Response = [PlannedWorkout]
var endPoint: String = "/workout/planned_workouts/"
}
class CreateWorkoutFetchable: Postable {
var postableData: [String : Any]?
var successStatus = 201
typealias Response = Workout
var endPoint: String = "/workout/create/"
init(postData: [String: Any]) {
self.postableData = postData
}
}
class CompleteWorkoutFetchable: Postable {
var postableData: [String : Any]?
var successStatus = 201
typealias Response = CompletedWorkoutPOSTReturn
var endPoint: String = "/workout/complete/"
init(postData: [String: Any]) {
self.postableData = postData
}
}
class LoginFetchable: Postable {
var postableData: [String : Any]?
var successStatus = 200
typealias Response = RegisteredUser
var endPoint: String = "/registered_user/login/"
var attachToken: Bool {
return false
}
init(postData: [String: Any]) {
self.postableData = postData
}
}
class PlanWorkoutFetchable: Postable {
var postableData: [String : Any]?
var successStatus = 201
typealias Response = RegisteredUser
var endPoint: String = "/workout/plan_workout/"
init(postData: [String: Any]) {
self.postableData = postData
}
}
class AllNSFWVideosFetchable: Fetchable {
typealias Response = [NSFWVideo]
var endPoint: String = "/videos/nsfw_videos/"
}
class RefreshUserInfoFetcable: Fetchable {
typealias Response = RegisteredUser
var endPoint: String = "/registered_user/refresh/"
}

View File

@@ -0,0 +1,138 @@
//
// Network.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/19/23.
//
import Foundation
enum FetchableError: Error {
case apiError(Error)
case noData
case decodeError(Error)
case endOfFileError
case noPostData
case noToken
case statusError(Int, String?)
}
protocol Fetchable {
associatedtype Response: Codable
var attachToken: Bool { get }
var baseURL: String { get }
var endPoint: String { get }
func fetch(completion: @escaping (Result<Response, FetchableError>) -> Void)
}
protocol Postable: Fetchable {
var postableData: [String: Any]? { get }
var successStatus: Int { get }
}
extension Fetchable {
var baseURL: String {
BaseURLs.currentBaseURL
}
var attachToken: Bool {
return true
}
func fetch(completion: @escaping (Result<Response, FetchableError>) -> Void) {
let url = URL(string: baseURL+endPoint)!
var request = URLRequest(url: url,timeoutInterval: Double.infinity)
if attachToken {
guard let token = UserStore.shared.token else {
completion(.failure(.noPostData))
return
}
request.addValue(token, forHTTPHeaderField: "Authorization")
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completion(.failure(.apiError(error)))
return
}
guard let data = data else {
completion(.failure(.noData))
return
}
do {
let model = try JSONDecoder().decode(Response.self, from: data)
completion(.success(model))
return
} catch {
completion(.failure(.decodeError(error)))
return
}
})
task.resume()
}
}
extension Postable {
func fetch(completion: @escaping (Result<Response, FetchableError>) -> Void) {
guard let postableData = postableData else {
completion(.failure(.noPostData))
return
}
let url = URL(string: baseURL+endPoint)!
let postData = try! JSONSerialization.data(withJSONObject:postableData)
var request = URLRequest(url: url,timeoutInterval: Double.infinity)
if attachToken {
guard let token = UserStore.shared.token else {
completion(.failure(.noPostData))
return
}
request.addValue(token, forHTTPHeaderField: "Authorization")
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
completion(.failure(.apiError(error)))
return
}
if let httpRespone = response as? HTTPURLResponse {
if httpRespone.statusCode != successStatus {
var returnStr: String?
if let data = data {
returnStr = String(data: data, encoding: .utf8)
}
completion(.failure(.statusError(httpRespone.statusCode, returnStr)))
return
}
}
guard let data = data else {
completion(.failure(.noData))
return
}
do {
let model = try JSONDecoder().decode(Response.self, from: data)
completion(.success(model))
return
} catch {
completion(.failure(.decodeError(error)))
return
}
})
task.resume()
}
}

View File

@@ -0,0 +1,56 @@
//
// Persistence.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/13/23.
//
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Werkout_ios")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,31 @@
//
// ThotStyle.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/13/23.
//
import Foundation
enum ThotStyle: Int, CaseIterable {
case always = 1
case never = 2
case recovery = 3
case random = 4
case off
func stringValue() -> String {
switch(self) {
case .always:
return "Always"
case .never:
return "Never"
case .recovery:
return "Recovery"
case .random:
return "Random"
case .off:
return "Off"
}
}
}

12
iphone/Werkout_ios/ToDo Normal file
View File

@@ -0,0 +1,12 @@
-completed workout view
- add notes and slider for difficulty
-apple watch
-account view
workout history view
-calorie upload
-video view on iphone during workout
edit weights on workouts

View File

@@ -0,0 +1,116 @@
//
// UserStore.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/25/23.
//
import Foundation
class UserStore: ObservableObject {
static let userNameKeychainValue = "username"
static let passwordKeychainValue = "password"
static let userDefaultsRegisteredUserKey = "registeredUserKey"
static let shared = UserStore()
@Published public private(set) var registeredUser: RegisteredUser?
var plannedWorkouts = [PlannedWorkout]()
init(registeredUser: RegisteredUser? = nil) {
self.registeredUser = registeredUser
if let data = UserDefaults.standard.data(forKey: UserStore.userDefaultsRegisteredUserKey),
let model = try? JSONDecoder().decode(RegisteredUser.self, from: data) {
self.registeredUser = model
}
}
public var token: String? {
guard let token = registeredUser?.token else {
return nil
}
return "Token \(token)"
}
func login(postData: [String: Any], completion: @escaping (Bool)-> Void) {
LoginFetchable(postData: postData).fetch(completion: { result in
switch result {
case .success(let model):
if let email = postData["email"] as? String,
let password = postData["password"] as? String,
let data = password.data(using: .utf8) {
try? KeychainInterface.save(password: data,
account: email)
}
DispatchQueue.main.async {
self.registeredUser = model
let data = try! JSONEncoder().encode(model)
UserDefaults.standard.set(data, forKey: UserStore.userDefaultsRegisteredUserKey)
completion(true)
}
case .failure(let failure):
completion(false)
}
})
}
public func refreshUserData() {
RefreshUserInfoFetcable().fetch(completion: { result in
switch result {
case .success(let registeredUser):
DispatchQueue.main.async {
if let data = try? JSONEncoder().encode(registeredUser) {
UserDefaults.standard.set(data, forKey: UserStore.userDefaultsRegisteredUserKey)
}
if let data = UserDefaults.standard.data(forKey: UserStore.userDefaultsRegisteredUserKey),
let model = try? JSONDecoder().decode(RegisteredUser.self, from: data) {
self.registeredUser = model
}
}
case .failure(let failure):
fatalError()
}
})
}
func logout() {
self.registeredUser = nil
UserDefaults.standard.set(nil, forKey: UserStore.userDefaultsRegisteredUserKey)
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name("CreatedNewWorkout"), object: nil, userInfo: nil)
}
}
func setFakeUser() {
self.registeredUser = PreviewData.parseRegisterdUser()
}
func fetchPlannedWorkouts() {
PlannedWorkoutFetchable().fetch(completion: { result in
switch result {
case .success(let models):
self.plannedWorkouts = models
case .failure(let failure):
UserStore.shared.logout()
// fatalError("shit broke")
}
})
}
func plannedWorkoutFor(date: Date) -> PlannedWorkout? {
for plannedWorkout in plannedWorkouts {
if let plannedworkoutDate = plannedWorkout.date {
if Calendar.current.isDate(date, equalTo: plannedworkoutDate, toGranularity: .day) {
return plannedWorkout
}
}
}
return nil
}
func setTreyDevRegisterdUser() {
self.registeredUser = RegisteredUser(id: 1, firstName: "t", lastName: "t", image: nil, nickName: "t", token: "15d7565cde9e8c904ae934f8235f68f6a24b4a03", email: nil, hasNSFWToggle: nil)
}
}

View File

@@ -0,0 +1,45 @@
//
// AccountView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/15/23.
//
import Foundation
import SwiftUI
struct AccountView: View {
@ObservedObject var userStore = UserStore.shared
var body: some View {
VStack(alignment: .leading) {
NameView()
CompletedWorkoutsView()
Divider()
ThotPreferenceView()
ShowNextUpView()
Spacer()
Logoutview()
}
.padding()
}
}
//struct AccountView_Previews: PreviewProvider {
// static let userStore = UserStore.shared
// static let completedWorkouts = PreviewData.parseCompletedWorkouts()
//
// static var previews: some View {
// AccountView(completedWorkouts: completedWorkouts)
// .onAppear{
// userStore.setFakeUser()
// }
// }
//}

View File

@@ -0,0 +1,95 @@
//
// CreateWorkout.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/15/23.
//
import Foundation
import SwiftUI
import Combine
struct AddExerciseView: View {
enum CreateWorkoutItemPickerViewType {
case muscles
case equipment
}
@State var selectedMuscles = [Muscle]()
@State var selectedEquipment = [Equipment]()
@State var filteredExercises = [Exercise]()
@StateObject var bridgeModule = BridgeModule.shared
let selectedExercise: ((Exercise) -> Void)
var body: some View {
VStack {
AllExerciseView(filteredExercises: $filteredExercises,
selectedExercise: { excercise in
selectedExercise(excercise)
})
.padding(.top)
HStack {
AllMusclesView(selectedMuscles: $selectedMuscles)
.frame(maxWidth: .infinity)
Divider()
AllEquipmentView(selectedEquipment: $selectedEquipment)
.frame(maxWidth: .infinity)
}
.padding(.top)
.frame(height: 44)
}
.onChange(of: selectedMuscles, perform: { _ in
filterExercises()
}) .onChange(of: selectedEquipment, perform: { _ in
filterExercises()
})
}
func filterExercises() {
guard let exercises = DataStore.shared.allExercise else {
filteredExercises = []
return
}
let filtered = exercises.filter({ exercise in
var hasCorrectMuscles = false
if selectedMuscles.count == 0 {
hasCorrectMuscles = true
} else {
let exerciseMuscleIds = exercise.muscles.map({ $0.muscle ?? -1 })
let selctedMuscleIds = selectedMuscles.map({ $0.id })
// if one items match
if exerciseMuscleIds.contains(where: selctedMuscleIds.contains) {
// if all items match
hasCorrectMuscles = true
}
}
var hasCorrectEquipment = false
if selectedEquipment.count == 0 {
hasCorrectEquipment = true
} else {
let exerciseEquipmentIds = exercise.equipment.map({ $0.equipment ?? -1 })
let selctedEquipmentIds = selectedEquipment.map({ $0.id })
// if one items match
if exerciseEquipmentIds.contains(where: selctedEquipmentIds.contains) {
// if all items match
hasCorrectEquipment = true
}
}
return hasCorrectMuscles && hasCorrectEquipment
})
filteredExercises = filtered
}
}
//struct AddExerciseView_Previews: PreviewProvider {
// static var previews: some View {
// AddExerciseView(selectedExercise: { _ in })
// }
//}

View File

@@ -0,0 +1,161 @@
//
// AllWorkoutsListView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct AllWorkoutsListView: View {
enum SortType: String, CaseIterable {
case name = "Name"
case date = "Date"
}
@State var searchString: String = ""
@Binding var uniqueWorkoutUsers: [RegisteredUser]?
@State private var filteredRegisterdUser: RegisteredUser?
let workouts: [Workout]
let selectedWorkout: ((Workout) -> Void)
@State var filteredWorkouts = [Workout]()
var refresh: (() -> Void)
@State var currentSort: SortType?
var body: some View {
VStack {
if let filteredRegisterdUser = filteredRegisterdUser {
Text((filteredRegisterdUser.firstName ?? "NA") + "'s Workouts")
}
ScrollView {
LazyVStack(spacing: 20) {
ForEach(filteredWorkouts, id:\.id) { workout in
WorkoutOverviewView(workout: workout)
.padding([.leading, .trailing])
.contentShape(Rectangle())
.onTapGesture {
selectedWorkout(workout)
}
}
}
}
.refreshable {
refresh()
}
HStack {
TextField("Filter" ,text: $searchString)
.padding()
.textFieldStyle(OvalTextFieldStyle())
if let uniqueWorkoutUsers = uniqueWorkoutUsers {
Menu(content: {
ForEach(uniqueWorkoutUsers, id: \.self) { index in
Button(action: {
filteredRegisterdUser = index
filteredWorkouts = filterWorkouts()
}, label: {
Text((index.firstName ?? "") + " -" + (index.lastName ?? ""))
})
}
Button(action: {
filteredRegisterdUser = nil
filteredWorkouts = filterWorkouts()
}, label: {
Text("All")
})
}, label: {
Image(systemName: filteredRegisterdUser == nil ? "person.2" : "person.2.fill")
.padding(.trailing)
})
}
Menu(content: {
ForEach(SortType.allCases, id: \.self) { index in
Button(action: {
sortWorkouts(sortType: index)
}, label: {
Text(index.rawValue)
})
}
}, label: {
Image(systemName: "list.number")
.padding(.trailing)
})
}
}
.onChange(of: searchString) { newValue in
filteredWorkouts = filterWorkouts()
}
.onAppear{
filteredWorkouts = filterWorkouts()
}
}
func sortWorkouts(sortType: SortType) {
if currentSort == sortType {
filteredWorkouts = filteredWorkouts.reversed()
return
}
switch sortType {
case .name:
filteredWorkouts = filteredWorkouts.sorted(by: {
$0.name < $1.name
})
case .date:
filteredWorkouts = filteredWorkouts.sorted(by: {
$0.createdAt ?? Date() < $1.createdAt ?? Date()
})
}
currentSort = sortType
}
func filterWorkouts() -> [Workout] {
var matchingWorkouts = [Workout]()
if (!searchString.isEmpty && searchString.count > 0) {
matchingWorkouts = workouts.filter({
if $0.name.lowercased().contains(searchString.lowercased()) {
return true
}
if let equipment = $0.equipment?.joined(separator: "").lowercased(),
equipment.contains(searchString.lowercased()) {
return true
}
if let muscles = $0.muscles?.joined(separator: "").lowercased(),
muscles.contains(searchString.lowercased()) {
return true
}
return false
})
}
if matchingWorkouts.isEmpty {
matchingWorkouts.append(contentsOf: workouts)
}
if let filteredRegisterdUser = filteredRegisterdUser {
matchingWorkouts = matchingWorkouts.filter({
$0.registeredUser == filteredRegisterdUser
})
}
return matchingWorkouts
}
}
struct AllWorkoutsListView_Previews: PreviewProvider {
static var previews: some View {
AllWorkoutsListView(uniqueWorkoutUsers: .constant([]),
workouts: PreviewData.allWorkouts(),
selectedWorkout: { workout in },
refresh: { })
}
}

View File

@@ -0,0 +1,204 @@
//
// AllWorkoutsView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/15/23.
//
import Foundation
import SwiftUI
import HealthKit
enum MainViewTypes: Int, CaseIterable {
case AllWorkout = 0
case MyWorkouts
var title: String {
switch self {
case .AllWorkout:
return "All Workouts"
case .MyWorkouts:
return "Planned Workouts"
}
}
}
struct AllWorkoutsView: View {
@State var isUpdating = false
@State var workouts: [Workout]?
@State var uniqueWorkoutUsers: [RegisteredUser]?
let healthStore = HKHealthStore()
var bridgeModule = BridgeModule.shared
@State public var needsUpdating: Bool = true
@ObservedObject var dataStore = DataStore.shared
@State private var showWorkoutDetail = false
@State private var selectedWorkout: Workout? {
didSet {
bridgeModule.currentExerciseInfo.workout = selectedWorkout
}
}
@State private var selectedPlannedWorkout: Workout? {
didSet {
bridgeModule.currentExerciseInfo.workout = selectedPlannedWorkout
}
}
@State private var showLoginView = false
@State private var selectedSegment: MainViewTypes = .AllWorkout
@State var selectedDate: Date = Date()
let pub = NotificationCenter.default.publisher(for: NSNotification.Name("CreatedNewWorkout"))
var body: some View {
ZStack {
if let workouts = workouts {
VStack {
AllWorkoutPickerView(mainViews: MainViewTypes.allCases,
selectedSegment: $selectedSegment,
showCurrentWorkout: {
selectedWorkout = bridgeModule.currentExerciseInfo.workout
})
switch selectedSegment {
case .AllWorkout:
if isUpdating {
ProgressView()
.progressViewStyle(.circular)
}
AllWorkoutsListView(uniqueWorkoutUsers: $uniqueWorkoutUsers,
workouts: workouts,
selectedWorkout: { workout in
selectedWorkout = workout
}, refresh: {
self.needsUpdating = true
maybeUpdateShit()
})
Divider()
case .MyWorkouts:
plannedWorkout(workouts: UserStore.shared.plannedWorkouts)
}
}
} else {
ProgressView("Updating")
}
}.onAppear{
// UserStore.shared.logout()
authorizeHealthKit()
maybeUpdateShit()
}
.sheet(item: $selectedWorkout) { item in
var isPreview = item.id == bridgeModule.currentExerciseInfo.workout?.id
let viewModel = WorkoutDetailViewModel(workout: item, isPreview: isPreview)
WorkoutDetailView(viewModel: viewModel)
}
.sheet(item: $selectedPlannedWorkout) { item in
let viewModel = WorkoutDetailViewModel(workout: item, isPreview: true)
WorkoutDetailView(viewModel: viewModel)
}
.sheet(isPresented: $showLoginView) {
LoginView(completion: {
self.needsUpdating = true
maybeUpdateShit()
})
.interactiveDismissDisabled()
}
.onReceive(pub) { (output) in
self.needsUpdating = true
maybeUpdateShit()
}
}
func plannedWorkout(workouts: [PlannedWorkout]) -> some View {
List {
ForEach(workouts, id:\.workout.name) { plannedWorkout in
HStack {
VStack(alignment: .leading) {
Text(plannedWorkout.onDate.plannedDate?.weekDay ?? "-")
.font(.title)
Text(plannedWorkout.onDate.plannedDate?.monthString ?? "-")
.font(.title)
Text(plannedWorkout.onDate.plannedDate?.dateString ?? "-")
.font(.title)
}
Divider()
VStack {
Text(plannedWorkout.workout.name)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
Text(plannedWorkout.workout.description ?? "")
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
Text(plannedWorkout.onDate)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
.contentShape(Rectangle())
.onTapGesture {
selectedPlannedWorkout = plannedWorkout.workout
}
}
}
}
}
func maybeUpdateShit() {
if UserStore.shared.token != nil{
if UserStore.shared.plannedWorkouts.isEmpty {
UserStore.shared.fetchPlannedWorkouts()
}
if needsUpdating {
self.isUpdating = true
dataStore.fetchAllData(completion: {
DispatchQueue.main.async {
guard let allWorkouts = dataStore.allWorkouts else {
return
}
self.workouts = allWorkouts.sorted(by: {
$0.createdAt ?? Date() < $1.createdAt ?? Date()
})
self.isUpdating = false
self.uniqueWorkoutUsers = dataStore.workoutsUniqueUsers
}
self.isUpdating = false
})
}
} else {
showLoginView = true
}
}
func authorizeHealthKit() {
let healthKitTypes: Set = [
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!,
HKQuantityType.workoutType()
]
healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { (succ, error) in
if !succ {
fatalError("Error requesting authorization from health store: \(String(describing: error)))")
}
}
}
}
struct AllWorkoutsView_Previews: PreviewProvider {
static var previews: some View {
AllWorkoutsView(workouts: PreviewData.allWorkouts())
}
}

View File

@@ -0,0 +1,211 @@
//
// CompletedWorkoutView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/22/23.
//
import SwiftUI
import HealthKit
struct CompletedWorkoutView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
@State var healthKitWorkoutData: HealthKitWorkoutData?
@State var difficulty: Float = 0
@State var notes: String = ""
@State var isUploading: Bool = false
@State var gettingHealthKitData: Bool = false
var postData: [String: Any]
let healthKitHelper = HealthKitHelper()
let workout: Workout
let completedWorkoutDismissed: ((Bool) -> Void)?
@Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
if isUploading {
ProgressView("Uploading")
}
VStack {
topViews()
Divider()
HStack {
if let calsBurned = healthKitWorkoutData?.caloriesBurned {
HStack {
HStack {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
.font(.title)
VStack {
Text("\(calsBurned, specifier: "%.0f")")
}
}
.frame(maxWidth: .infinity)
}
if let minHeart = healthKitWorkoutData?.minHeartRate,
let maxHeart = healthKitWorkoutData?.maxHeartRate,
let avgHeart = healthKitWorkoutData?.avgHeartRate {
VStack {
HStack {
Image(systemName: "heart")
.foregroundColor(.red)
.font(.title)
VStack {
HStack {
Text("\(minHeart, specifier: "%.0f")")
Text("-")
Text("\(maxHeart, specifier: "%.0f")")
}
Text("\(avgHeart, specifier: "%.0f")")
}
}
}
.frame(maxWidth: .infinity)
}
}
}
rateWorkout()
.frame(maxHeight: 88)
Divider()
TextField("Notes", text: $notes)
.frame(height: 55)
.textFieldStyle(PlainTextFieldStyle())
.padding([.horizontal], 4)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 200/255, green: 200/255, blue: 200/255, alpha: 0.2)))
.cornerRadius(8)
if gettingHealthKitData {
ProgressView("Getting HealthKit data")
.padding()
}
Spacer()
Button("Upload", action: {
isUploading = true
upload(postBody: postData)
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
}
.padding([.leading, .trailing])
}
.onAppear{
bridgeModule.sendWorkoutCompleteToWatch()
}
.onChange(of: bridgeModule.healthKitUUID, perform: { healthKitUUID in
if let healthKitUUID = healthKitUUID {
gettingHealthKitData = true
healthKitHelper.getDetails(forHealthKitUUID: healthKitUUID,
completion: { healthKitWorkoutData in
self.healthKitWorkoutData = healthKitWorkoutData
gettingHealthKitData = false
})
}
})
}
func topViews() -> some View {
VStack {
Text(workout.name)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.title3)
.padding(.top
)
if let desc = workout.description {
Text(desc)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body)
.padding(.top)
}
}
}
func rateWorkout() -> some View {
VStack {
Divider()
HStack {
Text("No Rate")
.foregroundColor(.black)
Text("Easy")
.foregroundColor(.green)
Spacer()
Text("Death")
.foregroundColor(.red)
}
ZStack {
LinearGradient(
gradient: Gradient(colors: [.black, .green, .red]),
startPoint: .leading,
endPoint: .trailing
)
.mask(Slider(value: $difficulty, in: 0...5, step: 1))
// Dummy replicated slider, to allow sliding
Slider(value: $difficulty, in: 0...5, step: 1)
.opacity(0.05) // Opacity is the trick here.
.accentColor(.clear)
}
}
}
func upload(postBody: [String: Any]) {
var _postBody = postBody
_postBody["difficulty"] = difficulty
_postBody["notes"] = notes
if let healthKitUUID = bridgeModule.healthKitUUID {
_postBody["health_kit_workout_uuid"] = healthKitUUID.uuidString
}
CompleteWorkoutFetchable(postData: _postBody).fetch(completion: { result in
switch result {
case .success(_):
DispatchQueue.main.async {
bridgeModule.resetCurrentWorkout()
dismiss()
completedWorkoutDismissed?(true)
}
case .failure(let failure):
DispatchQueue.main.async {
self.isUploading = false
}
print(failure)
}
})
}
}
struct CompletedWorkoutView_Previews: PreviewProvider {
static let postBody = [
"difficulty": 1,
"workout_start_time": Date().timeFormatForUpload,
"workout": 1,
"total_time": 140,
"total_calories": Float(120.0),
"heart_rates": [65,65,4,54,232,12]
] as [String : Any]
static let workout = PreviewData.workout()
static var previews: some View {
CompletedWorkoutView(postData: CompletedWorkoutView_Previews.postBody,
workout: workout,
completedWorkoutDismissed: { _ in })
}
}

View File

@@ -0,0 +1,111 @@
//
// CreateViewRepsWeightView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/18/23.
//
import SwiftUI
struct CreateExerciseActionsView: View {
@ObservedObject var workoutExercise: CreateWorkoutExercise
var superset: CreateWorkoutSuperSet
var viewModel: WorkoutViewModel
var body: some View {
VStack {
HStack {
VStack {
VStack {
Text("Reps: ")
Text("\(workoutExercise.reps)")
.foregroundColor(workoutExercise.reps == 0 && workoutExercise.duration == 0 ? .red : Color(uiColor: .label))
.bold()
}
Stepper("", onIncrement: {
workoutExercise.increaseReps()
}, onDecrement: {
workoutExercise.decreaseReps()
})
.labelsHidden()
}
.frame(maxWidth: .infinity)
Divider()
VStack{
VStack {
Text("Weight: ")
Text("\(workoutExercise.weight)")
}
Stepper("", onIncrement: {
workoutExercise.increaseWeight()
}, onDecrement: {
workoutExercise.decreaseWeight()
})
.labelsHidden()
}
.frame(maxWidth: .infinity)
Divider()
VStack{
VStack {
Text("Duration: ")
Text("\(workoutExercise.duration)")
.foregroundColor(workoutExercise.reps == 0 && workoutExercise.duration == 0 ? .red : Color(uiColor: .label))
.bold()
}
Stepper("", onIncrement: {
workoutExercise.increaseDuration()
}, onDecrement: {
workoutExercise.decreaseDuration()
})
}
}
HStack {
Spacer()
Button(action: {
}) {
Image(systemName: "video.fill")
}
.frame(width: 88, height: 44)
.foregroundColor(.white)
.background(.blue)
.cornerRadius(10)
.buttonStyle(BorderlessButtonStyle())
Spacer()
Divider()
Spacer()
Button(action: {
superset.deleteExerciseForChosenSuperset(exercise: workoutExercise)
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
}) {
Image(systemName: "trash.fill")
}
.frame(width: 88, height: 44)
.foregroundColor(.white)
.background(.red)
.cornerRadius(10)
.buttonStyle(BorderlessButtonStyle())
Spacer()
}
Divider()
.background(.blue)
}
}
}
//struct CreateViewRepsWeightView_Previews: PreviewProvider {
// static var previews: some View {
// CreateViewRepsWeightView()
// }
//}

View File

@@ -0,0 +1,170 @@
//
// CreateViewModels.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/18/23.
//
import SwiftUI
class CreateWorkoutExercise: ObservableObject, Identifiable {
let id = UUID()
var exercise: Exercise
@Published var reps: Int = 0
@Published var duration: Int = 0
@Published var weight: Int = 0
init(exercise: Exercise, reps: Int = 0, duration: Int = 0, weight: Int = 0) {
self.exercise = exercise
self.reps = reps
self.duration = duration
self.weight = weight
}
func increaseReps() {
self.reps += 1
}
func decreaseReps() {
self.reps -= 1
if self.reps < 0 {
self.reps = 0
}
}
func increaseDuration() {
self.duration += 15
self.reps = 0
}
func decreaseDuration() {
self.duration -= 15
if self.duration < 0 {
self.duration = 0
}
}
func increaseWeight() {
self.weight += 5
}
func decreaseWeight() {
self.weight -= 15
if self.weight < 0 {
self.weight = 0
}
}
}
class CreateWorkoutSuperSet: ObservableObject, Identifiable, Equatable {
static func == (lhs: CreateWorkoutSuperSet, rhs: CreateWorkoutSuperSet) -> Bool {
lhs.id == rhs.id
}
let id = UUID()
@Published var exercises = [CreateWorkoutExercise]()
@Published var numberOfRounds = 0
func increaseNumberOfRounds() {
self.numberOfRounds += 1
}
func decreaseNumberOfRounds() {
self.numberOfRounds -= 1
if self.numberOfRounds < 0 {
self.numberOfRounds = 0
}
}
func deleteExerciseForChosenSuperset(exercise: CreateWorkoutExercise) {
if let idx = exercises.firstIndex(where: {
$0.id == exercise.id
}) {
exercises.remove(at: idx)
}
}
}
class WorkoutViewModel: ObservableObject {
@Published var superSets = [CreateWorkoutSuperSet]()
@Published var title = String()
@Published var description = String()
@Published var randomValueForUpdatingValue = 0
func increaseRandomNumberForUpdating() {
randomValueForUpdatingValue += 1
}
func addNewSuperset() {
increaseRandomNumberForUpdating()
superSets.append(CreateWorkoutSuperSet())
}
func delete(superset: CreateWorkoutSuperSet) {
if let idx = superSets.firstIndex(where: {
$0.id == superset.id
}) {
superSets.remove(at: idx)
increaseRandomNumberForUpdating()
}
}
func showRoundsError() {
}
func showNoDurationOrReps() {
}
func uploadWorkout() {
var supersets = [[String: Any]]()
var supersetOrder = 1
superSets.forEach({ superset in
if superset.numberOfRounds == 0 {
showRoundsError()
return
}
var supersetInfo = [String: Any]()
supersetInfo["name"] = ""
supersetInfo["rounds"] = superset.numberOfRounds
supersetInfo["order"] = supersetOrder
var exercises = [[String: Any]]()
var exerciseOrder = 1
for exercise in superset.exercises {
if exercise.reps == 0 && exercise.duration == 0 {
showNoDurationOrReps()
return
}
let item = ["id": exercise.exercise.id,
"reps": exercise.reps,
"weight": exercise.weight,
"duration": exercise.duration,
"order": exerciseOrder] as [String : Any]
exercises.append(item)
exerciseOrder += 1
}
supersetInfo["exercises"] = exercises
supersets.append(supersetInfo)
supersetOrder += 1
})
let uploadBody = ["name": title,
"description": description,
"supersets": supersets] as [String : Any]
CreateWorkoutFetchable(postData: uploadBody).fetch(completion: { result in
DispatchQueue.main.async {
switch result {
case .success(_):
self.superSets.removeAll()
self.title = ""
NotificationCenter.default.post(name: NSNotification.Name("CreatedNewWorkout"), object: nil, userInfo: nil)
case .failure(let failure):
print(failure)
}
}
})
}
}

View File

@@ -0,0 +1,113 @@
//
// CreateWorkoutItemPicker.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/25/23.
//
import SwiftUI
import Combine
struct CreateWorkoutItemPickerModel {
let id: Int
let name: String
}
class CreateWorkoutItemPickerViewModel: Identifiable, ObservableObject {
let allValues: [CreateWorkoutItemPickerModel]
@Published var selectedIds: [Int]
init(allValues: [CreateWorkoutItemPickerModel], selectedIds: [Int]) {
self.allValues = allValues
self.selectedIds = selectedIds
}
func toggleAll() {
if selectedIds.isEmpty {
selectedIds.append(contentsOf: allValues.map({ $0.id }))
} else {
selectedIds.removeAll()
}
}
}
struct CreateWorkoutItemPickerView: View {
@ObservedObject var viewModel: CreateWorkoutItemPickerViewModel
var completed: (([Int]) -> Void)
@Environment(\.dismiss) var dismiss
@State var searchString: String = ""
var body: some View {
VStack {
List() {
ForEach(viewModel.allValues, id:\.self.id) { value in
if searchString.isEmpty || value.name.lowercased().contains(searchString.lowercased()) {
HStack {
Circle()
.stroke(.blue, lineWidth: 1)
.background(Circle().fill(viewModel.selectedIds.contains(value.id) ? .blue :.clear))
.frame(width: 33, height: 33)
Text(value.name)
}
.contentShape(Rectangle())
.onTapGesture {
if viewModel.selectedIds.contains(value.id) {
if let idx = viewModel.selectedIds.firstIndex(of: value.id){
viewModel.selectedIds.remove(at: idx)
}
} else {
viewModel.selectedIds.append(value.id)
}
}
}
}
}
TextField("Filter", text: $searchString)
.padding()
HStack {
Button(action: {
viewModel.toggleAll()
}, label: {
Image(systemName: "checklist")
.font(.title)
})
.frame(maxWidth: 44, alignment: .center)
.frame(height: 44)
.foregroundColor(.green)
.background(.white)
.cornerRadius(8)
.padding()
Button(action: {
completed(viewModel.selectedIds)
dismiss()
}, label: {
Text("done")
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
}
}
}
}
struct CreateWorkoutItemPickerView_Previews: PreviewProvider {
static let fakeValues = [CreateWorkoutItemPickerModel(id: 1, name: "one"),
CreateWorkoutItemPickerModel(id: 2, name: "two"),
CreateWorkoutItemPickerModel(id: 3, name: "three")]
static var previews: some View {
CreateWorkoutItemPickerView(viewModel: CreateWorkoutItemPickerViewModel(allValues: fakeValues, selectedIds: [1]), completed: { selectedIds in
})
}
}

View File

@@ -0,0 +1,164 @@
//
// CreateWorkoutView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/15/23.
//
import SwiftUI
struct CreateWorkoutMainView: View {
@ObservedObject var viewModel = WorkoutViewModel()
@State private var showAddExercise = false
@State var selectedCreateWorkoutSuperSet: CreateWorkoutSuperSet?
var body: some View {
VStack {
TextField("Title", text: $viewModel.title)
.padding()
.frame(height: 55)
.textFieldStyle(OvalTextFieldStyle())
TextField("Description", text: $viewModel.description)
.padding()
.frame(height: 55)
.textFieldStyle(OvalTextFieldStyle())
ScrollViewReader { proxy in
List() {
ForEach($viewModel.superSets, id: \.id) { superset in
Section {
ForEach(superset.exercises, id: \.id) { exercise in
HStack {
VStack {
Text(exercise.wrappedValue.exercise.name)
.font(.title2)
.frame(maxWidth: .infinity)
if exercise.wrappedValue.exercise.side != nil && exercise.wrappedValue.exercise.side!.count > 0 {
Text(exercise.wrappedValue.exercise.side!)
.font(.title3)
.frame(maxWidth: .infinity, alignment: .center)
}
CreateExerciseActionsView(workoutExercise: exercise.wrappedValue,
superset: superset.wrappedValue,
viewModel: viewModel)
}
}
}
HStack {
Stepper("Number of rounds", onIncrement: {
superset.wrappedValue.increaseNumberOfRounds()
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
}, onDecrement: {
superset.wrappedValue.decreaseNumberOfRounds()
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
})
Text("\(superset.wrappedValue.numberOfRounds)")
.foregroundColor(superset.numberOfRounds.wrappedValue > 0 ? .black : .red)
.bold()
}
CreateWorkoutSupersetActionsView(workoutSuperSet: superset.wrappedValue,
showAddExercise: $showAddExercise,
viewModel: viewModel,
selectedCreateWorkoutSuperSet: $selectedCreateWorkoutSuperSet)
}
}
Text("this is the bottom 🤷‍♂️")
.id(999)
.listRowSeparator(.hidden)
}
.onChange(of: viewModel.randomValueForUpdatingValue, perform: { newValue in
withAnimation {
proxy.scrollTo(999, anchor: .bottom)
}
})
}
// .overlay(Group {
// if($viewModel.superSets.isEmpty) {
// ZStack() {
// Color(uiColor: .secondarySystemBackground)
// }
// }
// })
Divider()
HStack {
Button("Add Superset", action: {
viewModel.addNewSuperset()
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
Divider()
Button("Done", action: {
viewModel.uploadWorkout()
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.white)
.background(.blue)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
.disabled(viewModel.title.isEmpty)
}
.frame(height: 44)
.padding(.bottom)
}
.sheet(isPresented: $showAddExercise) {
AddExerciseView(selectedExercise: { exercise in
let workoutExercise = CreateWorkoutExercise(exercise: exercise)
selectedCreateWorkoutSuperSet?.exercises.append(workoutExercise)
// if left or right auto add the other side
// with a recover in between b/c its
// eaiser to delete a recover than add one
if exercise.side != nil && exercise.side!.count > 0 {
let exercises = DataStore.shared.allExercise?.filter({
$0.name == exercise.name
})
let recover = DataStore.shared.allExercise?.first(where: {
$0.name.lowercased() == "recover"
})
if let exercises = exercises, let recover = recover {
if exercises.count == 2 {
let recoverWorkoutExercise = CreateWorkoutExercise(exercise: recover)
selectedCreateWorkoutSuperSet?.exercises.append(recoverWorkoutExercise)
for LRExercise in exercises {
if LRExercise.id != exercise.id {
let otherSideExercise = CreateWorkoutExercise(exercise: LRExercise)
selectedCreateWorkoutSuperSet?.exercises.append(otherSideExercise)
}
}
}
}
}
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
selectedCreateWorkoutSuperSet = nil
})
}
}
}
//struct CreateWorkoutView_Previews: PreviewProvider {
// static var previews: some View {
// CreateWorkoutView()
// }
//}

View File

@@ -0,0 +1,53 @@
//
// CreateWorkoutSupersetActionsView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/18/23.
//
import SwiftUI
struct CreateWorkoutSupersetActionsView: View {
var workoutSuperSet: CreateWorkoutSuperSet
@Binding var showAddExercise: Bool
var viewModel: WorkoutViewModel
@Binding var selectedCreateWorkoutSuperSet: CreateWorkoutSuperSet?
var body: some View {
HStack {
Button(action: {
selectedCreateWorkoutSuperSet = workoutSuperSet
showAddExercise.toggle()
}) {
Text("Add exercise")
.padding()
}
.foregroundColor(.white)
.background(.green)
.cornerRadius(10)
.frame(maxWidth: .infinity, alignment: .center)
.buttonStyle(BorderlessButtonStyle())
Button(action: {
viewModel.delete(superset: workoutSuperSet)
viewModel.increaseRandomNumberForUpdating()
viewModel.objectWillChange.send()
}) {
Text("Delete superset")
.padding()
}
.foregroundColor(.white)
.background(.red)
.cornerRadius(10)
.frame(maxWidth: .infinity, alignment: .center)
.buttonStyle(BorderlessButtonStyle())
}
}
}
//struct CreateWorkoutSupersetActionsView_Previews: PreviewProvider {
// static var previews: some View {
// CreateWorkoutSupersetActionsView()
// }
//}

View File

@@ -0,0 +1,116 @@
//
// ExternalView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/13/23.
//
import SwiftUI
import AVKit
struct ExternalWorkoutDetailView: View {
@StateObject var bridgeModule = BridgeModule.shared
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
@State var smallAVPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
@AppStorage(Constants.extThotStyle) private var extThotStyle: ThotStyle = .never
@AppStorage(Constants.extShowNextVideo) private var extShowNextVideo: Bool = false
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
var body: some View {
ZStack {
if let workout = bridgeModule.currentExerciseInfo.workout {
GeometryReader { metrics in
VStack {
HStack {
if extThotStyle != .off {
PlayerView(player: $avPlayer)
.frame(width: metrics.size.width * 0.5, height: metrics.size.height * 0.8)
.onAppear{
avPlayer.play()
}
}
VStack {
ExtExerciseList(workout: workout,
allSupersetExecerciseIndex: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex)
}
.frame(width: metrics.size.width * 0.4, height: metrics.size.height * 0.8)
.padding([.top, .bottom], 20)
}
HStack {
ExtCountdownView()
.padding(.leading, 50)
.padding(.bottom, 20)
if extShowNextVideo && extThotStyle != .off {
PlayerView(player: $smallAVPlayer)
.frame(width: metrics.size.width * 0.2,
height: metrics.size.height * 0.2)
.onAppear{
avPlayer.play()
}
}
}
.background(Color(uiColor: .tertiarySystemGroupedBackground))
}
}
} else {
Image("icon")
.resizable()
.edgesIgnoringSafeArea(.all)
.scaledToFill()
}
}
.onChange(of: bridgeModule.isInWorkout, perform: { _ in
playVideos()
})
.onChange(of: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex, perform: { _ in
playVideos()
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(bridgeModule.currentExerciseInfo.workout == nil ? Color(red: 157/255, green: 138/255, blue: 255/255) : Color(uiColor: .systemBackground))
.onReceive(NotificationCenter.default.publisher(
for: UIScene.willEnterForegroundNotification)) { _ in
avPlayer.play()
smallAVPlayer.play()
}
}
func playVideos() {
if let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise {
if let videoURL = VideoURLCreator.videoURL(
thotStyle: extThotStyle,
gender: thotGenderOption,
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentExerciseInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.play()
}
if let smallVideoURL = VideoURLCreator.videoURL(
thotStyle: .never,
gender: thotGenderOption,
defaultVideoURLStr: BridgeModule.shared.currentExerciseInfo.nextExerciseInfo?.exercise.videoURL,
exerciseName: BridgeModule.shared.currentExerciseInfo.nextExerciseInfo?.exercise.name,
workout: bridgeModule.currentExerciseInfo.workout),
extShowNextVideo {
smallAVPlayer = AVPlayer(url: smallVideoURL)
smallAVPlayer.play()
}
}
}
}
//struct ExternalWorkoutDetailView_Previews: PreviewProvider {
// static var bridge = BridgeModule.shared
//
// static var previews: some View {
// ExternalWorkoutDetailView().environmentObject({ () -> BridgeModule in
// let envObj = BridgeModule.shared
// envObj.currentWorkout = nil //PreviewData.workout()
// bridge.currentExercise = PreviewData.workout().exercisesSortedByCreated_at.first!
// return envObj
// }() )
// }
//}

View File

@@ -0,0 +1,93 @@
//
// LoginView.swift
// WekoutThotViewer
//
// Created by Trey Tartt on 6/18/24.
//
import SwiftUI
struct LoginView: View {
@State var email: String = ""
@State var password: String = ""
@Environment(\.dismiss) var dismiss
let completion: (() -> Void)
@State var doingNetworkShit: Bool = false
var body: some View {
VStack {
TextField("Email", text: $email)
.textContentType(.username)
.autocapitalization(.none)
.frame(height: 55)
.textFieldStyle(PlainTextFieldStyle())
.padding([.horizontal], 4)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 255/255, green: 255/255, blue: 255/255, alpha: 1)))
.cornerRadius(8)
.padding(.top, 25)
SecureField("Password", text: $password)
.textContentType(.password)
.autocapitalization(.none)
.frame(height: 55)
.textFieldStyle(PlainTextFieldStyle())
.padding([.horizontal], 4)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 255/255, green: 255/255, blue: 255/255, alpha: 1)))
.cornerRadius(8)
if doingNetworkShit {
ProgressView("Logging In")
.padding()
.foregroundColor(.white)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.5, anchor: .center)
} else {
Button("Login", action: {
login()
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
.disabled(password.isEmpty || email.isEmpty)
}
Spacer()
}
.padding()
.background(
Image("icon")
.resizable()
.edgesIgnoringSafeArea(.all)
.scaledToFill()
)
}
func login() {
let postData = [
"email": email,
"password": password
]
doingNetworkShit = true
UserStore.shared.login(postData: postData, completion: { success in
doingNetworkShit = false
if success {
completion()
dismiss()
}
})
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView(completion: {
})
}
}

View File

@@ -0,0 +1,31 @@
//
// ContentView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/13/23.
//
import SwiftUI
import CoreData
struct MainView: View {
@State var workout: Workout?
@StateObject var bridgeModule = BridgeModule.shared
var body: some View {
ZStack {
if let workout = workout {
let vm = WorkoutDetailViewModel(workout: workout, isPreview: true)
WorkoutDetailView(viewModel: vm)
} else {
Text("no workout selected")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MainView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,94 @@
//
// PlanWorkoutView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/2/23.
//
import SwiftUI
struct PlanWorkoutView: View {
@State var selectedDate = Date()
let workout: Workout
@Environment(\.dismiss) var dismiss
var addedPlannedWorkout: (() -> Void)?
var body: some View {
VStack() {
Text(workout.name)
.font(.title)
Text(selectedDate.formatted(date: .abbreviated, time: .omitted))
.font(.system(size: 28))
.bold()
.foregroundColor(Color.accentColor)
.padding()
.animation(.spring(), value: selectedDate)
Divider().frame(height: 1)
DatePicker("Select Date", selection: $selectedDate, displayedComponents: [.date])
.padding(.horizontal)
.datePickerStyle(.graphical)
Divider()
HStack {
Button(action: {
planWorkout()
}, label: {
Image(systemName: "plus.app")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(8)
.padding()
Button(action: {
dismiss()
}, label: {
Image(systemName: "xmark.octagon.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.white)
.background(.red)
.cornerRadius(8)
.padding()
}
Spacer()
}
.padding()
}
func planWorkout() {
let postData = [
"on_date": selectedDate.formatForPlannedWorkout,
"workout": workout.id
] as [String : Any]
PlanWorkoutFetchable(postData: postData).fetch(completion: { result in
switch result {
case .success(_):
UserStore.shared.fetchPlannedWorkouts()
dismiss()
addedPlannedWorkout?()
case .failure(_):
fatalError("shit broke")
}
})
}
}
struct PlanWorkoutView_Previews: PreviewProvider {
static var previews: some View {
PlanWorkoutView(workout: PreviewData.workout())
}
}

View File

@@ -0,0 +1,21 @@
//
// CurrentWorkoutElapsedTimeView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct CurrentWorkoutElapsedTimeView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var body: some View {
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
VStack {
Text("\(Double(bridgeModule.currentWorkoutRunTimeInSeconds).asString(style: .positional))")
.font(.title2)
}
}
}
}

View File

@@ -0,0 +1,160 @@
//
// ExerciseListView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
import AVKit
struct ExerciseListView: View {
@AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never
@ObservedObject var bridgeModule = BridgeModule.shared
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
var workout: Workout
@Binding var showExecersizeInfo: Bool
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
@State var videoExercise: Exercise? {
didSet {
if let videoURL = VideoURLCreator.videoURL(
thotStyle: phoneThotStyle,
gender: thotGenderOption,
defaultVideoURLStr: self.videoExercise?.videoURL,
exerciseName: self.videoExercise?.name,
workout: bridgeModule.currentExerciseInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.play()
}
}
}
var body: some View {
if let supersets = workout.supersets {
ScrollViewReader { proxy in
List() {
ForEach(supersets.indices, id: \.self) { supersetIndex in
let superset = supersets[supersetIndex]
Section(content: {
ForEach(superset.exercises.indices, id: \.self) { exerciseIndex in
let supersetExecercise = superset.exercises[exerciseIndex]
VStack {
HStack {
if bridgeModule.isInWorkout &&
supersetIndex == bridgeModule.currentExerciseInfo.supersetIndex &&
exerciseIndex == bridgeModule.currentExerciseInfo.exerciseIndex {
Image(systemName: "figure.run")
.foregroundColor(Color("appColor"))
}
Text(supersetExecercise.exercise.extName)
Spacer()
if let reps = supersetExecercise.reps,
reps > 0 {
HStack {
Image(systemName: "number")
.foregroundColor(.white)
.frame(width: 20, alignment: .leading)
Text("\(reps)")
.foregroundColor(.white)
.frame(width: 30, alignment: .trailing)
}
.padding([.top, .bottom], 5)
.padding([.leading], 10)
.padding([.trailing], 15)
.background(.blue)
.cornerRadius(5, corners: [.topLeft, .bottomLeft])
.frame(alignment: .trailing)
}
if let duration = supersetExecercise.duration,
duration > 0 {
HStack {
Image(systemName: "stopwatch")
.foregroundColor(.white)
.frame(width: 20, alignment: .leading)
Text("\(duration)")
.foregroundColor(.white)
.frame(width: 30, alignment: .trailing)
}
.padding([.top, .bottom], 5)
.padding([.leading], 10)
.padding([.trailing], 15)
.background(.green)
.cornerRadius(5, corners: [.topLeft, .bottomLeft])
}
}
.padding(.trailing, -20)
.contentShape(Rectangle())
.onTapGesture {
if bridgeModule.isInWorkout {
bridgeModule.goToExerciseAt(section: supersetIndex, row: exerciseIndex)
} else {
videoExercise = supersetExecercise.exercise
}
}
if bridgeModule.isInWorkout &&
supersetIndex == bridgeModule.currentExerciseInfo.supersetIndex &&
exerciseIndex == bridgeModule.currentExerciseInfo.exerciseIndex &&
showExecersizeInfo {
detailView(forExercise: supersetExecercise)
}
}.id(supersetExecercise.id)
}
}, header: {
HStack {
Text(superset.name ?? "--")
.foregroundColor(Color("appColor"))
.bold()
Spacer()
Text("\(superset.rounds) rounds")
.foregroundColor(Color("appColor"))
.bold()
if let estimatedTime = superset.estimatedTime {
Text("@ " + estimatedTime.asString(style: .abbreviated))
.foregroundColor(Color("appColor"))
.bold()
}
}
})
}
}
.onChange(of: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex, perform: { newValue in
if let newCurrentExercise = bridgeModule.currentExerciseInfo.currentExercise {
withAnimation {
proxy.scrollTo(newCurrentExercise.id, anchor: .top)
}
}
})
.sheet(item: $videoExercise) { exercise in
PlayerView(player: $avPlayer)
.onAppear{
avPlayer.play()
}
}
}
}
}
func detailView(forExercise supersetExecercise: SupersetExercise) -> some View {
VStack {
Text(supersetExecercise.exercise.description)
.frame(alignment: .leading)
Divider()
Text(supersetExecercise.exercise.muscles.map({ $0.name }).joined(separator: ", "))
.frame(alignment: .leading)
}
}
}
//struct ExerciseListView_Previews: PreviewProvider {
// static var previews: some View {
// ExerciseListView(workout: PreviewData.workout(), showExecersizeInfo: )
// }
//}

View File

@@ -0,0 +1,229 @@
//
// MainView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import SwiftUI
import AVKit
struct WorkoutDetailView: View {
@StateObject var viewModel: WorkoutDetailViewModel
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
@StateObject var bridgeModule = BridgeModule.shared
@Environment(\.dismiss) var dismiss
@AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
enum Sheet: Identifiable {
case completedWorkout([String: Any])
var id: String { return UUID().uuidString }
}
@State var presentedSheet: Sheet?
@State var workoutToPlan: Workout?
@State var showExecersizeInfo: Bool = false
var body: some View {
ZStack {
switch viewModel.status {
case .loading:
Text("Loading")
case .showWorkout(let workout):
VStack(spacing: 0) {
if bridgeModule.isInWorkout {
HStack {
CountdownView()
}
.padding()
.frame(maxWidth: .infinity)
if phoneThotStyle != .off {
GeometryReader { metrics in
ZStack {
PlayerView(player: $avPlayer)
.frame(width: metrics.size.width * 1, height: metrics.size.height * 1)
.onAppear{
avPlayer.play()
}
Button(action: {
if let assetURL = ((avPlayer.currentItem?.asset) as? AVURLAsset)?.url,
let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise,
let otherVideoURL = VideoURLCreator.videoURL(
thotStyle: VideoURLCreator.otherVideoType(forVideoURL: assetURL),
gender: thotGenderOption,
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentExerciseInfo.workout) {
avPlayer = AVPlayer(url: otherVideoURL)
avPlayer.play()
}
}, label: {
Image(systemName: "arrow.triangle.2.circlepath.camera.fill")
.frame(width: 44, height: 44)
.foregroundColor(Color("appColor"))
})
.foregroundColor(.blue)
.cornerRadius(4)
.frame(width: 160, height: 120)
.position(x: metrics.size.width - 22, y: metrics.size.height - 30)
Button(action: {
showExecersizeInfo.toggle()
}, label: {
Image(systemName: "info.circle.fill")
.frame(width: 44, height: 44)
.foregroundColor(Color("appColor"))
})
.foregroundColor(.blue)
.cornerRadius(4)
.frame(width: 120, height: 120)
.position(x: 22, y: metrics.size.height - 30)
}
}
.padding([.top, .bottom])
.background(Color(uiColor: .tertiarySystemBackground))
}
}
if !bridgeModule.isInWorkout {
InfoView(workout: workout)
.padding(.bottom)
}
if bridgeModule.isInWorkout {
Divider()
.background(Color(uiColor: .secondaryLabel))
HStack {
Text("\(bridgeModule.currentExerciseInfo.currentRound) of \(bridgeModule.currentExerciseInfo.numberOfRoundsInCurrentSuperSet)")
.font(.title3)
.bold()
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 10)
CurrentWorkoutElapsedTimeView()
.frame(maxWidth: .infinity, alignment: .center)
Text("\(bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex+1)/\(bridgeModule.currentExerciseInfo.workout?.allSupersetExecercise?.count ?? -99)")
.font(.title3)
.bold()
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 10)
}
.padding([.top, .bottom])
.background(Color(uiColor: .tertiarySystemBackground))
}
Divider()
.background(Color(uiColor: .secondaryLabel))
ExerciseListView(workout: workout, showExecersizeInfo: $showExecersizeInfo)
.padding([.top, .bottom], 10)
.background(Color(uiColor: .systemGroupedBackground))
ActionsView(completedWorkout: {
bridgeModule.completeWorkout()
}, planWorkout: { workout in
workoutToPlan = workout
}, workout: workout, showAddToCalendar: viewModel.isPreview, startWorkoutAction: {
startWorkout(workout: workout)
})
.frame(height: 44)
}
.sheet(item: $presentedSheet) { item in
switch item {
case .completedWorkout(let data):
CompletedWorkoutView(postData: data,
workout: workout,
completedWorkoutDismissed: { uploaded in
if uploaded {
dismiss()
}
})
}
}
.sheet(item: $workoutToPlan) { workout in
PlanWorkoutView(workout: workout, addedPlannedWorkout: {
dismiss()
})
}
}
}
.onChange(of: bridgeModule.currentExerciseInfo.allSupersetExecerciseIndex, perform: { _ in
playVideos()
})
.onChange(of: bridgeModule.isInWorkout, perform: { _ in
playVideos()
})
.onAppear{
if let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise {
if let videoURL = VideoURLCreator.videoURL(
thotStyle: phoneThotStyle,
gender: thotGenderOption,
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentExerciseInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.play()
}
}
}
.onReceive(NotificationCenter.default.publisher(
for: UIScene.willEnterForegroundNotification)) { _ in
avPlayer.play()
}
}
func playVideos() {
if let currentExtercise = bridgeModule.currentExerciseInfo.currentExercise {
if let videoURL = VideoURLCreator.videoURL(
thotStyle: phoneThotStyle,
gender: thotGenderOption,
defaultVideoURLStr: currentExtercise.exercise.videoURL,
exerciseName: currentExtercise.exercise.name,
workout: bridgeModule.currentExerciseInfo.workout) {
avPlayer = AVPlayer(url: videoURL)
avPlayer.play()
}
}
}
func startWorkout(workout: Workout) {
bridgeModule.completedWorkout = {
if let workoutData = createWorkoutData() {
presentedSheet = .completedWorkout(workoutData)
bridgeModule.resetCurrentWorkout()
}
}
bridgeModule.start(workout: workout)
}
func createWorkoutData() -> [String:Any]? {
guard let workoutid = bridgeModule.currentExerciseInfo.workout?.id,
let startTime = bridgeModule.workoutStartDate?.timeFormatForUpload,
let endTime = bridgeModule.workoutEndDate?.timeFormatForUpload else {
return nil
}
let postBody = [
"difficulty": 1,
"workout_start_time": startTime,
"workout_end_time": endTime,
"workout": workoutid,
"total_time": bridgeModule.currentWorkoutRunTimeInSeconds
] as [String : Any]
return postBody
}
}
struct WorkoutDetailView_Previews: PreviewProvider {
static let workoutDetail = PreviewData.workout()
static var previews: some View {
WorkoutDetailView(viewModel: WorkoutDetailViewModel(workout: WorkoutDetailView_Previews.workoutDetail, status: .showWorkout(WorkoutDetailView_Previews.workoutDetail), isPreview: true))
}
}

View File

@@ -0,0 +1,39 @@
//
// MainViewViewModel.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/14/23.
//
import Combine
import Foundation
class WorkoutDetailViewModel: ObservableObject {
enum WorkoutDetailViewModelStatus {
case loading
case showWorkout(Workout)
}
@Published var status: WorkoutDetailViewModelStatus
let isPreview: Bool
init(workout: Workout, status: WorkoutDetailViewModelStatus? = nil, isPreview: Bool) {
self.status = .loading
self.isPreview = isPreview
if let passedStatus = status {
self.status = passedStatus
} else {
WorkoutDetailFetchable(workoutID: workout.id).fetch(completion: { result in
switch result {
case .success(let model):
DispatchQueue.main.async {
self.status = .showWorkout(model)
}
case .failure(let failure):
fatalError("failed \(failure.localizedDescription)")
}
})
}
}
}

View File

@@ -0,0 +1,93 @@
//
// WorkoutHistoryView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/26/23.
//
import SwiftUI
struct WorkoutHistoryView: View {
enum DifficltyString: Int {
case easy = 1
case moderate
case average
case hard
case death
var stringValue: String {
switch self {
case .easy:
return "Easy"
case .moderate:
return "Moderate"
case .average:
return "Average"
case .hard:
return "Hard"
case .death:
return "Death"
}
}
}
let completedWorkouts: [CompletedWorkout]
@State private var selectedPlannedWorkout: Workout?
var body: some View {
List {
ForEach(completedWorkouts, id:\.self.id) { completedWorkout in
HStack {
VStack {
if let date = completedWorkout.workoutStartTime.dateFromServerDate {
Text(DateFormatter().shortMonthSymbols[date.get(.month) - 1])
Text("\(date.get(.day))")
Text("\(date.get(.hour))")
}
}
VStack(alignment: .leading) {
Text(completedWorkout.workout.name)
.font(.title3)
if let desc = completedWorkout.workout.description {
Text(desc)
.font(.footnote)
}
Divider()
if let difficulty = completedWorkout.difficulty,
let string = DifficltyString.init(rawValue: difficulty)?.stringValue {
Text(string)
}
if let notes = completedWorkout.notes {
Text(notes)
}
}
.padding(.leading)
.contentShape(Rectangle())
.onTapGesture {
selectedPlannedWorkout = completedWorkout.workout
}
}
}
}
.sheet(item: $selectedPlannedWorkout) { item in
let viewModel = WorkoutDetailViewModel(workout: item, isPreview: true)
WorkoutDetailView(viewModel: viewModel)
}
}
}
struct WorkoutHistoryView_Previews: PreviewProvider {
static let fakeHistory = PreviewData.parseCompletedWorkouts()
static var previews: some View {
WorkoutHistoryView(completedWorkouts: fakeHistory)
}
}

View File

@@ -0,0 +1,19 @@
//
// WatchPackageModel.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/22/23.
//
import Foundation
struct WatchPackageModel: Codable {
var currentExerciseName: String
var currentTimeLeft: Int
var workoutStartDate: Date
var workoutEndDate: Date?
}
struct WatchFinishWorkoutModel: Codable {
var healthKitUUID: UUID
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:dev.werkout.fitness</string>
</array>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array/>
<key>com.apple.developer.healthkit.background-delivery</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Werkout_ios.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<elements>
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
</elements>
</model>

View File

@@ -0,0 +1,105 @@
//
// Werkout_iosApp.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/13/23.
//
import SwiftUI
import Combine
import AVKit
struct Constants {
static let phoneThotStyle = "phoneThotStyle"
static let extThotStyle = "extThotStyle"
static let extShowNextVideo = "extShowNextVideo"
static let thotGenderOption = "thotGenderOption"
}
@main
struct Werkout_iosApp: App {
let persistenceController = PersistenceController.shared
@State var additionalWindows: [UIWindow] = []
@State private var tabSelection = 1
let pub = NotificationCenter.default.publisher(for: NSNotification.Name("CreatedNewWorkout"))
private var screenDidConnectPublisher: AnyPublisher<UIScreen, Never> {
NotificationCenter.default
.publisher(for: UIScreen.didConnectNotification)
.compactMap { $0.object as? UIScreen }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
private var screenDidDisconnectPublisher: AnyPublisher<UIScreen, Never> {
NotificationCenter.default
.publisher(for: UIScreen.didDisconnectNotification)
.compactMap { $0.object as? UIScreen }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
var body: some Scene {
WindowGroup {
TabView(selection: $tabSelection) {
AllWorkoutsView()
.onReceive(
screenDidConnectPublisher,
perform: screenDidConnect
)
.onReceive(
screenDidDisconnectPublisher,
perform: screenDidDisconnect
)
.tabItem {
Label("All Workouts", systemImage: "figure.strengthtraining.traditional")
}
.tag(1)
CreateWorkoutMainView()
.tabItem {
Label("Create Workout", systemImage: "plus.app.fill")
}
.tag(2)
AccountView()
.tabItem {
Label("Accounts", systemImage: "person.fill.turn.down")
}
.tag(3)
}
.accentColor(Color("appColor"))
.onAppear{
UIApplication.shared.isIdleTimerDisabled = true
_ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
// UserStore.shared.logout()
}
.onReceive(pub) { (output) in
self.tabSelection = 1
}
}
}
private func screenDidDisconnect(_ screen: UIScreen) {
additionalWindows.removeAll { $0.screen == screen }
BridgeModule.shared.isShowingOnExternalDisplay = false
}
private func screenDidConnect(_ screen: UIScreen) {
let window = UIWindow(frame: screen.bounds)
window.windowScene = UIApplication.shared.connectedScenes
.first { ($0 as? UIWindowScene)?.screen == screen }
as? UIWindowScene
let view = ExternalWorkoutDetailView()
.preferredColorScheme(.dark)
let controller = UIHostingController(rootView: view)
window.rootViewController = controller
window.isHidden = false
additionalWindows.append(window)
BridgeModule.shared.isShowingOnExternalDisplay = true
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,110 @@
//
// ActionsView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct ActionsView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var completedWorkout: (() -> Void)?
var planWorkout: ((Workout) -> Void)?
var workout: Workout
@Environment(\.dismiss) var dismiss
var showAddToCalendar: Bool
var startWorkoutAction: (() -> Void)
var body: some View {
HStack {
if bridgeModule.isInWorkout == false {
Button(action: {
bridgeModule.resetCurrentWorkout()
dismiss()
}, label: {
Image(systemName: "xmark.octagon.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
.foregroundColor(.white)
if showAddToCalendar {
Button(action: {
planWorkout?(workout)
}, label: {
Image(systemName: "calendar.badge.plus")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
.foregroundColor(.white)
}
Button(action: {
startWorkoutAction()
}, label: {
Image(systemName: "arrowtriangle.forward.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
.foregroundColor(.white)
} else {
Button(action: {
nextExercise()
}, label: {
Image(systemName: "arrow.forward")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
.foregroundColor(.white)
Button(action: {
bridgeModule.pauseWorkout()
}, label: {
bridgeModule.isPaused ?
Image(systemName: "play.circle.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
:
Image(systemName: "pause.circle.fill")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(bridgeModule.isPaused ? .mint : .yellow)
.foregroundColor(.white)
Button(action: {
completedWorkout?()
}, label: {
Image(systemName: "checkmark")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
.foregroundColor(.white)
}
}
}
func nextExercise() {
bridgeModule.nextExercise()
}
}
struct ActionsView_Previews: PreviewProvider {
static var previews: some View {
ActionsView(workout: PreviewData.workout(), showAddToCalendar: true, startWorkoutAction: {})
}
}

View File

@@ -0,0 +1,56 @@
//
// AllEquipmentview.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct AllEquipmentView: View {
@Binding var selectedEquipment: [Equipment]
@State var createWorkoutItemPickerViewModel: CreateWorkoutItemPickerViewModel?
var body: some View {
VStack {
if let _ = DataStore.shared.allEquipment {
Text("Select Equipment")
.foregroundColor(.cyan)
Text("\(selectedEquipment.count) Selected")
}
}
.onTapGesture {
if let equipment = DataStore.shared.allEquipment {
var createWorkoutItemPickerModels = [CreateWorkoutItemPickerModel]()
equipment.forEach({
let model = CreateWorkoutItemPickerModel(id: $0.id,
name: $0.name.lowercased())
createWorkoutItemPickerModels.append(model)
})
createWorkoutItemPickerModels = createWorkoutItemPickerModels.sorted(by: {
$0.name < $1.name
})
let selectedIds = selectedEquipment.map { $0.id }
createWorkoutItemPickerViewModel = CreateWorkoutItemPickerViewModel(allValues: createWorkoutItemPickerModels, selectedIds: selectedIds)
}
}
.sheet(item: $createWorkoutItemPickerViewModel) { item in
CreateWorkoutItemPickerView(viewModel: item, completed: { selectedids in
if let equipment = DataStore.shared.allEquipment {
selectedEquipment.removeAll()
for id in selectedids {
if let equipment = equipment.first(where: {
$0.id == id
}) {
selectedEquipment.append(equipment)
}
}
}
})
}
}
}
//#Preview {
// AllEquipmentview()
//}

View File

@@ -0,0 +1,92 @@
//
// AllExerciseView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
import AVKit
struct AllExerciseView: View {
@Environment(\.dismiss) var dismiss
@State var searchString: String = ""
@Binding var filteredExercises: [Exercise]
var selectedExercise: ((Exercise) -> Void)
@State var avPlayer = AVPlayer(url: URL(string: "https://dev.werkout.fitness/media/exercise_videos/2_Dumbbell_Lateral_Lunges.mp4")!)
@State var videoExercise: Exercise? {
didSet {
if let viddd = self.videoExercise?.videoURL,
let url = URL(string: BaseURLs.currentBaseURL + viddd) {
self.avPlayer = AVPlayer(url: url)
}
}
}
var body: some View {
VStack {
TextField("Filter", text: $searchString)
.padding()
List() {
ForEach(filteredExercises, id: \.self) { exercise in
if searchString.isEmpty || (exercise.name.lowercased().contains(searchString.lowercased()) || (exercise.muscleGroups ?? "").lowercased().contains(searchString.lowercased())) {
HStack {
VStack {
Text(exercise.name)
.frame(maxWidth: .infinity, alignment: .leading)
if exercise.side != nil && !exercise.side!.isEmpty {
Text(exercise.side!)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
if exercise.equipmentRequired != nil && !exercise.equipmentRequired!.isEmpty {
Text(exercise.spacedEquipmentRequired)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
if exercise.muscleGroups != nil && !exercise.muscleGroups!.isEmpty {
Text(exercise.spacedMuscleGroups)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.contentShape(Rectangle())
.onTapGesture {
selectedExercise(exercise)
dismiss()
}
Button(action: {
videoExercise = exercise
}) {
ZStack {
Circle()
.fill(.blue)
.frame(width: 33, height: 33)
Image(systemName: "video.fill")
.frame(width: 33, height: 33)
.foregroundColor(.white )
}
}
.frame(width: 33, height: 33)
}
}
}
}
}
.sheet(item: $videoExercise) { exercise in
PlayerView(player: $avPlayer)
.onAppear{
avPlayer.play()
}
}
}
}
//
//#Preview {
// AllExerciseView()
//}

View File

@@ -0,0 +1,66 @@
//
// AllMusclesView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct AllMusclesView: View {
@Binding var selectedMuscles: [Muscle]
@State var createWorkoutItemPickerViewModel: CreateWorkoutItemPickerViewModel?
var body: some View {
VStack {
if let _ = DataStore.shared.allMuscles {
Text("Select Muscles")
.foregroundColor(.cyan)
Text("\(selectedMuscles.count) Selected")
}
}
.onTapGesture {
if let muscles = DataStore.shared.allMuscles {
var createWorkoutItemPickerModels = [CreateWorkoutItemPickerModel]()
muscles.forEach({
let model = CreateWorkoutItemPickerModel(id: $0.id, name: $0.name.lowercased())
createWorkoutItemPickerModels.append(model)
})
createWorkoutItemPickerModels = createWorkoutItemPickerModels.sorted(by: {
$0.name < $1.name
})
let selectedIds = selectedMuscles.map { $0.id }
createWorkoutItemPickerViewModel = CreateWorkoutItemPickerViewModel(allValues: createWorkoutItemPickerModels, selectedIds: selectedIds)
}
}
.onAppear{
if #function.hasPrefix("__preview") {
DataStore.shared.setupFakeData()
}
guard let _ = DataStore.shared.allExercise,
let muscles = DataStore.shared.allMuscles,
let _ = DataStore.shared.allEquipment else {
return
}
selectedMuscles = muscles
}
.sheet(item: $createWorkoutItemPickerViewModel) { item in
CreateWorkoutItemPickerView(viewModel: item, completed: { selectedids in
if let muscles = DataStore.shared.allMuscles {
selectedMuscles.removeAll()
for id in selectedids {
if let muscle = muscles.first(where: {
$0.id == id
}) {
selectedMuscles.append(muscle)
}
}
}
})
}
}
}
//#Preview {
// AllMusclesView()
//}

View File

@@ -0,0 +1,37 @@
//
// AllWorkoutPickerView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct AllWorkoutPickerView: View {
var mainViews: [MainViewTypes]
@Binding var selectedSegment: MainViewTypes
@StateObject var bridgeModule = BridgeModule.shared
var showCurrentWorkout: (() -> Void)
var body: some View {
HStack {
Picker("", selection: $selectedSegment) {
ForEach(mainViews, id: \.self) { viewType in
Text(viewType.title)
}
}
.pickerStyle(.segmented)
.padding([.top, .leading, .trailing])
if bridgeModule.isInWorkout {
Button(action: {
showCurrentWorkout()
}, label: {
Image(systemName: "figure.strengthtraining.traditional")
.padding(.trailing)
})
.tint(Color("appColor"))
}
}
}
}

View File

@@ -0,0 +1,71 @@
//
// CompletedWorkoutsView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct CompletedWorkoutsView: View {
@State var completedWorkouts: [CompletedWorkout]?
@State var showCompletedWorkouts: Bool = false
var body: some View {
VStack(alignment: .leading) {
if let completedWorkouts = completedWorkouts {
Divider()
Text("Workout History:")
HStack {
Text("Number of workouts:")
Text("\(completedWorkouts.count)")
}
if let lastWorkout = completedWorkouts.last {
HStack {
Text("Last workout:")
Text(lastWorkout.workoutStartTime)
}
Button("View All Workouts", action: {
showCompletedWorkouts = true
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.blue)
.background(.yellow)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
}
} else {
Text("loading completed workouts")
}
}
.onAppear{
fetchCompletedWorkouts()
}
.sheet(isPresented: $showCompletedWorkouts) {
if let completedWorkouts = completedWorkouts {
WorkoutHistoryView(completedWorkouts: completedWorkouts)
}
}
}
func fetchCompletedWorkouts() {
CompletedWorkoutFetchable().fetch(completion: { result in
switch result {
case .success(let model):
completedWorkouts = model
case .failure(let failure):
fatalError(failure.localizedDescription)
}
})
}
}
#Preview {
CompletedWorkoutsView()
}

View File

@@ -0,0 +1,30 @@
//
// CountdownView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct CountdownView: View {
@StateObject var bridgeModule = BridgeModule.shared
var body: some View {
if let duration = bridgeModule.currentExerciseInfo.currentExercise?.duration,
duration > 0 {
HStack {
if bridgeModule.currentExerciseTimeLeft >= 0 && duration > bridgeModule.currentExerciseTimeLeft {
ProgressView(value: Float(bridgeModule.currentExerciseTimeLeft), total: Float(duration))
.tint(Color("appColor"))
}
}
}
}
}
struct CountdownView_Previews: PreviewProvider {
static var previews: some View {
CountdownView()
}
}

View File

@@ -0,0 +1,81 @@
//
// ExtCountdownView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/17/24.
//
import SwiftUI
struct ExtCountdownView: View {
@StateObject var bridgeModule = BridgeModule.shared
var body: some View {
GeometryReader { metrics in
VStack {
if let currenExercise = bridgeModule.currentExerciseInfo.currentExercise {
HStack {
Text(currenExercise.exercise.extName)
.font(.system(size: 200))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
Text("\(Double(bridgeModule.currentWorkoutRunTimeInSeconds).asString(style: .positional))")
.font(Font.system(size: 100))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 100)
}
}
.frame(height: metrics.size.height * 0.5)
HStack {
if let duration = currenExercise.duration,
duration > 0 {
ProgressView(value: Float(bridgeModule.currentExerciseTimeLeft), total: Float(duration))
.scaleEffect(x: 1, y: 6, anchor: .center)
Text("\(bridgeModule.currentExerciseTimeLeft)")
.font(Font.system(size: 100))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding(.leading)
.padding(.trailing, 100)
}
if let reps = currenExercise.reps,
reps > 0 {
Text(" X \(reps)")
.font(Font.system(size: 100))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let weight = currenExercise.weight,
weight > 0 {
Text(" @ \(weight)")
.font(Font.system(size: 100))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding(.trailing, 100)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(height: metrics.size.height * 0.5)
}
}
}
}
}
#Preview {
ExtCountdownView()
}

View File

@@ -0,0 +1,76 @@
//
// ExtExerciseList.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/17/24.
//
import SwiftUI
struct ExtExerciseList: View {
var workout: Workout
var allSupersetExecerciseIndex: Int
var body: some View {
if let allSupersetExecercise = workout.allSupersetExecercise {
ZStack {
ScrollViewReader { proxy in
List() {
ForEach(allSupersetExecercise.indices, id: \.self) { supersetExecerciseIdx in
let supersetExecercise = allSupersetExecercise[supersetExecerciseIdx]
HStack {
if supersetExecerciseIdx == allSupersetExecerciseIndex {
Image(systemName: "figure.run")
.foregroundColor(Color("appColor"))
.font(Font.system(size: 55))
.minimumScaleFactor(0.01)
.lineLimit(1)
}
Text(supersetExecercise.exercise.name)
.font(Font.system(size: 55))
.minimumScaleFactor(0.01)
.lineLimit(3)
.padding()
Spacer()
}
.id(supersetExecerciseIdx)
}
}
.onChange(of: allSupersetExecerciseIndex, perform: { newValue in
withAnimation {
proxy.scrollTo(allSupersetExecerciseIndex, anchor: .top)
}
})
}
VStack {
Text("\(allSupersetExecerciseIndex+1)/\(workout.allSupersetExecercise?.count ?? 0)")
.font(Font.system(size: 55))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.bold()
.foregroundColor(.white)
.background(
Capsule()
.strokeBorder(Color.black, lineWidth: 0.8)
.background(Color(uiColor: UIColor(red: 148/255,
green: 0,
blue: 211/255,
alpha: 0.5)))
.clipped()
)
.clipShape(Capsule())
Spacer()
}
}
}
}
}
//#Preview {
// ExtExerciseList()
//}

View File

@@ -0,0 +1,42 @@
//
// InfoView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/7/23.
//
import SwiftUI
struct InfoView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var workout: Workout
var body: some View {
VStack {
Text(workout.name)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.title3)
.padding()
if let desc = workout.description {
Text(desc)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body)
.padding([.leading, .trailing])
}
if let estimatedTime = workout.estimatedTime {
Text(estimatedTime.asString(style: .abbreviated))
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body)
.padding([.leading, .trailing])
}
}
}
}
struct InfoView_Previews: PreviewProvider {
static var previews: some View {
InfoView(workout: PreviewData.workout())
}
}

View File

@@ -0,0 +1,43 @@
//
// Logoutview.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct Logoutview: View {
@ObservedObject var userStore = UserStore.shared
var body: some View {
HStack {
Button("Logout", action: {
userStore.logout()
})
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 44)
.foregroundColor(.white)
.background(.red)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
Button(action: {
userStore.refreshUserData()
}, label: {
Image(systemName: "arrow.triangle.2.circlepath")
})
.frame(width: 44, height: 44)
.foregroundColor(.white)
.background(.green)
.cornerRadius(8)
.padding()
.frame(maxWidth: .infinity)
}
}
}
#Preview {
Logoutview()
}

View File

@@ -0,0 +1,34 @@
//
// NameView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct NameView: View {
@ObservedObject var userStore = UserStore.shared
var body: some View {
if let registeredUser = userStore.registeredUser {
if let nickName = registeredUser.nickName {
Text(nickName)
.font(.title)
}
HStack {
Text(registeredUser.firstName ?? "-")
Text(registeredUser.lastName ?? "-")
}
if let email = registeredUser.email {
Text(email)
}
}
}
}
#Preview {
NameView()
}

View File

@@ -0,0 +1,18 @@
//
// OvalTextFieldStyle.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/6/23.
//
import SwiftUI
struct OvalTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(10)
.background(LinearGradient(gradient: Gradient(colors: [Color(uiColor: .secondarySystemBackground), Color(uiColor: .secondarySystemBackground)]), startPoint: .topLeading, endPoint: .bottomTrailing))
.cornerRadius(20)
.shadow(color: Color(red: 120/255, green: 120/255, blue: 120/255, opacity: 1), radius: 5)
}
}

View File

@@ -0,0 +1,127 @@
//
// PlayerUIView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/5/23.
//
import SwiftUI
import AVKit
class PlayerUIView: UIView {
// MARK: Class Property
let playerLayer = AVPlayerLayer()
// MARK: Init
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(player: AVPlayer) {
super.init(frame: .zero)
self.playerSetup(player: player)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Life-Cycle
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
// MARK: Class Methods
private func playerSetup(player: AVPlayer) {
playerLayer.player = player
player.actionAtItemEnd = .none
layer.addSublayer(playerLayer)
self.setObserver()
}
func setObserver() {
NotificationCenter.default.removeObserver(self)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: playerLayer.player?.currentItem)
}
@objc func playerItemDidReachEnd(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
self.playerLayer.player?.play()
}
}
}
struct PlayerView: UIViewRepresentable {
@Binding var player: AVPlayer
func makeUIView(context: Context) -> PlayerUIView {
return PlayerUIView(player: player)
}
func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
uiView.playerLayer.player = player
//Add player observer.
uiView.setObserver()
}
}
class VideoURLCreator {
class func otherVideoType(forVideoURL videoURL: URL) -> ThotStyle {
var otherVideoStyle = ThotStyle.never
if videoURL.absoluteString.contains("exercise_videos") {
otherVideoStyle = .always
}
return otherVideoStyle
}
class func videoURL(thotStyle: ThotStyle, gender: String, defaultVideoURLStr: String?, exerciseName: String?, workout: Workout?) -> URL? {
var urlString: String?
if UserStore.shared.registeredUser?.NSFWValue ?? false {
switch thotStyle {
case .always:
urlString = DataStore.shared.randomVideoFor(gender: gender)
case .never:
urlString = defaultVideoURLStr
case .recovery:
if exerciseName?.lowercased() == "recover" {
urlString = DataStore.shared.randomVideoFor(gender: gender)
} else {
urlString = defaultVideoURLStr
}
case .random:
if Bool.random() {
urlString = DataStore.shared.randomVideoFor(gender: gender)
} else {
urlString = defaultVideoURLStr
}
case .off:
return nil
}
} else {
urlString = defaultVideoURLStr
}
if let urlString = urlString,
let url = URL(string: BaseURLs.currentBaseURL + urlString) {
return url
}
return nil
}
}

View File

@@ -0,0 +1,22 @@
//
// ShowNextUpView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct ShowNextUpView: View {
@AppStorage(Constants.extShowNextVideo) private var extShowNextVideo: Bool = false
var body: some View {
Toggle(isOn: $extShowNextVideo, label: {
Text("Show next up video")
})
}
}
#Preview {
ShowNextUpView()
}

View File

@@ -0,0 +1,57 @@
//
// ThosPreferenceView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/16/24.
//
import SwiftUI
struct ThotPreferenceView: View {
@ObservedObject var userStore = UserStore.shared
@AppStorage(Constants.phoneThotStyle) private var phoneThotStyle: ThotStyle = .never
@AppStorage(Constants.extThotStyle) private var extThotStyle: ThotStyle = .never
@AppStorage(Constants.thotGenderOption) private var thotGenderOption: String = "female"
var body: some View {
if userStore.registeredUser?.NSFWValue ?? false {
Group {
Text("Phone THOT Style:")
Picker("Phone THOT Style:", selection: $phoneThotStyle) {
ForEach(ThotStyle.allCases, id: \.self) { style in
Text(style.stringValue())
.tag(phoneThotStyle.rawValue)
}
}
.pickerStyle(.segmented)
Divider()
Text("External THOT Style:")
Picker("External THOT Style:", selection: $extThotStyle) {
ForEach(ThotStyle.allCases, id: \.self) { style in
Text(style.stringValue())
.tag(extThotStyle.rawValue)
}
}
.pickerStyle(.segmented)
if let genderOptions = DataStore.shared.nsfwGenderOptions {
Divider()
Text("Video Gender:")
Picker("Video Gender:", selection: $thotGenderOption) {
ForEach(genderOptions, id: \.self) { option in
Text(option.capitalized)
.tag(option.lowercased())
}
}
.pickerStyle(.segmented)
}
}
}
}
}
#Preview {
ThotPreferenceView()
}

View File

@@ -0,0 +1,33 @@
//
// TitleView.swift
// Werkout_ios
//
// Created by Trey Tartt on 6/17/24.
//
import SwiftUI
struct TitleView: View {
@ObservedObject var bridgeModule = BridgeModule.shared
var body: some View {
HStack {
if let workout = bridgeModule.currentExerciseInfo.workout {
Text(workout.name)
.font(Font.system(size: 100))
.frame(maxWidth: .infinity, alignment: .leading)
}
if bridgeModule.currentWorkoutRunTimeInSeconds > -1 {
Text("\(bridgeModule.currentWorkoutRunTimeInSeconds)")
.font(Font.system(size: 100))
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 100)
}
}
}
}
#Preview {
TitleView()
}

View File

@@ -0,0 +1,71 @@
//
// WorkoutOverviewView.swift
// Werkout_ios
//
// Created by Trey Tartt on 7/9/23.
//
import SwiftUI
struct WorkoutOverviewView: View {
let workout: Workout
var body: some View {
VStack {
HStack {
VStack {
Text(workout.name)
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
Text(workout.description ?? "")
.frame(maxWidth: .infinity, alignment: .leading)
if let estimatedTime = workout.estimatedTime {
Text("Time: " + estimatedTime.asString(style: .abbreviated))
.frame(maxWidth: .infinity, alignment: .leading)
}
if let createdAt = workout.createdAt {
Text(createdAt, style: .date)
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
if let exerciseCount = workout.exercise_count {
VStack {
Text("\(exerciseCount)")
.font(.body.bold())
Text("exercises")
.font(.footnote)
.foregroundColor(Color(uiColor: .systemGray2))
}
}
}
if let muscles = workout.muscles,
muscles.joined(separator: ", ").count > 0{
Divider()
Text(muscles.joined(separator: ", "))
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let equipment = workout.equipment,
equipment.joined(separator: ", ").count > 0 {
Divider()
Text(equipment.joined(separator: ", "))
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding()
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(15)
}
}
struct WorkoutOverviewView_Previews: PreviewProvider {
static var previews: some View {
WorkoutOverviewView(workout: PreviewData.allWorkouts()[2])
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "logo_2.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,25 @@
//
// ContentView.swift
// Werkout_watch Watch App
//
// Created by Trey Tartt on 6/22/23.
//
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
MainWatchView()
WatchControlView()
}
.tabViewStyle(PageTabViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

View File

@@ -0,0 +1,75 @@
//
// MainView.swift
// Werkout_watch Watch App
//
// Created by Trey Tartt on 7/8/23.
//
import SwiftUI
struct MainWatchView: View {
@StateObject var vm = WatchMainViewModel.shared
var body: some View {
VStack {
HStack {
if vm.isInWorkout {
Text(vm.watchPackageModel.currentExerciseName)
.font(.body)
.foregroundColor(.white)
.lineLimit(10)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
Divider()
Text("\(vm.watchPackageModel.currentTimeLeft )")
.font(.title)
.foregroundColor(.white)
.lineLimit(10)
.fixedSize(horizontal: false, vertical: true)
}
}
HStack {
if let heartValue = vm.heartValue {
VStack {
Image(systemName: "heart.fill")
.font(Font.system(size: 22))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.foregroundColor(.red)
Text("\(heartValue)")
.font(Font.system(size: 55))
.scaledToFit()
.minimumScaleFactor(0.01)
.lineLimit(1)
.foregroundColor(.red)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
if vm.isInWorkout {
Button(action: {
vm.nextExercise()
}, label: {
Image(systemName: "arrow.forward")
.font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.buttonStyle(BorderedButtonStyle(tint: .green))
} else {
VStack {
Text("No Werkout")
Text("🍑")
}
}
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainWatchView()
}
}

Some files were not shown because too many files have changed in this diff Show More