├── 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 | --------------------------------------------------------------------------------