├── CoachUI
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── views
│ ├── rows
│ │ └── ExerciseRow.swift
│ ├── WorkoutsView.swift
│ ├── WorkoutProgressView.swift
│ ├── WorkoutDetailView.swift
│ ├── WorkoutCreatorView.swift
│ ├── WorkoutResultView.swift
│ └── WorkoutTimer.swift
├── launch
│ ├── AppDelegate.swift
│ └── SceneDelegate.swift
├── models
│ ├── Exercise.swift
│ ├── Workout.swift
│ └── WorkoutsStore.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
└── Info.plist
├── CoachUI.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── TeacherNote.md
└── .gitignore
/CoachUI/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/CoachUI/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/CoachUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CoachUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TeacherNote.md:
--------------------------------------------------------------------------------
1 | # Bugs
2 | * The favorite feature is not working.
3 | * The workout creation form is not dismissed on save or cancel button.
4 | * The workout lists is not refreshed when I delete a workout.
5 | * On iPad the workout creation form is displayed as a two column nav controller.
6 | * I can create a workout without a name and exercies.
7 |
8 | # Feature requests
9 | * I would like to edit my workout.
10 | * The progress timer could be a bit more fancier
11 | * Can we get rid of the Timer and use a more functionnal approach?
12 | * The workout graph don't have labels for axis and capsules.
13 | * It woule be cool to save an history of done workout so the graph don't show random workouts.
--------------------------------------------------------------------------------
/CoachUI/views/rows/ExerciseRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExerciseRow.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ExerciseRow: View {
12 | let exercise: Exercise
13 |
14 | var body: some View {
15 | HStack {
16 | Text(exercise.type.rawValue.capitalized)
17 | Spacer()
18 | Text("\(exercise.amount)").foregroundColor(.secondary)
19 | }
20 | }
21 | }
22 |
23 | struct ExerciseRow_Previews: PreviewProvider {
24 | static var previews: some View {
25 | ExerciseRow(exercise: Exercise(type: .pushups, amount: 10))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/CoachUI/launch/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | return true
15 | }
16 |
17 | // MARK: UISceneSession Lifecycle
18 |
19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/CoachUI/launch/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | let workouts = WorkoutsStore()
18 | let contentView = WorkoutsView().environmentObject(workouts)
19 |
20 | if let windowScene = scene as? UIWindowScene {
21 | let window = UIWindow(windowScene: windowScene)
22 | window.rootViewController = UIHostingController(rootView: contentView)
23 | self.window = window
24 | window.makeKeyAndVisible()
25 | }
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/CoachUI/models/Exercise.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exercise.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum ExerciseType: String, CaseIterable, Codable {
12 | case pushups, squats, lunges, deadlifts
13 |
14 | func duration() -> Int {
15 | switch self {
16 | case .pushups: return 3
17 | case .squats, .lunges: return 4
18 | case .deadlifts: return 5
19 | }
20 | }
21 |
22 | var name : String {
23 | switch self {
24 | case .pushups:
25 | return "Pushups"
26 | case .squats:
27 | return "Squats"
28 | case .lunges:
29 | return "Lunges"
30 | case .deadlifts:
31 | return "Deadlifts"
32 | }
33 | }
34 | }
35 |
36 | struct Exercise: Hashable, Equatable, Codable {
37 | let id = UUID().uuidString
38 |
39 | let type: ExerciseType
40 | let amount: Int
41 | var duration: Double {
42 | Double(type.duration() * amount)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/CoachUI/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 | .DS_Store
70 |
--------------------------------------------------------------------------------
/CoachUI/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/CoachUI/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/CoachUI/views/WorkoutsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkoutsView.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WorkoutsView: View {
12 | @EnvironmentObject var store: WorkoutsStore
13 | @State private var isWorkoutFormPresented = false
14 |
15 | private var addWorkoutButton: some View {
16 | Button(action: {
17 | self.isWorkoutFormPresented = true
18 | }, label: {
19 | Image(systemName: "plus.circle").imageScale(.large)
20 | })
21 | }
22 |
23 | private func workoutContextMenu(workout: Workout) -> some View {
24 | VStack {
25 | Button(action: {
26 | self.store.toggleFavorite(workout: workout)
27 | }, label: {
28 | HStack {
29 | Image(systemName: store.favorites.contains(workout.id) ? "star.fill" : "star")
30 | Text(store.favorites.contains(workout.id) ? "Remove from favorite" : "Add to favorite")
31 | }
32 | })
33 | }
34 | }
35 |
36 | private func workoutRow(workout: Workout) -> some View {
37 | NavigationLink(destination: WorkoutDetailView(workout: workout)) {
38 | Text(workout.name)
39 | .contextMenu(menuItems: { self.workoutContextMenu(workout: workout) })
40 | }
41 | }
42 |
43 | var body: some View {
44 | NavigationView {
45 | List {
46 | Section(header: Text("Favorites")) {
47 | ForEach(store.workouts.filter{ store.favorites.contains($0.id) }) { workout in
48 | self.workoutRow(workout: workout)
49 | }
50 | }
51 | Section(header: Text("All")) {
52 | ForEach(store.workouts) { workout in
53 | self.workoutRow(workout: workout)
54 | }.onDelete { indexes in
55 | self.store.removeWorkout(at: indexes.first!)
56 | }
57 | }
58 | }
59 | .navigationBarTitle("Workouts")
60 | .navigationBarItems(trailing: addWorkoutButton)
61 | .sheet(isPresented: $isWorkoutFormPresented,
62 | content: { WorkoutCreatorView().environmentObject(self.store) })
63 | }
64 | }
65 | }
66 |
67 | struct WorkoutsView_Previews: PreviewProvider {
68 | static var previews: some View {
69 | WorkoutsView().environmentObject(WorkoutsStore())
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/CoachUI/views/WorkoutProgressView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkoutProgressView.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 04/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WorkoutProgressView: View {
12 | @ObservedObject var workout: Workout
13 |
14 | @State private var isStarted = false
15 | @State private var currentExercice: Exercise!
16 | @State private var showResultView: Bool = false {
17 | willSet {
18 | if newValue {
19 | // done, so save the new session
20 | Workout.saveWorkout(self.workout)
21 | }
22 | }
23 | }
24 |
25 | private var buttonsView: some View {
26 | HStack(alignment: .center, spacing: 32) {
27 | Button(action: {
28 | if let exercice = self.workout.previousExercise(exercice: self.currentExercice) {
29 | self.currentExercice = exercice
30 | }
31 | }, label: {
32 | Image(systemName: "backward")
33 | .font(.title)
34 | })
35 | Button(action: {
36 | self.isStarted.toggle()
37 | }, label: {
38 | Image(systemName: self.isStarted ? "pause.circle" : "play.circle")
39 | .font(.title)
40 | })
41 | Button(action: {
42 | if let exercice = self.workout.nextExercise(exercice: self.currentExercice) {
43 | self.currentExercice = exercice
44 | }
45 | }, label: {
46 | Image(systemName: "forward")
47 | .font(.title)
48 | })
49 | }
50 | }
51 |
52 | var body: some View {
53 | VStack(alignment: .center) {
54 | if self.currentExercice != nil {
55 | // exerciseView
56 | WorkoutTimer(workout: workout,
57 | exercise: $currentExercice,
58 | isStarted: $isStarted,
59 | showResultView: $showResultView)
60 | .frame(height: 300)
61 |
62 | Rectangle()
63 | .frame(height: 2)
64 | .background(Color.secondary)
65 | buttonsView
66 | }
67 | }.onAppear {
68 | self.currentExercice = self.workout.exercises.first
69 | }.sheet(isPresented: $showResultView, content: { WorkoutResultView(workout: self.workout) })
70 | }
71 | }
72 |
73 | struct WorkoutProgressView_Previews: PreviewProvider {
74 | static var previews: some View {
75 | WorkoutProgressView(workout: WorkoutsStore().workouts.first!)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/CoachUI/views/WorkoutDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkoutDetailView.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WorkoutDetailView: View {
12 | @EnvironmentObject var store: WorkoutsStore
13 | @ObservedObject var workout: Workout
14 | @State private var isWorkoutFormPresented = false
15 |
16 | private var favButton: some View {
17 | Button(action: {
18 | self.store.toggleFavorite(workout: self.workout)
19 | }, label: {
20 | Image(systemName: store.favorites.contains(workout.id) ? "star.fill" : "star")
21 | .imageScale(.large)
22 | .foregroundColor(.yellow)
23 | })
24 | }
25 |
26 | private var deleteButton: some View {
27 | Button(action: {
28 | if let index = self.store.workouts.firstIndex(of: self.workout) {
29 | self.store.removeWorkout(at: index)
30 | }
31 | }, label: {
32 | Image(systemName: "trash")
33 | .imageScale(.large)
34 | .foregroundColor(.red)
35 | })
36 | }
37 |
38 |
39 | private var editButton: some View {
40 | Button(action: {
41 | self.isWorkoutFormPresented = true
42 | }, label: {
43 | Text("Edit").foregroundColor(.blue)
44 | })
45 | }
46 |
47 | var body: some View {
48 | List {
49 | Section(header: Text("Exercices")) {
50 | ForEach(workout.exercises, id: \.self) { exercise in
51 | ExerciseRow(exercise: exercise)
52 | }
53 | }
54 |
55 | Section(header: Text("Details")) {
56 | Text("Workout duration: \(String(format: "%.f", workout.duration)) seconds")
57 | Text("Pause between exercises: 30 seconds")
58 | NavigationLink(destination: WorkoutProgressView(workout: workout)) {
59 | Text("Start")
60 | .foregroundColor(.green)
61 | }
62 | }
63 | }
64 | .navigationBarItems(trailing:
65 | HStack {
66 | favButton.padding()
67 | deleteButton
68 | editButton
69 | })
70 | .listStyle(GroupedListStyle())
71 | .navigationBarTitle(workout.name)
72 | .sheet(isPresented: $isWorkoutFormPresented,
73 | content: { WorkoutCreatorView(editingWorkout: self.workout).environmentObject(self.store)
74 |
75 | })
76 | }
77 | }
78 |
79 | struct WorkoutDetailView_Previews: PreviewProvider {
80 | static var previews: some View {
81 | WorkoutDetailView(workout: WorkoutsStore().workouts.first!)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/CoachUI/models/Workout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Workout.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct HistoryItem : Codable, Identifiable {
12 | let id = UUID().uuidString
13 |
14 | let date: Date
15 | let exercise : Exercise
16 | }
17 |
18 | class Workout: ObservableObject, Identifiable, Equatable {
19 | let id = UUID().uuidString
20 |
21 | @Published var name: String
22 | @Published var exercises: [Exercise] = []
23 |
24 | static var history : [HistoryItem] {
25 | get {
26 | if let saved = UserDefaults.standard.data(forKey: "history"), let extracted = try? JSONDecoder().decode([HistoryItem].self, from: saved) {
27 | return extracted
28 | } else {
29 | // build a fake one
30 | var result = [HistoryItem]()
31 | for i in 0..<7 {
32 | let store = WorkoutsStore.sharedStore
33 | let date = Calendar.autoupdatingCurrent.date(bySettingHour: 12, minute: 0, second: 0, of: Date(timeIntervalSinceNow: -(7-Double(i))*24*3600)) ?? Date()
34 | for exo in store?.workouts.randomElement()?.exercises ?? [] {
35 | result.append(HistoryItem(date: date, exercise: exo))
36 | }
37 | }
38 |
39 | return result
40 | }
41 | }
42 | }
43 |
44 | static func saveWorkout(_ workout: Workout) {
45 | var pastsave = history
46 | let calendar = NSCalendar.autoupdatingCurrent
47 | let midday = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: Date()) ?? Date()
48 | for exo in workout.exercises {
49 | pastsave.append(HistoryItem(date:midday, exercise: exo))
50 | }
51 |
52 | if let data = try? JSONEncoder().encode(pastsave) {
53 | UserDefaults.standard.set(data, forKey: "history")
54 | UserDefaults.standard.synchronize()
55 | }
56 | }
57 |
58 | var duration: Double {
59 | exercises.map{ $0.duration }.reduce(0, +)
60 | }
61 |
62 | init(name: String) {
63 | self.name = name
64 | }
65 |
66 | func addExercise(exercise: Exercise) {
67 | exercises.append(exercise)
68 | }
69 |
70 | func removeExercice(at index: Int) {
71 | exercises.remove(at: index)
72 | }
73 |
74 | func previousExercise(exercice: Exercise) -> Exercise? {
75 | if let index = exercises.firstIndex(of: exercice),
76 | index > 0 {
77 | return exercises[index - 1]
78 | }
79 | return nil
80 | }
81 |
82 | func nextExercise(exercice: Exercise) -> Exercise? {
83 | if let index = exercises.firstIndex(of: exercice),
84 | index < exercises.count - 1 {
85 | return exercises[index + 1]
86 | }
87 | return nil
88 | }
89 |
90 | static func == (lhs: Workout, rhs: Workout) -> Bool {
91 | lhs.id == rhs.id
92 | }
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/CoachUI/models/WorkoutsStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Workouts.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | class WorkoutsStore: ObservableObject {
13 | static var sharedStore : WorkoutsStore!
14 |
15 | @Published var workouts: [Workout] = []
16 | @Published var favorites: [String] = []
17 |
18 | init() {
19 | workouts.append(contentsOf: buildDefaultWorkouts())
20 | favorites.append(workouts.first!.id)
21 | favorites.append(workouts.last!.id)
22 |
23 | WorkoutsStore.sharedStore = self
24 | }
25 |
26 | func insertWorkout(workout: Workout) {
27 | workouts.append(workout)
28 | }
29 |
30 | func editWorkout(workout: Workout) {
31 |
32 | }
33 |
34 | func removeWorkout(at index: Int) {
35 | let workout = workouts.remove(at: index)
36 | favorites.removeAll{ $0 == workout.id }
37 | }
38 |
39 | func toggleFavorite(workout: Workout) {
40 | if favorites.contains(workout.id) {
41 | favorites.removeAll{ workout.id == $0 }
42 | } else {
43 | favorites.append(workout.id)
44 | }
45 | }
46 |
47 |
48 | private func buildDefaultWorkouts() -> [Workout] {
49 | let workout1 = Workout(name: "10 minutes Abs")
50 | workout1.addExercise(exercise: Exercise(type: .pushups, amount: 20))
51 | workout1.addExercise(exercise: Exercise(type: .squats, amount: 20))
52 | workout1.addExercise(exercise: Exercise(type: .pushups, amount: 30))
53 | workout1.addExercise(exercise: Exercise(type: .pushups, amount: 20))
54 | workout1.addExercise(exercise: Exercise(type: .squats, amount: 20))
55 | workout1.addExercise(exercise: Exercise(type: .pushups, amount: 30))
56 |
57 | let workout2 = Workout(name: "Back & Chest routine")
58 | workout2.addExercise(exercise: Exercise(type: .lunges, amount: 20))
59 | workout2.addExercise(exercise: Exercise(type: .pushups, amount: 20))
60 | workout2.addExercise(exercise: Exercise(type: .lunges, amount: 30))
61 | workout2.addExercise(exercise: Exercise(type: .lunges, amount: 20))
62 | workout2.addExercise(exercise: Exercise(type: .pushups, amount: 20))
63 | workout2.addExercise(exercise: Exercise(type: .lunges, amount: 30))
64 |
65 | let workout3 = Workout(name: "Easy morning")
66 | workout3.addExercise(exercise: Exercise(type: .lunges, amount: 20))
67 | workout3.addExercise(exercise: Exercise(type: .pushups, amount: 20))
68 | workout3.addExercise(exercise: Exercise(type: .lunges, amount: 30))
69 |
70 | let workout4 = Workout(name: "Debug workout")
71 | workout4.addExercise(exercise: Exercise(type: .lunges, amount: 2))
72 | workout4.addExercise(exercise: Exercise(type: .pushups, amount: 2))
73 |
74 | let workout5 = Workout(name: "Forces of Nature")
75 | workout5.addExercise(exercise: Exercise(type: .lunges, amount: 20))
76 | workout5.addExercise(exercise: Exercise(type: .pushups, amount: 20))
77 | workout5.addExercise(exercise: Exercise(type: .lunges, amount: 30))
78 |
79 | return [workout1, workout2, workout3, workout4, workout5]
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/CoachUI/views/WorkoutCreatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkoutCreatorView.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 02/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WorkoutCreatorView: View {
12 |
13 | //MARK: - Properties
14 | @EnvironmentObject var store: WorkoutsStore
15 | @Environment(\.presentationMode) var presentationMode
16 |
17 | var editingWorkout: Workout?
18 |
19 | @State private var workoutName = ""
20 | @State private var exercises: [Exercise] = []
21 | @State private var inInsertMode = false
22 | @State private var selectedExercice: ExerciseType = .pushups
23 | @State private var selectedAmount: Double = 10
24 |
25 | // MARK: - nav buttons
26 | private var saveButton: some View {
27 | Button(action: {
28 | if let editing = self.editingWorkout {
29 | self.editingWorkout?.name = self.workoutName
30 | self.editingWorkout?.exercises = self.exercises
31 | } else {
32 | let workout = Workout(name: self.workoutName)
33 | for exercise in self.exercises {
34 | workout.addExercise(exercise: exercise)
35 | }
36 | self.store.insertWorkout(workout: workout)
37 | }
38 | self.presentationMode.wrappedValue.dismiss()
39 | }, label: {
40 | Text("Save")
41 | })
42 | }
43 |
44 | private var cancelButton: some View {
45 | Button(action: {
46 | self.presentationMode.wrappedValue.dismiss()
47 | }, label: {
48 | Text("Cancel")
49 | .foregroundColor(.red)
50 | })
51 | }
52 |
53 | // MARK: - Sections views
54 | private var basicInfoSection: some View {
55 | Section(header: Text("Basic infos")) {
56 | HStack {
57 | Text("Workout name:")
58 | TextField("My cool workout", text: $workoutName)
59 | }
60 | }
61 | }
62 |
63 | private var exercicesSection: some View {
64 | Section(header: Text("Exercises")) {
65 | ForEach(exercises, id: \.self) { exercise in
66 | ExerciseRow(exercise: exercise)
67 | }.onDelete { indexes in
68 | self.exercises.remove(at: indexes.first!)
69 | }
70 | }
71 | }
72 |
73 | private var addExerciseSection: some View {
74 | Section {
75 | if inInsertMode {
76 | Text("\(Int(selectedAmount)) \(selectedExercice.rawValue.capitalized)")
77 | Picker(selection: $selectedExercice, label: Text("")) {
78 | ForEach(ExerciseType.allCases, id: \.self) { exerciseType in
79 | Text(exerciseType.rawValue.capitalized)
80 | }
81 | }.pickerStyle(WheelPickerStyle())
82 | Slider(value: $selectedAmount, in: 1...50, step: 1)
83 | Button(action: {
84 | self.inInsertMode = false
85 | self.exercises.append(Exercise(type: self.selectedExercice, amount: Int(self.selectedAmount)))
86 | }, label: {
87 | Text("Add this exercise")
88 | })
89 | } else {
90 | Button(action: {
91 | self.inInsertMode = true
92 | }, label: {
93 | Text("Add exercise")
94 | })
95 | }
96 | }
97 | }
98 |
99 | // MARK: - Body
100 | var body: some View {
101 | NavigationView {
102 | Form {
103 | basicInfoSection
104 | if exercises.count > 0 {
105 | exercicesSection
106 | }
107 | addExerciseSection
108 | }
109 | .onAppear{
110 | if let editing = self.editingWorkout {
111 | self.workoutName = editing.name
112 | self.exercises = editing.exercises
113 | }
114 | }
115 | .navigationBarTitle("Create a new workout",
116 | displayMode: .inline)
117 | .navigationBarItems(leading: cancelButton, trailing: saveButton)
118 |
119 | }.navigationViewStyle(StackNavigationViewStyle())
120 | }
121 | }
122 |
123 | struct WorkoutCreatorView_Previews: PreviewProvider {
124 | static var previews: some View {
125 | WorkoutCreatorView()
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/CoachUI/views/WorkoutResultView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkoutResultView.swift
3 | // CoachUI
4 | //
5 | // Created by Thomas Ricouard on 09/09/2019.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | func rangeOfRanges(_ ranges: C) -> Range
12 | where C.Element == Range {
13 | guard !ranges.isEmpty else { return 0..<0 }
14 | let low = ranges.lazy.map { $0.lowerBound }.min()!
15 | let high = ranges.lazy.map { $0.upperBound }.max()!
16 | return low..) -> Double {
20 | return range.upperBound - range.lowerBound
21 | }
22 |
23 | struct GraphCapsule: View {
24 | var index: Int
25 | var height: CGFloat
26 | var range: Range
27 | var overallRange: Range
28 |
29 | var heightRatio: CGFloat {
30 | max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
31 | }
32 |
33 | var offsetRatio: CGFloat {
34 | CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
35 | }
36 |
37 | var animation: Animation {
38 | Animation.spring(dampingFraction: 0.5)
39 | .speed(2)
40 | .delay(0.03 * Double(index))
41 | }
42 |
43 | var body: some View {
44 | Capsule()
45 | .fill(Color.white)
46 | .frame(height: height * heightRatio, alignment: .bottom)
47 | .offset(x: 0, y: height * -offsetRatio)
48 | .animation(animation)
49 | }
50 | }
51 |
52 | enum GraphType {
53 | case duration
54 | case count
55 | }
56 |
57 | struct ToggleTextButton: View {
58 | @Binding var isOn: Bool
59 | @Binding var type : GraphType
60 | var body: some View {
61 | Button(
62 | action: {
63 | self.isOn.toggle()
64 | self.type = self.isOn ? .duration : .count
65 | },
66 | label: { Text(self.isOn ? "Duration" : "Count") }
67 | )
68 | }
69 | }
70 |
71 | // from https://stackoverflow.com/questions/24051314/precision-string-format-specifier-in-swift
72 | extension Double {
73 | func format(_ f: String) -> String {
74 | return String(format: "%\(f)f", self)
75 | }
76 | }
77 |
78 | struct WorkoutResultView: View {
79 | @ObservedObject var workout: Workout
80 | @State var type : GraphType = .duration
81 | @State var toggle : Bool = true
82 |
83 | var color : Color {
84 | switch type {
85 | case .duration:
86 | return Color.orange
87 | case .count:
88 | return Color.purple
89 | @unknown default:
90 | break
91 | }
92 | }
93 |
94 | struct GraphElement : Identifiable {
95 | let id = UUID().uuidString
96 | let index : Int
97 | let count : Double
98 | let duration : Double
99 | }
100 | var stats : [GraphElement] {
101 | var result = [GraphElement]()
102 | var cursor = 0
103 | var dateCursor = Calendar.autoupdatingCurrent.date(bySettingHour: 12, minute: 0, second: 0, of: Date(timeIntervalSinceNow: -7*24*3600)) ?? Date()
104 | let now = Date()
105 |
106 | while dateCursor < now {
107 | let filtered = Workout.history.filter { $0.date == dateCursor }
108 | let counts = filtered.map( { $0.exercise.amount} ).reduce(0,+)
109 | let durations = filtered.map( { $0.exercise.duration} ).reduce(0,+)
110 | let ge = GraphElement(index: cursor, count: Double(counts), duration: durations)
111 |
112 | result.append(ge)
113 | cursor += 1
114 | dateCursor = dateCursor.addingTimeInterval(24*3600)
115 | }
116 |
117 | return result
118 | }
119 |
120 | @Environment(\.presentationMode) var presentationMode
121 | var body: some View {
122 | // last 7 days
123 | let cStats = stats
124 | let overallRange = rangeOfRanges(cStats.map { (self.type == .duration ? Range(uncheckedBounds: (0.0, $0.duration)) : Range(uncheckedBounds: (0.0, $0.count)) ) } )
125 | let maxMagnitude = overallRange.upperBound
126 | let heightRatio = 1 - CGFloat(maxMagnitude / magnitude(of: overallRange))
127 | return
128 | VStack {
129 | ToggleTextButton(isOn: $toggle, type: $type)
130 | GeometryReader { geom in
131 | ZStack {
132 | HStack(alignment: .bottom, spacing: geom.size.width / 120) {
133 | ForEach(cStats) { datum in
134 | ZStack {
135 | GraphCapsule(index: datum.index,
136 | height: geom.size.height,
137 | range: Range(uncheckedBounds: (0, self.type == .duration ? datum.duration : datum.count)),
138 | overallRange: overallRange
139 | ).colorMultiply(self.color)
140 | Text("\(self.type == .duration ? datum.duration.format(".0") : datum.count.format(".0"))").rotationEffect(Angle(degrees: -90))
141 | }
142 | }.offset(x: 0, y: geom.size.height * heightRatio)
143 | }.padding()
144 | }
145 | }
146 | if self.type == .duration {
147 | Text("Total duration in s")
148 | } else {
149 | Text("Total count")
150 | }
151 | Button(action: {
152 | self.presentationMode.wrappedValue.dismiss()
153 | }) {
154 | Text("Close")
155 | }
156 | }
157 | .frame(height: 250)
158 | }
159 | }
160 |
161 | struct WorkoutResultView_Previews: PreviewProvider {
162 | static var previews: some View {
163 | WorkoutResultView(workout: WorkoutsStore().workouts.first!)
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/CoachUI/views/WorkoutTimer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkoutTimer.swift
3 | // CoachUI
4 | //
5 | // Created by Nicolas Zinovieff on 9/5/19.
6 | // Copyright © 2019 Thomas Ricouard. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension Double {
12 | public static func ~= (lhs: Double, rhs: Double) -> Bool {
13 | let epsilon = 0.05 // roughly equal
14 | return abs(lhs-rhs) < epsilon
15 | }
16 | }
17 |
18 | struct Hand : Identifiable, Hashable {
19 | let id = UUID().uuidString
20 |
21 | let i : Int
22 | }
23 | struct WorkoutTimer: View {
24 | @ObservedObject var workout: Workout
25 |
26 | @Binding var exercise: Exercise?
27 | @Binding var isStarted: Bool
28 | @Binding var showResultView: Bool
29 |
30 | @State private var progress : Double = 0
31 | @State private var timerTick: Timer?
32 |
33 | private let hands : [Hand] = [Hand(i:1),Hand(i:2),Hand(i:3),Hand(i:4),Hand(i:5),Hand(i:6),Hand(i:7),Hand(i:8),Hand(i:9),Hand(i:10),Hand(i:11),Hand(i:12)]
34 |
35 | private func startExercice() {
36 | if let exercice = exercise {
37 | self.progress = 0
38 | self.isStarted = true
39 | self.timerTick = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer) in
40 | if self.progress < 1 {
41 | self.progress += 0.2 / (self.exercise?.duration ?? 0.0001)
42 | } else {
43 | self.isStarted = false
44 | self.timerTick?.invalidate()
45 |
46 | if let next = self.workout.nextExercise(exercice: exercice) {
47 | self.exercise = next
48 | self.startExercice()
49 | } else {
50 | self.showResultView = true
51 | }
52 | }
53 | })
54 | }
55 | }
56 |
57 | static let defaultClockSize : CGFloat = 140.0
58 | private func handPosition(hour : Int, clockSize: CGFloat = defaultClockSize) -> CGSize {
59 | let angle : CGFloat = CGFloat(hour)*CGFloat.pi*2/12.0 - (CGFloat.pi / 2)
60 | let posX = cos(angle)*clockSize
61 | let posY = sin(angle)*clockSize
62 | return CGSize(width: posX, height: posY)
63 | }
64 |
65 | var body: some View {
66 | ZStack {
67 | // the middle will animate, the flames won't
68 | ZStack {
69 | // Gradient
70 | GeometryReader { geometry in
71 | Path { (path) in
72 | let size: CGFloat = min(geometry.size.width, geometry.size.height) * 0.7
73 | let center = CGPoint(x: geometry.size.width*0.5, y: geometry.size.height*0.5)
74 |
75 | path.addEllipse(in: CGRect(x: center.x-size*0.5, y: center.y-size*0.5, width: size, height: size))
76 |
77 | }.fill(LinearGradient(
78 | gradient: .init(colors: [Color.gray, Color.gray.opacity(0.7)]),
79 | startPoint: .init(x: 0.5, y: 0),
80 | endPoint: .init(x: 0.5, y: 0.6)
81 | ))
82 | }
83 | // Border
84 | GeometryReader { geometry in
85 | Path { (path) in
86 | let size: CGFloat = min(geometry.size.width, geometry.size.height) * 0.7
87 | let center = CGPoint(x: geometry.size.width*0.5, y: geometry.size.height*0.5)
88 |
89 | path.addEllipse(in: CGRect(x: center.x-size*0.5, y: center.y-size*0.5, width: size, height: size))
90 |
91 | }.stroke(lineWidth: 10).foregroundColor(Color.gray)
92 | }
93 | // Progress
94 | GeometryReader { geometry in
95 | Path { (path) in
96 | let size: CGFloat = min(geometry.size.width, geometry.size.height) * 0.7
97 | let center = CGPoint(x: geometry.size.width*0.5, y: geometry.size.height*0.5)
98 |
99 | path.addArc(center: center, radius: size*0.5, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: (360*self.progress)-90), clockwise: false)
100 |
101 | }
102 | .stroke(lineWidth: 10)
103 | .foregroundColor(Color.green)
104 | .animation(self.isStarted ? .easeInOut : nil)
105 | }
106 |
107 | VStack {
108 | Text(exercise?.type.name ?? "")
109 | .font(.largeTitle)
110 | .fontWeight(.semibold)
111 | .foregroundColor(Color.yellow)
112 | .animation(nil)
113 | Button(action: {
114 | if !self.isStarted {
115 | self.startExercice()
116 | } else {
117 | self.isStarted = false
118 | self.timerTick?.invalidate()
119 | self.progress = 0
120 | }
121 | }) {
122 | Text("GO!",tableName: nil,bundle: nil, comment: nil)
123 | .font(.title)
124 | .fontWeight(.bold)
125 | .foregroundColor(Color.yellow)
126 | .background(RoundedRectangle(cornerRadius: 4).padding(-4).foregroundColor(Color.gray))
127 | }.padding(2)
128 |
129 | }
130 | }.onDisappear {
131 | self.timerTick?.invalidate()
132 | self.timerTick = nil
133 | }
134 | .scaleEffect(isStarted ? 1.1 : 1.0)
135 | .animation(Animation.easeInOut.repeatCount(isStarted ? .max : 0))
136 |
137 | // Saint Seiya - like / tower clock with flames to be extinguished
138 | // No for loop, unfortunately
139 | // This is a bug. There is a maximum of items in a view/stack
140 | ZStack {
141 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
142 | .offset(self.handPosition(hour: 1))
143 | .opacity((self.progress * 12.0 < 1.0) ? 1.0 : 0.0)
144 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
145 | .offset(self.handPosition(hour: 2))
146 | .opacity((self.progress * 12.0 < 2.0) ? 1.0 : 0.0)
147 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
148 | .offset(self.handPosition(hour: 3))
149 | .opacity((self.progress * 12.0 < 3.0) ? 1.0 : 0.0)
150 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
151 | .offset(self.handPosition(hour: 4))
152 | .opacity((self.progress * 12.0 < 4.0) ? 1.0 : 0.0)
153 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
154 | .offset(self.handPosition(hour: 5))
155 | .opacity((self.progress * 12.0 < 5.0) ? 1.0 : 0.0)
156 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
157 | .offset(self.handPosition(hour: 6))
158 | .opacity((self.progress * 12.0 < 6.0) ? 1.0 : 0.0)
159 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
160 | .offset(self.handPosition(hour: 7))
161 | .opacity((self.progress * 12.0 < 7.0) ? 1.0 : 0.0)
162 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
163 | .offset(self.handPosition(hour: 8))
164 | .opacity((self.progress * 12.0 < 8.0) ? 1.0 : 0.0)
165 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
166 | .offset(self.handPosition(hour: 9))
167 | .opacity((self.progress * 12.0 < 9.0) ? 1.0 : 0.0)
168 | }
169 |
170 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
171 | .offset(self.handPosition(hour: 10))
172 | .opacity((self.progress * 12.0 < 10.0) ? 1.0 : 0.0)
173 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
174 | .offset(self.handPosition(hour: 11))
175 | .opacity((self.progress * 12.0 < 11.0) ? 1.0 : 0.0)
176 | Image(systemName: "flame.fill").foregroundColor(Color.blue)
177 | .offset(self.handPosition(hour: 12))
178 | .opacity((self.progress * 12.0 < 12.0) ? 1.0 : 0.0)
179 | }
180 | }
181 | }
182 |
183 | struct WorkoutTimer_Previews: PreviewProvider {
184 | static var previews: some View {
185 | WorkoutTimer(workout: WorkoutsStore().workouts.first!,
186 | exercise: .constant(Exercise(type: .pushups, amount: 20)),
187 | isStarted: .constant(false),
188 | showResultView: .constant(false))
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/CoachUI.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2B0CB56F2320F45D00CF8B52 /* WorkoutTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B0CB56E2320F45D00CF8B52 /* WorkoutTimer.swift */; };
11 | 69166E65231CF60800A5F192 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E64231CF60800A5F192 /* AppDelegate.swift */; };
12 | 69166E67231CF60800A5F192 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E66231CF60800A5F192 /* SceneDelegate.swift */; };
13 | 69166E6B231CF60B00A5F192 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69166E6A231CF60B00A5F192 /* Assets.xcassets */; };
14 | 69166E6E231CF60B00A5F192 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69166E6D231CF60B00A5F192 /* Preview Assets.xcassets */; };
15 | 69166E71231CF60B00A5F192 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 69166E6F231CF60B00A5F192 /* LaunchScreen.storyboard */; };
16 | 69166E7F231D1D9300A5F192 /* WorkoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E7E231D1D9300A5F192 /* WorkoutsView.swift */; };
17 | 69166E81231D1D9D00A5F192 /* WorkoutsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E80231D1D9D00A5F192 /* WorkoutsStore.swift */; };
18 | 69166E83231D1DB600A5F192 /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E82231D1DB600A5F192 /* Workout.swift */; };
19 | 69166E85231D20EF00A5F192 /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E84231D20EF00A5F192 /* WorkoutDetailView.swift */; };
20 | 69166E87231D20F800A5F192 /* WorkoutCreatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E86231D20F800A5F192 /* WorkoutCreatorView.swift */; };
21 | 69166E8E231D6BFE00A5F192 /* Exercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E8D231D6BFE00A5F192 /* Exercise.swift */; };
22 | 69166E94231D788B00A5F192 /* ExerciseRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69166E93231D788B00A5F192 /* ExerciseRow.swift */; };
23 | 69AF8F25232696B2002B763E /* WorkoutResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69AF8F24232696B2002B763E /* WorkoutResultView.swift */; };
24 | 69D91F65231FD958008AAE9E /* WorkoutProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D91F64231FD958008AAE9E /* WorkoutProgressView.swift */; };
25 | /* End PBXBuildFile section */
26 |
27 | /* Begin PBXFileReference section */
28 | 2B0CB56E2320F45D00CF8B52 /* WorkoutTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutTimer.swift; sourceTree = ""; };
29 | 69166E61231CF60800A5F192 /* CoachUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoachUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
30 | 69166E64231CF60800A5F192 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
31 | 69166E66231CF60800A5F192 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
32 | 69166E6A231CF60B00A5F192 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
33 | 69166E6D231CF60B00A5F192 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
34 | 69166E70231CF60B00A5F192 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
35 | 69166E72231CF60B00A5F192 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
36 | 69166E7E231D1D9300A5F192 /* WorkoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutsView.swift; sourceTree = ""; };
37 | 69166E80231D1D9D00A5F192 /* WorkoutsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutsStore.swift; sourceTree = ""; };
38 | 69166E82231D1DB600A5F192 /* Workout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workout.swift; sourceTree = ""; };
39 | 69166E84231D20EF00A5F192 /* WorkoutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutDetailView.swift; sourceTree = ""; };
40 | 69166E86231D20F800A5F192 /* WorkoutCreatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutCreatorView.swift; sourceTree = ""; };
41 | 69166E8D231D6BFE00A5F192 /* Exercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exercise.swift; sourceTree = ""; };
42 | 69166E93231D788B00A5F192 /* ExerciseRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseRow.swift; sourceTree = ""; };
43 | 69AF8F24232696B2002B763E /* WorkoutResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutResultView.swift; sourceTree = ""; };
44 | 69D91F64231FD958008AAE9E /* WorkoutProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutProgressView.swift; sourceTree = ""; };
45 | /* End PBXFileReference section */
46 |
47 | /* Begin PBXFrameworksBuildPhase section */
48 | 69166E5E231CF60800A5F192 /* Frameworks */ = {
49 | isa = PBXFrameworksBuildPhase;
50 | buildActionMask = 2147483647;
51 | files = (
52 | );
53 | runOnlyForDeploymentPostprocessing = 0;
54 | };
55 | /* End PBXFrameworksBuildPhase section */
56 |
57 | /* Begin PBXGroup section */
58 | 69166E58231CF60800A5F192 = {
59 | isa = PBXGroup;
60 | children = (
61 | 69166E63231CF60800A5F192 /* CoachUI */,
62 | 69166E62231CF60800A5F192 /* Products */,
63 | );
64 | sourceTree = "";
65 | };
66 | 69166E62231CF60800A5F192 /* Products */ = {
67 | isa = PBXGroup;
68 | children = (
69 | 69166E61231CF60800A5F192 /* CoachUI.app */,
70 | );
71 | name = Products;
72 | sourceTree = "";
73 | };
74 | 69166E63231CF60800A5F192 /* CoachUI */ = {
75 | isa = PBXGroup;
76 | children = (
77 | 69166E79231D1A7F00A5F192 /* launch */,
78 | 69166E7B231D1A9600A5F192 /* views */,
79 | 69166E7A231D1A9300A5F192 /* models */,
80 | 69166E6A231CF60B00A5F192 /* Assets.xcassets */,
81 | 69166E6F231CF60B00A5F192 /* LaunchScreen.storyboard */,
82 | 69166E72231CF60B00A5F192 /* Info.plist */,
83 | 69166E6C231CF60B00A5F192 /* Preview Content */,
84 | );
85 | path = CoachUI;
86 | sourceTree = "";
87 | };
88 | 69166E6C231CF60B00A5F192 /* Preview Content */ = {
89 | isa = PBXGroup;
90 | children = (
91 | 69166E6D231CF60B00A5F192 /* Preview Assets.xcassets */,
92 | );
93 | path = "Preview Content";
94 | sourceTree = "";
95 | };
96 | 69166E79231D1A7F00A5F192 /* launch */ = {
97 | isa = PBXGroup;
98 | children = (
99 | 69166E64231CF60800A5F192 /* AppDelegate.swift */,
100 | 69166E66231CF60800A5F192 /* SceneDelegate.swift */,
101 | );
102 | path = launch;
103 | sourceTree = "";
104 | };
105 | 69166E7A231D1A9300A5F192 /* models */ = {
106 | isa = PBXGroup;
107 | children = (
108 | 69166E80231D1D9D00A5F192 /* WorkoutsStore.swift */,
109 | 69166E82231D1DB600A5F192 /* Workout.swift */,
110 | 69166E8D231D6BFE00A5F192 /* Exercise.swift */,
111 | );
112 | path = models;
113 | sourceTree = "";
114 | };
115 | 69166E7B231D1A9600A5F192 /* views */ = {
116 | isa = PBXGroup;
117 | children = (
118 | 69166E92231D788100A5F192 /* rows */,
119 | 69166E7E231D1D9300A5F192 /* WorkoutsView.swift */,
120 | 69166E84231D20EF00A5F192 /* WorkoutDetailView.swift */,
121 | 69166E86231D20F800A5F192 /* WorkoutCreatorView.swift */,
122 | 69D91F64231FD958008AAE9E /* WorkoutProgressView.swift */,
123 | 2B0CB56E2320F45D00CF8B52 /* WorkoutTimer.swift */,
124 | 69AF8F24232696B2002B763E /* WorkoutResultView.swift */,
125 | );
126 | path = views;
127 | sourceTree = "";
128 | };
129 | 69166E92231D788100A5F192 /* rows */ = {
130 | isa = PBXGroup;
131 | children = (
132 | 69166E93231D788B00A5F192 /* ExerciseRow.swift */,
133 | );
134 | path = rows;
135 | sourceTree = "";
136 | };
137 | /* End PBXGroup section */
138 |
139 | /* Begin PBXNativeTarget section */
140 | 69166E60231CF60800A5F192 /* CoachUI */ = {
141 | isa = PBXNativeTarget;
142 | buildConfigurationList = 69166E75231CF60B00A5F192 /* Build configuration list for PBXNativeTarget "CoachUI" */;
143 | buildPhases = (
144 | 69166E5D231CF60800A5F192 /* Sources */,
145 | 69166E5E231CF60800A5F192 /* Frameworks */,
146 | 69166E5F231CF60800A5F192 /* Resources */,
147 | );
148 | buildRules = (
149 | );
150 | dependencies = (
151 | );
152 | name = CoachUI;
153 | productName = CoachUI;
154 | productReference = 69166E61231CF60800A5F192 /* CoachUI.app */;
155 | productType = "com.apple.product-type.application";
156 | };
157 | /* End PBXNativeTarget section */
158 |
159 | /* Begin PBXProject section */
160 | 69166E59231CF60800A5F192 /* Project object */ = {
161 | isa = PBXProject;
162 | attributes = {
163 | LastSwiftUpdateCheck = 1100;
164 | LastUpgradeCheck = 1100;
165 | ORGANIZATIONNAME = "Thomas Ricouard";
166 | TargetAttributes = {
167 | 69166E60231CF60800A5F192 = {
168 | CreatedOnToolsVersion = 11.0;
169 | };
170 | };
171 | };
172 | buildConfigurationList = 69166E5C231CF60800A5F192 /* Build configuration list for PBXProject "CoachUI" */;
173 | compatibilityVersion = "Xcode 9.3";
174 | developmentRegion = en;
175 | hasScannedForEncodings = 0;
176 | knownRegions = (
177 | en,
178 | Base,
179 | );
180 | mainGroup = 69166E58231CF60800A5F192;
181 | productRefGroup = 69166E62231CF60800A5F192 /* Products */;
182 | projectDirPath = "";
183 | projectRoot = "";
184 | targets = (
185 | 69166E60231CF60800A5F192 /* CoachUI */,
186 | );
187 | };
188 | /* End PBXProject section */
189 |
190 | /* Begin PBXResourcesBuildPhase section */
191 | 69166E5F231CF60800A5F192 /* Resources */ = {
192 | isa = PBXResourcesBuildPhase;
193 | buildActionMask = 2147483647;
194 | files = (
195 | 69166E71231CF60B00A5F192 /* LaunchScreen.storyboard in Resources */,
196 | 69166E6E231CF60B00A5F192 /* Preview Assets.xcassets in Resources */,
197 | 69166E6B231CF60B00A5F192 /* Assets.xcassets in Resources */,
198 | );
199 | runOnlyForDeploymentPostprocessing = 0;
200 | };
201 | /* End PBXResourcesBuildPhase section */
202 |
203 | /* Begin PBXSourcesBuildPhase section */
204 | 69166E5D231CF60800A5F192 /* Sources */ = {
205 | isa = PBXSourcesBuildPhase;
206 | buildActionMask = 2147483647;
207 | files = (
208 | 69166E65231CF60800A5F192 /* AppDelegate.swift in Sources */,
209 | 69166E8E231D6BFE00A5F192 /* Exercise.swift in Sources */,
210 | 2B0CB56F2320F45D00CF8B52 /* WorkoutTimer.swift in Sources */,
211 | 69AF8F25232696B2002B763E /* WorkoutResultView.swift in Sources */,
212 | 69166E85231D20EF00A5F192 /* WorkoutDetailView.swift in Sources */,
213 | 69D91F65231FD958008AAE9E /* WorkoutProgressView.swift in Sources */,
214 | 69166E67231CF60800A5F192 /* SceneDelegate.swift in Sources */,
215 | 69166E94231D788B00A5F192 /* ExerciseRow.swift in Sources */,
216 | 69166E7F231D1D9300A5F192 /* WorkoutsView.swift in Sources */,
217 | 69166E83231D1DB600A5F192 /* Workout.swift in Sources */,
218 | 69166E87231D20F800A5F192 /* WorkoutCreatorView.swift in Sources */,
219 | 69166E81231D1D9D00A5F192 /* WorkoutsStore.swift in Sources */,
220 | );
221 | runOnlyForDeploymentPostprocessing = 0;
222 | };
223 | /* End PBXSourcesBuildPhase section */
224 |
225 | /* Begin PBXVariantGroup section */
226 | 69166E6F231CF60B00A5F192 /* LaunchScreen.storyboard */ = {
227 | isa = PBXVariantGroup;
228 | children = (
229 | 69166E70231CF60B00A5F192 /* Base */,
230 | );
231 | name = LaunchScreen.storyboard;
232 | sourceTree = "";
233 | };
234 | /* End PBXVariantGroup section */
235 |
236 | /* Begin XCBuildConfiguration section */
237 | 69166E73231CF60B00A5F192 /* Debug */ = {
238 | isa = XCBuildConfiguration;
239 | buildSettings = {
240 | ALWAYS_SEARCH_USER_PATHS = NO;
241 | CLANG_ANALYZER_NONNULL = YES;
242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
244 | CLANG_CXX_LIBRARY = "libc++";
245 | CLANG_ENABLE_MODULES = YES;
246 | CLANG_ENABLE_OBJC_ARC = YES;
247 | CLANG_ENABLE_OBJC_WEAK = YES;
248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
249 | CLANG_WARN_BOOL_CONVERSION = YES;
250 | CLANG_WARN_COMMA = YES;
251 | CLANG_WARN_CONSTANT_CONVERSION = YES;
252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
255 | CLANG_WARN_EMPTY_BODY = YES;
256 | CLANG_WARN_ENUM_CONVERSION = YES;
257 | CLANG_WARN_INFINITE_RECURSION = YES;
258 | CLANG_WARN_INT_CONVERSION = YES;
259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
264 | CLANG_WARN_STRICT_PROTOTYPES = YES;
265 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
267 | CLANG_WARN_UNREACHABLE_CODE = YES;
268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
269 | COPY_PHASE_STRIP = NO;
270 | DEBUG_INFORMATION_FORMAT = dwarf;
271 | ENABLE_STRICT_OBJC_MSGSEND = YES;
272 | ENABLE_TESTABILITY = YES;
273 | GCC_C_LANGUAGE_STANDARD = gnu11;
274 | GCC_DYNAMIC_NO_PIC = NO;
275 | GCC_NO_COMMON_BLOCKS = YES;
276 | GCC_OPTIMIZATION_LEVEL = 0;
277 | GCC_PREPROCESSOR_DEFINITIONS = (
278 | "DEBUG=1",
279 | "$(inherited)",
280 | );
281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
283 | GCC_WARN_UNDECLARED_SELECTOR = YES;
284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
285 | GCC_WARN_UNUSED_FUNCTION = YES;
286 | GCC_WARN_UNUSED_VARIABLE = YES;
287 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
289 | MTL_FAST_MATH = YES;
290 | ONLY_ACTIVE_ARCH = YES;
291 | SDKROOT = iphoneos;
292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
294 | };
295 | name = Debug;
296 | };
297 | 69166E74231CF60B00A5F192 /* Release */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ALWAYS_SEARCH_USER_PATHS = NO;
301 | CLANG_ANALYZER_NONNULL = YES;
302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
304 | CLANG_CXX_LIBRARY = "libc++";
305 | CLANG_ENABLE_MODULES = YES;
306 | CLANG_ENABLE_OBJC_ARC = YES;
307 | CLANG_ENABLE_OBJC_WEAK = YES;
308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
309 | CLANG_WARN_BOOL_CONVERSION = YES;
310 | CLANG_WARN_COMMA = YES;
311 | CLANG_WARN_CONSTANT_CONVERSION = YES;
312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
315 | CLANG_WARN_EMPTY_BODY = YES;
316 | CLANG_WARN_ENUM_CONVERSION = YES;
317 | CLANG_WARN_INFINITE_RECURSION = YES;
318 | CLANG_WARN_INT_CONVERSION = YES;
319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
323 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
324 | CLANG_WARN_STRICT_PROTOTYPES = YES;
325 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
326 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
327 | CLANG_WARN_UNREACHABLE_CODE = YES;
328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
329 | COPY_PHASE_STRIP = NO;
330 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
331 | ENABLE_NS_ASSERTIONS = NO;
332 | ENABLE_STRICT_OBJC_MSGSEND = YES;
333 | GCC_C_LANGUAGE_STANDARD = gnu11;
334 | GCC_NO_COMMON_BLOCKS = YES;
335 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
336 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
337 | GCC_WARN_UNDECLARED_SELECTOR = YES;
338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
339 | GCC_WARN_UNUSED_FUNCTION = YES;
340 | GCC_WARN_UNUSED_VARIABLE = YES;
341 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
342 | MTL_ENABLE_DEBUG_INFO = NO;
343 | MTL_FAST_MATH = YES;
344 | SDKROOT = iphoneos;
345 | SWIFT_COMPILATION_MODE = wholemodule;
346 | SWIFT_OPTIMIZATION_LEVEL = "-O";
347 | VALIDATE_PRODUCT = YES;
348 | };
349 | name = Release;
350 | };
351 | 69166E76231CF60B00A5F192 /* Debug */ = {
352 | isa = XCBuildConfiguration;
353 | buildSettings = {
354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
355 | CODE_SIGN_STYLE = Automatic;
356 | DEVELOPMENT_ASSET_PATHS = "\"CoachUI/Preview Content\"";
357 | DEVELOPMENT_TEAM = Z6P74P6T99;
358 | ENABLE_PREVIEWS = YES;
359 | INFOPLIST_FILE = CoachUI/Info.plist;
360 | LD_RUNPATH_SEARCH_PATHS = (
361 | "$(inherited)",
362 | "@executable_path/Frameworks",
363 | );
364 | PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.CoachUI;
365 | PRODUCT_NAME = "$(TARGET_NAME)";
366 | SWIFT_VERSION = 5.0;
367 | TARGETED_DEVICE_FAMILY = "1,2";
368 | };
369 | name = Debug;
370 | };
371 | 69166E77231CF60B00A5F192 /* Release */ = {
372 | isa = XCBuildConfiguration;
373 | buildSettings = {
374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
375 | CODE_SIGN_STYLE = Automatic;
376 | DEVELOPMENT_ASSET_PATHS = "\"CoachUI/Preview Content\"";
377 | DEVELOPMENT_TEAM = Z6P74P6T99;
378 | ENABLE_PREVIEWS = YES;
379 | INFOPLIST_FILE = CoachUI/Info.plist;
380 | LD_RUNPATH_SEARCH_PATHS = (
381 | "$(inherited)",
382 | "@executable_path/Frameworks",
383 | );
384 | PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.CoachUI;
385 | PRODUCT_NAME = "$(TARGET_NAME)";
386 | SWIFT_VERSION = 5.0;
387 | TARGETED_DEVICE_FAMILY = "1,2";
388 | };
389 | name = Release;
390 | };
391 | /* End XCBuildConfiguration section */
392 |
393 | /* Begin XCConfigurationList section */
394 | 69166E5C231CF60800A5F192 /* Build configuration list for PBXProject "CoachUI" */ = {
395 | isa = XCConfigurationList;
396 | buildConfigurations = (
397 | 69166E73231CF60B00A5F192 /* Debug */,
398 | 69166E74231CF60B00A5F192 /* Release */,
399 | );
400 | defaultConfigurationIsVisible = 0;
401 | defaultConfigurationName = Release;
402 | };
403 | 69166E75231CF60B00A5F192 /* Build configuration list for PBXNativeTarget "CoachUI" */ = {
404 | isa = XCConfigurationList;
405 | buildConfigurations = (
406 | 69166E76231CF60B00A5F192 /* Debug */,
407 | 69166E77231CF60B00A5F192 /* Release */,
408 | );
409 | defaultConfigurationIsVisible = 0;
410 | defaultConfigurationName = Release;
411 | };
412 | /* End XCConfigurationList section */
413 | };
414 | rootObject = 69166E59231CF60800A5F192 /* Project object */;
415 | }
416 |
--------------------------------------------------------------------------------