diff --git a/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj b/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj index e73fe5e5390..9b2a19981b3 100755 --- a/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj +++ b/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj @@ -280,19 +280,14 @@ 7F44095B19F65B5B0021BF22 /* CoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7F44095919F65B5B0021BF22 /* CoreData.xcdatamodeld */; }; 7F4D2F541A1C8EE50038E7D1 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7F4D2F531A1C8EE50038E7D1 /* Images.xcassets */; }; 7F83F15619F154F000C53328 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F83F15519F154F000C53328 /* AppDelegate.swift */; }; - 7F83F15819F154F000C53328 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F83F15719F154F000C53328 /* HomeViewController.swift */; }; 7F83F16C19F154F000C53328 /* SAPTrackerAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F83F16B19F154F000C53328 /* SAPTrackerAPITests.swift */; }; 7F83F17619F1574700C53328 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F83F17519F1574700C53328 /* CoreLocation.framework */; }; - 7F87CAA11A2CCE8B0076C617 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F87CA9D1A2CCE8B0076C617 /* OpenSans-Bold.ttf */; }; - 7F87CAA21A2CCE8B0076C617 /* OpenSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F87CA9E1A2CCE8B0076C617 /* OpenSans-Light.ttf */; }; - 7F87CAA31A2CCE8B0076C617 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F87CA9F1A2CCE8B0076C617 /* OpenSans-Regular.ttf */; }; - 7F87CAA41A2CCE8B0076C617 /* OpenSans-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7F87CAA01A2CCE8B0076C617 /* OpenSans-Semibold.ttf */; }; 7F8B8D3F1A37356C00C34B88 /* TrackingPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B8D3E1A37356C00C34B88 /* TrackingPageViewController.swift */; }; 7F8B8D411A373B0200C34B88 /* SpeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B8D401A373B0200C34B88 /* SpeedViewController.swift */; }; 7F8B8D431A373D4400C34B88 /* CourseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B8D421A373D4400C34B88 /* CourseViewController.swift */; }; 7F8CC2081A10D47A00C56D7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8CC2071A10D47A00C56D7F /* SettingsViewController.swift */; }; 7F950A911A35E967003C8C5C /* LeaderboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F950A901A35E967003C8C5C /* LeaderboardViewController.swift */; }; - 7F99DE3C1A126554009B3619 /* CompetitorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F99DE3B1A126554009B3619 /* CompetitorViewController.swift */; }; + 7F99DE3C1A126554009B3619 /* RegattaCompetitorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F99DE3B1A126554009B3619 /* RegattaCompetitorViewController.swift */; }; 7FA2220B1A25E55400FD364B /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F83F17519F1574700C53328 /* CoreLocation.framework */; }; 7FA6551A19F6661900A1B92D /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA6551919F6661900A1B92D /* CoreDataManager.swift */; }; 7FAFA7951A126B5700D230F1 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FAFA7941A126B5700D230F1 /* AboutViewController.swift */; }; @@ -305,24 +300,33 @@ 9E95C3D61C723AB30016632B /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E95C3D51C723AB30016632B /* Constants.swift */; }; 9ECBDDAE1C9C4F0300F76AEB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9ECBDDAD1C9C4F0300F76AEB /* LaunchScreen.storyboard */; }; C8150BA51DCC6FDB0087C9C2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C8150BA71DCC6FDB0087C9C2 /* InfoPlist.strings */; }; + C8289F3A1F386D8300A76953 /* SignUpRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8289F391F386D8300A76953 /* SignUpRequestManager.swift */; }; C843D47F1EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D4791EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift */; }; C843D4801EBC54A300250F01 /* CompetitorCheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D47A1EBC54A300250F01 /* CompetitorCheckIn.swift */; }; C843D4811EBC54A300250F01 /* MarkCheckIn+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D47B1EBC54A300250F01 /* MarkCheckIn+CoreDataProperties.swift */; }; C843D4821EBC54A300250F01 /* MarkCheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D47C1EBC54A300250F01 /* MarkCheckIn.swift */; }; C843D4831EBC54A300250F01 /* CheckIn+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D47D1EBC54A300250F01 /* CheckIn+CoreDataProperties.swift */; }; C843D4841EBC54A300250F01 /* CheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D47E1EBC54A300250F01 /* CheckIn.swift */; }; - C843D4861EBC927800250F01 /* MarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D4851EBC927800250F01 /* MarkViewController.swift */; }; + C843D4861EBC927800250F01 /* RegattaMarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D4851EBC927800250F01 /* RegattaMarkViewController.swift */; }; C843D4881EBCADDC00250F01 /* CompetitorSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D4871EBCADDC00250F01 /* CompetitorSessionController.swift */; }; C843D48A1EBCADF500250F01 /* MarkSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D4891EBCADF500250F01 /* MarkSessionController.swift */; }; C843D48C1EBCB36900250F01 /* SessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D48B1EBCB36900250F01 /* SessionViewController.swift */; }; C843D48F1EC099EB00250F01 /* CoreData2-3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */; }; C843D4911EC0A93600250F01 /* CoreData1-3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = C843D4901EC0A93600250F01 /* CoreData1-3.xcmappingmodel */; }; + C843D532201F3050003725F0 /* NewCheckInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D531201F3050003725F0 /* NewCheckInViewController.swift */; }; C84936181D40D295000F30E8 /* GPSFixController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84936171D40D295000F30E8 /* GPSFixController.swift */; }; - C84936291D47645F000F30E8 /* HomeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84936281D47645F000F30E8 /* HomeViewCell.swift */; }; + C84936291D47645F000F30E8 /* CheckInTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84936281D47645F000F30E8 /* CheckInTableViewCell.swift */; }; C852914F1D48FB91009E9A9D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C85291511D48FB91009E9A9D /* Localizable.strings */; }; - C852BAA21CF5E1EA0093B0C5 /* RequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852BAA11CF5E1EA0093B0C5 /* RequestManager.swift */; }; + C852BAA21CF5E1EA0093B0C5 /* CheckInRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852BAA11CF5E1EA0093B0C5 /* CheckInRequestManager.swift */; }; C852BAAB1CF736680093B0C5 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852BAAA1CF736680093B0C5 /* Preferences.swift */; }; + C85DB8251F30B680009559CE /* SignUp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C85DB8241F30B680009559CE /* SignUp.storyboard */; }; + C85DB8281F31AAE0009559CE /* SignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85DB8271F31AAE0009559CE /* SignUpViewController.swift */; }; + C85DB82A1F31AAFD009559CE /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85DB8291F31AAFD009559CE /* LoginViewController.swift */; }; + C85DB82C1F31AB1A009559CE /* ForgotPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85DB82B1F31AB1A009559CE /* ForgotPasswordViewController.swift */; }; + C85DB82E1F31D804009559CE /* SignUpController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85DB82D1F31D804009559CE /* SignUpController.swift */; }; C8603D511C52892A002B2F28 /* Acknowledgements.markdown in Resources */ = {isa = PBXBuildFile; fileRef = C8603D491C528832002B2F28 /* Acknowledgements.markdown */; }; + C867BD091F4B0274007E78E0 /* TrainingCompetitorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C867BD081F4B0274007E78E0 /* TrainingCompetitorViewController.swift */; }; + C867BD0B1F4B0290007E78E0 /* CreateTrainingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C867BD0A1F4B0290007E78E0 /* CreateTrainingViewController.swift */; }; C8739E061D2AA2A500A89F3C /* GPSFix+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8739DFC1D2AA2A500A89F3C /* GPSFix+CoreDataProperties.swift */; }; C8739E071D2AA2A500A89F3C /* GPSFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8739DFD1D2AA2A500A89F3C /* GPSFix.swift */; }; C8739E081D2AA2A500A89F3C /* Leaderboard+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8739DFE1D2AA2A500A89F3C /* Leaderboard+CoreDataProperties.swift */; }; @@ -335,16 +339,50 @@ C8739E161D2BF17900A89F3C /* CompetitorData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8739E151D2BF17900A89F3C /* CompetitorData.swift */; }; C8739E181D2BF2DC00A89F3C /* BaseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8739E171D2BF2DC00A89F3C /* BaseData.swift */; }; C8739E1C1D2D5F1700A89F3C /* LocationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8739E1B1D2D5F1700A89F3C /* LocationData.swift */; }; + C87A74E11F4EE570003DE396 /* TrainingRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87A74E01F4EE570003DE396 /* TrainingRequestManager.swift */; }; + C87A74E31F4F0E34003DE396 /* TrainingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87A74E21F4F0E34003DE396 /* TrainingController.swift */; }; + C87A74E81F504EBB003DE396 /* CreateEventData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87A74E71F504EBB003DE396 /* CreateEventData.swift */; }; + C87A74EA1F5053AE003DE396 /* RegattaCompetitorCreateAndAddData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87A74E91F5053AE003DE396 /* RegattaCompetitorCreateAndAddData.swift */; }; + C87A74EC1F5068D0003DE396 /* CreateTrainingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87A74EB1F5068D0003DE396 /* CreateTrainingData.swift */; }; + C88F6D2B1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C88F6D2A1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift */; }; + C88F6D2D1FEACC5700A616BE /* LeaderboardGroupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C88F6D2C1FEACC5700A616BE /* LeaderboardGroupData.swift */; }; + C88F6D2F1FEACD7400A616BE /* RegattaRaceCourseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C88F6D2E1FEACD7400A616BE /* RegattaRaceCourseData.swift */; }; + C88F6D311FFBC35F00A616BE /* ErrorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C88F6D301FFBC35F00A616BE /* ErrorHelper.swift */; }; + C891886E2057DB4A00E37CC3 /* BoatData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C891886D2057DB4A00E37CC3 /* BoatData.swift */; }; + C89188712057E7D700E37CC3 /* BoatCheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = C891886F2057E7D700E37CC3 /* BoatCheckIn.swift */; }; + C89188722057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89188702057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift */; }; + C89188742057F32300E37CC3 /* RegattaBoatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89188732057F32300E37CC3 /* RegattaBoatViewController.swift */; }; + C89188762057F36300E37CC3 /* BoatSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89188752057F36300E37CC3 /* BoatSessionViewController.swift */; }; + C89188782057F44200E37CC3 /* BoatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89188772057F44200E37CC3 /* BoatViewController.swift */; }; + C891887A2057F55100E37CC3 /* BoatSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89188792057F55100E37CC3 /* BoatSessionController.swift */; }; + C891887C20580A4100E37CC3 /* TrainingBoatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C891887B20580A4100E37CC3 /* TrainingBoatViewController.swift */; }; C89C5FC51D06A289002AAAD2 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89C5FC41D06A289002AAAD2 /* GradientView.swift */; }; C89C5FCA1D06CECD002AAAD2 /* TrackingViewStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89C5FC91D06CECD002AAAD2 /* TrackingViewStatusCell.swift */; }; C89C5FCC1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89C5FCB1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift */; }; C89C5FCE1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89C5FCD1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift */; }; C89C5FD01D06CF46002AAAD2 /* TrackingViewModeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89C5FCF1D06CF46002AAAD2 /* TrackingViewModeCell.swift */; }; C8A4400B1C18153600DF70A0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = C8A4400A1C18153600DF70A0 /* Settings.bundle */; }; + C8B3B9BC1F39E64400B3B6BA /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */; }; + C8B3B9C01F3C3B4400B3B6BA /* SignUp.strings in Resources */ = {isa = PBXBuildFile; fileRef = C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */; }; + C8B3B9C21F3C6B3900B3B6BA /* SignUpTranslation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */; }; C8BBAC791C52649E004CA551 /* LicenseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8BBAC781C52649E004CA551 /* LicenseViewController.swift */; }; C8BFCCC41D51C53500FC1A27 /* TeamData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8BFCCC31D51C53500FC1A27 /* TeamData.swift */; }; C8BFCCCC1D523C5D00FC1A27 /* TitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8BFCCCB1D523C5D00FC1A27 /* TitleView.swift */; }; + C8CF22A31F6194C600A01E0A /* RegattaCheckInController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22A21F6194C600A01E0A /* RegattaCheckInController.swift */; }; + C8CF22A51F61965C00A01E0A /* TrainingCheckInController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22A41F61965C00A01E0A /* TrainingCheckInController.swift */; }; + C8CF22A91F62780C00A01E0A /* CheckInDataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22A81F62780C00A01E0A /* CheckInDataCollector.swift */; }; + C8CF22AD1F668BAB00A01E0A /* TrainingMarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22AC1F668BAB00A01E0A /* TrainingMarkViewController.swift */; }; + C8CF22B41F67C4D800A01E0A /* RegattaRaceColumnAddData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22B31F67C4D800A01E0A /* RegattaRaceColumnAddData.swift */; }; + C8CF22B81F68133000A01E0A /* TrainingRaceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22B71F68133000A01E0A /* TrainingRaceData.swift */; }; + C8CF22BE1F69187800A01E0A /* CompetitorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22BD1F69187800A01E0A /* CompetitorViewController.swift */; }; + C8CF22C01F6926E500A01E0A /* CompetitorSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22BF1F6926E500A01E0A /* CompetitorSessionViewController.swift */; }; + C8CF22C21F692B5300A01E0A /* MarkSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22C11F692B5300A01E0A /* MarkSessionViewController.swift */; }; + C8CF22C41F692B7700A01E0A /* MarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22C31F692B7700A01E0A /* MarkViewController.swift */; }; + C8CF22C61F694EB900A01E0A /* TrainingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8CF22C51F694EB900A01E0A /* TrainingViewController.swift */; }; C8D5E81D1D479FA800DA0D61 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C8D5E81C1D479FA800DA0D61 /* Main.storyboard */; }; + C8E0BD0A1F39A08B002FCCA5 /* FormularViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E0BD091F39A08B002FCCA5 /* FormularViewController.swift */; }; + C8E0BD0D1F39ADAD002FCCA5 /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E0BD0C1F39ADAD002FCCA5 /* KeychainPasswordItem.swift */; }; + C8E0D3131F59475B001EEF5C /* CheckInTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E0D3121F59475B001EEF5C /* CheckInTableViewController.swift */; }; C8F1B1E11D488B6D004C3379 /* Translation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F1B1E01D488B6D004C3379 /* Translation.swift */; }; C8F71EDA1D8691F700826DE4 /* AFHTTPRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = C8F71EA61D8691F700826DE4 /* AFHTTPRequestOperation.m */; }; C8F71EDB1D8691F700826DE4 /* AFHTTPRequestOperationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C8F71EA81D8691F700826DE4 /* AFHTTPRequestOperationManager.m */; }; @@ -660,21 +698,16 @@ 7F83F15019F154F000C53328 /* SAPTracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SAPTracker.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7F83F15419F154F000C53328 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7F83F15519F154F000C53328 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7F83F15719F154F000C53328 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 7F83F16519F154F000C53328 /* SAPTrackerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SAPTrackerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7F83F16A19F154F000C53328 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7F83F16B19F154F000C53328 /* SAPTrackerAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPTrackerAPITests.swift; sourceTree = ""; }; 7F83F17519F1574700C53328 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; - 7F87CA9D1A2CCE8B0076C617 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Bold.ttf"; sourceTree = ""; }; - 7F87CA9E1A2CCE8B0076C617 /* OpenSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Light.ttf"; sourceTree = ""; }; - 7F87CA9F1A2CCE8B0076C617 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Regular.ttf"; sourceTree = ""; }; - 7F87CAA01A2CCE8B0076C617 /* OpenSans-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Semibold.ttf"; sourceTree = ""; }; 7F8B8D3E1A37356C00C34B88 /* TrackingPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingPageViewController.swift; sourceTree = ""; }; 7F8B8D401A373B0200C34B88 /* SpeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeedViewController.swift; sourceTree = ""; }; 7F8B8D421A373D4400C34B88 /* CourseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseViewController.swift; sourceTree = ""; }; 7F8CC2071A10D47A00C56D7F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 7F950A901A35E967003C8C5C /* LeaderboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeaderboardViewController.swift; sourceTree = ""; }; - 7F99DE3B1A126554009B3619 /* CompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorViewController.swift; sourceTree = ""; }; + 7F99DE3B1A126554009B3619 /* RegattaCompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaCompetitorViewController.swift; sourceTree = ""; }; 7FA6551919F6661900A1B92D /* CoreDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CoreDataManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 7FAFA7941A126B5700D230F1 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 7FC5232C19F6AEE800BBFDCC /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; @@ -686,25 +719,35 @@ 9ECBDDAD1C9C4F0300F76AEB /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; C8150BA61DCC6FDB0087C9C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; C8150BA81DCC6FDD0087C9C2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; + C8289F391F386D8300A76953 /* SignUpRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpRequestManager.swift; sourceTree = ""; }; C843D4791EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CompetitorCheckIn+CoreDataProperties.swift"; sourceTree = ""; }; C843D47A1EBC54A300250F01 /* CompetitorCheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorCheckIn.swift; sourceTree = ""; }; C843D47B1EBC54A300250F01 /* MarkCheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MarkCheckIn+CoreDataProperties.swift"; sourceTree = ""; }; C843D47C1EBC54A300250F01 /* MarkCheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkCheckIn.swift; sourceTree = ""; }; C843D47D1EBC54A300250F01 /* CheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CheckIn+CoreDataProperties.swift"; sourceTree = ""; }; C843D47E1EBC54A300250F01 /* CheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckIn.swift; sourceTree = ""; }; - C843D4851EBC927800250F01 /* MarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkViewController.swift; sourceTree = ""; }; + C843D4851EBC927800250F01 /* RegattaMarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaMarkViewController.swift; sourceTree = ""; }; C843D4871EBCADDC00250F01 /* CompetitorSessionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorSessionController.swift; sourceTree = ""; }; C843D4891EBCADF500250F01 /* MarkSessionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkSessionController.swift; sourceTree = ""; }; C843D48B1EBCB36900250F01 /* SessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionViewController.swift; sourceTree = ""; }; C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = "CoreData2-3.xcmappingmodel"; sourceTree = ""; }; C843D4901EC0A93600250F01 /* CoreData1-3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = "CoreData1-3.xcmappingmodel"; sourceTree = ""; }; + C843D522201F1F17003725F0 /* CoreData 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 4.xcdatamodel"; sourceTree = ""; }; + C843D531201F3050003725F0 /* NewCheckInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCheckInViewController.swift; sourceTree = ""; }; C84936171D40D295000F30E8 /* GPSFixController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPSFixController.swift; sourceTree = ""; }; - C84936281D47645F000F30E8 /* HomeViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewCell.swift; sourceTree = ""; }; + C84936281D47645F000F30E8 /* CheckInTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInTableViewCell.swift; sourceTree = ""; }; C85291501D48FB91009E9A9D /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; - C852BAA11CF5E1EA0093B0C5 /* RequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestManager.swift; sourceTree = ""; }; + C852BAA11CF5E1EA0093B0C5 /* CheckInRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInRequestManager.swift; sourceTree = ""; }; C852BAAA1CF736680093B0C5 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + C85DB8241F30B680009559CE /* SignUp.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SignUp.storyboard; sourceTree = ""; }; + C85DB8271F31AAE0009559CE /* SignUpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = ""; }; + C85DB8291F31AAFD009559CE /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + C85DB82B1F31AB1A009559CE /* ForgotPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForgotPasswordViewController.swift; sourceTree = ""; }; + C85DB82D1F31D804009559CE /* SignUpController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpController.swift; sourceTree = ""; }; C85FAA501D4F84460087C2A2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; C8603D491C528832002B2F28 /* Acknowledgements.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Acknowledgements.markdown; sourceTree = ""; }; + C867BD081F4B0274007E78E0 /* TrainingCompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingCompetitorViewController.swift; sourceTree = ""; }; + C867BD0A1F4B0290007E78E0 /* CreateTrainingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateTrainingViewController.swift; sourceTree = ""; }; C8739D9A1D25317100A89F3C /* CoreData 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 2.xcdatamodel"; sourceTree = ""; }; C8739DFC1D2AA2A500A89F3C /* GPSFix+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPSFix+CoreDataProperties.swift"; sourceTree = ""; }; C8739DFD1D2AA2A500A89F3C /* GPSFix.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPSFix.swift; sourceTree = ""; }; @@ -718,17 +761,51 @@ C8739E151D2BF17900A89F3C /* CompetitorData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorData.swift; sourceTree = ""; }; C8739E171D2BF2DC00A89F3C /* BaseData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseData.swift; sourceTree = ""; }; C8739E1B1D2D5F1700A89F3C /* LocationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationData.swift; sourceTree = ""; }; + C87A74E01F4EE570003DE396 /* TrainingRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingRequestManager.swift; sourceTree = ""; }; + C87A74E21F4F0E34003DE396 /* TrainingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingController.swift; sourceTree = ""; }; + C87A74E71F504EBB003DE396 /* CreateEventData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateEventData.swift; sourceTree = ""; }; + C87A74E91F5053AE003DE396 /* RegattaCompetitorCreateAndAddData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaCompetitorCreateAndAddData.swift; sourceTree = ""; }; + C87A74EB1F5068D0003DE396 /* CreateTrainingData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateTrainingData.swift; sourceTree = ""; }; + C88F6D2A1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardAutoCourseData.swift; sourceTree = ""; }; + C88F6D2C1FEACC5700A616BE /* LeaderboardGroupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardGroupData.swift; sourceTree = ""; }; + C88F6D2E1FEACD7400A616BE /* RegattaRaceCourseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegattaRaceCourseData.swift; sourceTree = ""; }; + C88F6D301FFBC35F00A616BE /* ErrorHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHelper.swift; sourceTree = ""; }; + C891886D2057DB4A00E37CC3 /* BoatData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoatData.swift; sourceTree = ""; }; + C891886F2057E7D700E37CC3 /* BoatCheckIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatCheckIn.swift; sourceTree = ""; }; + C89188702057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoatCheckIn+CoreDataProperties.swift"; sourceTree = ""; }; + C89188732057F32300E37CC3 /* RegattaBoatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegattaBoatViewController.swift; sourceTree = ""; }; + C89188752057F36300E37CC3 /* BoatSessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatSessionViewController.swift; sourceTree = ""; }; + C89188772057F44200E37CC3 /* BoatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatViewController.swift; sourceTree = ""; }; + C89188792057F55100E37CC3 /* BoatSessionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatSessionController.swift; sourceTree = ""; }; + C891887B20580A4100E37CC3 /* TrainingBoatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainingBoatViewController.swift; sourceTree = ""; }; C89C5FC41D06A289002AAAD2 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; C89C5FC91D06CECD002AAAD2 /* TrackingViewStatusCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewStatusCell.swift; sourceTree = ""; }; C89C5FCB1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewGPSAccuracyCell.swift; sourceTree = ""; }; C89C5FCD1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewGPSFixesCell.swift; sourceTree = ""; }; C89C5FCF1D06CF46002AAAD2 /* TrackingViewModeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewModeCell.swift; sourceTree = ""; }; C8A4400A1C18153600DF70A0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = SignUp.strings; sourceTree = ""; }; + C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpTranslation.swift; sourceTree = ""; }; C8BBAC781C52649E004CA551 /* LicenseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LicenseViewController.swift; sourceTree = ""; }; C8BFCCC31D51C53500FC1A27 /* TeamData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamData.swift; sourceTree = ""; }; C8BFCCCB1D523C5D00FC1A27 /* TitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleView.swift; sourceTree = ""; }; + C8CF22A21F6194C600A01E0A /* RegattaCheckInController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaCheckInController.swift; sourceTree = ""; }; + C8CF22A41F61965C00A01E0A /* TrainingCheckInController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingCheckInController.swift; sourceTree = ""; }; + C8CF22A81F62780C00A01E0A /* CheckInDataCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInDataCollector.swift; sourceTree = ""; }; + C8CF22AC1F668BAB00A01E0A /* TrainingMarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingMarkViewController.swift; sourceTree = ""; }; + C8CF22B31F67C4D800A01E0A /* RegattaRaceColumnAddData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaRaceColumnAddData.swift; sourceTree = ""; }; + C8CF22B71F68133000A01E0A /* TrainingRaceData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingRaceData.swift; sourceTree = ""; }; + C8CF22BD1F69187800A01E0A /* CompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorViewController.swift; sourceTree = ""; }; + C8CF22BF1F6926E500A01E0A /* CompetitorSessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorSessionViewController.swift; sourceTree = ""; }; + C8CF22C11F692B5300A01E0A /* MarkSessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkSessionViewController.swift; sourceTree = ""; }; + C8CF22C31F692B7700A01E0A /* MarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkViewController.swift; sourceTree = ""; }; + C8CF22C51F694EB900A01E0A /* TrainingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingViewController.swift; sourceTree = ""; }; C8D5E81C1D479FA800DA0D61 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; C8D938B11EB9AFDB00B1B4BC /* CoreData 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 3.xcdatamodel"; sourceTree = ""; }; + C8E0BD091F39A08B002FCCA5 /* FormularViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormularViewController.swift; sourceTree = ""; }; + C8E0BD0C1F39ADAD002FCCA5 /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = ""; }; + C8E0D3121F59475B001EEF5C /* CheckInTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInTableViewController.swift; sourceTree = ""; }; C8F1B1E01D488B6D004C3379 /* Translation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Translation.swift; sourceTree = ""; }; C8F71EA51D8691F700826DE4 /* AFHTTPRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AFHTTPRequestOperation.h; sourceTree = ""; }; C8F71EA61D8691F700826DE4 /* AFHTTPRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AFHTTPRequestOperation.m; sourceTree = ""; }; @@ -819,6 +896,8 @@ 7F06590019F7FE750077FF95 /* CoreData */ = { isa = PBXGroup; children = ( + C891886F2057E7D700E37CC3 /* BoatCheckIn.swift */, + C89188702057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift */, C843D47E1EBC54A300250F01 /* CheckIn.swift */, C843D47D1EBC54A300250F01 /* CheckIn+CoreDataProperties.swift */, C843D47A1EBC54A300250F01 /* CompetitorCheckIn.swift */, @@ -841,13 +920,12 @@ 7F06590119F7FE920077FF95 /* ViewController */ = { isa = PBXGroup; children = ( + 7FAFA7941A126B5700D230F1 /* AboutViewController.swift */, C8BBAC781C52649E004CA551 /* LicenseViewController.swift */, 7F8CC2071A10D47A00C56D7F /* SettingsViewController.swift */, - 7FAFA7941A126B5700D230F1 /* AboutViewController.swift */, - 7F83F15719F154F000C53328 /* HomeViewController.swift */, - C84936281D47645F000F30E8 /* HomeViewCell.swift */, - 7F0658FC19F7D6700077FF95 /* ScanViewController.swift */, + C8CF22B01F669CA700A01E0A /* CheckIn */, C89C60101D0FF036002AAAD2 /* Session */, + C85DB8261F31AAAA009559CE /* SignUp */, ); name = ViewController; sourceTree = ""; @@ -855,12 +933,12 @@ 7F06590219F7FEA40077FF95 /* DataController */ = { isa = PBXGroup; children = ( - 7F23F0871A1A3DEF00F03F1C /* CheckInController.swift */, - C843D4871EBCADDC00250F01 /* CompetitorSessionController.swift */, - C84936171D40D295000F30E8 /* GPSFixController.swift */, - C843D4891EBCADF500250F01 /* MarkSessionController.swift */, - C8739E0E1D2BEE7900A89F3C /* SessionController.swift */, + C87A74E51F504CA7003DE396 /* CheckIn */, C8739E101D2BF13200A89F3C /* Data */, + C8CF22A71F619B5F00A01E0A /* GPSFix */, + C8CF22A61F619B4800A01E0A /* Session */, + C87A74E61F504CB4003DE396 /* SignUp */, + C87A74E41F504C97003DE396 /* Training */, ); name = DataController; sourceTree = ""; @@ -1138,10 +1216,10 @@ 7F83F14719F154F000C53328 = { isa = PBXGroup; children = ( + 7AC6457280AEA3EC18F5E846 /* Frameworks */, + 7F83F15119F154F000C53328 /* Products */, 7F83F15219F154F000C53328 /* SAPTracker */, 7F83F16819F154F000C53328 /* SAPTrackerTests */, - 7F83F15119F154F000C53328 /* Products */, - 7AC6457280AEA3EC18F5E846 /* Frameworks */, ); sourceTree = ""; }; @@ -1157,9 +1235,9 @@ 7F83F15219F154F000C53328 /* SAPTracker */ = { isa = PBXGroup; children = ( - C8F71EA31D8691F700826DE4 /* Libraries */, 7F83F15519F154F000C53328 /* AppDelegate.swift */, C8739DC71D2A653F00A89F3C /* Classes */, + C8F71EA31D8691F700826DE4 /* Libraries */, C89C60131D0FF61E002AAAD2 /* Resources */, 7F83F15319F154F000C53328 /* Supporting Files */, ); @@ -1169,18 +1247,22 @@ 7F83F15319F154F000C53328 /* Supporting Files */ = { isa = PBXGroup; children = ( - C85291511D48FB91009E9A9D /* Localizable.strings */, - C8D5E81C1D479FA800DA0D61 /* Main.storyboard */, - 9ECBDDAD1C9C4F0300F76AEB /* LaunchScreen.storyboard */, - 7F4D2F531A1C8EE50038E7D1 /* Images.xcassets */, - 7FD9D3021A1236080058FEAD /* Extensions.swift */, - 9E95C3D51C723AB30016632B /* Constants.swift */, + C8A4400A1C18153600DF70A0 /* Settings.bundle */, + 7F03FD3F19FA4D7400FA1327 /* bridge.h */, C8603D491C528832002B2F28 /* Acknowledgements.markdown */, 7F83F15419F154F000C53328 /* Info.plist */, - 7F03FD3F19FA4D7400FA1327 /* bridge.h */, - C8A4400A1C18153600DF70A0 /* Settings.bundle */, + 9ECBDDAD1C9C4F0300F76AEB /* LaunchScreen.storyboard */, + C8D5E81C1D479FA800DA0D61 /* Main.storyboard */, + C85DB8241F30B680009559CE /* SignUp.storyboard */, + C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */, + 9E95C3D51C723AB30016632B /* Constants.swift */, + 7FD9D3021A1236080058FEAD /* Extensions.swift */, + C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */, C8F1B1E01D488B6D004C3379 /* Translation.swift */, + 7F4D2F531A1C8EE50038E7D1 /* Images.xcassets */, C8150BA71DCC6FDB0087C9C2 /* InfoPlist.strings */, + C85291511D48FB91009E9A9D /* Localizable.strings */, + C88F6D301FFBC35F00A616BE /* ErrorHelper.swift */, ); name = "Supporting Files"; sourceTree = ""; @@ -1216,8 +1298,8 @@ C843D48D1EC0999D00250F01 /* Migration */ = { isa = PBXGroup; children = ( - C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */, C843D4901EC0A93600250F01 /* CoreData1-3.xcmappingmodel */, + C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */, ); name = Migration; sourceTree = ""; @@ -1225,17 +1307,30 @@ C852BAA91CF736440093B0C5 /* Preferences */ = { isa = PBXGroup; children = ( + C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */, + C8E0BD0C1F39ADAD002FCCA5 /* KeychainPasswordItem.swift */, C852BAAA1CF736680093B0C5 /* Preferences.swift */, ); name = Preferences; sourceTree = ""; }; + C85DB8261F31AAAA009559CE /* SignUp */ = { + isa = PBXGroup; + children = ( + C85DB82B1F31AB1A009559CE /* ForgotPasswordViewController.swift */, + C8E0BD091F39A08B002FCCA5 /* FormularViewController.swift */, + C85DB8291F31AAFD009559CE /* LoginViewController.swift */, + C85DB8271F31AAE0009559CE /* SignUpViewController.swift */, + ); + name = SignUp; + sourceTree = ""; + }; C8739D8B1D23C01500A89F3C /* Managers */ = { isa = PBXGroup; children = ( - 7F2C605919F160E400C32D93 /* LocationManager.swift */, 7F1B66F81A0CDCF600904A41 /* BatteryManager.swift */, - C852BAA11CF5E1EA0093B0C5 /* RequestManager.swift */, + 7F2C605919F160E400C32D93 /* LocationManager.swift */, + C8CF22AE1F669C1A00A01E0A /* Request */, ); name = Managers; sourceTree = ""; @@ -1256,18 +1351,54 @@ C8739E101D2BF13200A89F3C /* Data */ = { isa = PBXGroup; children = ( + C891886D2057DB4A00E37CC3 /* BoatData.swift */, + C8739E171D2BF2DC00A89F3C /* BaseData.swift */, 7F0658FE19F7EE910077FF95 /* CheckInData.swift */, + C87A74E91F5053AE003DE396 /* RegattaCompetitorCreateAndAddData.swift */, + C8739E151D2BF17900A89F3C /* CompetitorData.swift */, + C87A74E71F504EBB003DE396 /* CreateEventData.swift */, + C87A74EB1F5068D0003DE396 /* CreateTrainingData.swift */, C8739E111D2BF14F00A89F3C /* EventData.swift */, C8739E131D2BF16300A89F3C /* LeaderboardData.swift */, - C8739E151D2BF17900A89F3C /* CompetitorData.swift */, - C8739E171D2BF2DC00A89F3C /* BaseData.swift */, C8739E1B1D2D5F1700A89F3C /* LocationData.swift */, - C8BFCCC31D51C53500FC1A27 /* TeamData.swift */, C8FF5B7B1EB8CF2C00CF9764 /* MarkData.swift */, + C8BFCCC31D51C53500FC1A27 /* TeamData.swift */, + C8CF22B31F67C4D800A01E0A /* RegattaRaceColumnAddData.swift */, + C8CF22B71F68133000A01E0A /* TrainingRaceData.swift */, + C88F6D2A1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift */, + C88F6D2C1FEACC5700A616BE /* LeaderboardGroupData.swift */, + C88F6D2E1FEACD7400A616BE /* RegattaRaceCourseData.swift */, ); name = Data; sourceTree = ""; }; + C87A74E41F504C97003DE396 /* Training */ = { + isa = PBXGroup; + children = ( + C87A74E21F4F0E34003DE396 /* TrainingController.swift */, + ); + name = Training; + sourceTree = ""; + }; + C87A74E51F504CA7003DE396 /* CheckIn */ = { + isa = PBXGroup; + children = ( + 7F23F0871A1A3DEF00F03F1C /* CheckInController.swift */, + C8CF22A81F62780C00A01E0A /* CheckInDataCollector.swift */, + C8CF22A21F6194C600A01E0A /* RegattaCheckInController.swift */, + C8CF22A41F61965C00A01E0A /* TrainingCheckInController.swift */, + ); + name = CheckIn; + sourceTree = ""; + }; + C87A74E61F504CB4003DE396 /* SignUp */ = { + isa = PBXGroup; + children = ( + C85DB82D1F31D804009559CE /* SignUpController.swift */, + ); + name = SignUp; + sourceTree = ""; + }; C89C5FC31D06A269002AAAD2 /* Views */ = { isa = PBXGroup; children = ( @@ -1281,10 +1412,10 @@ C89C5FC61D06CE62002AAAD2 /* Tracking */ = { isa = PBXGroup; children = ( + 7F8B8D3E1A37356C00C34B88 /* TrackingPageViewController.swift */, + 7F1875B21A10F1B800C25BE0 /* TrackingViewController.swift */, C89C5FC81D06CEA0002AAAD2 /* Cells */, C89C5FC71D06CE87002AAAD2 /* Pages */, - 7F1875B21A10F1B800C25BE0 /* TrackingViewController.swift */, - 7F8B8D3E1A37356C00C34B88 /* TrackingPageViewController.swift */, ); name = Tracking; sourceTree = ""; @@ -1292,9 +1423,9 @@ C89C5FC71D06CE87002AAAD2 /* Pages */ = { isa = PBXGroup; children = ( - 7F193A8E1A39ACF500D71773 /* TimerViewController.swift */, - 7F8B8D401A373B0200C34B88 /* SpeedViewController.swift */, 7F8B8D421A373D4400C34B88 /* CourseViewController.swift */, + 7F8B8D401A373B0200C34B88 /* SpeedViewController.swift */, + 7F193A8E1A39ACF500D71773 /* TimerViewController.swift */, ); name = Pages; sourceTree = ""; @@ -1302,10 +1433,10 @@ C89C5FC81D06CEA0002AAAD2 /* Cells */ = { isa = PBXGroup; children = ( - C89C5FC91D06CECD002AAAD2 /* TrackingViewStatusCell.swift */, C89C5FCB1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift */, C89C5FCD1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift */, C89C5FCF1D06CF46002AAAD2 /* TrackingViewModeCell.swift */, + C89C5FC91D06CECD002AAAD2 /* TrackingViewStatusCell.swift */, ); name = Cells; sourceTree = ""; @@ -1313,11 +1444,17 @@ C89C60101D0FF036002AAAD2 /* Session */ = { isa = PBXGroup; children = ( - 7F99DE3B1A126554009B3619 /* CompetitorViewController.swift */, + C8CF22BF1F6926E500A01E0A /* CompetitorSessionViewController.swift */, + C8CF22BD1F69187800A01E0A /* CompetitorViewController.swift */, 7F950A901A35E967003C8C5C /* LeaderboardViewController.swift */, - C843D4851EBC927800250F01 /* MarkViewController.swift */, + C8CF22C11F692B5300A01E0A /* MarkSessionViewController.swift */, + C8CF22C31F692B7700A01E0A /* MarkViewController.swift */, C843D48B1EBCB36900250F01 /* SessionViewController.swift */, + C8CF22B11F669DA700A01E0A /* Regatta */, C89C5FC61D06CE62002AAAD2 /* Tracking */, + C8CF22B21F669DB100A01E0A /* Training */, + C89188752057F36300E37CC3 /* BoatSessionViewController.swift */, + C89188772057F44200E37CC3 /* BoatViewController.swift */, ); name = Session; sourceTree = ""; @@ -1326,22 +1463,10 @@ isa = PBXGroup; children = ( 7F14A0031A13B9A6001C1ECB /* Flags */, - C89C60141D0FF63C002AAAD2 /* Fonts */, ); name = Resources; sourceTree = ""; }; - C89C60141D0FF63C002AAAD2 /* Fonts */ = { - isa = PBXGroup; - children = ( - 7F87CA9D1A2CCE8B0076C617 /* OpenSans-Bold.ttf */, - 7F87CA9E1A2CCE8B0076C617 /* OpenSans-Light.ttf */, - 7F87CA9F1A2CCE8B0076C617 /* OpenSans-Regular.ttf */, - 7F87CAA01A2CCE8B0076C617 /* OpenSans-Semibold.ttf */, - ); - name = Fonts; - sourceTree = ""; - }; C8BFCC1B1D50F17800FC1A27 /* SMTWiFiStatus */ = { isa = PBXGroup; children = ( @@ -1352,6 +1477,76 @@ path = ..; sourceTree = ""; }; + C8CF22A61F619B4800A01E0A /* Session */ = { + isa = PBXGroup; + children = ( + C843D4871EBCADDC00250F01 /* CompetitorSessionController.swift */, + C843D4891EBCADF500250F01 /* MarkSessionController.swift */, + C8739E0E1D2BEE7900A89F3C /* SessionController.swift */, + C89188792057F55100E37CC3 /* BoatSessionController.swift */, + ); + name = Session; + sourceTree = ""; + }; + C8CF22A71F619B5F00A01E0A /* GPSFix */ = { + isa = PBXGroup; + children = ( + C84936171D40D295000F30E8 /* GPSFixController.swift */, + ); + name = GPSFix; + sourceTree = ""; + }; + C8CF22AE1F669C1A00A01E0A /* Request */ = { + isa = PBXGroup; + children = ( + C852BAA11CF5E1EA0093B0C5 /* CheckInRequestManager.swift */, + C8289F391F386D8300A76953 /* SignUpRequestManager.swift */, + C87A74E01F4EE570003DE396 /* TrainingRequestManager.swift */, + ); + name = Request; + sourceTree = ""; + }; + C8CF22AF1F669C8F00A01E0A /* New */ = { + isa = PBXGroup; + children = ( + C843D531201F3050003725F0 /* NewCheckInViewController.swift */, + C867BD0A1F4B0290007E78E0 /* CreateTrainingViewController.swift */, + 7F0658FC19F7D6700077FF95 /* ScanViewController.swift */, + ); + name = New; + sourceTree = ""; + }; + C8CF22B01F669CA700A01E0A /* CheckIn */ = { + isa = PBXGroup; + children = ( + C84936281D47645F000F30E8 /* CheckInTableViewCell.swift */, + C8E0D3121F59475B001EEF5C /* CheckInTableViewController.swift */, + C8CF22AF1F669C8F00A01E0A /* New */, + ); + name = CheckIn; + sourceTree = ""; + }; + C8CF22B11F669DA700A01E0A /* Regatta */ = { + isa = PBXGroup; + children = ( + 7F99DE3B1A126554009B3619 /* RegattaCompetitorViewController.swift */, + C843D4851EBC927800250F01 /* RegattaMarkViewController.swift */, + C89188732057F32300E37CC3 /* RegattaBoatViewController.swift */, + ); + name = Regatta; + sourceTree = ""; + }; + C8CF22B21F669DB100A01E0A /* Training */ = { + isa = PBXGroup; + children = ( + C867BD081F4B0274007E78E0 /* TrainingCompetitorViewController.swift */, + C8CF22AC1F668BAB00A01E0A /* TrainingMarkViewController.swift */, + C8CF22C51F694EB900A01E0A /* TrainingViewController.swift */, + C891887B20580A4100E37CC3 /* TrainingBoatViewController.swift */, + ); + name = Training; + sourceTree = ""; + }; C8F71EA31D8691F700826DE4 /* Libraries */ = { isa = PBXGroup; children = ( @@ -1494,6 +1689,7 @@ CreatedOnToolsVersion = 6.0.1; DevelopmentTeam = RRE8EFVCE3; LastSwiftMigration = 0820; + ProvisioningStyle = Automatic; }; 7F83F16419F154F000C53328 = { CreatedOnToolsVersion = 6.0.1; @@ -1542,6 +1738,7 @@ 7F14A12D1A13B9A6001C1ECB /* AZ.png in Resources */, 7F14A1D21A13B9A6001C1ECB /* PT.png in Resources */, 7F14A1241A13B9A6001C1ECB /* AN.png in Resources */, + C85DB8251F30B680009559CE /* SignUp.storyboard in Resources */, 7F14A2071A13B9A6001C1ECB /* VN.png in Resources */, 7F14A1791A13B9A6001C1ECB /* Hello.txt in Resources */, 7F14A1E81A13B9A6001C1ECB /* SS.png in Resources */, @@ -1587,6 +1784,7 @@ 7F14A1681A13B9A6001C1ECB /* GA.png in Resources */, 7F14A10C1A13B9A6001C1ECB /* _british-antarctic-territory.png in Resources */, 7F14A1771A13B9A6001C1ECB /* GW.png in Resources */, + C8B3B9C01F3C3B4400B3B6BA /* SignUp.strings in Resources */, 7F14A1811A13B9A6001C1ECB /* IE.png in Resources */, 7F14A14D1A13B9A6001C1ECB /* CR.png in Resources */, 7F14A1991A13B9A6001C1ECB /* LB.png in Resources */, @@ -1707,7 +1905,6 @@ 7F14A1C71A13B9A6001C1ECB /* OM.png in Resources */, 7F14A1F21A13B9A6001C1ECB /* TJ.png in Resources */, 7F14A12C1A13B9A6001C1ECB /* AX.png in Resources */, - 7F87CAA11A2CCE8B0076C617 /* OpenSans-Bold.ttf in Resources */, 7F14A1951A13B9A6001C1ECB /* KW.png in Resources */, 7F14A1C21A13B9A6001C1ECB /* NO.png in Resources */, 7F14A16E1A13B9A6001C1ECB /* GI.png in Resources */, @@ -1772,13 +1969,10 @@ 7F14A10F1A13B9A6001C1ECB /* _gosquared.png in Resources */, 7F14A1171A13B9A6001C1ECB /* _scotland.png in Resources */, 7F14A1FB1A13B9A6001C1ECB /* TW.png in Resources */, - 7F87CAA41A2CCE8B0076C617 /* OpenSans-Semibold.ttf in Resources */, 7F14A1451A13B9A6001C1ECB /* CG.png in Resources */, 7F14A1F11A13B9A6001C1ECB /* TH.png in Resources */, 7F14A1AC1A13B9A6001C1ECB /* ML.png in Resources */, - 7F87CAA31A2CCE8B0076C617 /* OpenSans-Regular.ttf in Resources */, 7F14A1AE1A13B9A6001C1ECB /* MN.png in Resources */, - 7F87CAA21A2CCE8B0076C617 /* OpenSans-Light.ttf in Resources */, 7F14A1D41A13B9A6001C1ECB /* PY.png in Resources */, 7F14A1E11A13B9A6001C1ECB /* SI.png in Resources */, 7F14A1921A13B9A6001C1ECB /* KN.png in Resources */, @@ -1838,60 +2032,86 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C867BD091F4B0274007E78E0 /* TrainingCompetitorViewController.swift in Sources */, + C85DB82A1F31AAFD009559CE /* LoginViewController.swift in Sources */, C8F71EDA1D8691F700826DE4 /* AFHTTPRequestOperation.m in Sources */, 7F1875B31A10F1B800C25BE0 /* TrackingViewController.swift in Sources */, C8739E081D2AA2A500A89F3C /* Leaderboard+CoreDataProperties.swift in Sources */, C8739E121D2BF14F00A89F3C /* EventData.swift in Sources */, C843D4881EBCADDC00250F01 /* CompetitorSessionController.swift in Sources */, C8F71EDF1D8691F700826DE4 /* AFURLConnectionOperation.m in Sources */, + C891887A2057F55100E37CC3 /* BoatSessionController.swift in Sources */, C8F71EE31D8691F700826DE4 /* SVIndefiniteAnimatedView.m in Sources */, C8739E0F1D2BEE7900A89F3C /* SessionController.swift in Sources */, C8739E1C1D2D5F1700A89F3C /* LocationData.swift in Sources */, - 7F83F15819F154F000C53328 /* HomeViewController.swift in Sources */, + C85DB82C1F31AB1A009559CE /* ForgotPasswordViewController.swift in Sources */, 7F8B8D431A373D4400C34B88 /* CourseViewController.swift in Sources */, 7FAFA7951A126B5700D230F1 /* AboutViewController.swift in Sources */, C8739E161D2BF17900A89F3C /* CompetitorData.swift in Sources */, + C8289F3A1F386D8300A76953 /* SignUpRequestManager.swift in Sources */, C8739E0C1D2AA2A500A89F3C /* Event+CoreDataProperties.swift in Sources */, C8739E071D2AA2A500A89F3C /* GPSFix.swift in Sources */, + C8E0BD0D1F39ADAD002FCCA5 /* KeychainPasswordItem.swift in Sources */, + C891887C20580A4100E37CC3 /* TrainingBoatViewController.swift in Sources */, + C87A74E11F4EE570003DE396 /* TrainingRequestManager.swift in Sources */, + C89188762057F36300E37CC3 /* BoatSessionViewController.swift in Sources */, C843D4811EBC54A300250F01 /* MarkCheckIn+CoreDataProperties.swift in Sources */, + C843D532201F3050003725F0 /* NewCheckInViewController.swift in Sources */, C8F71EEE1D8691F700826DE4 /* UIProgressView+AFNetworking.m in Sources */, + C8CF22A91F62780C00A01E0A /* CheckInDataCollector.swift in Sources */, + C85DB82E1F31D804009559CE /* SignUpController.swift in Sources */, + C8CF22AD1F668BAB00A01E0A /* TrainingMarkViewController.swift in Sources */, 7F44095B19F65B5B0021BF22 /* CoreData.xcdatamodeld in Sources */, C89C5FD01D06CF46002AAAD2 /* TrackingViewModeCell.swift in Sources */, C8FF5B7C1EB8CF2C00CF9764 /* MarkData.swift in Sources */, + C88F6D311FFBC35F00A616BE /* ErrorHelper.swift in Sources */, C8F71EEA1D8691F700826DE4 /* UIActivityIndicatorView+AFNetworking.m in Sources */, 7F1B66F91A0CDCF600904A41 /* BatteryManager.swift in Sources */, 7FA6551A19F6661900A1B92D /* CoreDataManager.swift in Sources */, C8F71EE71D8691F700826DE4 /* SVRadialGradientLayer.m in Sources */, - C852BAA21CF5E1EA0093B0C5 /* RequestManager.swift in Sources */, + C852BAA21CF5E1EA0093B0C5 /* CheckInRequestManager.swift in Sources */, C8F71EE01D8691F700826DE4 /* AFURLRequestSerialization.m in Sources */, C8BBAC791C52649E004CA551 /* LicenseViewController.swift in Sources */, C8BFCCC41D51C53500FC1A27 /* TeamData.swift in Sources */, C843D4911EC0A93600250F01 /* CoreData1-3.xcmappingmodel in Sources */, + C89188742057F32300E37CC3 /* RegattaBoatViewController.swift in Sources */, + C88F6D2B1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift in Sources */, 7F23F0881A1A3DEF00F03F1C /* CheckInController.swift in Sources */, C843D47F1EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift in Sources */, C8F71EDC1D8691F700826DE4 /* AFHTTPSessionManager.m in Sources */, C8F71EE91D8691F700826DE4 /* AFNetworkActivityIndicatorManager.m in Sources */, - C84936291D47645F000F30E8 /* HomeViewCell.swift in Sources */, + C84936291D47645F000F30E8 /* CheckInTableViewCell.swift in Sources */, + C891886E2057DB4A00E37CC3 /* BoatData.swift in Sources */, 7F2C605A19F160E400C32D93 /* LocationManager.swift in Sources */, C89C5FCC1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift in Sources */, C89C5FCA1D06CECD002AAAD2 /* TrackingViewStatusCell.swift in Sources */, - C843D4861EBC927800250F01 /* MarkViewController.swift in Sources */, + C843D4861EBC927800250F01 /* RegattaMarkViewController.swift in Sources */, C8F71EDE1D8691F700826DE4 /* AFSecurityPolicy.m in Sources */, C8F71EDB1D8691F700826DE4 /* AFHTTPRequestOperationManager.m in Sources */, 7F8B8D3F1A37356C00C34B88 /* TrackingPageViewController.swift in Sources */, + C89188712057E7D700E37CC3 /* BoatCheckIn.swift in Sources */, C843D48F1EC099EB00250F01 /* CoreData2-3.xcmappingmodel in Sources */, + C8CF22B81F68133000A01E0A /* TrainingRaceData.swift in Sources */, C84936181D40D295000F30E8 /* GPSFixController.swift in Sources */, - 7F99DE3C1A126554009B3619 /* CompetitorViewController.swift in Sources */, + 7F99DE3C1A126554009B3619 /* RegattaCompetitorViewController.swift in Sources */, + C89188722057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift in Sources */, C843D4841EBC54A300250F01 /* CheckIn.swift in Sources */, C8BFCCCC1D523C5D00FC1A27 /* TitleView.swift in Sources */, + C8CF22C01F6926E500A01E0A /* CompetitorSessionViewController.swift in Sources */, C8F71EF31D86A15F00826DE4 /* SMTWiFiStatus.m in Sources */, 7F8CC2081A10D47A00C56D7F /* SettingsViewController.swift in Sources */, C8739E0D1D2AA2A500A89F3C /* Event.swift in Sources */, + C87A74EC1F5068D0003DE396 /* CreateTrainingData.swift in Sources */, + C8CF22A31F6194C600A01E0A /* RegattaCheckInController.swift in Sources */, 7F8B8D411A373B0200C34B88 /* SpeedViewController.swift in Sources */, 9E95C3D61C723AB30016632B /* Constants.swift in Sources */, + C8CF22A51F61965C00A01E0A /* TrainingCheckInController.swift in Sources */, + C89188782057F44200E37CC3 /* BoatViewController.swift in Sources */, C8F71EF01D8691F700826DE4 /* UIWebView+AFNetworking.m in Sources */, + C87A74E81F504EBB003DE396 /* CreateEventData.swift in Sources */, 7FD9D3031A1236080058FEAD /* Extensions.swift in Sources */, C8739E091D2AA2A500A89F3C /* Leaderboard.swift in Sources */, + C8B3B9BC1F39E64400B3B6BA /* Keychain.swift in Sources */, C8739E141D2BF16300A89F3C /* LeaderboardData.swift in Sources */, C89C5FC51D06A289002AAAD2 /* GradientView.swift in Sources */, C8F71EEF1D8691F700826DE4 /* UIRefreshControl+AFNetworking.m in Sources */, @@ -1899,26 +2119,40 @@ C843D4821EBC54A300250F01 /* MarkCheckIn.swift in Sources */, C8F71EDD1D8691F700826DE4 /* AFNetworkReachabilityManager.m in Sources */, C843D4801EBC54A300250F01 /* CompetitorCheckIn.swift in Sources */, + C8CF22C41F692B7700A01E0A /* MarkViewController.swift in Sources */, C852BAAB1CF736680093B0C5 /* Preferences.swift in Sources */, C843D48A1EBCADF500250F01 /* MarkSessionController.swift in Sources */, + C8CF22C21F692B5300A01E0A /* MarkSessionViewController.swift in Sources */, 7F0658FD19F7D6700077FF95 /* ScanViewController.swift in Sources */, C843D4831EBC54A300250F01 /* CheckIn+CoreDataProperties.swift in Sources */, C8F71EEC1D8691F700826DE4 /* UIButton+AFNetworking.m in Sources */, + C867BD0B1F4B0290007E78E0 /* CreateTrainingViewController.swift in Sources */, + C87A74E31F4F0E34003DE396 /* TrainingController.swift in Sources */, + C8CF22C61F694EB900A01E0A /* TrainingViewController.swift in Sources */, C8F1B1E11D488B6D004C3379 /* Translation.swift in Sources */, C8F71EE81D8691F700826DE4 /* TSMarkdownParser.m in Sources */, C8739E061D2AA2A500A89F3C /* GPSFix+CoreDataProperties.swift in Sources */, 7F193A8F1A39ACF500D71773 /* TimerViewController.swift in Sources */, + C88F6D2F1FEACD7400A616BE /* RegattaRaceCourseData.swift in Sources */, C8739E181D2BF2DC00A89F3C /* BaseData.swift in Sources */, + C8CF22B41F67C4D800A01E0A /* RegattaRaceColumnAddData.swift in Sources */, 7F83F15619F154F000C53328 /* AppDelegate.swift in Sources */, + C8B3B9C21F3C6B3900B3B6BA /* SignUpTranslation.swift in Sources */, 7F950A911A35E967003C8C5C /* LeaderboardViewController.swift in Sources */, C8F71EED1D8691F700826DE4 /* UIImageView+AFNetworking.m in Sources */, C8F71EEB1D8691F700826DE4 /* UIAlertView+AFNetworking.m in Sources */, + C88F6D2D1FEACC5700A616BE /* LeaderboardGroupData.swift in Sources */, + C87A74EA1F5053AE003DE396 /* RegattaCompetitorCreateAndAddData.swift in Sources */, + C8E0D3131F59475B001EEF5C /* CheckInTableViewController.swift in Sources */, C843D48C1EBCB36900250F01 /* SessionViewController.swift in Sources */, + C85DB8281F31AAE0009559CE /* SignUpViewController.swift in Sources */, C89C5FCE1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift in Sources */, C8F71EE11D8691F700826DE4 /* AFURLResponseSerialization.m in Sources */, C8F71EE61D8691F700826DE4 /* SVProgressHUD.m in Sources */, 7F0658FF19F7EE910077FF95 /* CheckInData.swift in Sources */, 7F193A911A39E54600D71773 /* GPSAccuracyView.swift in Sources */, + C8E0BD0A1F39A08B002FCCA5 /* FormularViewController.swift in Sources */, + C8CF22BE1F69187800A01E0A /* CompetitorViewController.swift in Sources */, C8F71EE41D8691F700826DE4 /* SVProgressAnimatedView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2059,15 +2293,16 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = SAPTracker/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "Q3U792Z354/“SAP Sailing Boat Tracker”"; - PROVISIONING_PROFILE_SPECIFIER = "Q3U792Z354/“SAP Sailing Boat Tracker”"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = SAPTracker/bridge.h; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2080,15 +2315,16 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = SAPTracker/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "Q3U792Z354/“SAP Sailing Boat Tracker”"; - PROVISIONING_PROFILE_SPECIFIER = "Q3U792Z354/“SAP Sailing Boat Tracker”"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = SAPTracker/bridge.h; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2170,11 +2406,12 @@ 7F44095919F65B5B0021BF22 /* CoreData.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + C843D522201F1F17003725F0 /* CoreData 4.xcdatamodel */, C8D938B11EB9AFDB00B1B4BC /* CoreData 3.xcdatamodel */, C8739D9A1D25317100A89F3C /* CoreData 2.xcdatamodel */, 7F44095A19F65B5B0021BF22 /* CoreData.xcdatamodel */, ); - currentVersion = C8D938B11EB9AFDB00B1B4BC /* CoreData 3.xcdatamodel */; + currentVersion = C843D522201F1F17003725F0 /* CoreData 4.xcdatamodel */; path = CoreData.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/xcshareddata/xcschemes/SAPTracker.xcscheme b/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/xcshareddata/xcschemes/SAPTracker.xcscheme index 54a5ff1c73d..c7b1d4562dc 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/xcshareddata/xcschemes/SAPTracker.xcscheme +++ b/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/xcshareddata/xcschemes/SAPTracker.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -73,6 +73,10 @@ + + 0 { noData = false - let gpsFixController = GPSFixController(checkIn: checkIn) + let gpsFixController = GPSFixController(checkIn: checkIn, coreDataManager: CoreDataManager.shared) gpsFixController.sendAll(completion: { (withSuccess) in allSuccess = allSuccess && withSuccess }) @@ -54,7 +54,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - CoreDataManager.sharedManager.saveContext() + CoreDataManager.shared.saveContext() } func applicationWillEnterForeground(_ application: UIApplication) { @@ -67,7 +67,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - CoreDataManager.sharedManager.saveContext() + CoreDataManager.shared.saveContext() } // MARK: - Setup @@ -91,12 +91,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } fileprivate func setupCoreData() { - _ = CoreDataManager.sharedManager // Initialize core data, migrate database if needed, or delete if migration needed but not possible + _ = CoreDataManager.shared // Initialize core data, migrate database if needed, or delete if migration needed but not possible } fileprivate func setupNavigationBarApperance() { UINavigationBar.appearance().tintColor = Colors.NavigationBarTintColor - UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName: Colors.NavigationBarTitleColor, NSFontAttributeName: Fonts.OpenSansBold17] } fileprivate func setupPageControlApperance() { @@ -105,7 +104,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } fileprivate func setupSVProgressHUD() { - SVProgressHUD.setDefaultMaskType(.gradient) + SVProgressHUD.setDefaultMaskType(.clear) } // MARK: - Helper diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings index b658e7ad19f..0faec172b97 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings +++ b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings @@ -6,6 +6,7 @@ // Copyright © 2016 com.sap.sailing. All rights reserved. // -"NSLocationAlwaysUsageDescription" = "In order to track your entire race your phone needs to continue tracking also when you switch off or lock the screen which helps save battery and avoids unintentionally stopping the tracking process."; +"NSLocationWhenInUseUsageDescription" = "In order to track your entire race your phone needs to continue tracking also when you switch off or lock the screen which helps save battery and avoids unintentionally stopping the tracking process."; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "In order to track your entire race your phone needs to continue tracking also when you switch off or lock the screen which helps save battery and avoids unintentionally stopping the tracking process."; "NSCameraUsageDescription" = "The camera is required to perform a check-in using QR code."; "NSPhotoLibraryUsageDescription" = "To replace the Team Photo the app requires access to the photo library."; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings index 8cff97dd46e..9a7c34ec360 100644 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings and b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/BoatCheckIn+CoreDataProperties.swift b/ios/SAPTracker/src/xcode/SAPTracker/BoatCheckIn+CoreDataProperties.swift new file mode 100644 index 00000000000..ae4276b2f51 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/BoatCheckIn+CoreDataProperties.swift @@ -0,0 +1,19 @@ +// +// BoatCheckIn+CoreDataProperties.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// +// + +import Foundation +import CoreData + +extension BoatCheckIn { + + @NSManaged public var boatID: String + @NSManaged public var color: String + @NSManaged public var sailID: String + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/BoatCheckIn.swift b/ios/SAPTracker/src/xcode/SAPTracker/BoatCheckIn.swift new file mode 100644 index 00000000000..4048b87446b --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/BoatCheckIn.swift @@ -0,0 +1,27 @@ +// +// BoatCheckIn+CoreDataClass.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// +// + +import Foundation +import CoreData + +class BoatCheckIn: CheckIn { + + override func updateWithCheckInData(checkInData: CheckInData) { + super.updateWithCheckInData(checkInData: checkInData) + boatID = checkInData.boatData.boatID + color = checkInData.boatData.color + name = checkInData.boatData.name + sailID = checkInData.boatData.sailID + } + + func displayName() -> String { + return name.count > 0 ? name : sailID + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/BoatData.swift b/ios/SAPTracker/src/xcode/SAPTracker/BoatData.swift new file mode 100644 index 00000000000..ad5c80cb6d5 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/BoatData.swift @@ -0,0 +1,28 @@ +// +// BoatData.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import Foundation + +class BoatData: BaseData { + + fileprivate enum Keys { + static let Color = "color" + static let ID = "id" + static let Name = "name" + static let SailID = "sailId" + } + + var boatID: String { get { return stringValue(forKey: Keys.ID) } } + + var color: String { get { return stringValue(forKey: Keys.Color) } } + + var name: String { get { return stringValue(forKey: Keys.Name) } } + + var sailID: String { get { return stringValue(forKey: Keys.SailID) } } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/BoatSessionController.swift b/ios/SAPTracker/src/xcode/SAPTracker/BoatSessionController.swift new file mode 100644 index 00000000000..1e1636c5bc7 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/BoatSessionController.swift @@ -0,0 +1,13 @@ +// +// BoatSessionController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +class BoatSessionController: SessionController { + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/BoatSessionViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/BoatSessionViewController.swift new file mode 100644 index 00000000000..33174848f40 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/BoatSessionViewController.swift @@ -0,0 +1,41 @@ +// +// BoatSessionViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +class BoatSessionViewController: SessionViewController { + + struct BoatSessionSegue { + static let EmbedBoat = "EmbedBoat" + } + + weak var boatCheckIn: BoatCheckIn! + weak var boatCoreDataManager: CoreDataManager! + weak var boatViewController: BoatViewController? + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == BoatSessionSegue.EmbedBoat) { + if let boatViewController = segue.destination as? BoatViewController { + boatViewController.boatCheckIn = boatCheckIn + boatViewController.boatSessionController = boatSessionController + boatViewController.boatCoreDataManager = boatCoreDataManager + self.boatViewController = boatViewController + } + } + } + + // MARK: - Properties + + lazy var boatSessionController: BoatSessionController = { + return BoatSessionController(checkIn: self.boatCheckIn, coreDataManager: self.boatCoreDataManager) + }() + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/BoatViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/BoatViewController.swift new file mode 100644 index 00000000000..15d5004e266 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/BoatViewController.swift @@ -0,0 +1,42 @@ +// +// BoatViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +class BoatViewController: UIViewController { + + weak var boatCheckIn: BoatCheckIn! + weak var boatCoreDataManager: CoreDataManager! + weak var boatSessionController: BoatSessionController! + + @IBOutlet weak var boatNameView: UIView! + @IBOutlet weak var boatNameLabel: UILabel! + + // MARK: - Refresh + + func refresh(_ animated: Bool) { + refreshBoatNameLabel(animated) + } + + fileprivate func refreshBoatNameLabel(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshBoatNameLabel() } + } else { + refreshBoatNameLabel() + } + } + + fileprivate func refreshBoatNameLabel() { + boatNameLabel.text = boatCheckIn.displayName() + if let color = UIColor.init(hexString: boatCheckIn.color) { + boatNameView.backgroundColor = color + boatNameLabel.textColor = UIColor.init(contrastColorFor: color) + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift index fb4902c92d0..b450c45676a 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift @@ -16,6 +16,7 @@ extension CheckIn { @NSManaged var serverURL: String @NSManaged var name: String + @NSManaged var isTraining: NSNumber @NSManaged var event: Event @NSManaged var gpsFixes: NSSet? @NSManaged var leaderboard: Leaderboard diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift index ea227180bc9..06c46182e45 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift @@ -12,12 +12,8 @@ import CoreData @objc(CheckIn) class CheckIn: NSManagedObject { - func initialize() { - event = CoreDataManager.sharedManager.newEvent(checkIn: self) - leaderboard = CoreDataManager.sharedManager.newLeaderboard(checkIn: self) - } - func updateWithCheckInData(checkInData: CheckInData) { + isTraining = NSNumber(booleanLiteral: checkInData.isTraining) serverURL = checkInData.serverURL event.updateWithEventData(eventData: checkInData.eventData) leaderboard.updateWithLeaderboardData(leaderboardData: checkInData.leaderboardData) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift index fc9ba6e3dbc..17f05d12c53 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift @@ -10,131 +10,200 @@ import Foundation import UIKit import AVFoundation -@objc protocol CheckInControllerDelegate { - - func checkInController(_ sender: CheckInController, show alertController: UIAlertController) +// MARK: CheckInControllerDelegate +protocol CheckInControllerDelegate: class { + + func checkInController( + _ controller: CheckInController, postBoatCheckIn + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + + func checkInController( + _ controller: CheckInController, postCompetitorCheckIn + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + + func checkInController( + _ controller: CheckInController, postMarkCheckIn + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + +} + +// MARK: CheckInControllerDelegate (default implementation) + +extension CheckInControllerDelegate { + + func checkInController( + _ controller: CheckInController, postBoatCheckIn + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + controller.postCheckIn(checkInData: checkInData, success: success, failure: failure) + } + + func checkInController( + _ controller: CheckInController, postCompetitorCheckIn + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + controller.postCheckIn(checkInData: checkInData, success: success, failure: failure) + } + + func checkInController( + _ controller: CheckInController, postMarkCheckIn + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + controller.postCheckIn(checkInData: checkInData, success: success, failure: failure) + } + } class CheckInController : NSObject { - var delegate: CheckInControllerDelegate? + enum CheckInControllerError: Error { + case cancelled + } - fileprivate var requestManager = RequestManager() + fileprivate unowned let coreDataManager: CoreDataManager + + weak var viewController: UIViewController? + weak var delegate: CheckInControllerDelegate? + + init(coreDataManager: CoreDataManager) { + self.coreDataManager = coreDataManager + super.init() + } // MARK: - CheckIn - func checkIn(checkInData: CheckInData, completion: @escaping (_ withSuccess: Bool) -> Void) { - SVProgressHUD.show() - requestManager = RequestManager(baseURLString: checkInData.serverURL) - requestManager.getCheckInData(checkInData: checkInData, success: - { (checkInData) in - SVProgressHUD.popActivity() - self.checkInSuccess(checkInData: checkInData, completion: completion) - }, failure: { (error) in - SVProgressHUD.popActivity() - self.checkInFailure(error: error, completion: completion) - } - ) + func checkInWithViewController( + _ viewController: UIViewController?, + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + self.viewController = viewController + collectCheckInData(checkInData: checkInData, success: success, failure: failure) } - fileprivate func checkInSuccess(checkInData: CheckInData, completion: @escaping (_ withSuccess: Bool) -> Void) { + // MARK: - CollectCheckInData + + fileprivate func collectCheckInData( + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + SVProgressHUD.show() + let checkInDataCollector = CheckInDataCollector(checkInData: checkInData) + checkInDataCollector.collect(checkInData: checkInData, success: { [weak self] checkInData in + SVProgressHUD.popActivity() + self?.collectCheckInDataSuccess(checkInData: checkInData, success: success, failure: failure) + }) { [weak self] error in + SVProgressHUD.popActivity() + self?.didFailCheckIn(withError: error, failure: failure) + } + } + + fileprivate func collectCheckInDataSuccess( + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { switch checkInData.type { + case .boat: + delegate?.checkInController(self, postBoatCheckIn: checkInData, success: success, failure: failure) + break case .competitor: - let alertController = UIAlertController( - title: String(format: Translation.CheckInController.WelcomeAlert.Title.String, checkInData.competitorData.name), - message: String(format: Translation.CheckInController.WelcomeAlert.Message.String, checkInData.competitorData.sailID), - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in - self.postCheckIn(checkInData: checkInData, completion: completion) - } - let cancelAction = UIAlertAction(title: Translation.CheckInController.WelcomeAlert.CancelAction.Title.String, style: .cancel) { (action) in - self.checkInDidFinish(withSuccess: false, completion: completion) - } - alertController.addAction(okAction) - alertController.addAction(cancelAction) - showCheckInAlert(alertController: alertController) + delegate?.checkInController(self, postCompetitorCheckIn: checkInData, success: success, failure: failure) break case .mark: - self.postCheckIn(checkInData: checkInData, completion: completion) + delegate?.checkInController(self, postMarkCheckIn: checkInData, success: success, failure: failure) break } } - fileprivate func checkInFailure(error: Error, completion: @escaping (_ withSuccess: Bool) -> Void) { - let alertController = UIAlertController( - title: Translation.Common.Error.String, - message: error.localizedDescription, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in - self.checkInDidFinish(withSuccess: false, completion: completion) - } - alertController.addAction(okAction) - showCheckInAlert(alertController: alertController) - } - // MARK: - PostCheckIn - fileprivate func postCheckIn(checkInData: CheckInData, completion: @escaping (_ withSuccess: Bool) -> Void) { + func postCheckIn( + checkInData: CheckInData, + success: @escaping (_ checkIn: CheckIn) -> Void, + failure: @escaping (_ error: Error) -> Void) + { SVProgressHUD.show() - requestManager.postCheckIn(checkInData: checkInData, success: { () -> Void in - SVProgressHUD.popActivity() - self.postCheckInSuccess(checkInData: checkInData, completion: completion) - }, failure: { (error) -> Void in - SVProgressHUD.popActivity() - self.postCheckInFailure(error: error, completion: completion) - } - ) + let checkInRequestManager = CheckInRequestManager(baseURLString: checkInData.serverURL) + checkInRequestManager.postCheckIn(checkInData: checkInData, success: { [weak self] () in + SVProgressHUD.popActivity() + self?.postCheckInSuccess(checkInData: checkInData, success: success) + }) { [weak self] error in + SVProgressHUD.popActivity() + self?.didFailCheckIn(withError: error, failure: failure) + } } - fileprivate func postCheckInSuccess(checkInData: CheckInData, completion: (_ withSuccess: Bool) -> Void) { + fileprivate func postCheckInSuccess( + checkInData: CheckInData, + success: (_ checkIn: CheckIn) -> Void) + { switch checkInData.type { + case .boat: + let boatCheckIn = coreDataManager.fetchBoatCheckIn( + eventID: checkInData.eventID, + leaderboardName: checkInData.leaderboardName, + boatID: checkInData.boatID! + ) ?? coreDataManager.newBoatCheckIn() + boatCheckIn.updateWithCheckInData(checkInData: checkInData) + coreDataManager.saveContext() + didFinishCheckIn(boatCheckIn, success: success) + break case .competitor: - let competitorCheckIn = CoreDataManager.sharedManager.fetchCompetitorCheckIn( + let competitorCheckIn = coreDataManager.fetchCompetitorCheckIn( eventID: checkInData.eventID, leaderboardName: checkInData.leaderboardName, competitorID: checkInData.competitorID! - ) ?? CoreDataManager.sharedManager.newCompetitorCheckIn() + ) ?? coreDataManager.newCompetitorCheckIn() competitorCheckIn.updateWithCheckInData(checkInData: checkInData) - CoreDataManager.sharedManager.saveContext() - checkInDidFinish(withSuccess: true, completion: completion) + coreDataManager.saveContext() + didFinishCheckIn(competitorCheckIn, success: success) break case .mark: - let markCheckIn = CoreDataManager.sharedManager.fetchMarkCheckIn( + let markCheckIn = coreDataManager.fetchMarkCheckIn( eventID: checkInData.eventID, leaderboardName: checkInData.leaderboardName, markID: checkInData.markID! - ) ?? CoreDataManager.sharedManager.newMarkCheckIn() + ) ?? coreDataManager.newMarkCheckIn() markCheckIn.updateWithCheckInData(checkInData: checkInData) - CoreDataManager.sharedManager.saveContext() - checkInDidFinish(withSuccess: true, completion: completion) + coreDataManager.saveContext() + didFinishCheckIn(markCheckIn, success: success) break } } - fileprivate func postCheckInFailure(error: Error, completion: @escaping (_ withSuccess: Bool) -> Void) { - let alertController = UIAlertController( - title: Translation.Common.Error.String, - message: error.localizedDescription, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in - self.checkInDidFinish(withSuccess: false, completion: completion) + // MARK: - Finish, Cancel, Fail + + func didFinishCheckIn(_ checkIn: CheckIn, success:(_ checkIn: CheckIn) -> Void) { + success(checkIn) + } + + func didCancelCheckIn(failure:(_ error: Error) -> Void) { + failure(CheckInControllerError.cancelled) + } + + func didFailCheckIn(withError error: Error, failure: @escaping (_ error: Error) -> Void) { + let alertController = UIAlertController(title: Translation.Common.Error.String, message: error.localizedDescription, preferredStyle: .alert) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { action in + failure(error) } alertController.addAction(okAction) - showCheckInAlert(alertController: alertController) - } - - fileprivate func checkInDidFinish(withSuccess success: Bool, completion: (_ withSuccess: Bool) -> Void) { - completion(success) - } - - // MARK: - Controller - - fileprivate func showCheckInAlert(alertController: UIAlertController) { - self.delegate?.checkInController(self, show: alertController) + viewController?.present(alertController, animated: true) } } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift index fefa830e522..77b09f4801d 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift @@ -8,9 +8,10 @@ import Foundation -open class CheckInData: NSObject { +class CheckInData: NSObject { fileprivate struct ItemNames { + static let BoatID = "boat_id" static let EventID = "event_id" static let LeaderboardName = "leaderboard_name" static let CompetitorID = "competitor_id" @@ -18,6 +19,7 @@ open class CheckInData: NSObject { } public enum CheckInDataType { + case boat case competitor case mark } @@ -25,12 +27,15 @@ open class CheckInData: NSObject { let serverURL: String let eventID: String let leaderboardName: String + let boatID: String? let competitorID: String? let markID: String? + let isTraining: Bool let type: CheckInDataType var eventData = EventData() var leaderboardData = LeaderboardData() + var boatData = BoatData() var competitorData = CompetitorData() var markData = MarkData() var teamImageURL: String? @@ -38,13 +43,16 @@ open class CheckInData: NSObject { init(serverURL: String, eventID: String, leaderboardName: String, - competitorID: String) + competitorID: String, + isTraining: Bool) { + self.boatID = nil self.competitorID = competitorID self.eventID = eventID self.leaderboardName = leaderboardName self.markID = nil self.serverURL = serverURL + self.isTraining = isTraining self.type = CheckInDataType.competitor super.init() } @@ -52,23 +60,44 @@ open class CheckInData: NSObject { init(serverURL: String, eventID: String, leaderboardName: String, - markID: String) + markID: String, + isTraining: Bool) { + self.boatID = nil self.competitorID = nil self.eventID = eventID self.leaderboardName = leaderboardName self.markID = markID self.serverURL = serverURL + self.isTraining = isTraining type = CheckInDataType.mark super.init() } + init(serverURL: String, + eventID: String, + leaderboardName: String, + boatID: String, + isTraining: Bool) + { + self.boatID = boatID + self.competitorID = nil + self.eventID = eventID + self.leaderboardName = leaderboardName + self.markID = nil + self.serverURL = serverURL + self.isTraining = isTraining + type = CheckInDataType.boat + super.init() + } + convenience init(competitorCheckIn: CompetitorCheckIn) { self.init( serverURL: competitorCheckIn.serverURL, eventID: competitorCheckIn.event.eventID, leaderboardName: competitorCheckIn.leaderboard.name, - competitorID: competitorCheckIn.competitorID + competitorID: competitorCheckIn.competitorID, + isTraining: competitorCheckIn.isTraining.boolValue ) } @@ -77,7 +106,8 @@ open class CheckInData: NSObject { serverURL: markCheckIn.serverURL, eventID: markCheckIn.event.eventID, leaderboardName: markCheckIn.leaderboard.name, - markID: markCheckIn.markID + markID: markCheckIn.markID, + isTraining: markCheckIn.isTraining.boolValue ) } @@ -100,14 +130,24 @@ open class CheckInData: NSObject { serverURL: serverURL, eventID: eventID, leaderboardName: leaderboardName, - competitorID: competitorID + competitorID: competitorID, + isTraining: false ) } else if let markID = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.MarkID) { self.init( serverURL: serverURL, eventID: eventID, leaderboardName: leaderboardName, - markID: markID + markID: markID, + isTraining: false + ) + } else if let boatID = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.BoatID) { + self.init( + serverURL: serverURL, + eventID: eventID, + leaderboardName: leaderboardName, + boatID: boatID, + isTraining: false ) } else { logError(name: "\(#function)", error: "unknown check-in type") @@ -132,7 +172,20 @@ open class CheckInData: NSObject { return nil } } - + + convenience init?(createTrainingData: CreateTrainingData) { + guard let eventID = createTrainingData.createEventData?.eventID else { return nil } + guard let leaderboardName = createTrainingData.createEventData?.leaderboardName else { return nil } + guard let competitorID = createTrainingData.competitorID else { return nil } + self.init( + serverURL: createTrainingData.serverURL, + eventID: eventID, + leaderboardName: leaderboardName, + competitorID: competitorID, + isTraining: true + ) + } + // MARK: - Helper class fileprivate func queryItemValue(queryItems: [URLQueryItem]?, itemName: String) -> String? { diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift new file mode 100644 index 00000000000..08d36df6695 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift @@ -0,0 +1,152 @@ +// +// CheckInDataCollector.swift +// SAPTracker +// +// Created by Raimund Wege on 08.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +enum CheckInDataCollectorError: Error { + case checkInDataIsIncomplete +} + +extension CheckInDataCollectorError: LocalizedError { + var errorDescription: String? { + switch self { + case .checkInDataIsIncomplete: + return Translation.CheckInDataCollectorError.CheckInDataIsIncomplete.String + } + } +} + +class CheckInDataCollector: NSObject { + + fileprivate let checkInRequestManager: CheckInRequestManager + + init(checkInData: CheckInData) { + checkInRequestManager = CheckInRequestManager(baseURLString: checkInData.serverURL) + super.init() + } + + func collect( + checkInData: CheckInData, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + getEventData(collector: checkInData, success: success, failure: failure) + } + + // MARK: - Event + + fileprivate func getEventData( + collector: CheckInData, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + checkInRequestManager.getEvent(eventID: collector.eventID, success: { (eventData) in + collector.eventData = eventData + self.getLeaderboardData(collector: collector, success: success, failure: failure) + }) { (error) in + failure(error) + } + } + + // MARK: - Leaderboard + + fileprivate func getLeaderboardData( + collector: CheckInData, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + checkInRequestManager.getLeaderboard(leaderboardName: collector.leaderboardName, success: { (leaderboardData) in + collector.leaderboardData = leaderboardData + self.getLeaderboardDataSuccess(collector: collector, success: success, failure: failure) + }) { (error) in + failure(error) + } + } + + fileprivate func getLeaderboardDataSuccess( + collector: CheckInData, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + if let boatID = collector.boatID { + getBoatData(collector: collector, boatID: boatID, success: success, failure: failure) + } else if let competitorID = collector.competitorID { + getCompetitorData(collector: collector, competitorID: competitorID, success: success, failure: failure) + } else if let markID = collector.markID { + getMarkData(collector: collector, markID: markID, success: success, failure: failure) + } else { + failure(CheckInDataCollectorError.checkInDataIsIncomplete) + } + } + + // MARK: - Boat + + fileprivate func getBoatData( + collector: CheckInData, + boatID: String, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + checkInRequestManager.getBoat(boatID: boatID, success: { (boatData) in + collector.boatData = boatData + success(collector) + }) { error in + failure(error) + } + } + + // MARK: - Competitor + + fileprivate func getCompetitorData( + collector: CheckInData, + competitorID: String, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + checkInRequestManager.getCompetitor(competitorID: competitorID, success: { (competitorData) in + collector.competitorData = competitorData + self.getCompetitorDataSuccess( + collector: collector, + competitorID: competitorID, + success: success, + failure: failure + ) + }) { (error) in + failure(error) + } + } + + fileprivate func getCompetitorDataSuccess( + collector: CheckInData, + competitorID: String, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + checkInRequestManager.getTeamImageURL(competitorID: competitorID, result: { (imageURL) in + collector.teamImageURL = imageURL + success(collector) + }) + } + + // MARK: - Mark + + fileprivate func getMarkData( + collector: CheckInData, + markID: String, + success: @escaping (_ collector: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + checkInRequestManager.getMark(leaderboardName: collector.leaderboardName, markID: markID, success: { (markData) in + collector.markData = markData + success(collector) + }) { error in + failure(error) + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RequestManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift similarity index 71% rename from ios/SAPTracker/src/xcode/SAPTracker/RequestManager.swift rename to ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift index 8dccafa62ee..20f26747c81 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/RequestManager.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift @@ -1,5 +1,5 @@ // -// RequestManager.swift +// CheckInRequestManager.swift // SAPTracker // // Created by Raimund Wege on 25.05.16. @@ -9,9 +9,9 @@ import UIKit -enum RequestManagerError: Error { - case checkInDataIsIncomplete +enum CheckInRequestManagerError: Error { case communicationFailed + case getBoatFailed case getCompetitorFailed case getEventFailed case getLeaderboardFailed @@ -24,42 +24,43 @@ enum RequestManagerError: Error { case teamImageURLIsInvalid } -extension RequestManagerError: LocalizedError { +extension CheckInRequestManagerError: LocalizedError { var errorDescription: String? { switch self { - case .checkInDataIsIncomplete: - return Translation.RequestManagerError.CheckInDataIsIncomplete.String case .communicationFailed: - return Translation.RequestManagerError.CommunicationFailed.String + return Translation.CheckInRequestManagerError.CommunicationFailed.String + case .getBoatFailed: + return Translation.CheckInRequestManagerError.GetBoatFailed.String case .getCompetitorFailed: - return Translation.RequestManagerError.GetCompetitorFailed.String + return Translation.CheckInRequestManagerError.GetCompetitorFailed.String case .getEventFailed: - return Translation.RequestManagerError.GetEventFailed.String + return Translation.CheckInRequestManagerError.GetEventFailed.String case .getLeaderboardFailed: - return Translation.RequestManagerError.GetLeaderboardFailed.String + return Translation.CheckInRequestManagerError.GetLeaderboardFailed.String case .getMarkFailed: - return Translation.RequestManagerError.GetMarkFailed.String + return Translation.CheckInRequestManagerError.GetMarkFailed.String case .getTeamFailed: - return Translation.RequestManagerError.GetTeamFailed.String + return Translation.CheckInRequestManagerError.GetTeamFailed.String case .postCheckInFailed: - return Translation.RequestManagerError.PostCheckInFailed.String + return Translation.CheckInRequestManagerError.PostCheckInFailed.String case .postCheckOutFailed: - return Translation.RequestManagerError.PostCheckOutFailed.String + return Translation.CheckInRequestManagerError.PostCheckOutFailed.String case .postGPSFixFailed: - return Translation.RequestManagerError.PostGPSFixFailed.String + return Translation.CheckInRequestManagerError.PostGPSFixFailed.String case .postTeamImageFailed: - return Translation.RequestManagerError.PostTeamImageFailed.String + return Translation.CheckInRequestManagerError.PostTeamImageFailed.String case .teamImageURLIsInvalid: - return Translation.RequestManagerError.TeamImageURLIsInvalid.String + return Translation.CheckInRequestManagerError.TeamImageURLIsInvalid.String } } } -class RequestManager: NSObject { +class CheckInRequestManager: NSObject { fileprivate let basePathString = "/sailingserver/api/v1" fileprivate enum BodyKeys { + static let BoatID = "boatId" static let CompetitorID = "competitorId" static let DeviceType = "deviceType" static let DeviceUUID = "deviceUuid" @@ -91,133 +92,16 @@ class RequestManager: NSObject { self.baseURLString = baseURLString manager = AFHTTPRequestOperationManager(baseURL: URL(string: baseURLString)) manager.requestSerializer = AFJSONRequestSerializer() as AFHTTPRequestSerializer + manager.requestSerializer.timeoutInterval = Application.RequestTimeout manager.responseSerializer = AFJSONResponseSerializer() as AFHTTPResponseSerializer sessionManager = AFURLSessionManager(sessionConfiguration: URLSessionConfiguration.default) sessionManager.responseSerializer = AFJSONResponseSerializer() as AFHTTPResponseSerializer super.init() } - // MARK: - CheckIn - - func getCheckInData( - checkInData: CheckInData, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - getEventData(checkInData: checkInData, success: success, failure: failure) - } - - fileprivate func getEventData( - checkInData: CheckInData, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - getEvent( - eventID: checkInData.eventID, - success: { (data) in self.getEventDataSuccess(eventData: data, checkInData: checkInData, success: success, failure: failure) }, - failure: { (error) in failure(error) } - ) - } - - fileprivate func getEventDataSuccess( - eventData: EventData, - checkInData: CheckInData, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - checkInData.eventData = eventData - getLeaderboardData(checkInData: checkInData, success: success, failure: failure) - } - - fileprivate func getLeaderboardData( - checkInData: CheckInData, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - getLeaderboard( - leaderboardName: checkInData.leaderboardName, - success: { (data) in self.getLeaderboardDataSuccess(leaderboardData: data, checkInData: checkInData, success: success, failure: failure) }, - failure: { (error) in failure(error) } - ) - } - - fileprivate func getLeaderboardDataSuccess( - leaderboardData: LeaderboardData, - checkInData: CheckInData, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - checkInData.leaderboardData = leaderboardData - if (checkInData.competitorID != nil) { - getCompetitorData(checkInData: checkInData, competitorID: checkInData.competitorID!, success: success, failure: failure) - } else if (checkInData.markID != nil) { - getMarkData(checkInData: checkInData, markID: checkInData.markID!, success: success, failure: failure) - } else { - failure(RequestManagerError.checkInDataIsIncomplete) - } - } - - fileprivate func getCompetitorData( - checkInData: CheckInData, - competitorID: String, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - getCompetitor(competitorID: competitorID, - success: { (data) in - self.getCompetitorDataSuccess( - competitorData: data, - checkInData: checkInData, - competitorID: competitorID, - success: success, - failure: failure - )}, - failure: { (error) in - failure(error) - }) - } - - fileprivate func getCompetitorDataSuccess( - competitorData: CompetitorData, - checkInData: CheckInData, - competitorID: String, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - checkInData.competitorData = competitorData - getTeamImageURL(competitorID: competitorID, result: { (imageURL) in - checkInData.teamImageURL = imageURL - success(checkInData) - }) - } - - fileprivate func getMarkData( - checkInData: CheckInData, - markID: String, - success: @escaping (_ checkInData: CheckInData) -> Void, - failure: @escaping (_ error: Error) -> Void) - { - getMark( - leaderboardName: checkInData.leaderboardName, - markID: markID, - success: { (data) in self.getMarkDataSuccess(markData: data, checkInData: checkInData, success: success, failure: failure) }, - failure: { (error) in failure(error) } - ) - } - - fileprivate func getMarkDataSuccess( - markData: MarkData, - checkInData: CheckInData, - success: (_ checkInData: CheckInData) -> Void, - failure: (_ error: Error) -> Void) - { - checkInData.markData = markData - success(checkInData) - } - // MARK: - Event - fileprivate func getEvent( + func getEvent( eventID: String, success: @escaping (_ eventData: EventData) -> Void, failure: @escaping (_ error: Error) -> Void) @@ -239,12 +123,12 @@ class RequestManager: NSObject { fileprivate func getEventFailure(error: Error, failure: (_ error: Error) -> Void) -> Void { logError(name: "\(#function)", error: error) - failure(RequestManagerError.getEventFailed) + failure(CheckInRequestManagerError.getEventFailed) } // MARK: - Leaderboard - fileprivate func getLeaderboard( + func getLeaderboard( leaderboardName: String, success: @escaping (_ leaderboardData: LeaderboardData) -> Void, failure: @escaping (_ error: Error) -> Void) @@ -266,12 +150,39 @@ class RequestManager: NSObject { fileprivate func getLeaderboardFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.getLeaderboardFailed) + failure(CheckInRequestManagerError.getLeaderboardFailed) } - + + // MARK: - Boat + + func getBoat( + boatID: String, + success: @escaping (_ boatData: BoatData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + let encodedBoatID = boatID.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed) ?? "" + let urlString = "\(basePathString)/boats/\(encodedBoatID)" + manager.get( + urlString, + parameters: nil, + success: { (requestOperation, responseObject) in self.getBoatSuccess(responseObject: responseObject, success: success) }, + failure: { (requestOperation, error) in self.getBoatFailure(error: error, failure: failure) } + ) + } + + fileprivate func getBoatSuccess(responseObject: Any, success: (_ boatData: BoatData) -> Void) { + logInfo(name: "\(#function)", info: responseObjectToString(responseObject: responseObject)) + success(BoatData(dictionary: responseObject as? [String: AnyObject])) + } + + fileprivate func getBoatFailure(error: Error, failure: (_ error: Error) -> Void) { + logError(name: "\(#function)", error: error) + failure(CheckInRequestManagerError.getBoatFailed) + } + // MARK: - Competitor - fileprivate func getCompetitor( + func getCompetitor( competitorID: String, success: @escaping (_ competitorData: CompetitorData) -> Void, failure: @escaping (_ error: Error) -> Void) @@ -293,12 +204,12 @@ class RequestManager: NSObject { fileprivate func getCompetitorFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.getCompetitorFailed) + failure(CheckInRequestManagerError.getCompetitorFailed) } // MARK: - Mark - fileprivate func getMark( + func getMark( leaderboardName: String, markID: String, success: @escaping (_ markData: MarkData) -> Void, @@ -322,12 +233,12 @@ class RequestManager: NSObject { fileprivate func getMarkFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.getMarkFailed) + failure(CheckInRequestManagerError.getMarkFailed) } // MARK: - Team - fileprivate func getTeam( + func getTeam( competitorID: String, success: @escaping (_ teamData: TeamData) -> Void, failure: @escaping (_ error: Error) -> Void) @@ -349,10 +260,10 @@ class RequestManager: NSObject { fileprivate func getTeamFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.getTeamFailed) + failure(CheckInRequestManagerError.getTeamFailed) } - fileprivate func getTeamImageURL(competitorID: String, result: @escaping (_ imageURL: String?) -> Void) { + func getTeamImageURL(competitorID: String, result: @escaping (_ imageURL: String?) -> Void) { getTeam( competitorID: competitorID, success: { (teamData) in result(teamData.imageURL) }, @@ -374,6 +285,9 @@ class RequestManager: NSObject { body[BodyKeys.PushDeviceID] = "" as AnyObject? body[BodyKeys.FromMillis] = millisSince1970() switch checkInData.type { + case .boat: + body[BodyKeys.BoatID] = checkInData.boatData.boatID as AnyObject? + break case .competitor: body[BodyKeys.CompetitorID] = checkInData.competitorData.competitorID as AnyObject? break @@ -399,7 +313,7 @@ class RequestManager: NSObject { fileprivate func postCheckInFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.postCheckInFailed) + failure(CheckInRequestManagerError.postCheckInFailed) } // MARK: - CheckOut @@ -438,7 +352,7 @@ class RequestManager: NSObject { fileprivate func postCheckOutFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.postCheckOutFailed) + failure(CheckInRequestManagerError.postCheckOutFailed) } // MARK: - GPSFixes @@ -479,7 +393,7 @@ class RequestManager: NSObject { fileprivate func postGPSFixesFailure(error: Error, failure: (_ error: Error) -> Void) { logError(name: "\(#function)", error: error) - failure(RequestManagerError.postGPSFixFailed) + failure(CheckInRequestManagerError.postGPSFixFailed) } // MARK: - TeamImage @@ -491,7 +405,7 @@ class RequestManager: NSObject { failure: @escaping (_ error: Error) -> Void) { guard let teamImageURL = URL(string: "\(baseURLString)\(basePathString)/competitors/\(competitorID)/team/image") else { - failure(RequestManagerError.teamImageURLIsInvalid) + failure(CheckInRequestManagerError.teamImageURLIsInvalid) return } @@ -529,11 +443,11 @@ class RequestManager: NSObject { failure: (_ error: Error) -> Void) { guard let teamImageDictionary = responseObject as? [String: AnyObject] else { - postTeamImageDataFailure(error: RequestManagerError.communicationFailed, failure: failure) + postTeamImageDataFailure(error: CheckInRequestManagerError.communicationFailed, failure: failure) return } guard let teamImageURL = teamImageDictionary[TeamImageKeys.TeamImageURL] as? String else { - postTeamImageDataFailure(error: RequestManagerError.communicationFailed, failure: failure) + postTeamImageDataFailure(error: CheckInRequestManagerError.communicationFailed, failure: failure) return } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/HomeViewCell.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInTableViewCell.swift similarity index 76% rename from ios/SAPTracker/src/xcode/SAPTracker/HomeViewCell.swift rename to ios/SAPTracker/src/xcode/SAPTracker/CheckInTableViewCell.swift index 6a172e051a0..93db8b7fada 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/HomeViewCell.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInTableViewCell.swift @@ -1,5 +1,5 @@ // -// HomeViewCell.swift +// CheckInTableViewCell.swift // SAPTracker // // Created by Raimund Wege on 26.07.16. @@ -8,10 +8,10 @@ import UIKit -class HomeViewCell: UITableViewCell { - +class CheckInTableViewCell: UITableViewCell { + @IBOutlet weak var eventLabel: UILabel! @IBOutlet weak var leaderboardLabel: UILabel! @IBOutlet weak var competitorLabel: UILabel! - + } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInTableViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInTableViewController.swift new file mode 100644 index 00000000000..793c3848aba --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInTableViewController.swift @@ -0,0 +1,574 @@ +// +// BaseCheckInTableViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 01.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit +import CoreData + +class CheckInTableViewController: UIViewController { + + struct CheckInSegue { + static let NewCheckIn = "NewCheckIn" + static let RegattaBoat = "RegattaBoat" + static let RegattaCompetitor = "RegattaCompetitor" + static let RegattaMark = "RegattaMark" + static let TrainingBoat = "TrainingBoat" + static let TrainingCompetitor = "TrainingCompetitor" + static let TrainingMark = "TrainingMark" + } + + struct CheckInSegues { + static let boat = [CheckInSegue.RegattaBoat, CheckInSegue.TrainingBoat] + static let competitor = [CheckInSegue.RegattaCompetitor, CheckInSegue.TrainingCompetitor] + static let mark = [CheckInSegue.RegattaMark, CheckInSegue.TrainingMark] + } + + var segueCheckIn: CheckIn? + + // Strong reference needed to avoid deallocation when not attached to table view + @IBOutlet var headerView: UIView! + @IBOutlet var footerTextView: UITextView! + + @IBOutlet weak var headerTitleLabel: UILabel! + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var footerView: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + setup() + subscribeForTrainingEndpointChangedNotifications() + } + + deinit { + unsubscribeFromTrainingEndpointChangedNotifications() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableViewDeselectRow() + titleView.setSubtitle(subtitle: signUpController.userName ?? "") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + review() + subscribeForNewCheckInURLNotifications() + } + + override func viewWillDisappear(_ animated: Bool) { + unsubscribeFromNewCheckInURLNotifications() + super.viewWillDisappear(animated) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + layout() + } + + // MARK: - Setup + + fileprivate func setup() { + setupLocalization() + setupNavigationBar() + setupTableView() + setupTableViewDataSource() + setupTableViewHeader() + setupTableViewFooter() + } + + fileprivate func setupLocalization() { + navigationItem.title = Translation.RegattaCheckInListView.Title.String + headerTitleLabel.text = Translation.RegattaCheckInListView.HeaderTitleLabel.Text.String + footerTextView.text = Translation.RegattaCheckInListView.FooterTextView.Text.String + } + + fileprivate func setupNavigationBar() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIImageView(image: UIImage(named: "sap_logo"))) + // TODO: Activate Training Feature -> Use this custom title view: + // navigationItem.titleView = titleView + navigationController?.navigationBar.setNeedsLayout() + } + + fileprivate func setupTableView() { + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 140 + } + + fileprivate func setupTableViewDataSource() { + do { + try fetchedResultsController?.performFetch() + tableView.reloadData() + } catch { + logError(name: "\(#function)", error: error) + } + } + + fileprivate func setupTableViewHeader() { + if fetchedResultsController?.sections?[0].numberOfObjects ?? 0 == 0 { + tableView.tableHeaderView = nil + } else { + tableView.tableHeaderView = headerView + } + } + + fileprivate func setupTableViewFooter() { + tableView.tableFooterView?.isHidden = false + } + + // MARK: - Review + + fileprivate func review() { + reviewTerms(completion: { [weak self] in + logInfo(name: "\(#function)", info: "Review terms done.") + self?.reviewCodeConvention(completion: { [weak self] in + logInfo(name: "\(#function)", info: "Review code convention done.") + self?.reviewGPSFixes(completion: { [weak self] in + logInfo(name: "\(#function)", info: "Review GPS fixes done.") + self?.reviewNewCheckIn(completion: { [weak self] (checkIn) in + logInfo(name: "\(#function)", info: "Review new check-in done.") + self?.performSegue(forCheckIn: checkIn) + }) + }) + }) + }) + } + + // MARK: 1. Review Terms + + fileprivate func reviewTerms(completion: @escaping () -> Void) { + guard Preferences.termsAccepted == false else { + completion() + return + } + let alertController = UIAlertController( + title: Translation.RegattaCheckInListView.TermsAlert.Title.String, + message: Translation.RegattaCheckInListView.TermsAlert.Message.String, + preferredStyle: .alert + ) + let showTermsAction = UIAlertAction(title: Translation.RegattaCheckInListView.TermsAlert.ShowTermsAction.Title.String, style: .default) { [weak self] (action) in + UIApplication.shared.openURL(URLs.Terms) + self?.reviewTerms(completion: completion) // Review terms until user accepted terms + } + let acceptTermsAction = UIAlertAction(title: Translation.RegattaCheckInListView.TermsAlert.AcceptTermsAction.Title.String, style: .default) { (action) in + Preferences.termsAccepted = true + completion() // Terms accepted + } + alertController.addAction(showTermsAction) + alertController.addAction(acceptTermsAction) + present(alertController, animated: true, completion: nil) + } + + // MARK: 2. Review Code Convention + + fileprivate func reviewCodeConvention(completion: @escaping () -> Void) { + #if DEBUG + guard Preferences.codeConventionRead == false else { completion(); return } + let alertController = UIAlertController( + title: "Code Convention", + message: "Please try to respect the code convention which is used for this project.", + preferredStyle: .alert + ) + let showCodeConventionAction = UIAlertAction(title: "Code Convention", style: .default) { [weak self] (action) in + UIApplication.shared.openURL(URLs.CodeConvention) + self?.reviewCodeConvention(completion: completion) + } + let okAction = UIAlertAction(title: "OK", style: .default) { (action) in + Preferences.codeConventionRead = true + completion() + } + alertController.addAction(showCodeConventionAction) + alertController.addAction(okAction) + present(alertController, animated: true, completion: nil) + #else + completion() + #endif + } + + // MARK: 3. Review GPS Fixes + + fileprivate func reviewGPSFixes(completion: @escaping () -> Void) { + SVProgressHUD.show() + let checkIns = coreDataManager.fetchCheckIns() ?? [] + reviewGPSFixes(checkIns: checkIns) { [weak self] in + self?.reviewGPSFixesCompleted(completion: completion) + } + } + + fileprivate func reviewGPSFixes(checkIns: [CheckIn], completion: @escaping () -> Void) { + guard checkIns.count > 0 else { completion(); return } + let gpsFixController = GPSFixController.init(checkIn: checkIns[0], coreDataManager: coreDataManager) + gpsFixController.sendAll(completion: { [weak self] (withSuccess) in + self?.reviewGPSFixes(checkIns: Array(checkIns[1.. Void) { + SVProgressHUD.popActivity() + completion() + } + + fileprivate func reviewNewCheckIn(completion: @escaping (_ checkIn: CheckIn?) -> Void) { + guard let urlString = Preferences.newCheckInURL else { completion(nil); return } + guard let checkInData = CheckInData(urlString: urlString) else { completion(nil); return } + regattaCheckInController.checkInWithViewController(self, checkInData: checkInData, success: { (checkIn) in + Preferences.newCheckInURL = nil + completion(checkIn) + }) { (error) in + Preferences.newCheckInURL = nil // TODO: Ask user for retry, retry later or dismiss before deleting check-in url + completion(nil) + } + } + + // MARK: - Notifications + + fileprivate func subscribeForNewCheckInURLNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(CheckInTableViewController.newCheckInURLNotification(_:)), + name: NSNotification.Name(rawValue: Preferences.NotificationType.NewCheckInURLChanged), + object: nil + ) + } + + fileprivate func unsubscribeFromNewCheckInURLNotifications() { + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name(rawValue: Preferences.NotificationType.NewCheckInURLChanged), + object: nil + ) + } + + @objc fileprivate func newCheckInURLNotification(_ notification: Notification) { + DispatchQueue.main.async(execute: { + self.reviewNewCheckIn(completion: { checkIn in + logInfo(name: "\(#function)", info: "Review new check-in done.") + self.performSegue(forCheckIn: checkIn) + }) + }) + } + + fileprivate func subscribeForTrainingEndpointChangedNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(trainingEndpointChanged(_:)), + name: NSNotification.Name(rawValue: Preferences.NotificationType.TrainingEndpointChanged), + object: nil + ) + } + + fileprivate func unsubscribeFromTrainingEndpointChangedNotifications() { + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name(rawValue: Preferences.NotificationType.TrainingEndpointChanged), + object: nil + ) + } + + @objc fileprivate func trainingEndpointChanged(_ notification: Notification) { + DispatchQueue.main.async(execute: { + guard let trainingEndpoint = notification.userInfo?[Preferences.UserInfo.TrainingEndpoint] as? String else { return } + self.signUpController = SignUpController(baseURLString: trainingEndpoint) + }) + } + + // MARK: - Layout + + fileprivate func layout() { + layoutFooterView() + } + + fileprivate func layoutFooterView() { + let height = footerTextView.sizeThatFits(CGSize(width: footerView.frame.width, height: CGFloat.greatestFiniteMagnitude)).height + footerView.frame = CGRect( + x: footerView.frame.origin.x, + y: footerView.frame.origin.y, + width: footerView.frame.width, + height: height + ) + } + + // MARK: - TableView + + fileprivate func tableViewDeselectRow() { + if let indexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: indexPath, animated: true) + } + } + + // MARK: - Segues + + func performSegue(forCheckIn checkIn: CheckIn?) { + segueCheckIn = checkIn + guard let checkIn = checkIn else { + logInfo(name: "\(#function)", info: "check-in is nil") + return + } + if checkIn is BoatCheckIn { + if checkIn.isTraining.boolValue { + performSegue(withIdentifier: CheckInSegue.TrainingBoat, sender: self) + } else { + performSegue(withIdentifier: CheckInSegue.RegattaBoat, sender: self) + } + } else if checkIn is CompetitorCheckIn { + if checkIn.isTraining.boolValue { + performSegue(withIdentifier: CheckInSegue.TrainingCompetitor, sender: self) + } else { + performSegue(withIdentifier: CheckInSegue.RegattaCompetitor, sender: self) + } + } else if (checkIn is MarkCheckIn) { + if checkIn.isTraining.boolValue { + performSegue(withIdentifier: CheckInSegue.TrainingMark, sender: self) + } else { + performSegue(withIdentifier: CheckInSegue.RegattaMark, sender: self) + } + } else { + logInfo(name: "\(#function)", info: "unknown check-in type") + } + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if CheckInSegues.boat.contains(identifier) { + return segueCheckIn != nil && segueCheckIn is BoatCheckIn + } else if CheckInSegues.competitor.contains(identifier) { + return segueCheckIn != nil && segueCheckIn is CompetitorCheckIn + } else if CheckInSegues.mark.contains(identifier) { + return segueCheckIn != nil && segueCheckIn is MarkCheckIn + } + return true + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if let identifier = segue.identifier { + if CheckInSegues.boat.contains(identifier) { + if let boatCheckIn = segueCheckIn as? BoatCheckIn { + if let regattaBoatVC = segue.destination as? RegattaBoatViewController { + regattaBoatVC.boatCheckIn = boatCheckIn + regattaBoatVC.boatCoreDataManager = coreDataManager + } else if let trainingBoatVC = segue.destination as? TrainingBoatViewController { + trainingBoatVC.boatCheckIn = boatCheckIn + trainingBoatVC.boatCoreDataManager = coreDataManager + } + } + } else if CheckInSegues.competitor.contains(identifier) { + if let competitorCheckIn = segueCheckIn as? CompetitorCheckIn { + if let regattaCompetitorVC = segue.destination as? RegattaCompetitorViewController { + regattaCompetitorVC.competitorCheckIn = competitorCheckIn + regattaCompetitorVC.competitorCoreDataManager = coreDataManager + } else if let trainingCompetitorVC = segue.destination as? TrainingCompetitorViewController { + trainingCompetitorVC.competitorCheckIn = competitorCheckIn + trainingCompetitorVC.competitorCoreDataManager = coreDataManager + } + } + } else if CheckInSegues.mark.contains(identifier) { + if let markCheckIn = segueCheckIn as? MarkCheckIn { + if let regattaMarkVC = segue.destination as? RegattaMarkViewController { + regattaMarkVC.markCheckIn = markCheckIn + regattaMarkVC.markCoreDataManager = coreDataManager + } else if let trainingMarkVC = segue.destination as? TrainingMarkViewController { + trainingMarkVC.markCheckIn = markCheckIn + trainingMarkVC.markCoreDataManager = coreDataManager + } + } + } else if (identifier == CheckInSegue.NewCheckIn) { + guard let newCheckInNC = segue.destination as? UINavigationController else { return } + guard let newCheckInVC = newCheckInNC.viewControllers[0] as? NewCheckInViewController else { return } + newCheckInVC.coreDataManager = coreDataManager + newCheckInVC.delegate = self + } + } + } + + // MARK: - Actions + + @IBAction func optionButtonTapped(_ sender: Any) { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = sender as? UIBarButtonItem + } + let settingsAction = UIAlertAction(title: Translation.SettingsView.Title.String, style: .default) { [weak self] action in + self?.presentSettingsViewController() + } + let infoAction = UIAlertAction(title: Translation.Common.Info.String, style: .default) { [weak self] action in + self?.presentAboutViewController() + } + let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) + alertController.addAction(settingsAction) + alertController.addAction(infoAction) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + } + + // TODO: Activate Training Feature -> Set sort button visible in IB + @IBAction func sortButtonTapped(_ sender: Any) { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = sender as? UIBarButtonItem + } + let dateAction = UIAlertAction(title: "DATE", style: .default) { [weak self] action in + self?.sortByDate() + } + let leaderboardAction = UIAlertAction(title: "LEADERBOARD", style: .default) { [weak self] action in + self?.sortByLeaderboard() + } + let competitorAction = UIAlertAction(title: "COMPETITOR", style: .default) { [weak self] action in + self?.sortByCompetitor() + } + let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) + alertController.addAction(dateAction) + alertController.addAction(leaderboardAction) + alertController.addAction(competitorAction) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + } + + func sortByDate() { + fetchedResultsController?.fetchRequest.sortDescriptors = [NSSortDescriptor(key: "event.startDate", ascending: true)] + setupTableViewDataSource() + } + + func sortByLeaderboard() { + fetchedResultsController?.fetchRequest.sortDescriptors = [NSSortDescriptor(key: "leaderboard.name", ascending: true)] + setupTableViewDataSource() + } + + func sortByCompetitor() { + fetchedResultsController?.fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] + setupTableViewDataSource() + } + + // MARK: - Properties + + fileprivate lazy var titleView: TitleView = { + return TitleView( + title: Translation.TrainingCheckInListView.Title.String, + subtitle: self.signUpController.userName ?? "" + ) + }() + + lazy var fetchedResultsController: NSFetchedResultsController? = { + let fetchedResultsController = self.coreDataManager.checkInFetchedResultsController() + fetchedResultsController.delegate = self + return fetchedResultsController + }() + + lazy var regattaCheckInController: RegattaCheckInController = { + return RegattaCheckInController(coreDataManager: self.coreDataManager) + }() + + lazy var signUpController: SignUpController = { + return SignUpController(baseURLString: Preferences.trainingEndpoint) + }() + + lazy var coreDataManager: CoreDataManager = { + return CoreDataManager(name: "CoreData") + }() + +} + +// MARK: - UITableViewDataSource + +extension CheckInTableViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return fetchedResultsController?.sections?[section].numberOfObjects ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell() + if let checkIn = fetchedResultsController?.object(at: indexPath) { + configureCell(cell, forCheckIn: checkIn) + } + return cell + } + + func configureCell(_ cell: UITableViewCell, forCheckIn checkIn: CheckIn) { + guard let regattaCheckInTableViewCell = cell as? CheckInTableViewCell else { return } + regattaCheckInTableViewCell.eventLabel.text = checkIn.event.name + regattaCheckInTableViewCell.leaderboardLabel.text = checkIn.leaderboard.name + regattaCheckInTableViewCell.competitorLabel.textAlignment = .right + regattaCheckInTableViewCell.competitorLabel.backgroundColor = nil + if let boatCheckIn = checkIn as? BoatCheckIn { + regattaCheckInTableViewCell.competitorLabel.text = boatCheckIn.displayName() + regattaCheckInTableViewCell.competitorLabel.textAlignment = .center + if let color = UIColor.init(hexString: boatCheckIn.color) { + regattaCheckInTableViewCell.competitorLabel.backgroundColor = color + regattaCheckInTableViewCell.competitorLabel.textColor = UIColor.init(contrastColorFor: color) + } + } else { + regattaCheckInTableViewCell.competitorLabel.text = checkIn.name + } + } + +} + +// MARK: - UITableViewDelegate + +extension CheckInTableViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + performSegue(forCheckIn: fetchedResultsController?.object(at: indexPath)) + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + cell.removeSeparatorInset() + } + +} + +// MARK: - NSFetchedResultsControllerDelegate + +extension CheckInTableViewController: NSFetchedResultsControllerDelegate { + + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + tableView.beginUpdates() + } + + func controller( + _ controller: NSFetchedResultsController, + didChange object: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, + newIndexPath: IndexPath?) + { + switch type { + case .insert: + if indexPath == nil { // Workaround for an iOS 8.4 bug https://forums.developer.apple.com/thread/12184 + tableView.insertRows(at: [newIndexPath!], with: UITableViewRowAnimation.automatic) + } + case .update: + if let cell = tableView.cellForRow(at: indexPath!) { + if let checkIn = fetchedResultsController?.object(at: indexPath!) { + configureCell(cell, forCheckIn: checkIn) + } + tableView.reloadRows(at: [indexPath!], with: UITableViewRowAnimation.automatic) + } + case .move: + tableView.deleteRows(at: [indexPath!], with: UITableViewRowAnimation.automatic) + tableView.insertRows(at: [newIndexPath!], with: .automatic) + case .delete: + tableView.deleteRows(at: [indexPath!], with: UITableViewRowAnimation.automatic) + } + } + + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + tableView.endUpdates() + setupTableViewHeader() + setupTableViewFooter() + } + +} + +// MARK: - NewCheckInViewControllerDelegate + +extension CheckInTableViewController: NewCheckInViewControllerDelegate { + + func newCheckInViewController(_ controller: NewCheckInViewController, didCheckIn checkIn: CheckIn) { + performSegue(forCheckIn: checkIn) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift index 059f969c6b4..68653c67bfe 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift @@ -18,7 +18,7 @@ class CompetitorSessionController: SessionController { success: @escaping (_ teamImageURL: String) -> Void, failure: @escaping (_ error: Error) -> Void ) { - requestManager.postTeamImageData( + checkInRequestManager.postTeamImageData( imageData: imageData, competitorID: competitorID, success: success, diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionViewController.swift new file mode 100644 index 00000000000..4e9627402c6 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionViewController.swift @@ -0,0 +1,41 @@ +// +// CompetitorSessionViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class CompetitorSessionViewController: SessionViewController { + + struct CompetitorSessionSegue { + static let EmbedCompetitor = "EmbedCompetitor" + } + + weak var competitorCheckIn: CompetitorCheckIn! + weak var competitorCoreDataManager: CoreDataManager! + weak var competitorViewController: CompetitorViewController? + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == CompetitorSessionSegue.EmbedCompetitor) { + if let competitorViewController = segue.destination as? CompetitorViewController { + competitorViewController.competitorCheckIn = competitorCheckIn + competitorViewController.competitorSessionController = competitorSessionController + competitorViewController.competitorCoreDataManager = competitorCoreDataManager + self.competitorViewController = competitorViewController + } + } + } + + // MARK: - Properties + + lazy var competitorSessionController: CompetitorSessionController = { + return CompetitorSessionController(checkIn: self.competitorCheckIn, coreDataManager: self.competitorCoreDataManager) + }() + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift index c2c745a023c..4b52115539e 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift @@ -2,13 +2,17 @@ // CompetitorViewController.swift // SAPTracker // -// Created by computing on 10/11/14. -// Copyright (c) 2014 com.sap.sailing. All rights reserved. +// Created by Raimund Wege on 13.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. // -import Foundation +import UIKit -class CompetitorViewController : SessionViewController, UINavigationControllerDelegate { +class CompetitorViewController: UIViewController { + + weak var competitorCheckIn: CompetitorCheckIn! + weak var competitorCoreDataManager: CoreDataManager! + weak var competitorSessionController: CompetitorSessionController! @IBOutlet weak var teamImageView: UIImageView! @IBOutlet weak var teamImageAddButton: UIButton! @@ -16,101 +20,57 @@ class CompetitorViewController : SessionViewController, UINavigationControllerDe @IBOutlet weak var teamImageRetryButton: UIButton! @IBOutlet weak var competitorNameLabel: UILabel! @IBOutlet weak var competitorFlagImageView: UIImageView! - @IBOutlet weak var competitorSailLabel: UILabel! - @IBOutlet weak var regattaStartLabel: UILabel! - @IBOutlet weak var countdownView: UIView! - @IBOutlet weak var countdownViewHeight: NSLayoutConstraint! - @IBOutlet weak var countdownDaysLabel: UILabel! - @IBOutlet weak var countdownDaysTitleLabel: UILabel! - @IBOutlet weak var countdownHoursLabel: UILabel! - @IBOutlet weak var countdownHoursTitleLabel: UILabel! - @IBOutlet weak var countdownMinutesLabel: UILabel! - @IBOutlet weak var countdownMinutesTitleLabel: UILabel! - @IBOutlet weak var leaderboardButton: UIButton! - @IBOutlet weak var eventButton: UIButton! - @IBOutlet weak var announcementLabel: UILabel! - - var competitorCheckIn: CompetitorCheckIn! - - var countdownTimer: Timer? + @IBOutlet weak var competitorSailIDLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() - delegate = self setup() - update() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - refresh() } // MARK: - Setup fileprivate func setup() { setupButtons() - setupCountdownTimer() setupLocalization() - setupNavigationBar() } fileprivate func setupButtons() { - eventButton.setBackgroundImage(Images.BlueHighlighted, for: .highlighted) - leaderboardButton.setBackgroundImage(Images.BlueHighlighted, for: .highlighted) - startTrackingButton.setBackgroundImage(Images.GreenHighlighted, for: .highlighted) - } - - fileprivate func setupCountdownTimer() { - countdownTimer?.invalidate() - countdownTimer = Timer.scheduledTimer( - timeInterval: 1, - target: self, - selector: #selector(CompetitorViewController.countdownTimerTick), - userInfo: nil, - repeats: true - ) - countdownTimerTick() + makeTranslucent(button: teamImageAddButton) + makeTranslucent(button: teamImageRetryButton) } fileprivate func setupLocalization() { - announcementLabel.text = Translation.CompetitorView.AnnouncementLabel.Text.String - countdownDaysTitleLabel.text = Translation.CompetitorView.CountdownDaysTitleLabel.Text.String - countdownHoursTitleLabel.text = Translation.CompetitorView.CountdownHoursTitleLabel.Text.String - countdownMinutesTitleLabel.text = Translation.CompetitorView.CountdownMinutesTitleLabel.Text.String - eventButton.setTitle(Translation.CompetitorView.EventButton.Title.String, for: .normal) - leaderboardButton.setTitle(Translation.LeaderboardView.Title.String, for: .normal) - startTrackingButton.setTitle(Translation.CompetitorView.StartTrackingButton.Title.String, for: .normal) teamImageAddButton.setTitle(Translation.CompetitorView.TeamImageAddButton.Title.String, for: .normal) teamImageRetryButton.setTitle(Translation.CompetitorView.TeamImageUploadRetryButton.Title.String, for: .normal) } - fileprivate func setupNavigationBar() { - navigationItem.titleView = TitleView(title: competitorCheckIn.event.name, subtitle: competitorCheckIn.leaderboard.name) - navigationController?.navigationBar.setNeedsLayout() - } - - // MARK: - Update - - fileprivate func update() { - SVProgressHUD.show() - competitorSessionController.update { - self.refresh() - SVProgressHUD.popActivity() - } - } - // MARK: - Refresh - fileprivate func refresh() { - refreshCompetitor() - refreshTeamImage() + func refresh(_ animated: Bool) { + refreshCompetitor(animated) + refreshTeamImage(animated) + } + + fileprivate func refreshCompetitor(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshCompetitor() } + } else { + refreshCompetitor() + } } fileprivate func refreshCompetitor() { competitorNameLabel.text = competitorCheckIn.name competitorFlagImageView.image = UIImage(named: competitorCheckIn.countryCode) - competitorSailLabel.text = competitorCheckIn.sailID + competitorSailIDLabel.text = competitorCheckIn.sailID + } + + fileprivate func refreshTeamImage(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshTeamImage() } + } else { + refreshTeamImage() + } } fileprivate func refreshTeamImage() { @@ -139,7 +99,7 @@ class CompetitorViewController : SessionViewController, UINavigationControllerDe } // MARK: - TeamImage - + fileprivate func setTeamImageWithURLRequest(urlString: String?, completion: @escaping (_ withSuccess: Bool) -> Void) { setTeamImageWithURLRequest(urlString: urlString, success: { () in completion(true) @@ -151,19 +111,18 @@ class CompetitorViewController : SessionViewController, UINavigationControllerDe fileprivate func setTeamImageWithURLRequest(urlString: String?, success: @escaping () -> Void, failure: @escaping () -> Void) { guard let string = urlString else { failure(); return } guard let url = URL(string: string) else { failure(); return } - teamImageView.setImageWith( - URLRequest(url: url), - placeholderImage: nil, - success: { (request, response, image) in self.setTeamImageWithURLSuccess(image: image, success: success) }, - failure: { (request, response, error) in self.setTeamImageWithURLFailure(error: error, failure: failure) } - ) + teamImageView.setImageWith(URLRequest(url: url), placeholderImage: nil, success: { (request, response, image) in + self.setTeamImageWithURLSuccess(image: image, success: success) + }) { (request, response, error) in + self.setTeamImageWithURLFailure(error: error, failure: failure) + } } fileprivate func setTeamImageWithURLSuccess(image: UIImage, success: () -> Void) { teamImageView.image = image competitorCheckIn.teamImageRetry = false competitorCheckIn.teamImageData = UIImageJPEGRepresentation(image, 0.8) - CoreDataManager.sharedManager.saveContext() + competitorCoreDataManager.saveContext() success() } @@ -172,92 +131,36 @@ class CompetitorViewController : SessionViewController, UINavigationControllerDe failure() } - // MARK: - Timer - - @objc fileprivate func countdownTimerTick() { - if competitorCheckIn.event.startDate - Date().timeIntervalSince1970 > 0 { - regattaStartLabel.text = Translation.CompetitorView.RegattaStartLabel.Text.BeforeRegattaDidStart.String - let duration = competitorCheckIn.event.startDate - Date().timeIntervalSince1970 - let days = Int(duration / (60 * 60 * 24)) - let hours = Int(duration / (60 * 60)) - (days * 24) - let minutes = Int(duration / 60) - (days * 24 * 60) - (hours * 60) - countdownDaysLabel.text = String(format: "%02d", days) - countdownHoursLabel.text = String(format: "%02d", hours) - countdownMinutesLabel.text = String(format: "%02d", minutes) - countdownView.isHidden = false - countdownViewHeight.constant = 60 - } else { - regattaStartLabel.text = Translation.CompetitorView.RegattaStartLabel.Text.AfterRegattaDidStart.String - countdownView.isHidden = true - countdownViewHeight.constant = 0 - } - } - // MARK: - Actions - @IBAction func teamImageAddButtonTapped(_ sender: AnyObject) { + @IBAction func teamImageAddButtonTapped(_ sender: Any) { showSelectImageAlert() } - @IBAction func teamImageEditButtonTapped(_ sender: AnyObject) { + @IBAction func teamImageEditButtonTapped(_ sender: Any) { showSelectImageAlert() } - @IBAction func teamImageRetryButtonTapped(_ sender: AnyObject) { + @IBAction func teamImageRetryButtonTapped(_ sender: Any) { if let data = competitorCheckIn.teamImageData { uploadTeamImageData(imageData: data) } } - @IBAction func eventButtonTapped(_ sender: UIButton) { - if let eventURL = competitorCheckIn.eventURL() { - UIApplication.shared.openURL(eventURL) - } - } - - @IBAction func optionButtonTapped(_ sender: AnyObject) { - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - if let popoverController = alertController.popoverPresentationController { - popoverController.barButtonItem = sender as? UIBarButtonItem - } - let settingsAction = UIAlertAction(title: Translation.SettingsView.Title.String, style: .default) { (action) in - self.performSegue(withIdentifier: Segue.Settings, sender: self) - } - let checkOutAction = UIAlertAction(title: Translation.CompetitorView.OptionSheet.CheckOutAction.Title.String, style: .default) { (action) in - self.checkOut() - } - let replaceImageAction = UIAlertAction(title: Translation.CompetitorView.OptionSheet.ReplaceImageAction.Title.String, style: .default) { (action) in - self.showSelectImageAlert() - } - let updateAction = UIAlertAction(title: Translation.CompetitorView.OptionSheet.UpdateAction.Title.String, style: .default) { (action) -> Void in - self.update() - } - let aboutAction = UIAlertAction(title: Translation.Common.Info.String, style: .default) { (action) -> Void in - self.performSegue(withIdentifier: Segue.About, sender: alertController) - } - let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) - alertController.addAction(settingsAction) - alertController.addAction(checkOutAction) - alertController.addAction(replaceImageAction) - alertController.addAction(updateAction) - alertController.addAction(aboutAction) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - // MARK: - Alerts - fileprivate func showSelectImageAlert() { + func showSelectImageAlert() { if UIImagePickerController.isSourceTypeAvailable(.camera) && UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { - let alertController = UIAlertController(title: Translation.CompetitorView.SelectImageAlert.Title.String, - message: Translation.CompetitorView.SelectImageAlert.Message.String, - preferredStyle: .alert + let alertController = UIAlertController( + title: Translation.CompetitorView.SelectImageAlert.Title.String, + message: Translation.CompetitorView.SelectImageAlert.Message.String, + preferredStyle: .alert ) - let cameraAction = UIAlertAction(title: Translation.CompetitorView.SelectImageAlert.CameraAction.Title.String, style: .default) { (action) in - self.showImagePicker(sourceType: .camera) + let cameraAction = UIAlertAction(title: Translation.CompetitorView.SelectImageAlert.CameraAction.Title.String, style: .default) { [weak self] (action) in + self?.showImagePicker(sourceType: .camera) } - let photoLibraryAction = UIAlertAction(title: Translation.CompetitorView.SelectImageAlert.PhotoLibraryAction.Title.String, style: .default) { (action) in - self.showImagePicker(sourceType: .photoLibrary) + let photoLibraryAction = UIAlertAction(title: Translation.CompetitorView.SelectImageAlert.PhotoLibraryAction.Title.String, style: .default) { [weak self] (action) in + self?.showImagePicker(sourceType: .photoLibrary) } let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) alertController.addAction(cameraAction) @@ -271,26 +174,13 @@ class CompetitorViewController : SessionViewController, UINavigationControllerDe } } - // MARK: - Segues +} + +// MARK: - UINavigationControllerDelegate + +extension CompetitorViewController: UINavigationControllerDelegate { - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if (segue.identifier == Segue.Tracking) { - let trackingNC = segue.destination as! UINavigationController - let trackingVC = trackingNC.viewControllers[0] as! TrackingViewController - trackingVC.checkIn = competitorCheckIn - trackingVC.sessionController = competitorSessionController - } else if (segue.identifier == Segue.Leaderboard) { - let leaderboardNC = segue.destination as! UINavigationController - let leaderboardVC = leaderboardNC.viewControllers[0] as! LeaderboardViewController - leaderboardVC.checkIn = competitorCheckIn - } - } - // MARK: - Properties - - fileprivate lazy var competitorSessionController: CompetitorSessionController = { - return CompetitorSessionController(checkIn: self.competitorCheckIn) - }() } @@ -318,7 +208,7 @@ extension CompetitorViewController: UIImagePickerControllerDelegate { fileprivate func pickedImage(image: UIImage) { teamImageView.image = image competitorCheckIn.teamImageData = UIImageJPEGRepresentation(image, 0.8) - CoreDataManager.sharedManager.saveContext() + competitorCoreDataManager.saveContext() if let data = competitorCheckIn.teamImageData { uploadTeamImageData(imageData: data) } @@ -328,28 +218,25 @@ extension CompetitorViewController: UIImagePickerControllerDelegate { fileprivate func uploadTeamImageData(imageData: Data!) { SVProgressHUD.show() - competitorSessionController.postTeamImageData(imageData: imageData, competitorID: competitorCheckIn.competitorID, - success: { (teamImageURL) in - SVProgressHUD.popActivity() - self.uploadTeamImageDataSuccess(teamImageURL: teamImageURL) - }, - failure: { (error) in - SVProgressHUD.popActivity() - self.uploadTeamImageDataFailure(error: error) - } - ) + competitorSessionController.postTeamImageData(imageData: imageData, competitorID: competitorCheckIn.competitorID, success: { (teamImageURL) in + SVProgressHUD.popActivity() + self.uploadTeamImageDataSuccess(teamImageURL: teamImageURL) + }) { (error) in + SVProgressHUD.popActivity() + self.uploadTeamImageDataFailure(error: error) + } } fileprivate func uploadTeamImageDataSuccess(teamImageURL: String) { competitorCheckIn.teamImageRetry = false competitorCheckIn.teamImageURL = teamImageURL - CoreDataManager.sharedManager.saveContext() + competitorCoreDataManager.saveContext() refreshTeamImage() } fileprivate func uploadTeamImageDataFailure(error: Error) { competitorCheckIn.teamImageRetry = true - CoreDataManager.sharedManager.saveContext() + competitorCoreDataManager.saveContext() showUploadTeamImageFailureAlert(error: error) refreshTeamImage() } @@ -368,25 +255,3 @@ extension CompetitorViewController: UIImagePickerControllerDelegate { } } - -// MARK: SessionViewControllerDelegate - -extension CompetitorViewController: SessionViewControllerDelegate { - - func performCheckOut() { - competitorSessionController.checkOut { (withSuccess) in - self.performCheckOutCompleted(withSuccess: withSuccess) - } - } - - fileprivate func performCheckOutCompleted(withSuccess: Bool) { - CoreDataManager.sharedManager.deleteObject(object: competitorCheckIn) - CoreDataManager.sharedManager.saveContext() - self.navigationController!.popViewController(animated: true) - } - - func startTracking() throws { - try competitorSessionController.startTracking() - } - -} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Constants.swift b/ios/SAPTracker/src/xcode/SAPTracker/Constants.swift index 592922ab18d..dc45af50b01 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Constants.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/Constants.swift @@ -10,6 +10,11 @@ import Foundation struct Application { static let Title = "SAP Sail InSight" + static let RequestTimeout: TimeInterval = 7 +} + +struct AutoCourse { + static let MinGPSFixesNeeded = 10 } struct URLs { @@ -18,36 +23,150 @@ struct URLs { } struct Colors { - static let Blue = UIColor(hex:0x009de0) - static let BlueHighlighted = UIColor(hex:0x007aad) - static let Gray = UIColor(hex:0xf3f3f3) - static let GrayHighlighted = UIColor(hex:0xf3f3f3) - static let Green = UIColor(hex:0x0e7733) - static let GreenHighlighted = UIColor(hex:0x08451e) - static let Red = UIColor(hex:0xcd201b) - static let RedHighlighted = UIColor(hex:0x991814) + static let BlueButton = UIColor(hex:0x009de0) + static let BlueButtonHighlighted = UIColor(hex:0x007aad) + static let BlueButtonTitleColor = UIColor.white + static let GrayButton = UIColor(hex:0xf3f3f3) + static let GrayButtonBorder = UIColor(hex:0x999999) + static let GrayButtonHighlighted = UIColor(hex:0xbfbfbf) + static let GrayButtonTitleColor = UIColor.black + static let GreenButton = UIColor(hex:0x0e7733) + static let GreenButtonHighlighted = UIColor(hex:0x08451e) + static let GreenButtonTitleColor = UIColor.white + static let RedButton = UIColor(hex:0xcd201b) + static let RedButtonHighlighted = UIColor(hex:0x991814) + static let RedButtonTitleColor = UIColor.white + static let TranslucentButton = UIColor(hex:0xffffff, alpha: 0.5) + static let TranslucentButtonBorder = UIColor(hex:0xffffff) + static let TranslucentButtonHighlighted = UIColor(hex:0xffffff, alpha: 0.5) + static let TranslucentButtonTitleColor = UIColor.white static let NavigationBarTitleColor = UIColor.black static let NavigationBarTintColor = UIColor(hex: 0x009de0) } -struct FontNames { - static let OpenSans = "OpenSans" - static let OpenSansBold = "OpenSans-Bold" -} - -struct Fonts { - static let OpenSansBold17 = UIFont(name: FontNames.OpenSansBold, size: CGFloat(17.0)) ?? UIFont.systemFont(ofSize: 17.0) - static let OpenSansBold13 = UIFont(name: FontNames.OpenSansBold, size: CGFloat(13.0)) ?? UIFont.systemFont(ofSize: 13.0) - static let OpenSans10 = UIFont(name: FontNames.OpenSans, size: CGFloat(10.0)) ?? UIFont.systemFont(ofSize: 10.0) -} - struct Images { - static let Blue = UIImage(color: Colors.Blue) - static let BlueHighlighted = UIImage(color: Colors.BlueHighlighted) - static let Gray = UIImage(color: Colors.Gray) - static let GrayHighlighted = UIImage(color: Colors.GrayHighlighted) - static let Green = UIImage(color: Colors.Green) - static let GreenHighlighted = UIImage(color: Colors.GreenHighlighted) - static let Red = UIImage(color: Colors.Red) - static let RedHighlighted = UIImage(color: Colors.RedHighlighted) + static let BlueButton = UIImage(color: Colors.BlueButton) + static let BlueButtonHighlighted = UIImage(color: Colors.BlueButtonHighlighted) + static let GrayButton = UIImage(color: Colors.GrayButton) + static let GrayButtonHighlighted = UIImage(color: Colors.GrayButtonHighlighted) + static let GreenButton = UIImage(color: Colors.GreenButton) + static let GreenButtonHighlighted = UIImage(color: Colors.GreenButtonHighlighted) + static let RedButton = UIImage(color: Colors.RedButton) + static let RedButtonHighlighted = UIImage(color: Colors.RedButtonHighlighted) + static let TranslucentButton = UIImage(color: Colors.TranslucentButton) + static let TranslucentButtonHighlighted = UIImage(color: Colors.TranslucentButtonHighlighted) } + +let BoatClassNames = [ + "18Footer", + "2.4 Meter", + "12 Meter", + "29er", + "49er", + "49erFX", + "420", + "470", + "5O5", + "5.5mR", + "8mR", + "A-Catamaran", + "Albin Express", + "Albin Ballad", + "B/ONE", + "Cadet", + "International Canoe", + "Canoe Taifun", + "Contender", + "C&C 30", + "D-One", + "Dragon Int.", + "Delphia 24", + "Dyas", + "Extreme 40", + "D35", + "Elliott 6m", + "Europe Int.", + "Formula 18", + "Farr 30", + "Farr 280", + "Finn", + "Flying Dutchman", + "Flying Phantom", + "Folkboat", + "FUN", + "Formula 16", + "GC 32", + "GP 26", + "Hobie 16", + "H-Boat", + "Hansa 303", + "Hobie Tiger", + "Hobie Wild Cat", + "International 14", + "J/22", + "J/24", + "J/70", + "J/80", + "J/88", + "J/105", + "J/111", + "Kielzugvogel", + "Kite", + "Laser 2", + "Laser 4.7", + "Laser Radial", + "Laser Int.", + "Laser SB3", + "Lago 26", + "Longtze", + "M32", + "Melges 20", + "Melges 24", + "Mini Transat 6.50", + "Musto Skiff", + "Nacra 15", + "Nacra 17", + "Nacra 17 Foiling", + "O-Jolle", + "OK Dinghy", + "O'pen BIC", + "Optimist", + "Pirate", + "Platu 25", + "PWA", + "RC44", + "RS 100", + "RS 200", + "RS 400", + "RS 500", + "RS 800", + "RS Aero", + "RS:X", + "RS Feva", + "SKUD 18", + "Sonar", + "Soling", + "Splash Blue", + "Splash Red", + "Splash Green", + "Star", + "Streamline", + "Sunbeam 22", + "Swan 45", + "Tartan 10", + "Techno 293", + "Techno 293 Plus", + "Teeny", + "Tornado Catamaran", + "Tom 28 MAX", + "Trias", + "Viper 640", + "VO60", + "VX ONE", + "Weta", + "X-99", + "ORC", + "ORC Club", + "ORC International", + "PHRF" +].sorted { $0 < $1 } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion index cdd244c9cea..1d5ea989f87 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion +++ b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 3.xcdatamodel + CoreData 4.xcdatamodel diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents new file mode 100644 index 00000000000..d8f009db476 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/CoreData 4.xcdatamodel/contents @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CoreDataManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/CoreDataManager.swift index 6c8f9ed6a30..a7e8073795e 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CoreDataManager.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CoreDataManager.swift @@ -9,9 +9,10 @@ import Foundation import CoreData -open class CoreDataManager: NSObject { +class CoreDataManager: NSObject { fileprivate enum Entities: String { + case BoatCheckIn case CheckIn case CompetitorCheckIn case Event @@ -20,13 +21,20 @@ open class CoreDataManager: NSObject { case MarkCheckIn } - open class var sharedManager: CoreDataManager { - struct Singleton { - static let sharedManager = CoreDataManager() - } - return Singleton.sharedManager - } + let name: String + init(name: String) { + self.name = name + super.init() + } + + class var shared: CoreDataManager { + struct Singleton { + static let shared = CoreDataManager(name: "CoreData") + } + return Singleton.shared + } + // MARK: - Fetch func fetchCheckIns() -> [CheckIn]? { @@ -44,6 +52,12 @@ open class CoreDataManager: NSObject { func fetchCheckIn(checkInData: CheckInData) -> CheckIn? { switch checkInData.type { + case .boat: + return fetchBoatCheckIn( + eventID: checkInData.eventID, + leaderboardName: checkInData.leaderboardName, + boatID: checkInData.boatID! + ) case .competitor: return fetchCompetitorCheckIn( eventID: checkInData.eventID, @@ -59,6 +73,45 @@ open class CoreDataManager: NSObject { } } + func fetchBoatCheckIn(eventID: String, leaderboardName: String, boatID: String) -> BoatCheckIn? { + let fetchRequest = NSFetchRequest() + fetchRequest.entity = NSEntityDescription.entity( + forEntityName: Entities.BoatCheckIn.rawValue, + in: managedObjectContext + ) + fetchRequest.predicate = NSPredicate( + format: "event.eventID = %@ AND leaderboard.name = %@ AND boatID = %@", + eventID, + leaderboardName, + boatID + ) + do { + let checkIns = try managedObjectContext.fetch(fetchRequest) + if checkIns.count == 0 { + return nil + } else { + return checkIns[0] + } + } catch { + logError(name: "\(#function)", error: error) + } + return nil + } + + func fetchCompetitorCheckIn(serverURL: String, boatClassName: String) -> [CompetitorCheckIn]? { + let fetchRequest = NSFetchRequest() + fetchRequest.entity = NSEntityDescription.entity(forEntityName: Entities.CompetitorCheckIn.rawValue, in: managedObjectContext) + fetchRequest.includesSubentities = true + fetchRequest.predicate = NSPredicate(format: "serverURL = %@ AND boatClassName = %@", serverURL, boatClassName) + var checkIns: [AnyObject]? + do { + checkIns = try managedObjectContext.fetch(fetchRequest) + } catch { + logError(name: "\(#function)", error: error) + } + return checkIns as? [CompetitorCheckIn] + } + func fetchCompetitorCheckIn(eventID: String, leaderboardName: String, competitorID: String) -> CompetitorCheckIn? { let fetchRequest = NSFetchRequest() fetchRequest.entity = NSEntityDescription.entity( @@ -117,16 +170,25 @@ open class CoreDataManager: NSObject { } // MARK: - Insert - + + func newBoatCheckIn() -> BoatCheckIn { + let checkIn = NSEntityDescription.insertNewObject(forEntityName: Entities.BoatCheckIn.rawValue, into: managedObjectContext) as! BoatCheckIn + checkIn.event = newEvent(checkIn: checkIn) + checkIn.leaderboard = newLeaderboard(checkIn: checkIn) + return checkIn + } + func newCompetitorCheckIn() -> CompetitorCheckIn { let checkIn = NSEntityDescription.insertNewObject(forEntityName: Entities.CompetitorCheckIn.rawValue, into: managedObjectContext) as! CompetitorCheckIn - checkIn.initialize() + checkIn.event = newEvent(checkIn: checkIn) + checkIn.leaderboard = newLeaderboard(checkIn: checkIn) return checkIn } func newMarkCheckIn() -> MarkCheckIn { let checkIn = NSEntityDescription.insertNewObject(forEntityName: Entities.MarkCheckIn.rawValue, into: managedObjectContext) as! MarkCheckIn - checkIn.initialize() + checkIn.event = newEvent(checkIn: checkIn) + checkIn.leaderboard = newLeaderboard(checkIn: checkIn) return checkIn } @@ -173,7 +235,7 @@ open class CoreDataManager: NSObject { lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) - let url = self.applicationDocumentsDirectory.appendingPathComponent("CoreData.sqlite") + let url = self.applicationDocumentsDirectory.appendingPathComponent("\(self.name).sqlite") let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true] do { logInfo(name: "\(#function)", info: "Connecting to database...") diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CreateEventData.swift b/ios/SAPTracker/src/xcode/SAPTracker/CreateEventData.swift new file mode 100644 index 00000000000..9bbd1acdac7 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CreateEventData.swift @@ -0,0 +1,31 @@ +// +// CreateEventData.swift +// SAPTracker +// +// Created by Raimund Wege on 25.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class CreateEventData: BaseData { + + fileprivate struct Keys { + static let EndDate = "eventenddate" + static let ID = "eventid" + static let LeaderboardName = "leaderboard" + static let LeaderboardGroupID = "leaderboardgroupid" + static let Name = "eventname" + static let Regatta = "regatta" + static let StartDate = "eventstartdate" + } + + var endDate: Double { get { return doubleValue(forKey: Keys.EndDate) / 1000 } } + var eventID: String { get { return stringValue(forKey: Keys.ID) } } + var name: String { get { return stringValue(forKey: Keys.Name) } } + var leaderboardName: String { get { return stringValue(forKey: Keys.LeaderboardName) } } + var leaderboardGroupID: String { get { return stringValue(forKey: Keys.LeaderboardGroupID) } } + var regatta: String { get { return stringValue(forKey: Keys.Regatta) } } + var startDate: Double { get { return doubleValue(forKey: Keys.StartDate) / 1000 } } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CreateTrainingData.swift b/ios/SAPTracker/src/xcode/SAPTracker/CreateTrainingData.swift new file mode 100644 index 00000000000..4a812ab3e4b --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CreateTrainingData.swift @@ -0,0 +1,29 @@ +// +// CreateTrainingData.swift +// SAPTracker +// +// Created by Raimund Wege on 25.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class CreateTrainingData: NSObject { + + let serverURL: String + let boatClassName: String + let sailID: String + let nationality: String + + var createEventData: CreateEventData? + var competitorID: String? + + init(serverURL: String, boatClassName: String, sailID: String, nationality: String) { + self.serverURL = serverURL + self.boatClassName = boatClassName + self.sailID = sailID + self.nationality = nationality + super.init() + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CreateTrainingViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CreateTrainingViewController.swift new file mode 100644 index 00000000000..4973533e6dc --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CreateTrainingViewController.swift @@ -0,0 +1,158 @@ +// +// CreateTrainingViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 21.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +protocol CreateTrainingViewControllerDelegate: class { + + func createTrainingViewController(_ controller: CreateTrainingViewController, didCheckIn checkIn: CheckIn) + +} + +class CreateTrainingViewController: UIViewController { + + weak var delegate: CreateTrainingViewControllerDelegate? + + weak var coreDataManager: CoreDataManager! + + @IBOutlet var boatClassPickerView: UIPickerView! + @IBOutlet var boatClassNameLabel: UILabel! + @IBOutlet var createTrainingButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + setup() + login() + } + + // MARK: - Setup + + fileprivate func setup() { + setupBoatClassPickerView() + setupButtons() + setupLocalization() + } + + fileprivate func setupBoatClassPickerView() { + if let row = BoatClassNames.index(of: Preferences.boatClassName) { + boatClassPickerView.selectRow(row, inComponent: 0, animated: true) + } + } + + fileprivate func setupButtons() { + makeBlue(button: createTrainingButton) + } + + fileprivate func setupLocalization() { + navigationItem.title = Translation.CreateTrainingView.Title.String + boatClassNameLabel.text = Translation.CreateTrainingView.BoatClassNameLabel.Text.String + createTrainingButton.setTitle(Translation.CreateTrainingView.CreateTrainingButton.Title.String, for: .normal) + } + + // MARK: - Login + + fileprivate func login() { + SVProgressHUD.show() + signUpController.login(success: { (userName) in + SVProgressHUD.dismiss() + }) { (error, message) in + SVProgressHUD.dismiss() + } + } + + // MARK: - Actions + + @IBAction func cancelButtonTapped(_ sender: Any) { + presentingViewController?.dismiss(animated: true) + } + + @IBAction func createTrainingButtonTapped(_ sender: Any) { + // TODO: sailID and nationality shouldn't be static + createTrainingAndPerformCheckIn( + forBoatClassName: BoatClassNames[boatClassPickerView.selectedRow(inComponent: 0)], + sailID: "", + nationality: "GER" + ) + } + + // MARK: - CreateTraining + + fileprivate func createTrainingAndPerformCheckIn( + forBoatClassName boatClassName: String, + sailID: String, + nationality: String) + { + SVProgressHUD.show() + self.trainingController.createTraining(forBoatClassName: boatClassName, sailID: sailID, nationality: nationality, success: { checkInData in + Preferences.boatClassName = boatClassName + self.trainingCheckInController.checkInWithViewController(self, checkInData: checkInData, success: { [weak self] (checkIn) in + SVProgressHUD.dismiss() + if let strongSelf = self { + strongSelf.dismiss(animated: true) { + strongSelf.delegate?.createTrainingViewController(strongSelf, didCheckIn: checkIn) + } + } + }) { [weak self] (error) in + SVProgressHUD.dismiss() + self?.handle(error: error) + } + }) { [weak self] (error) in + SVProgressHUD.dismiss() + self?.handle(error: error) + } + } + + // MARK: - Propeties + + fileprivate lazy var signUpController: SignUpController = { + return SignUpController(baseURLString: self.trainingController.baseURLString) + }() + + fileprivate lazy var trainingCheckInController: TrainingCheckInController = { + return TrainingCheckInController(coreDataManager: self.coreDataManager) + }() + + fileprivate lazy var trainingController: TrainingController = { + return TrainingController(coreDataManager: self.coreDataManager, baseURLString: Preferences.trainingEndpoint) + }() + + // MARK: - Helper + + fileprivate func handle(error: Error) { + if ErrorHelper.isResponseUnauthorized(error: error as NSError) { + self.signUpController.loginWithViewController(self) + } else { + self.showAlert(forError: error) + } + } + +} + +// MARK: - UIPickerViewDataSource + +extension CreateTrainingViewController: UIPickerViewDataSource { + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return BoatClassNames.count + } + +} + +// MARK: - UIPickerViewDelegate + +extension CreateTrainingViewController: UIPickerViewDelegate { + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return BoatClassNames[row] + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/ErrorHelper.swift b/ios/SAPTracker/src/xcode/SAPTracker/ErrorHelper.swift new file mode 100644 index 00000000000..eb5a9857740 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/ErrorHelper.swift @@ -0,0 +1,20 @@ +// +// ResponseErrorHelper.swift +// SAPTracker +// +// Created by Raimund Wege on 02.01.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +class ErrorHelper: NSObject { + + static func isResponseUnauthorized(error: NSError) -> Bool { + if let response = error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey] as? HTTPURLResponse { + return response.statusCode == 401 + } + return false + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Extensions.swift b/ios/SAPTracker/src/xcode/SAPTracker/Extensions.swift index 454fd302c5c..0ef13f552d3 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Extensions.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/Extensions.swift @@ -26,21 +26,73 @@ func logError(name: String, error: String) { #endif } +// MARK: - UIButton + +extension UIButton { + + func updateLayer(cornerRadius: CGFloat) { + layer.cornerRadius = cornerRadius + layer.masksToBounds = true + } + + func updateLayer(cornerRadius: CGFloat, borderColor: UIColor) { + updateLayer(cornerRadius: cornerRadius) + layer.borderWidth = 1 + layer.borderColor = borderColor.cgColor + } + +} + // MARK: - UIColor -extension UIColor { // Set color to RGB hex value. See http://stackoverflow.com/a/24263296 +extension UIColor { + + // Set color to RGB hex value + // See http://stackoverflow.com/a/24263296 + // See https://cocoacasts.com/from-hex-to-uicolor-and-back-in-swift/ - convenience init(red: Int, green: Int, blue: Int) { - assert(red >= 0 && red <= 255, "Invalid red component") - assert(green >= 0 && green <= 255, "Invalid green component") - assert(blue >= 0 && blue <= 255, "Invalid blue component") - self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) + convenience init(hex: Int) { + self.init(hex: hex, alpha: 1) + } + + convenience init(hex: Int, alpha: CGFloat) { + let red = CGFloat((hex >> 16) & 0xff) / 255 + let green = CGFloat((hex >> 8) & 0xff) / 255 + let blue = CGFloat(hex & 0xff) / 255 + self.init(red: red, green: green, blue: blue, alpha: alpha) } - convenience init(hex:Int) { - self.init(red:(hex >> 16) & 0xff, green:(hex >> 8) & 0xff, blue:hex & 0xff) + convenience init?(hexString: String) { + var hexSanitized = hexString.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + var rgb: UInt32 = 0 + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + var a: CGFloat = 1.0 + let length = hexSanitized.count + guard Scanner(string: hexSanitized).scanHexInt32(&rgb) else { return nil } + if length == 6 { + r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 + g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 + b = CGFloat(rgb & 0x0000FF) / 255.0 + } else if length == 8 { + r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0 + g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0 + b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0 + a = CGFloat(rgb & 0x000000FF) / 255.0 + } else { + return nil + } + self.init(red: r, green: g, blue: b, alpha: a) } - + + convenience init(contrastColorFor color: UIColor) { + var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + color.getHue(&h, saturation: &s, brightness: &b, alpha: &a) + self.init(hue: h, saturation: s, brightness: 1 - b, alpha: a) + } + } // MARK: - UIImage @@ -71,3 +123,85 @@ extension UITableViewCell { } } + +// MARK: - UIViewController + +extension UIViewController { + + func makeRoundWithShadow(view: UIView) { + + // Corner radius + view.layer.cornerRadius = view.frame.height / 2 + + // Shadow + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOffset = CGSize(width: 3, height: 3) + view.layer.shadowOpacity = 0.5 + view.layer.shadowRadius = 4.0 + } + + func makeRound(view: UIView, cornerRadius: CGFloat) { + view.layer.cornerRadius = cornerRadius + view.layer.masksToBounds = true + } + + func makeBlue(button: UIButton) { + button.setTitleColor(Colors.BlueButtonTitleColor, for: .normal) + button.setBackgroundImage(Images.BlueButton, for: .normal) + button.setBackgroundImage(Images.BlueButtonHighlighted, for: .highlighted) + button.updateLayer(cornerRadius: 4) + } + + func makeGray(button: UIButton) { + button.setTitleColor(Colors.GrayButtonTitleColor, for: .normal) + button.setBackgroundImage(Images.GrayButton, for: .normal) + button.setBackgroundImage(Images.GrayButtonHighlighted, for: .highlighted) + button.updateLayer(cornerRadius: 4, borderColor: Colors.GrayButtonBorder) + } + + func makeGreen(button: UIButton) { + button.setTitleColor(Colors.GreenButtonTitleColor, for: .normal) + button.setBackgroundImage(Images.GreenButton, for: .normal) + button.setBackgroundImage(Images.GreenButtonHighlighted, for: .highlighted) + button.updateLayer(cornerRadius: 4) + } + + func makeRed(button: UIButton) { + button.setTitleColor(Colors.RedButtonTitleColor, for: .normal) + button.setBackgroundImage(Images.RedButton, for: .normal) + button.setBackgroundImage(Images.RedButtonHighlighted, for: .highlighted) + button.updateLayer(cornerRadius: 4) + } + + func makeTranslucent(button: UIButton) { + button.setTitleColor(Colors.TranslucentButtonTitleColor, for: .normal) + button.setBackgroundImage(Images.TranslucentButton, for: .normal) + button.setBackgroundImage(Images.TranslucentButtonHighlighted, for: .highlighted) + button.updateLayer(cornerRadius: 4, borderColor: Colors.TranslucentButtonBorder) + } + + func presentAboutViewController() { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + guard let aboutNC = storyboard.instantiateViewController(withIdentifier: "AboutNavigationController") as? UINavigationController else { return } + present(aboutNC, animated: true) + } + + func presentSettingsViewController() { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + guard let aboutNC = storyboard.instantiateViewController(withIdentifier: "SettingsNavigationController") as? UINavigationController else { return } + present(aboutNC, animated: true) + + } + + func showAlert(forError error: Error) { + let alertController = UIAlertController( + title: Translation.Common.Error.String, + message: error.localizedDescription, + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default, handler: nil) + alertController.addAction(okAction) + present(alertController, animated: true, completion: nil) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/ForgotPasswordViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/ForgotPasswordViewController.swift new file mode 100644 index 00000000000..cab3a27fcdb --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/ForgotPasswordViewController.swift @@ -0,0 +1,68 @@ +// +// ForgotPasswordViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 02.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +protocol ForgotPasswordViewControllerDelegate { + + func forgotPasswordViewController(_ controller: ForgotPasswordViewController, willChangePasswordForEmail email: String) + + func forgotPasswordViewController(_ controller: ForgotPasswordViewController, willChangePasswordForUserName userName: String) + +} + +class ForgotPasswordViewController: FormularViewController { + + weak var signUpController: SignUpController? + + @IBOutlet weak var infoLabel: UILabel! + @IBOutlet weak var emailLabel: UILabel! + @IBOutlet weak var emailTextField: UITextField! + @IBOutlet weak var userNameLabel: UILabel! + @IBOutlet weak var userNameTextField: UITextField! + @IBOutlet weak var resetPasswordButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + textFields.append(contentsOf: [emailTextField, userNameTextField]) + setup() + } + + fileprivate func setup() { + setupButtons() + setupLocalization() + } + + fileprivate func setupButtons() { + makeBlue(button: resetPasswordButton) + } + + fileprivate func setupLocalization() { + navigationItem.title = SignUpTranslation.ForgotPasswordView.Title.String + infoLabel.text = SignUpTranslation.ForgotPasswordView.InfoLabel.Text.String + emailLabel.text = SignUpTranslation.Common.Email.String + userNameLabel.text = SignUpTranslation.Common.UserName.String + resetPasswordButton.setTitle(SignUpTranslation.ForgotPasswordView.ResetPasswordButton.Title.String, for: .normal) + } + + @IBAction func resetPasswordButtonTapped(_ sender: Any) { + if let email = emailTextField.text { + if !email.isEmpty { + signUpController?.forgotPasswordViewController(self, willChangePasswordForEmail: email) + return + } + } + if let userName = userNameTextField.text { + if !userName.isEmpty { + signUpController?.forgotPasswordViewController(self, willChangePasswordForUserName: userName) + return + } + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/FormularViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/FormularViewController.swift new file mode 100644 index 00000000000..e304a4981e9 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/FormularViewController.swift @@ -0,0 +1,88 @@ +// +// FormularViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 08.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class FormularViewController: UIViewController { + + var textFields: [UITextField] = [] + + @IBOutlet weak var scrollView: UIScrollView! + + override func viewDidLoad() { + super.viewDidLoad() + subscribeForNotifications() + } + + deinit { + unsubscribeFromNotifications() + } + + // MARK: - Notifications + + fileprivate func subscribeForNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShowNotification(_:)), + name: .UIKeyboardWillShow, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHideNotification(_:)), + name: .UIKeyboardWillHide, + object: nil + ) + } + + fileprivate func unsubscribeFromNotifications() { + NotificationCenter.default.removeObserver(self) + } + + @objc fileprivate func keyboardWillShowNotification(_ notification: Notification) { + DispatchQueue.main.async(execute: { + if let keyboardSize = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? CGRect { + let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0) + self.scrollView.contentInset = contentInsets + self.scrollView.scrollIndicatorInsets = contentInsets + } + }) + } + + @objc fileprivate func keyboardWillHideNotification(_ notification: Notification) { + DispatchQueue.main.async(execute: { + let contentInsets = UIEdgeInsets.zero + self.scrollView.contentInset = contentInsets + self.scrollView.scrollIndicatorInsets = contentInsets + }) + } + + // MARK: - Actions + + @IBAction func viewTapped(_ sender: Any) { + textFields.forEach { (textField) in + textField.resignFirstResponder() + } + } + +} + +// MARK: - UITextFieldDelegate + +extension FormularViewController: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if let nextResponder = textField.superview?.viewWithTag(textField.tag + 1) { + nextResponder.becomeFirstResponder() + } else { + textField.resignFirstResponder() + } + return false + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/GPSFixController.swift b/ios/SAPTracker/src/xcode/SAPTracker/GPSFixController.swift index a95d35d55bb..e44f272b48d 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/GPSFixController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/GPSFixController.swift @@ -12,10 +12,15 @@ class GPSFixController: NSObject { struct NotificationType { static let ModeChanged = "GPSFixController.ModeChanged" + static let SentGPSFixes = "GPSFixController.SentGPSFixes" } struct UserInfo { static let Mode = "Mode" + static let Sent = "Sent" + struct SentKey { + static let Count = "Count" + } } struct GPSFixSending { @@ -39,10 +44,12 @@ class GPSFixController: NSObject { } } - let checkIn: CheckIn + weak var checkIn: CheckIn! + weak var coreDataManager: CoreDataManager! - init(checkIn: CheckIn) { + init(checkIn: CheckIn, coreDataManager: CoreDataManager) { self.checkIn = checkIn + self.coreDataManager = coreDataManager } // MARK: - Send All @@ -112,11 +119,11 @@ class GPSFixController: NSObject { failure: @escaping (_ error: Error, _ gpsFixesLeft: Set) -> Void) { log(info: "\(gpsFixes.count) GPS fixes will be sent and \(gpsFixesLeft.count) will be left") - requestManager.postGPSFixes( - gpsFixes: gpsFixes, - success: { () in self.sendSliceSuccess(gpsFixes: gpsFixes, gpsFixesLeft: gpsFixesLeft, success: success) }, - failure: { (error) in self.sendSliceFailure(error: error, gpsFixesLeft: gpsFixesLeft, failure: failure) } - ) + checkInRequestManager.postGPSFixes(gpsFixes: gpsFixes, success: { () in + self.sendSliceSuccess(gpsFixes: gpsFixes, gpsFixesLeft: gpsFixesLeft, success: success) + }) { (error) in + self.sendSliceFailure(error: error, gpsFixesLeft: gpsFixesLeft, failure: failure) + } } fileprivate func sendSliceSuccess( @@ -127,8 +134,9 @@ class GPSFixController: NSObject { DispatchQueue.main.async(execute: { self.log(info: "Sending \(gpsFixes.count) GPS fixes was successful") self.postModeChangedNotification(mode: BatteryManager.sharedManager.batterySaving ? .BatterySaving : .Online) - CoreDataManager.sharedManager.deleteObjects(objects: gpsFixes) - CoreDataManager.sharedManager.saveContext() + self.postSentGPSFixes(count: gpsFixes.count) + self.coreDataManager.deleteObjects(objects: gpsFixes) + self.coreDataManager.saveContext() self.log(info: "\(gpsFixes.count) GPS fixes deleted") success(gpsFixesLeft) }) @@ -151,14 +159,19 @@ class GPSFixController: NSObject { fileprivate func postModeChangedNotification(mode: Mode) { let userInfo = [UserInfo.Mode: mode.rawValue] let notification = Notification(name: Notification.Name(rawValue: NotificationType.ModeChanged), object: self, userInfo: userInfo) - NotificationQueue.default.enqueue(notification, postingStyle: NotificationQueue.PostingStyle.asap) + NotificationQueue.default.enqueue(notification, postingStyle: .asap) } - + + fileprivate func postSentGPSFixes(count: Int) { + let userInfo = [UserInfo.Sent: [UserInfo.SentKey.Count: count]] + let notification = Notification(name: Notification.Name(rawValue: NotificationType.SentGPSFixes), object: self, userInfo: userInfo) + NotificationQueue.default.enqueue(notification, postingStyle: .asap) + } + // MARK: - Properties - fileprivate lazy var requestManager: RequestManager = { - let requestManager = RequestManager(baseURLString: self.checkIn.serverURL) - return requestManager + fileprivate lazy var checkInRequestManager: CheckInRequestManager = { + return CheckInRequestManager(baseURLString: self.checkIn.serverURL) }() // MARK: - Helper diff --git a/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift deleted file mode 100755 index 067f8c721ec..00000000000 --- a/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift +++ /dev/null @@ -1,464 +0,0 @@ -// -// ViewController.swift -// SAPTracker -// -// Created by Raimund Wege on 17/10/14. -// Copyright (c) 2014 com.sap.sailing. All rights reserved. -// - -import UIKit -import CoreData - -class HomeViewController: UIViewController { - - fileprivate struct Segue { - static let About = "About" - static let Competitor = "Competitor" - static let Mark = "Mark" - static let Scan = "Scan" - static let Settings = "Settings" - } - - var selectedCheckIn: CheckIn? - - @IBOutlet var headerView: UIView! // Strong reference needed to avoid deallocation when not attached to table view - - @IBOutlet weak var headerTitleLabel: UILabel! - @IBOutlet weak var tableView: UITableView! - @IBOutlet weak var scanCodeButton: UIButton! - @IBOutlet weak var noCodeButton: UIButton! - @IBOutlet weak var infoCodeLabel: UILabel! - @IBOutlet weak var footerView: UIView! - @IBOutlet weak var footerTextView: UITextView! - - override func viewDidLoad() { - super.viewDidLoad() - setup() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - review() - subscribeForNewCheckInURLNotifications() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unsubscribeFromNewCheckInURLNotifications() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - layout() - } - - // MARK: - Setup - - fileprivate func setup() { - setupButtons() - setupLocalization() - setupNavigationBar() - setupTableView() - setupTableViewDataSource() - setupTableViewHeader() - } - - fileprivate func setupButtons() { - scanCodeButton.setBackgroundImage(Images.BlueHighlighted, for: .highlighted) - noCodeButton.setBackgroundImage(Images.GrayHighlighted, for: .highlighted) - } - - fileprivate func setupLocalization() { - headerTitleLabel.text = Translation.HomeView.HeaderTitleLabel.Text.String - scanCodeButton.setTitle(Translation.ScanView.Title.String, for: .normal) - noCodeButton.setTitle(Translation.HomeView.NoCodeAlert.Title.String, for: .normal) - infoCodeLabel.text = Translation.HomeView.InfoCodeLabel.Text.String - footerTextView.text = Translation.HomeView.FooterTextView.Text.String - } - - fileprivate func setupNavigationBar() { - navigationItem.title = Application.Title - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIImageView(image: UIImage(named: "sap_logo"))) - } - - fileprivate func setupTableView() { - tableView.rowHeight = UITableViewAutomaticDimension - tableView.estimatedRowHeight = 140 - } - - fileprivate func setupTableViewDataSource() { - do { - try fetchedResultsController.performFetch() - } catch { - logError(name: "\(#function)", error: error) - } - } - - fileprivate func setupTableViewHeader() { - if fetchedResultsController.sections?[0].numberOfObjects ?? 0 == 0 { - tableView.tableHeaderView = nil - } else { - tableView.tableHeaderView = headerView - } - } - - // MARK: - Layout - - fileprivate func layout() { - self.layoutFooterView() - } - - fileprivate func layoutFooterView() { - let height = footerTextView.sizeThatFits(CGSize(width: footerView.frame.width, height: CGFloat.greatestFiniteMagnitude)).height - footerView.frame = CGRect( - x: footerView.frame.origin.x, - y: footerView.frame.origin.y, - width: footerView.frame.width, - height: height - ) - } - - // MARK: - Review - - fileprivate func review() { - self.reviewTerms(completion: { - logInfo(name: "\(#function)", info: "Review terms done.") - self.reviewCodeConvention(completion: { - logInfo(name: "\(#function)", info: "Review code convention done.") - self.reviewGPSFixes(completion: { - logInfo(name: "\(#function)", info: "Review GPS fixes done.") - self.reviewNewCheckIn(completion: { - logInfo(name: "\(#function)", info: "Review new check-in done.") - self.reviewSelectedCheckIn(completion: { - logInfo(name: "\(#function)", info: "Review selected check-in done.") - }) - }) - }) - }) - }) - } - - // MARK: 1. Review Terms - - fileprivate func reviewTerms(completion: @escaping () -> Void) { - guard Preferences.termsAccepted == false else { completion(); return } - let alertController = UIAlertController( - title: Translation.HomeView.TermsAlert.Title.String, - message: Translation.HomeView.TermsAlert.Message.String, - preferredStyle: .alert - ) - let showTermsAction = UIAlertAction(title: Translation.HomeView.TermsAlert.ShowTermsAction.Title.String, style: .default) { action in - UIApplication.shared.openURL(URLs.Terms) - self.reviewTerms(completion: completion) // Review terms until user accepted terms - } - let acceptTermsAction = UIAlertAction(title: Translation.HomeView.TermsAlert.AcceptTermsAction.Title.String, style: .default) { action in - Preferences.termsAccepted = true - completion() // Terms accepted - } - alertController.addAction(showTermsAction) - alertController.addAction(acceptTermsAction) - present(alertController, animated: true, completion: nil) - } - - // MARK: 2. Review Code Convention - - fileprivate func reviewCodeConvention(completion: @escaping () -> Void) { - #if DEBUG - guard Preferences.codeConventionRead == false else { completion(); return } - let alertController = UIAlertController( - title: "Code Convention", - message: "Please try to respect the code convention which is used for this project.", - preferredStyle: .alert - ) - let showCodeConventionAction = UIAlertAction(title: "Code Convention", style: .default) { action in - UIApplication.shared.openURL(URLs.CodeConvention) - self.reviewCodeConvention(completion: completion) - } - let okAction = UIAlertAction(title: "OK", style: .default) { action in - Preferences.codeConventionRead = true - completion() - } - alertController.addAction(showCodeConventionAction) - alertController.addAction(okAction) - present(alertController, animated: true, completion: nil) - #else - completion() - #endif - } - - // MARK: 3. Review GPS Fixes - - fileprivate func reviewGPSFixes(completion: @escaping () -> Void) { - SVProgressHUD.show() - fetchedResultsController.delegate = nil - let checkIns = CoreDataManager.sharedManager.fetchCheckIns() ?? [] - reviewGPSFixes(checkIns: checkIns) { - self.reviewGPSFixesCompleted(completion: completion) - } - } - - fileprivate func reviewGPSFixes(checkIns: [CheckIn], completion: @escaping () -> Void) { - guard checkIns.count > 0 else { completion(); return } - let gpsFixController = GPSFixController.init(checkIn: checkIns[0]) - gpsFixController.sendAll(completion: { (withSuccess) in - self.reviewGPSFixes(checkIns: Array(checkIns[1.. Void) { - SVProgressHUD.popActivity() - fetchedResultsController.delegate = self - completion() - } - - // MARK: 4. Review New Check-In - - fileprivate func reviewNewCheckIn(completion: @escaping () -> Void) { - guard let urlString = Preferences.newCheckInURL else { completion(); return } - guard let checkInData = CheckInData(urlString: urlString) else { completion(); return } - checkInController.checkIn(checkInData: checkInData, completion: { (withSuccess) in - Preferences.newCheckInURL = nil - self.selectedCheckIn = CoreDataManager.sharedManager.fetchCheckIn(checkInData: checkInData) - completion() - }) - } - - // MARK: 5. Review Selected Check-In - - fileprivate func reviewSelectedCheckIn(completion: () -> Void) { - if shouldPerformCompetitorSegue() { - performSegue(withIdentifier: Segue.Competitor, sender: self) - } else if shouldPerformMarkSegue() { - performSegue(withIdentifier: Segue.Mark, sender: self) - } - completion() - } - - // MARK: - Notifications - - fileprivate func subscribeForNewCheckInURLNotifications() { - NotificationCenter.default.addObserver( - self, - selector: #selector(HomeViewController.newCheckInURLNotification(_:)), - name: NSNotification.Name(rawValue: Preferences.NotificationType.NewCheckInURLChanged), - object: nil - ) - } - - fileprivate func unsubscribeFromNewCheckInURLNotifications() { - NotificationCenter.default.removeObserver(self) - } - - @objc fileprivate func newCheckInURLNotification(_ notification: Notification) { - DispatchQueue.main.async(execute: { - self.reviewNewCheckIn(completion: { - logInfo(name: "\(#function)", info: "Review new check-in done.") - self.reviewSelectedCheckIn(completion: { - logInfo(name: "\(#function)", info: "Review selected check-in done.") - }) - }) - }) - } - - // MARK: - Actions - - @IBAction func optionButtonTapped(_ sender: AnyObject) { - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - if let popoverController = alertController.popoverPresentationController { - popoverController.barButtonItem = sender as? UIBarButtonItem - } - let settingsAction = UIAlertAction(title: Translation.SettingsView.Title.String, style: .default) { (action) -> Void in - self.performSegue(withIdentifier: Segue.Settings, sender: alertController) - } - let aboutAction = UIAlertAction(title: Translation.Common.Info.String, style: .default) { (action) -> Void in - self.performSegue(withIdentifier: Segue.About, sender: alertController) - } - let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) - alertController.addAction(settingsAction) - alertController.addAction(aboutAction) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - - @IBAction func scanButtonTapped(_ sender: AnyObject) { - if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) { - performSegue(withIdentifier: Segue.Scan, sender: sender) - } else { - showNoCameraAlert() - } - } - - @IBAction func noCodeButtonTapped(_ sender: AnyObject) { - showNoCodeAlert() - } - - // MARK: - Alerts - - fileprivate func showNoCameraAlert() { - let alertController = UIAlertController( - title: Translation.Common.Error.String, - message: Translation.HomeView.NoCameraAlert.Message.String, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default, handler: nil) - alertController.addAction(okAction) - present(alertController, animated: true, completion: nil) - } - - fileprivate func showNoCodeAlert() { - let alertController = UIAlertController( - title: Translation.HomeView.NoCodeAlert.Title.String, - message: Translation.HomeView.NoCodeAlert.Message.String, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default, handler: nil) - alertController.addAction(okAction) - present(alertController, animated: true, completion: nil) - } - - // MARK: - Segues - - override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { - if (identifier == Segue.Competitor) { - return shouldPerformCompetitorSegue() - } else if (identifier == Segue.Mark) { - return shouldPerformMarkSegue() - } else { - return true - } - } - - fileprivate func shouldPerformCompetitorSegue() -> Bool { - return selectedCheckIn != nil && selectedCheckIn is CompetitorCheckIn - } - - fileprivate func shouldPerformMarkSegue() -> Bool { - return selectedCheckIn != nil && selectedCheckIn is MarkCheckIn - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if (segue.identifier == Segue.Competitor) { - guard let competitorVC = segue.destination as? CompetitorViewController else { return } - guard let competitorCheckIn = selectedCheckIn as? CompetitorCheckIn else { return } - competitorVC.competitorCheckIn = competitorCheckIn - selectedCheckIn = nil - } else if (segue.identifier == Segue.Mark) { - guard let markVC = segue.destination as? MarkViewController else { return } - guard let markCheckIn = selectedCheckIn as? MarkCheckIn else { return } - markVC.markCheckIn = markCheckIn - selectedCheckIn = nil - } else if (segue.identifier == Segue.Scan) { - guard let scanVC = segue.destination as? ScanViewController else { return } - scanVC.homeViewController = self - } - } - - // MARK: - Properties - - fileprivate lazy var checkInController: CheckInController = { - let checkInController = CheckInController() - checkInController.delegate = self - return checkInController - }() - - fileprivate lazy var fetchedResultsController: NSFetchedResultsController = { - let fetchedResultsController = CoreDataManager.sharedManager.checkInFetchedResultsController() - fetchedResultsController.delegate = self - return fetchedResultsController - }() - -} - -// MARK: - UITableViewDataSource - -extension HomeViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return fetchedResultsController.sections?[section].numberOfObjects ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell() - self.configureCell(cell: cell, atIndexPath: indexPath) - return cell - } - - func configureCell(cell: UITableViewCell, atIndexPath indexPath: IndexPath) { - guard let homeViewCell = cell as? HomeViewCell else { return } - let checkIn = fetchedResultsController.object(at: indexPath) - homeViewCell.eventLabel.text = checkIn.event.name - homeViewCell.leaderboardLabel.text = checkIn.leaderboard.name - homeViewCell.competitorLabel.text = checkIn.name - } - -} - -// MARK: - UITableViewDelegate - -extension HomeViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - selectedCheckIn = fetchedResultsController.object(at: indexPath) - return indexPath - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - reviewSelectedCheckIn { - tableView.deselectRow(at: indexPath, animated: true) - } - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - cell.removeSeparatorInset() - } - -} - -// MARK: - NSFetchedResultsControllerDelegate - -extension HomeViewController: NSFetchedResultsControllerDelegate { - - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - tableView.beginUpdates() - } - - func controller( - _ controller: NSFetchedResultsController, - didChange object: Any, - at indexPath: IndexPath?, - for type: NSFetchedResultsChangeType, - newIndexPath: IndexPath?) - { - switch type { - case .insert: - tableView.insertRows(at: [newIndexPath!], with: UITableViewRowAnimation.automatic) - case .update: - let cell = tableView.cellForRow(at: indexPath!) - if cell != nil { - configureCell(cell: cell!, atIndexPath: indexPath!) - tableView.reloadRows(at: [indexPath!], with: UITableViewRowAnimation.automatic) - } - case .move: - tableView.deleteRows(at: [indexPath!], with: UITableViewRowAnimation.automatic) - tableView.insertRows(at: [newIndexPath!], with: .automatic) - case .delete: - tableView.deleteRows(at: [indexPath!], with: UITableViewRowAnimation.automatic) - } - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - tableView.endUpdates() - setupTableViewHeader() - } - -} - -// MARK: - CheckInControllerDelegate - -extension HomeViewController: CheckInControllerDelegate { - - func checkInController(_ sender: CheckInController, show alertController: UIAlertController) { - present(alertController, animated: true, completion: nil) - } - -} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json index dcbccf55a9c..140d558a03a 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json @@ -97,6 +97,11 @@ "idiom" : "ipad", "filename" : "167_iPhone_6Plus_App_Icon@2x.png", "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/ ic_edit.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/ ic_edit.png new file mode 100644 index 00000000000..533292e65cd Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/ ic_edit.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/ ic_edit@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/ ic_edit@2x.png new file mode 100644 index 00000000000..923530b4c7b Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/ ic_edit@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/Contents.json index 0471fb7710c..e1277092488 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/Contents.json +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/Contents.json @@ -2,12 +2,13 @@ "images" : [ { "idiom" : "universal", + "filename" : " ic_edit.png", "scale" : "1x" }, { "idiom" : "universal", - "scale" : "2x", - "filename" : "edit@3x.png" + "filename" : " ic_edit@2x.png", + "scale" : "2x" }, { "idiom" : "universal", diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/edit@3x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/edit@3x.png deleted file mode 100644 index 1189f8100a3..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit.imageset/edit@3x.png and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/ ic_edit_pressed.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/ ic_edit_pressed.png new file mode 100644 index 00000000000..7a06c947b37 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/ ic_edit_pressed.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/ ic_edit_pressed@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/ ic_edit_pressed@2x.png new file mode 100644 index 00000000000..c1c08d8f592 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/ ic_edit_pressed@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/Contents.json new file mode 100644 index 00000000000..8dc91584a07 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/edit_pressed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : " ic_edit_pressed.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : " ic_edit_pressed@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/Contents.json new file mode 100644 index 00000000000..ea51c158bfd --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "grey600.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "grey600@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "grey600@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600.png new file mode 100644 index 00000000000..e0ec2771daa Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600@2x.png new file mode 100644 index 00000000000..d20c98550f2 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600@3x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600@3x.png new file mode 100644 index 00000000000..b18f460aa66 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/editable.imageset/grey600@3x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/ FAB@1x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/ FAB@1x.png new file mode 100644 index 00000000000..0e49513a1c5 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/ FAB@1x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/ FAB@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/ FAB@2x.png new file mode 100644 index 00000000000..11ca9d4a295 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/ FAB@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/retry.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/Contents.json similarity index 79% rename from ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/retry.imageset/Contents.json rename to ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/Contents.json index 1786be5d31a..94dd368849f 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/retry.imageset/Contents.json +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab.imageset/Contents.json @@ -2,11 +2,12 @@ "images" : [ { "idiom" : "universal", + "filename" : " FAB@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "retry@2x.png", + "filename" : " FAB@2x.png", "scale" : "2x" }, { diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/ FAB_pressed@1x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/ FAB_pressed@1x.png new file mode 100644 index 00000000000..730f25e0388 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/ FAB_pressed@1x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/ FAB_pressed@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/ FAB_pressed@2x.png new file mode 100644 index 00000000000..ae181629499 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/ FAB_pressed@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/Contents.json new file mode 100644 index 00000000000..3c4d96c1065 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/fab_pressed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : " FAB_pressed@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : " FAB_pressed@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/Contents.json new file mode 100644 index 00000000000..ea51c158bfd --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "grey600.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "grey600@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "grey600@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600.png new file mode 100644 index 00000000000..089e964143f Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600@2x.png new file mode 100644 index 00000000000..3577a79ef0f Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600@3x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600@3x.png new file mode 100644 index 00000000000..c751879f2ff Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/not_editable.imageset/grey600@3x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/ ic_refresh@1x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/ ic_refresh@1x.png new file mode 100644 index 00000000000..d28faba3922 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/ ic_refresh@1x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/ ic_refresh@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/ ic_refresh@2x.png new file mode 100644 index 00000000000..9587a1fca3e Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/ ic_refresh@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/Contents.json new file mode 100644 index 00000000000..d4fc353a515 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/refresh.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : " ic_refresh@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : " ic_refresh@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Contents.json new file mode 100644 index 00000000000..21db3f4ad1a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Photo_1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Photo_1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Photo_1.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Photo_1.png new file mode 100644 index 00000000000..628e4d7d40c Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Photo_1.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Photo_1@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Photo_1@2x.png new file mode 100644 index 00000000000..3d1b0f414ee Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/regatta.imageset/Photo_1@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/retry.imageset/retry@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/retry.imageset/retry@2x.png deleted file mode 100644 index 6f985c9e2d7..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/retry.imageset/retry@2x.png and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/ ic_more_vert@1x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/ ic_more_vert@1x.png new file mode 100644 index 00000000000..0c6df0e05d4 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/ ic_more_vert@1x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/ ic_more_vert@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/ ic_more_vert@2x.png new file mode 100644 index 00000000000..326aac8715f Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/ ic_more_vert@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/Contents.json index d80ded53255..aa2e48027af 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/Contents.json +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", + "filename" : " ic_more_vert@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "scale" : "2x", - "filename" : "settings-44.png" + "filename" : " ic_more_vert@2x.png", + "scale" : "2x" }, { "idiom" : "universal", - "scale" : "3x", - "filename" : "settings@3x.png" + "scale" : "3x" } ], "info" : { diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/settings-44.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/settings-44.png deleted file mode 100644 index 9a9a9692877..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/settings-44.png and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/settings@3x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/settings@3x.png deleted file mode 100644 index a65bfcf7fe2..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/settings.imageset/settings@3x.png and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Contents.json new file mode 100644 index 00000000000..065755ade8b --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Photo_2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Photo_2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Photo_2.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Photo_2.png new file mode 100644 index 00000000000..c6af4dfae17 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Photo_2.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Photo_2@2x.png b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Photo_2@2x.png new file mode 100644 index 00000000000..783865bb524 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/training.imageset/Photo_2@2x.png differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Info.plist b/ios/SAPTracker/src/xcode/SAPTracker/Info.plist index f83bb27ca3a..3cc8e2a54c8 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Info.plist +++ b/ios/SAPTracker/src/xcode/SAPTracker/Info.plist @@ -48,13 +48,6 @@ NSLocationAlwaysUsageDescription - UIAppFonts - - OpenSans-Bold.ttf - OpenSans-Light.ttf - OpenSans-Regular.ttf - OpenSans-Semibold.ttf - UIBackgroundModes fetch diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Keychain.swift b/ios/SAPTracker/src/xcode/SAPTracker/Keychain.swift new file mode 100644 index 00000000000..44e76a484ac --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Keychain.swift @@ -0,0 +1,31 @@ +// +// Keychain.swift +// SAPTracker +// +// Created by Raimund Wege on 08.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class Keychain: NSObject { + + fileprivate enum Keychain { + static let UserNameAccount = "user.name" + static let UserPasswordAccount = "user.password" + static let UserAccessTokenAccount = "user.access_token" + } + + static func userName(forService service: String) -> KeychainPasswordItem { + return KeychainPasswordItem.init(service: service, account: Keychain.UserNameAccount) + } + + static func userPassword(forService service: String) -> KeychainPasswordItem { + return KeychainPasswordItem.init(service: service, account: Keychain.UserPasswordAccount) + } + + static func userAccessToken(forService service: String) -> KeychainPasswordItem { + return KeychainPasswordItem.init(service: service, account: Keychain.UserAccessTokenAccount) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/KeychainPasswordItem.swift b/ios/SAPTracker/src/xcode/SAPTracker/KeychainPasswordItem.swift new file mode 100644 index 00000000000..4e736d367dc --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/KeychainPasswordItem.swift @@ -0,0 +1,181 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A struct for accessing generic password keychain items. + */ + +// Source: https://developer.apple.com/library/content/samplecode/GenericKeychain/Listings/GenericKeychain_KeychainPasswordItem_swift.html#//apple_ref/doc/uid/DTS40007797-GenericKeychain_KeychainPasswordItem_swift-DontLinkElementID_7 + +import Foundation + +struct KeychainPasswordItem { + + // MARK: Types + + enum KeychainError: Error { + case noPassword + case unexpectedPasswordData + case unexpectedItemData + case unhandledError(status: OSStatus) + } + + // MARK: Properties + + let service: String + + private(set) var account: String + + let accessGroup: String? + + // MARK: Intialization + + init(service: String, account: String, accessGroup: String? = nil) { + self.service = service + self.account = account + self.accessGroup = accessGroup + } + + // MARK: Keychain access + + func readPassword() throws -> String { + /* + Build a query to find the item that matches the service, account and + access group. + */ + var query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) + query[kSecMatchLimit as String] = kSecMatchLimitOne + query[kSecReturnAttributes as String] = kCFBooleanTrue + query[kSecReturnData as String] = kCFBooleanTrue + + // Try to fetch the existing keychain item that matches the query. + var queryResult: AnyObject? + let status = withUnsafeMutablePointer(to: &queryResult) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + + // Check the return status and throw an error if appropriate. + guard status != errSecItemNotFound else { throw KeychainError.noPassword } + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + + // Parse the password string from the query result. + guard let existingItem = queryResult as? [String : AnyObject], + let passwordData = existingItem[kSecValueData as String] as? Data, + let password = String(data: passwordData, encoding: String.Encoding.utf8) + else { + throw KeychainError.unexpectedPasswordData + } + + return password + } + + func savePassword(_ password: String) throws { + // Encode the password into an Data object. + let encodedPassword = password.data(using: String.Encoding.utf8)! + + do { + // Check for an existing item in the keychain. + try _ = readPassword() + + // Update the existing item with the new password. + var attributesToUpdate = [String : AnyObject]() + attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject? + + let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) + let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) + + // Throw an error if an unexpected status was returned. + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + } + catch KeychainError.noPassword { + /* + No password was found in the keychain. Create a dictionary to save + as a new keychain item. + */ + var newItem = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) + newItem[kSecValueData as String] = encodedPassword as AnyObject? + + // Add a the new item to the keychain. + let status = SecItemAdd(newItem as CFDictionary, nil) + + // Throw an error if an unexpected status was returned. + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + } + } + + mutating func renameAccount(_ newAccountName: String) throws { + // Try to update an existing item with the new account name. + var attributesToUpdate = [String : AnyObject]() + attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject? + + let query = KeychainPasswordItem.keychainQuery(withService: service, account: self.account, accessGroup: accessGroup) + let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) + + // Throw an error if an unexpected status was returned. + guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } + + self.account = newAccountName + } + + func deleteItem() throws { + // Delete the existing item from the keychain. + let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup) + let status = SecItemDelete(query as CFDictionary) + + // Throw an error if an unexpected status was returned. + guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } + } + + static func passwordItems(forService service: String, accessGroup: String? = nil) throws -> [KeychainPasswordItem] { + // Build a query for all items that match the service and access group. + var query = KeychainPasswordItem.keychainQuery(withService: service, accessGroup: accessGroup) + query[kSecMatchLimit as String] = kSecMatchLimitAll + query[kSecReturnAttributes as String] = kCFBooleanTrue + query[kSecReturnData as String] = kCFBooleanFalse + + // Fetch matching items from the keychain. + var queryResult: AnyObject? + let status = withUnsafeMutablePointer(to: &queryResult) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + + // If no items were found, return an empty array. + guard status != errSecItemNotFound else { return [] } + + // Throw an error if an unexpected status was returned. + guard status == noErr else { throw KeychainError.unhandledError(status: status) } + + // Cast the query result to an array of dictionaries. + guard let resultData = queryResult as? [[String : AnyObject]] else { throw KeychainError.unexpectedItemData } + + // Create a `KeychainPasswordItem` for each dictionary in the query result. + var passwordItems = [KeychainPasswordItem]() + for result in resultData { + guard let account = result[kSecAttrAccount as String] as? String else { throw KeychainError.unexpectedItemData } + + let passwordItem = KeychainPasswordItem(service: service, account: account, accessGroup: accessGroup) + passwordItems.append(passwordItem) + } + + return passwordItems + } + + // MARK: Convenience + + private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] { + var query = [String : AnyObject]() + query[kSecClass as String] = kSecClassGenericPassword + query[kSecAttrService as String] = service as AnyObject? + + if let account = account { + query[kSecAttrAccount as String] = account as AnyObject? + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? + } + + return query + } +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard b/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard index 38c24f05ba0..bc95b27502c 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard +++ b/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard @@ -1,19 +1,14 @@ - - + + - + - - - OpenSans - - @@ -28,7 +23,7 @@ - + @@ -37,8 +32,8 @@ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardAutoCourseData.swift b/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardAutoCourseData.swift new file mode 100644 index 00000000000..a88c8666ca8 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardAutoCourseData.swift @@ -0,0 +1,13 @@ +// +// LeaderboardAutoCourseData.swift +// SAPTracker +// +// Created by Raimund Wege on 20.12.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class LeaderboardAutoCourseData: BaseData { + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardGroupData.swift b/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardGroupData.swift new file mode 100644 index 00000000000..2839a6f46d8 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardGroupData.swift @@ -0,0 +1,49 @@ +// +// LeaderboardGroupData.swift +// SAPTracker +// +// Created by Raimund Wege on 20.12.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class LeaderboardGroupData: BaseData { + + func raceName(trainingRaceData: TrainingRaceData) -> String? { + var raceName: String? + if let leaderboards = dictionary["leaderboards"] as? [[String: AnyObject]] { + + // Leaderboards + leaderboards.forEach({ (leaderboard) in + if leaderboard["name"] as? String == trainingRaceData.leaderboardName { + + // Series + if let series = leaderboard["series"] as? [[String: AnyObject]] { + series.forEach({ (serie) in + + // Fleets + if let fleets = serie["fleets"] as? [[String: AnyObject]] { + fleets.forEach({ (fleet) in + if fleet["name"] as? String == trainingRaceData.fleetName { + + // Races + if let races = fleet["races"] as? [[String: AnyObject]] { + races.forEach({ (race) in + if race["name"] as? String == trainingRaceData.raceColumnName { + raceName = race["trackedRaceName"] as? String + } + }) + } + } + }) + } + }) + } + } + }) + } + return raceName + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardViewController.swift index 0048b4cc465..fb326a5dd16 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/LeaderboardViewController.swift @@ -10,7 +10,7 @@ import Foundation class LeaderboardViewController: UIViewController { - var checkIn: CheckIn! + weak var checkIn: CheckIn! @IBOutlet weak var webView: UIWebView! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! @@ -24,6 +24,7 @@ class LeaderboardViewController: UIViewController { fileprivate func setup() { setupLocalization() + setupNavigationBar() setupWebView() } @@ -31,6 +32,10 @@ class LeaderboardViewController: UIViewController { navigationItem.title = Translation.LeaderboardView.Title.String } + fileprivate func setupNavigationBar() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIImageView(image: UIImage(named: "sap_logo"))) + } + fileprivate func setupWebView() { guard let url = checkIn.leaderboardURL() else { return } webView.loadRequest(URLRequest(url: url)) @@ -38,7 +43,7 @@ class LeaderboardViewController: UIViewController { // MARK: - Actions - @IBAction func doneButtonTapped(_ sender: AnyObject) { + @IBAction func doneButtonTapped(_ sender: Any) { presentingViewController!.dismiss(animated: true, completion: nil) } @@ -55,8 +60,8 @@ extension LeaderboardViewController: UIWebViewDelegate { func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { activityIndicator.stopAnimating() let alertController = UIAlertController(title: error.localizedDescription, message: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in - self.presentingViewController!.dismiss(animated: true, completion: nil) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { [weak self] action in + self?.presentingViewController!.dismiss(animated: true, completion: nil) } alertController.addAction(okAction) present(alertController, animated: true, completion: nil) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/LoginViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/LoginViewController.swift new file mode 100644 index 00000000000..863f5101031 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/LoginViewController.swift @@ -0,0 +1,97 @@ +// +// LoginViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 02.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +protocol LoginViewControllerDelegate { + + func loginViewController(_ controller: LoginViewController, willLoginWithUserName userName: String, password: String) + + func loginViewControllerWillCancel(_ controller: LoginViewController) + +} + +class LoginViewController: FormularViewController { + + var signUpController: SignUpController? + + @IBOutlet var endpointLabel: UILabel! + @IBOutlet var endpointValueLabel: UILabel! + @IBOutlet var userNameLabel: UILabel! + @IBOutlet var userNameTextField: UITextField! + @IBOutlet var passwordLabel: UILabel! + @IBOutlet var passwordTextField: UITextField! + @IBOutlet var forgotPasswordButton: UIButton! + @IBOutlet var signUpButton: UIButton! + @IBOutlet var loginButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + textFields.append(contentsOf: [userNameTextField, passwordTextField]) + setup() + } + + fileprivate func setup() { + setupButtons() + setupLocalization() + setupNavigationBar() + setupTextFields() + } + + fileprivate func setupButtons() { + makeGray(button: signUpButton) + makeBlue(button: loginButton) + } + + fileprivate func setupLocalization() { + navigationItem.title = SignUpTranslation.LoginView.Title.String + endpointLabel.text = SignUpTranslation.Common.Server.String + endpointValueLabel.text = signUpController?.baseURLString + userNameLabel.text = SignUpTranslation.Common.UserName.String + passwordLabel.text = SignUpTranslation.Common.Password.String + forgotPasswordButton.setTitle(SignUpTranslation.ForgotPasswordView.Title.String, for: .normal) + signUpButton.setTitle(SignUpTranslation.SignUpView.Title.String, for: .normal) + loginButton.setTitle(SignUpTranslation.LoginView.LoginButton.Title.String, for: .normal) + } + + fileprivate func setupNavigationBar() { + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIImageView(image: UIImage(named: "sap_logo"))) + } + + fileprivate func setupTextFields() { + userNameTextField.text = signUpController?.userName + passwordTextField.text = signUpController?.userPassword + } + + // MARK: - Actions + + @IBAction func cancelButtonTapped(_ sender: Any) { + signUpController?.loginViewControllerWillCancel(self) + } + + @IBAction func loginButtonTapped(_ sender: Any) { + signUpController?.loginViewController( + self, + willLoginWithUserName: userNameTextField.text!, + password: passwordTextField.text! + ) + } + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if let signUpVC = segue.destination as? SignUpViewController { + signUpVC.signUpController = signUpController + } + if let forgotPasswordVC = segue.destination as? ForgotPasswordViewController { + forgotPasswordVC.signUpController = signUpController + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Main.storyboard b/ios/SAPTracker/src/xcode/SAPTracker/Main.storyboard index 8cbb912090d..dcb025a3700 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Main.storyboard +++ b/ios/SAPTracker/src/xcode/SAPTracker/Main.storyboard @@ -1,46 +1,29 @@ - - + + - + - - - OpenSans-Bold - - - OpenSans-Light - - - OpenSans - - - OpenSans-Semibold - - - + - + - - + + - - - - + @@ -48,16 +31,24 @@ + - + + @@ -68,12 +59,11 @@ - + - @@ -82,37 +72,37 @@ - + - + @@ -122,12 +112,13 @@ - - - + + + - + + @@ -143,152 +134,42 @@ - - - - - - - - + - - - - - - - - - - + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - @@ -300,36 +181,18 @@ - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - + @@ -340,7 +203,7 @@ - + @@ -373,60 +236,402 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + - - - - - + + + + + + + @@ -445,196 +650,61 @@ - + - - - + - + - + - - + + - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -942,7 +984,7 @@ - + @@ -954,7 +996,7 @@ - + @@ -965,7 +1007,7 @@ - + @@ -977,23 +1019,17 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + - + - + - - - + + + + + - diff --git a/ios/SAPTracker/src/xcode/SAPTracker/MarkSessionViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/MarkSessionViewController.swift new file mode 100644 index 00000000000..c0d19fc90c1 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/MarkSessionViewController.swift @@ -0,0 +1,41 @@ +// +// MarkSessionViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class MarkSessionViewController: SessionViewController { + + struct MarkSessionSegue { + static let EmbedMark = "EmbedMark" + } + + weak var markCheckIn: MarkCheckIn! + weak var markCoreDataManager: CoreDataManager! + weak var markViewController: MarkViewController? + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == MarkSessionSegue.EmbedMark) { + if let markViewController = segue.destination as? MarkViewController { + markViewController.markCheckIn = markCheckIn + markViewController.markSessionController = markSessionController + markViewController.markCoreDataManager = markCoreDataManager + self.markViewController = markViewController + } + } + } + + // MARK: - Properties + + lazy var markSessionController: MarkSessionController = { + return MarkSessionController(checkIn: self.markCheckIn, coreDataManager: self.markCoreDataManager) + }() + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/MarkViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/MarkViewController.swift index 9762bfe3b87..c9583245070 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/MarkViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/MarkViewController.swift @@ -2,132 +2,36 @@ // MarkViewController.swift // SAPTracker // -// Created by Raimund Wege on 05.05.17. +// Created by Raimund Wege on 13.09.17. // Copyright © 2017 com.sap.sailing. All rights reserved. // import UIKit -class MarkViewController: SessionViewController { - +class MarkViewController: UIViewController { + + weak var markCheckIn: MarkCheckIn! + weak var markCoreDataManager: CoreDataManager! + weak var markSessionController: MarkSessionController! + @IBOutlet weak var markNameLabel: UILabel! - - var markCheckIn: MarkCheckIn! - - override func viewDidLoad() { - super.viewDidLoad() - delegate = self - setup() - update() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - refresh() - } - - // MARK: - Setup - - fileprivate func setup() { - setupButtons() - setupLocalization() - setupNavigationBar() - } - - fileprivate func setupButtons() { - startTrackingButton.setBackgroundImage(Images.GreenHighlighted, for: .highlighted) - } - - fileprivate func setupLocalization() { - startTrackingButton.setTitle(Translation.CompetitorView.StartTrackingButton.Title.String, for: .normal) - } - - fileprivate func setupNavigationBar() { - navigationItem.titleView = TitleView(title: markCheckIn.event.name, subtitle: markCheckIn.leaderboard.name) - navigationController?.navigationBar.setNeedsLayout() - } - - // MARK: - Update - - fileprivate func update() { - SVProgressHUD.show() - markSessionController.update { - self.refresh() - SVProgressHUD.popActivity() - } - } // MARK: - Refresh - fileprivate func refresh() { + func refresh(_ animated: Bool) { + refreshMarkNameLabel(animated) + } + + fileprivate func refreshMarkNameLabel(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshMarkNameLabel() } + } else { + refreshMarkNameLabel() + } + } + + fileprivate func refreshMarkNameLabel() { markNameLabel.text = markCheckIn.name } - - // MARK: - Actions - @IBAction func optionButtonTapped(_ sender: AnyObject) { - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - if let popoverController = alertController.popoverPresentationController { - popoverController.barButtonItem = sender as? UIBarButtonItem - } - let settingsAction = UIAlertAction(title: Translation.SettingsView.Title.String, style: .default) { (action) in - self.performSegue(withIdentifier: Segue.Settings, sender: self) - } - let checkOutAction = UIAlertAction(title: Translation.CompetitorView.OptionSheet.CheckOutAction.Title.String, style: .default) { (action) in - self.checkOut() - } - let updateAction = UIAlertAction(title: Translation.CompetitorView.OptionSheet.UpdateAction.Title.String, style: .default) { (action) -> Void in - self.update() - } - let aboutAction = UIAlertAction(title: Translation.Common.Info.String, style: .default) { (action) -> Void in - self.performSegue(withIdentifier: Segue.About, sender: alertController) - } - let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) - alertController.addAction(settingsAction) - alertController.addAction(checkOutAction) - alertController.addAction(updateAction) - alertController.addAction(aboutAction) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - - // MARK: - Segues - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if (segue.identifier == Segue.Tracking) { - let trackingNC = segue.destination as! UINavigationController - let trackingVC = trackingNC.viewControllers[0] as! TrackingViewController - trackingVC.checkIn = markCheckIn - trackingVC.sessionController = markSessionController - } - } - - // MARK: - Properties - - fileprivate lazy var markSessionController: MarkSessionController = { - return MarkSessionController(checkIn: self.markCheckIn) - }() - -} - -// MARK: SessionViewControllerDelegate - -extension MarkViewController: SessionViewControllerDelegate { - - func performCheckOut() { - markSessionController.checkOut { (withSuccess) in - self.performCheckOutCompleted(withSuccess: withSuccess) - } - } - - fileprivate func performCheckOutCompleted(withSuccess: Bool) { - CoreDataManager.sharedManager.deleteObject(object: markCheckIn) - CoreDataManager.sharedManager.saveContext() - self.navigationController!.popViewController(animated: true) - } - - func startTracking() throws { - try markSessionController.startTracking() - } - } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/NewCheckInViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/NewCheckInViewController.swift new file mode 100644 index 00000000000..25766698710 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/NewCheckInViewController.swift @@ -0,0 +1,143 @@ +// +// NewCheckInViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 29.01.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +protocol NewCheckInViewControllerDelegate: class { + + func newCheckInViewController(_ controller: NewCheckInViewController, didCheckIn checkIn: CheckIn) + +} + +class NewCheckInViewController: UIViewController { + + fileprivate struct Segue { + static let CreateTraining = "CreateTraining" + static let Scan = "Scan" + } + + weak var coreDataManager: CoreDataManager! + weak var delegate: NewCheckInViewControllerDelegate? + + @IBOutlet var scanCodeButton: UIButton! + @IBOutlet var noCodeButton: UIButton! + @IBOutlet var infoCodeLabel: UILabel! + @IBOutlet var createTrainingButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + // MARK: - Setup + + fileprivate func setup() { + setupButtons() + setupLocalization() + setupNavigationBar() + } + + fileprivate func setupButtons() { + makeBlue(button: scanCodeButton) + makeGray(button: noCodeButton) + makeBlue(button: createTrainingButton) + } + + fileprivate func setupLocalization() { + // navigationItem.title = Translation.RegattaCheckInListView.Title.String + scanCodeButton.setTitle(Translation.ScanView.Title.String, for: .normal) + noCodeButton.setTitle(Translation.RegattaCheckInListView.NoCodeAlert.Title.String, for: .normal) + infoCodeLabel.text = Translation.RegattaCheckInListView.InfoCodeLabel.Text.String + } + + fileprivate func setupNavigationBar() { + navigationItem.title = Application.Title + navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIImageView(image: UIImage(named: "sap_logo"))) + } + + // MARK: - Actions + + @IBAction func cancelButtonTapped(_ sender: Any) { + presentingViewController?.dismiss(animated: true) + } + + @IBAction func scanButtonTapped(_ sender: Any) { + if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) { + performSegue(withIdentifier: Segue.Scan, sender: sender) + } else { + showNoCameraAlert() + } + } + + @IBAction func noCodeButtonTapped(_ sender: Any) { + showNoCodeAlert() + } + + // MARK: - Alerts + + fileprivate func showNoCameraAlert() { + let alertController = UIAlertController( + title: Translation.Common.Error.String, + message: Translation.RegattaCheckInListView.NoCameraAlert.Message.String, + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default, handler: nil) + alertController.addAction(okAction) + present(alertController, animated: true, completion: nil) + } + + fileprivate func showNoCodeAlert() { + let alertController = UIAlertController( + title: Translation.RegattaCheckInListView.NoCodeAlert.Title.String, + message: Translation.RegattaCheckInListView.NoCodeAlert.Message.String, + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default, handler: nil) + alertController.addAction(okAction) + present(alertController, animated: true, completion: nil) + } + + // MARK: - Segues + + // TODO: Activate Training Feature -> Show training view in IB + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if segue.identifier == Segue.Scan { + guard let scanVC = segue.destination as? ScanViewController else { return } + scanVC.coreDataManager = coreDataManager + scanVC.delegate = self + } else if segue.identifier == Segue.CreateTraining { + guard let createTrainingVC = segue.destination as? CreateTrainingViewController else { return } + createTrainingVC.coreDataManager = coreDataManager + createTrainingVC.delegate = self + } + } + +} + +// MARK: - ScanViewControllerDelegate + +extension NewCheckInViewController: ScanViewControllerDelegate { + + func scanViewController(_ controller: ScanViewController, didCheckIn checkIn: CheckIn) { + delegate?.newCheckInViewController(self, didCheckIn: checkIn) + dismiss(animated: true) + } + +} + +// MARK: - CreateTrainingViewControllerDelegate + +extension NewCheckInViewController: CreateTrainingViewControllerDelegate { + + func createTrainingViewController(_ controller: CreateTrainingViewController, didCheckIn checkIn: CheckIn) { + delegate?.newCheckInViewController(self, didCheckIn: checkIn) + dismiss(animated: true) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Bold.ttf b/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Bold.ttf deleted file mode 100755 index fd79d43bea0..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Bold.ttf and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Light.ttf b/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Light.ttf deleted file mode 100755 index 0d381897da2..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Light.ttf and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Regular.ttf b/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Regular.ttf deleted file mode 100755 index db433349b70..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Regular.ttf and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Semibold.ttf b/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Semibold.ttf deleted file mode 100755 index 1a7679e3949..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/OpenSans-Semibold.ttf and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Preferences.swift b/ios/SAPTracker/src/xcode/SAPTracker/Preferences.swift index c53ff5b0839..4d78aded905 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Preferences.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/Preferences.swift @@ -13,23 +13,47 @@ class Preferences: NSObject { struct NotificationType { static let BatterySavingChanged = "BatterySavingChanged" static let NewCheckInURLChanged = "NewCheckInURLChanged" + static let TrainingEndpointChanged = "TrainingEndpointChanged" } struct UserInfo { static let BatterySaving = "BatterySaving" static let CheckInURL = "CheckInURL" + static let TrainingEndpoint = "TrainingEndpoint" } struct PreferenceKey { + static let ActiveTrainingRaceData = "ActiveTrainingRaceData" static let CodeConventionRead = "CodeConventionRead" static let BatterySaving = "BatterySaving" + static let BoatClassName = "BoatClassName" static let NewCheckInURL = "NewCheckInURL" + static let TrainingEndpoint = "TrainingEndpoint" static let TermsAccepted = "TermsAccepted" static let UUID = "udid" } fileprivate static let preferences = UserDefaults.standard + // MARK: - ActiveTrainingRaceData + + class var activeTrainingRaceData: TrainingRaceData? { + get { + guard let data = preferences.data(forKey: PreferenceKey.ActiveTrainingRaceData) else { return nil } + guard let trainingRaceData = NSKeyedUnarchiver.unarchiveObject(with: data) as? TrainingRaceData else { return nil } + return trainingRaceData + } + set (value) { + if let rootObject = value { + let data = NSKeyedArchiver.archivedData(withRootObject: rootObject) + preferences.set(data, forKey: PreferenceKey.ActiveTrainingRaceData) + } else { + preferences.set(nil, forKey: PreferenceKey.ActiveTrainingRaceData) + } + preferences.synchronize() + } + } + // MARK: - CodeConventionRead class var codeConventionRead: Bool { @@ -97,5 +121,43 @@ class Preferences: NSObject { NotificationQueue.default.enqueue(notification, postingStyle: NotificationQueue.PostingStyle.asap) } } - + + // MARK: - BoatClassName + + class var boatClassName: String { + get { + return preferences.string(forKey: PreferenceKey.BoatClassName) ?? "" + } + set(value) { + preferences.set(value, forKey: PreferenceKey.BoatClassName) + preferences.synchronize() + } + } + + // MARK: - TrainingEndpoint + + class var trainingEndpoint: String { + get { + if let url = URL(string: preferences.string(forKey: PreferenceKey.TrainingEndpoint) ?? "") { + return url.absoluteString + } else { + return Translation.Endpoint.Training.String + } + } + set (value) { + if let url = URL(string: value) { + preferences.set(url.absoluteString, forKey: PreferenceKey.TrainingEndpoint) + } else { + preferences.set(Translation.Endpoint.Training.String, forKey: PreferenceKey.TrainingEndpoint) + } + preferences.synchronize() + + // Send notification + var userInfo = [String: AnyObject]() + userInfo[UserInfo.TrainingEndpoint] = value as AnyObject? + let notification = Notification(name: Notification.Name(rawValue: NotificationType.TrainingEndpointChanged), object: self, userInfo: userInfo) + NotificationQueue.default.enqueue(notification, postingStyle: NotificationQueue.PostingStyle.asap) + } + } + } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaBoatViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaBoatViewController.swift new file mode 100644 index 00000000000..febcdc0d345 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaBoatViewController.swift @@ -0,0 +1,78 @@ +// +// RegattaBoatViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +class RegattaBoatViewController: BoatSessionViewController { + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + setup() + updateOptimistic() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refresh(false) + } + + // MARK: - Setup + + fileprivate func setup() { + setupButtons() + setupLocalization() + setupNavigationBar() + } + + fileprivate func setupButtons() { + makeGreen(button: startTrackingButton) + } + + fileprivate func setupLocalization() { + startTrackingButton.setTitle(Translation.RegattaView.StartTrackingButton.Title.String, for: .normal) + } + + fileprivate func setupNavigationBar() { + navigationItem.titleView = TitleView(title: boatCheckIn.event.name, subtitle: boatCheckIn.leaderboard.name) + navigationController?.navigationBar.setNeedsLayout() + } + +} + +// MARK: SessionViewControllerDelegate + +extension RegattaBoatViewController: SessionViewControllerDelegate { + + var checkIn: CheckIn { get { return boatCheckIn } } + + var checkOutActionTitle: String { get { return Translation.RegattaView.OptionSheet.CheckOutAction.Title.String } } + + var checkOutAlertMessage: String { get { return Translation.RegattaView.CheckOutAlert.Message.String } } + + var coreDataManager: CoreDataManager { get { return boatCoreDataManager } } + + var sessionController: SessionController { get { return boatSessionController } } + + func makeOptionSheet() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = self.optionButton + } + alertController.addAction(self.makeActionCheckOut()) + alertController.addAction(self.makeActionSettings()) + alertController.addAction(self.makeActionInfo()) + alertController.addAction(self.makeActionCancel()) + return alertController + } + + func refresh(_ animated: Bool) { + boatViewController?.refresh(animated) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaCheckInController.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaCheckInController.swift new file mode 100644 index 00000000000..10d23bc9dee --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaCheckInController.swift @@ -0,0 +1,42 @@ +// +// RegattaCheckInController.swift +// SAPTracker +// +// Created by Raimund Wege on 07.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class RegattaCheckInController: CheckInController { + + override init(coreDataManager: CoreDataManager) { + super.init(coreDataManager: coreDataManager) + delegate = self + } + +} + +extension RegattaCheckInController: CheckInControllerDelegate { + + func checkInController( + _ controller: CheckInController, + postCompetitorCheckIn checkInData: CheckInData, + success: @escaping (CheckIn) -> Void, + failure: @escaping (Error) -> Void) + { + let title = String(format: Translation.RegattaCheckInController.WelcomeAlert.Title.String, checkInData.competitorData.name) + let message = String(format: Translation.RegattaCheckInController.WelcomeAlert.Message.String, checkInData.competitorData.sailID) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in + controller.postCheckIn(checkInData: checkInData, success: success, failure: failure) + } + let cancelAction = UIAlertAction(title: Translation.RegattaCheckInController.WelcomeAlert.CancelAction.Title.String, style: .cancel) { (action) in + controller.didCancelCheckIn(failure: failure) + } + alertController.addAction(okAction) + alertController.addAction(cancelAction) + viewController?.present(alertController, animated: true) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaCompetitorCreateAndAddData.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaCompetitorCreateAndAddData.swift new file mode 100644 index 00000000000..e0c3f65dc5d --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaCompetitorCreateAndAddData.swift @@ -0,0 +1,19 @@ +// +// RegattaCompetitorCreateAndAddData.swift +// SAPTracker +// +// Created by Raimund Wege on 25.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class RegattaCompetitorCreateAndAddData: BaseData { + + fileprivate struct Keys { + static let ID = "id" + } + + var competitorID: String { get { return stringValue(forKey: Keys.ID) } } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaCompetitorViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaCompetitorViewController.swift new file mode 100644 index 00000000000..50e2e76d8f2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaCompetitorViewController.swift @@ -0,0 +1,146 @@ +// +// RegattaCompetitorViewController.swift +// SAPTracker +// +// Created by computing on 10/11/14. +// Copyright (c) 2014 com.sap.sailing. All rights reserved. +// + +import Foundation + +class RegattaCompetitorViewController : CompetitorSessionViewController { + + @IBOutlet weak var regattaStartLabel: UILabel! + @IBOutlet weak var countdownView: UIView! + @IBOutlet weak var countdownViewHeight: NSLayoutConstraint! + @IBOutlet weak var countdownDaysLabel: UILabel! + @IBOutlet weak var countdownDaysTitleLabel: UILabel! + @IBOutlet weak var countdownHoursLabel: UILabel! + @IBOutlet weak var countdownHoursTitleLabel: UILabel! + @IBOutlet weak var countdownMinutesLabel: UILabel! + @IBOutlet weak var countdownMinutesTitleLabel: UILabel! + @IBOutlet weak var leaderboardButton: UIButton! + @IBOutlet weak var eventButton: UIButton! + @IBOutlet weak var announcementLabel: UILabel! + + weak var countdownTimer: Timer? + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + setup() + updateOptimistic() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + validateTimer() + refresh(false) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + invalidateTimer() + } + + // MARK: - Timer + + fileprivate func validateTimer() { + tickTimer() + countdownTimer = Timer.scheduledTimer( + timeInterval: 1, + target: self, + selector: #selector(tickTimer), + userInfo: nil, + repeats: true + ) + } + + fileprivate func invalidateTimer() { + countdownTimer?.invalidate() + } + + // MARK: - Setup + + fileprivate func setup() { + setupButtons() + setupLocalization() + setupNavigationBar() + } + + fileprivate func setupButtons() { + makeBlue(button: eventButton) + makeBlue(button: leaderboardButton) + makeGreen(button: startTrackingButton) + } + + fileprivate func setupLocalization() { + announcementLabel.text = Translation.RegattaView.AnnouncementLabel.Text.String + countdownDaysTitleLabel.text = Translation.RegattaView.CountdownDaysTitleLabel.Text.String + countdownHoursTitleLabel.text = Translation.RegattaView.CountdownHoursTitleLabel.Text.String + countdownMinutesTitleLabel.text = Translation.RegattaView.CountdownMinutesTitleLabel.Text.String + eventButton.setTitle(Translation.RegattaView.EventButton.Title.String, for: .normal) + leaderboardButton.setTitle(Translation.LeaderboardView.Title.String, for: .normal) + startTrackingButton.setTitle(Translation.RegattaView.StartTrackingButton.Title.String, for: .normal) + } + + fileprivate func setupNavigationBar() { + navigationItem.titleView = TitleView(title: competitorCheckIn.event.name, subtitle: competitorCheckIn.leaderboard.name) + navigationController?.navigationBar.setNeedsLayout() + } + + // MARK: - Timer + + @objc fileprivate func tickTimer() { + guard competitorCheckIn != nil else { return } + if competitorCheckIn.event.startDate - Date().timeIntervalSince1970 > 0 { + regattaStartLabel.text = Translation.RegattaView.StartLabel.Text.BeforeRegattaDidStart.String + let duration = competitorCheckIn.event.startDate - Date().timeIntervalSince1970 + let days = Int(duration / (60 * 60 * 24)) + let hours = Int(duration / (60 * 60)) - (days * 24) + let minutes = Int(duration / 60) - (days * 24 * 60) - (hours * 60) + countdownDaysLabel.text = String(format: "%02d", days) + countdownHoursLabel.text = String(format: "%02d", hours) + countdownMinutesLabel.text = String(format: "%02d", minutes) + countdownView.isHidden = false + countdownViewHeight.constant = 60 + } else { + regattaStartLabel.text = Translation.RegattaView.StartLabel.Text.AfterRegattaDidStart.String + countdownView.isHidden = true + countdownViewHeight.constant = 0 + } + } + +} + +// MARK: SessionViewControllerDelegate + +extension RegattaCompetitorViewController: SessionViewControllerDelegate { + + var checkIn: CheckIn { get { return competitorCheckIn } } + + var checkOutActionTitle: String { get { return Translation.RegattaView.OptionSheet.CheckOutAction.Title.String } } + + var checkOutAlertMessage: String { get { return Translation.RegattaView.CheckOutAlert.Message.String } } + + var coreDataManager: CoreDataManager { get { return competitorCoreDataManager } } + + var sessionController: SessionController { get { return competitorSessionController } } + + func makeOptionSheet() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = self.optionButton + } + alertController.addAction(self.makeActionCheckOut()) + alertController.addAction(self.makeActionSettings()) + alertController.addAction(self.makeActionInfo()) + alertController.addAction(self.makeActionCancel()) + return alertController + } + + func refresh(_ animated: Bool) { + competitorViewController?.refresh(animated) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaMarkViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaMarkViewController.swift new file mode 100644 index 00000000000..d000dba1c19 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaMarkViewController.swift @@ -0,0 +1,78 @@ +// +// RegattaMarkViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 05.05.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class RegattaMarkViewController: MarkSessionViewController { + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + setup() + updateOptimistic() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refresh(false) + } + + // MARK: - Setup + + fileprivate func setup() { + setupButtons() + setupLocalization() + setupNavigationBar() + } + + fileprivate func setupButtons() { + makeGreen(button: startTrackingButton) + } + + fileprivate func setupLocalization() { + startTrackingButton.setTitle(Translation.RegattaView.StartTrackingButton.Title.String, for: .normal) + } + + fileprivate func setupNavigationBar() { + navigationItem.titleView = TitleView(title: markCheckIn.event.name, subtitle: markCheckIn.leaderboard.name) + navigationController?.navigationBar.setNeedsLayout() + } + +} + +// MARK: SessionViewControllerDelegate + +extension RegattaMarkViewController: SessionViewControllerDelegate { + + var checkIn: CheckIn { get { return markCheckIn } } + + var checkOutActionTitle: String { get { return Translation.RegattaView.OptionSheet.CheckOutAction.Title.String } } + + var checkOutAlertMessage: String { get { return Translation.RegattaView.CheckOutAlert.Message.String } } + + var coreDataManager: CoreDataManager { get { return markCoreDataManager } } + + var sessionController: SessionController { get { return markSessionController } } + + func makeOptionSheet() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = self.optionButton + } + alertController.addAction(self.makeActionCheckOut()) + alertController.addAction(self.makeActionSettings()) + alertController.addAction(self.makeActionInfo()) + alertController.addAction(self.makeActionCancel()) + return alertController + } + + func refresh(_ animated: Bool) { + markViewController?.refresh(animated) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaRaceColumnAddData.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaRaceColumnAddData.swift new file mode 100644 index 00000000000..d3cdc706ef6 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaRaceColumnAddData.swift @@ -0,0 +1,21 @@ +// +// RegattaRaceColumnAddData.swift +// SAPTracker +// +// Created by Raimund Wege on 12.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class RegattaRaceColumnAddData: BaseData { + + fileprivate struct Keys { + static let FleetName = "seriesname" + static let RaceColumnName = "racename" + } + + var fleetName: String { get { return stringValue(forKey: Keys.FleetName) } } + var raceColumnName: String { get { return stringValue(forKey: Keys.RaceColumnName) } } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaRaceCourseData.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaRaceCourseData.swift new file mode 100644 index 00000000000..dee46265b20 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaRaceCourseData.swift @@ -0,0 +1,22 @@ +// +// RegattaRaceCourseData.swift +// SAPTracker +// +// Created by Raimund Wege on 20.12.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class RegattaRaceCourseData: BaseData { + + func isEmpty() -> Bool { + guard let waypoints = dictionary["waypoints"] as? [[String: AnyObject]] else { return true } + return waypoints.count == 0 + } + + func isAutoCourse() -> Bool { + return dictionary["name"] as? String == "Auto-Course" + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/ScanViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/ScanViewController.swift index 156631f1f26..54043a81291 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/ScanViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/ScanViewController.swift @@ -9,9 +9,16 @@ import UIKit import AVFoundation +protocol ScanViewControllerDelegate: class { + + func scanViewController(_ controller: ScanViewController, didCheckIn checkIn: CheckIn) + +} + class ScanViewController: UIViewController { - weak var homeViewController: HomeViewController? + weak var coreDataManager: CoreDataManager! + weak var delegate: ScanViewControllerDelegate? @IBOutlet weak var previewView: UIView! @IBOutlet weak var targetImageView: UIImageView! @@ -105,14 +112,14 @@ class ScanViewController: UIViewController { message: error.localizedFailureReason, preferredStyle: .alert ) - let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { (action) in + let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { [weak self] action in if let settingsURL = URL(string: UIApplicationOpenSettingsURLString) { UIApplication.shared.openURL(settingsURL) } - _ = self.navigationController?.popViewController(animated: true) + _ = self?.navigationController?.popViewController(animated: true) } - let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel) { (action) in - _ = self.navigationController?.popViewController(animated: true) + let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel) { [weak self] action in + _ = self?.navigationController?.popViewController(animated: true) } alertController.addAction(settingsAction) alertController.addAction(cancelAction) @@ -136,10 +143,8 @@ class ScanViewController: UIViewController { // MARK: - Properties - fileprivate lazy var checkInController: CheckInController = { - let checkInController = CheckInController() - checkInController.delegate = self - return checkInController + fileprivate lazy var regattaCheckInController: RegattaCheckInController = { + return RegattaCheckInController(coreDataManager: self.coreDataManager) }() // MARK: - Helper @@ -156,16 +161,6 @@ class ScanViewController: UIViewController { } -// MARK: - CheckInControllerDelegate - -extension ScanViewController: CheckInControllerDelegate { - - func checkInController(_ sender: CheckInController, show alertController: UIAlertController) { - present(alertController, animated: true, completion: nil) - } - -} - // MARK: - AVCaptureMetadataOutputObjectsDelegate extension ScanViewController: AVCaptureMetadataOutputObjectsDelegate { @@ -191,33 +186,37 @@ extension ScanViewController: AVCaptureMetadataOutputObjectsDelegate { } fileprivate func captureOutputSuccess(checkInData: CheckInData) { - checkInController.checkIn(checkInData: checkInData, completion: { (withSuccess) in - if withSuccess { - self.homeViewController?.selectedCheckIn = CoreDataManager.sharedManager.fetchCheckIn(checkInData: checkInData) - _ = self.navigationController?.popViewController(animated: true) - } else { - self.startScanning() - } - }) + checkIn(withCheckInData: checkInData) } fileprivate func captureOutputFailure() { - showIncorrectCodeAlert() - } - - // MARK: - Alerts - - fileprivate func showIncorrectCodeAlert() { - let alertController = UIAlertController( - title: Translation.Common.Error.String, - message: Translation.ScanView.IncorrectCodeAlert.Message.String, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in - self.startScanning() + let title = Translation.Common.Error.String + let message = Translation.ScanView.IncorrectCodeAlert.Message.String + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { [weak self] action in + self?.startScanning() } alertController.addAction(okAction) present(alertController, animated: true, completion: nil) } + // MARK: - CheckIn + + fileprivate func checkIn(withCheckInData checkInData: CheckInData) { + regattaCheckInController.checkInWithViewController(self, checkInData: checkInData, success: { [weak self] (checkIn) in + self?.checkInSuccess(checkIn: checkIn) + }) { [weak self] (error) in + self?.checkInFailure(error: error) + } + } + + fileprivate func checkInSuccess(checkIn: CheckIn) { + _ = navigationController?.popViewController(animated: true) + delegate?.scanViewController(self, didCheckIn: checkIn) + } + + fileprivate func checkInFailure(error: Error) { + startScanning() + } + } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SessionController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SessionController.swift index 6660e07755e..589f4bcfe00 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/SessionController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/SessionController.swift @@ -8,17 +8,23 @@ import UIKit +enum SessionControllerError: Error { + case checkInDataIsIncomplete +} + class SessionController: NSObject { - - let checkIn: CheckIn + + weak var checkIn: CheckIn! + weak var coreDataManager: CoreDataManager! var sendingBackgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid var sendingDate: Date = Date() fileprivate (set) var isTracking: Bool = false - init(checkIn: CheckIn) { + init(checkIn: CheckIn, coreDataManager: CoreDataManager) { self.checkIn = checkIn + self.coreDataManager = coreDataManager super.init() subscribeForNotifications() } @@ -53,9 +59,9 @@ class SessionController: NSObject { guard self.isTracking else { return } guard let locationData = notification.userInfo?[LocationManager.UserInfo.LocationData] as? LocationData else { return } guard locationData.isValid else { return } - let gpsFix = CoreDataManager.sharedManager.newGPSFix(checkIn: self.checkIn) + let gpsFix = self.coreDataManager.newGPSFix(checkIn: self.checkIn) gpsFix.updateWithLocationData(locationData: locationData) - CoreDataManager.sharedManager.saveContext() + self.coreDataManager.saveContext() if self.sendingDate.compare(Date()) == .orderedAscending { self.sendingDate = Date().addingTimeInterval(BatteryManager.sharedManager.sendingPeriod) self.beginGPSFixSendingInBackgroundTask() @@ -92,25 +98,27 @@ class SessionController: NSObject { // MARK: - Update - func update(completion: @escaping () -> Void) { - guard let checkInData = CheckInData(checkIn: checkIn) else { updateFailure(completion: completion); return } - requestManager.getCheckInData( - checkInData: checkInData, - success: { (checkInData) in self.updateSuccess(checkInData: checkInData, completion: completion) }, - failure: { (error) in self.updateFailure(completion: completion) } - ) + func update( + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) throws + { + guard let checkInData = CheckInData(checkIn: checkIn) else { + throw SessionControllerError.checkInDataIsIncomplete + } + let checkInDataCollector = CheckInDataCollector(checkInData: checkInData) + checkInDataCollector.collect(checkInData: checkInData, success: { [weak self] (checkInData) in + self?.updateSuccess(checkInData: checkInData, success: success) + }) { (error) in + failure(error) + } } - func updateSuccess(checkInData: CheckInData, completion: () -> Void) { + fileprivate func updateSuccess(checkInData: CheckInData, success: () -> Void) { checkIn.event.updateWithEventData(eventData: checkInData.eventData) checkIn.leaderboard.updateWithLeaderboardData(leaderboardData: checkInData.leaderboardData) checkIn.updateWithCheckInData(checkInData: checkInData) - CoreDataManager.sharedManager.saveContext() - completion() - } - - func updateFailure(completion: () -> Void) { - completion() + coreDataManager.saveContext() + success() } // MARK: - Tracking @@ -129,7 +137,7 @@ class SessionController: NSObject { // MARK: - CheckOut func checkOut(completion: @escaping (_ withSuccess: Bool) -> Void) { - requestManager.postCheckOut( + checkInRequestManager.postCheckOut( checkIn, success: { () in completion(true) }, failure: { (error) in completion(false) } @@ -139,13 +147,11 @@ class SessionController: NSObject { // MARK: - Properties lazy var gpsFixController: GPSFixController = { - let gpsFixController = GPSFixController(checkIn: self.checkIn) - return gpsFixController + return GPSFixController(checkIn: self.checkIn, coreDataManager: self.coreDataManager) }() - lazy var requestManager: RequestManager = { - let requestManager = RequestManager(baseURLString: self.checkIn.serverURL) - return requestManager + lazy var checkInRequestManager: CheckInRequestManager = { + return CheckInRequestManager(baseURLString: self.checkIn.serverURL) }() } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift index 8516030e85e..e61b455bcc8 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift @@ -8,44 +8,41 @@ import UIKit -protocol SessionViewControllerDelegate { +protocol SessionViewControllerDelegate: class { + + var checkIn: CheckIn { get } - func performCheckOut() + var checkOutActionTitle: String { get } - func startTracking() throws + var checkOutAlertMessage: String { get } + var coreDataManager: CoreDataManager { get } + + var sessionController: SessionController { get } + + func makeOptionSheet() -> UIAlertController + + func refresh(_ animated: Bool) + } class SessionViewController: UIViewController { - - struct Segue { - static let About = "About" - static let Leaderboard = "Leaderboard" - static let Settings = "Settings" + + struct SessionSegue { static let Tracking = "Tracking" } - - @IBOutlet weak var startTrackingButton: UIButton! - - var delegate: SessionViewControllerDelegate! - // MARK: - Actions - - @IBAction func startTrackingButtonTapped(_ sender: AnyObject) { - // TODO: Add or add not WiFi Alert? - //if SMTWiFiStatus.wifiStatus() == WiFiStatus.On && !AFNetworkReachabilityManager.sharedManager().reachableViaWiFi { - // showStartTrackingWiFiAlert() - //} else { - startTracking() - //} - } - + @IBOutlet weak var optionButton: UIBarButtonItem! + @IBOutlet weak var startTrackingButton: UIButton! + + weak var delegate: SessionViewControllerDelegate! + // MARK: - Tracking - + fileprivate func startTracking() { do { - try delegate.startTracking() - performSegue(withIdentifier: Segue.Tracking, sender: self) + try delegate.sessionController.startTracking() + performSegue(withIdentifier: SessionSegue.Tracking, sender: self) } catch let error as LocationManager.LocationManagerError { showStartTrackingFailureAlert(message: error.description) } catch { @@ -53,42 +50,161 @@ class SessionViewController: UIViewController { } } + // MARK: - Update + + func updateOptimistic() { + update(success: { + // ... + }) { (error) in + // ... + } + } + + func updatePessimistic() { + SVProgressHUD.show() + update(success: { + SVProgressHUD.dismiss() + }) { [weak self] (error) in + SVProgressHUD.dismiss() + self?.showAlert(forError: error) + } + } + + fileprivate func update( + success: @escaping (() -> Void), + failure: @escaping ((_ error: Error) -> Void)) + { + do { + try delegate.sessionController.update(success: { [weak self] in + success() + self?.delegate.refresh(true) + }) { (error) in + failure(error) + } + } catch { + failure(error) + } + } + // MARK: - CheckOut func checkOut() { showCheckOutAlert() } - // MARK: - Alerts + // MARK: - Actions + @IBAction func optionButtonTapped(_ sender: Any) { + present(delegate.makeOptionSheet(), animated: true, completion: nil) + } + + func makeActionCancel() -> UIAlertAction { + return UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) + } + + func makeActionCheckOut() -> UIAlertAction { + return UIAlertAction(title: delegate?.checkOutActionTitle, style: .default) { [weak self] action in + self?.checkOut() + } + } + + func makeActionInfo() -> UIAlertAction { + return UIAlertAction(title: Translation.Common.Info.String, style: .default) { [weak self] action in + self?.presentAboutViewController() + } + } + + func makeActionSettings() -> UIAlertAction { + return UIAlertAction(title: Translation.SettingsView.Title.String, style: .default) { [weak self] action in + self?.presentSettingsViewController() + } + } + + @IBAction func refreshButtonTapped(_ sender: Any) { + updatePessimistic() + } + + @IBAction func startTrackingButtonTapped(_ sender: Any) { + // TODO: Add or add not WiFi Alert? + //if SMTWiFiStatus.wifiStatus() == WiFiStatus.On && !AFNetworkReachabilityManager.sharedManager().reachableViaWiFi { + // showStartTrackingWiFiAlert() + //} else { + startTracking() + //} + } + + @IBAction func eventButtonTapped(_ sender: UIButton) { + if let eventURL = delegate.checkIn.eventURL() { + UIApplication.shared.openURL(eventURL) + } + } + + @IBAction func leaderboardButtonTapped(_ sender: Any) { + presentLeaderboardViewController() + } + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == SessionSegue.Tracking) { + let trackingNC = segue.destination as! UINavigationController + let trackingVC = trackingNC.viewControllers[0] as! TrackingViewController + trackingVC.checkIn = delegate.checkIn + trackingVC.sessionController = delegate.sessionController + } + } + + fileprivate func presentLeaderboardViewController() { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + guard let leaderboardNC = storyboard.instantiateViewController(withIdentifier: "LeaderboardNavigationController") as? UINavigationController else { return } + guard let leaderboardVC = leaderboardNC.childViewControllers.first as? LeaderboardViewController else { return } + leaderboardVC.checkIn = delegate.checkIn + present(leaderboardNC, animated: true) + } + + // MARK: - Alerts + fileprivate func showCheckOutAlert() { let alertController = UIAlertController( title: Translation.Common.Warning.String, - message: Translation.CompetitorView.CheckOutAlert.Message.String, + message: delegate?.checkOutAlertMessage, preferredStyle: .alert ) - let yesAction = UIAlertAction(title: Translation.Common.Yes.String, style: .default) { (action) in - self.delegate.performCheckOut() + let yesAction = UIAlertAction(title: Translation.Common.Yes.String, style: .default) { [weak self] action in + self?.performCheckOut() } let noAction = UIAlertAction(title: Translation.Common.No.String, style: .cancel, handler: nil) alertController.addAction(yesAction) alertController.addAction(noAction) present(alertController, animated: true, completion: nil) } - + + fileprivate func performCheckOut() { + delegate.sessionController.checkOut { [weak self] (withSuccess) in + self?.performCheckOutCompleted(withSuccess: withSuccess) + } + } + + fileprivate func performCheckOutCompleted(withSuccess: Bool) { + delegate.coreDataManager.deleteObject(object: delegate.checkIn) + delegate.coreDataManager.saveContext() + navigationController?.popViewController(animated: true) + } + fileprivate func showStartTrackingWiFiAlert() { let alertController = UIAlertController( title: "INFO", message: "WIFI IS ON BUT NOT CONNECTED", preferredStyle: .alert ) - let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { (action) in + let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { action in if let settingsURL = URL(string: "prefs:root=WIFI") { UIApplication.shared.openURL(settingsURL) } } - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { (action) in - self.startTracking() + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { [weak self] action in + self?.startTracking() } let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) alertController.addAction(settingsAction) @@ -103,7 +219,7 @@ class SessionViewController: UIViewController { message: message, preferredStyle: .alert ) - let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { (action) in + let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { action in if let locationServiceURL = URL(string: "prefs:root=LOCATION_SERVICES") { UIApplication.shared.openURL(locationServiceURL) } @@ -113,5 +229,5 @@ class SessionViewController: UIViewController { alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } - + } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SettingsViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SettingsViewController.swift index c4cb28fec5d..28e5b904ff1 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/SettingsViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/SettingsViewController.swift @@ -10,12 +10,14 @@ import Foundation class SettingsViewController: UITableViewController { - @IBOutlet weak var batterySavingTitleLabel: UILabel! - @IBOutlet weak var batterySavingDescriptionLabel: UILabel! - @IBOutlet weak var batterySavingSwitch: UISwitch! - @IBOutlet weak var deviceIdentifierTitleLabel: UILabel! - @IBOutlet weak var deviceIdentifierLabel: UILabel! - + @IBOutlet var batterySavingTitleLabel: UILabel! + @IBOutlet var batterySavingDescriptionLabel: UILabel! + @IBOutlet var batterySavingSwitch: UISwitch! + @IBOutlet var deviceIdentifierTitleLabel: UILabel! + @IBOutlet var deviceIdentifierLabel: UILabel! + @IBOutlet var trainingServerTitleLabel: UILabel! + @IBOutlet var trainingEndpointTextField: UITextField! + override func viewDidLoad() { super.viewDidLoad() setup() @@ -30,6 +32,7 @@ class SettingsViewController: UITableViewController { setupLocalization() setupNavigationBar() setupTableView() + setupTrainingEndpointTextField() } fileprivate func setupBatterySavingSwitch() { @@ -51,6 +54,8 @@ class SettingsViewController: UITableViewController { navigationItem.title = Translation.SettingsView.Title.String batterySavingTitleLabel.text = Translation.SettingsView.BatterySavingTitleLabel.Text.String deviceIdentifierTitleLabel.text = Translation.SettingsView.DeviceIdentifierTitleLabel.Text.String + trainingServerTitleLabel.text = Translation.SettingsView.TrainingServerTitleLabel.Text.String + trainingEndpointTextField.placeholder = Preferences.trainingEndpoint } fileprivate func setupNavigationBar() { @@ -61,7 +66,12 @@ class SettingsViewController: UITableViewController { tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 140 } - + + fileprivate func setupTrainingEndpointTextField() { + trainingEndpointTextField.text = Preferences.trainingEndpoint + trainingEndpointTextField.delegate = self + } + // MARK: - Actions @IBAction func batterySavingChanged(_ sender: UISwitch) { @@ -70,8 +80,13 @@ class SettingsViewController: UITableViewController { setupBatterySavingDescriptionLabel() tableView.endUpdates() } - - @IBAction func doneButtonTapped(_ sender: AnyObject) { + + @IBAction func trainingEndpointEditingDidEnd(_ sender: Any) { + guard let trainingEndpoint = trainingEndpointTextField.text else { return } + Preferences.trainingEndpoint = trainingEndpoint + } + + @IBAction func doneButtonTapped(_ sender: Any) { presentingViewController!.dismiss(animated: true, completion: nil) } @@ -80,7 +95,8 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch section { case 0: return Translation.SettingsView.TableView.BatterySavingSection.Title.String - case 1: return Translation.SettingsView.TableView.OtherSettingsSection.Title.String + case 1: return Translation.SettingsView.TableView.DeviceSection.Title.String + case 2: return Translation.SettingsView.TableView.TrainingSection.Title.String default: return "" } } @@ -90,5 +106,21 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableViewAutomaticDimension } - + + // TODO: Activate Training Feature -> Delete this method + override func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + +} + +// MARK: - + +extension SettingsViewController: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return false + } + } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUp.storyboard b/ios/SAPTracker/src/xcode/SAPTracker/SignUp.storyboard new file mode 100644 index 00000000000..6ebf72f437f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/SignUp.storyboard @@ -0,0 +1,651 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings b/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings new file mode 100644 index 00000000000..fb997d137b2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings @@ -0,0 +1,47 @@ +/* + SignUp.strings + SAPTracker + + Created by Raimund Wege on 10.08.17. + Copyright © 2017 com.sap.sailing. All rights reserved. +*/ + +// MARK: - Common + +"Common.Email" = "E-Mail"; +"Common.Password" = "Passwort"; +"Common.Server" = "Server"; +"Common.UserName" = "Benutzername"; + +// MARK: - ForgotPasswordView + +"ForgotPasswordView.Title" = "Passwort vergessen"; +"ForgotPasswordView.InfoLabel.Text" = "Benutzernamen oder E-Mail Adresse angeben"; +"ForgotPasswordView.ResetPasswordButton.Title" = "Passwort zurücksetzen"; + +// MARK: - LoginView + +"LoginView.Title" = "Anmelden"; +"LoginView.ForgotPasswordButton.Title" = "Passwort vergessen"; +"LoginView.LoginButton.Title" = "Anmelden"; + +// MARK: - SignUpControllerError + +"SignUpControllerError.LoginFailed" = "Anmeldung fehlgeschlagen. Zugangsdaten überprüfen."; + +// MARK: - SignUpView + +"SignUpView.Title" = "Registrieren"; +"SignUpView.InfoLabel.Text" = "Herzlich Willkommen."; +"SignUpView.FullNameLabel.Text" = "Vollständiger Name"; +"SignUpView.CompanyLabel.Text" = "Unternehmen"; +"SignUpView.PasswordTextField.Placeholder" = "Geben Sie ein neues Passwort ein"; +"SignUpView.RepeatPasswordLabel.Text" = "Passwort Wiederholung"; +"SignUpView.RepeatPasswordTextField.Placeholder" = "Wiederholen Sie das Passwort"; +"SignUpView.SignUpButton.Title" = "Registrieren"; + +// MARK: - SignUpViewError + +"SignUpViewError.UserNameIsToShort" = "Bitte einen Benutzername mit mindestens 3 Zeichen eingeben."; +"SignUpViewError.PasswordIsToShort" = "Bitte ein Passwort mit mindestens 5 Zeichen eingeben."; +"SignUpViewError.PasswordsNotEqual" = "Passwort und Passwortwiederholung müssen übereinstimmen."; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUpController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SignUpController.swift new file mode 100644 index 00000000000..2f865fc8abd --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/SignUpController.swift @@ -0,0 +1,296 @@ +// +// SignUpController.swift +// SAPTracker +// +// Created by Raimund Wege on 02.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +enum SignUpControllerError: Error { + case loginFailed + case unauthenticated +} + +extension SignUpControllerError: LocalizedError { + var errorDescription: String? { + switch self { + case .loginFailed: + return SignUpTranslation.SignUpControllerError.LoginFailed.String + case .unauthenticated: + return SignUpTranslation.SignUpControllerError.Unauthenticated.String + } + } +} + +@objc protocol SignUpControllerDelegate: class { + + func signUpController(_ controller: SignUpController, didFinishLoginWithUserName userName: String) + + func signUpControllerDidCancelLogin(_ controller: SignUpController) + + func signUpControllerDidLogout(_ controller: SignUpController) + +} + +class SignUpController: NSObject { + + weak var delegate: SignUpControllerDelegate? + + fileprivate let requestManager: SignUpRequestManager + + init(baseURLString: String) { + requestManager = SignUpRequestManager(baseURLString: baseURLString) + super.init() + } + + // MARK: - Check Authentication + + func checkAuthentication( + success: @escaping (_ userName: String) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + requestManager.postHello(success: { (principal, authenticated, remembered) in + if authenticated { + success(principal) + } else { + failure(SignUpControllerError.unauthenticated, nil) + } + }) { (error, message) in + failure(error, message) + } + } + + // MARK: - Refresh Access Token + + func refreshAccessToken( + success: @escaping (_ userName: String) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + requestManager.postAccessToken(userName: userName ?? "", password: userPassword ?? "", success: { (userName, accessToken) in + self.userAccessToken = accessToken + success(userName) + }, failure: failure) + } + + // MARK: - Login + + func login( + success: @escaping (_ userName: String) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + self.checkAuthentication(success: success) { (error, message) in + self.refreshAccessToken(success: success, failure: failure) + } + } + + func loginWithViewController(_ controller: UIViewController) { + let storyboard = UIStoryboard(name: "SignUp", bundle: nil) + let loginNC = storyboard.instantiateInitialViewController() as! UINavigationController + let loginVC = loginNC.viewControllers[0] as! LoginViewController + loginVC.signUpController = self + controller.present(loginNC, animated: true, completion: nil) + } + + // MARK: - Logout + + func logout(completion: @escaping () -> Void) { + userName = nil + userPassword = nil + userAccessToken = nil + requestManager.postLogout(success: { [weak self] in + self?.didLogout() + completion() + }) { [weak self] (error, message) in + self?.didLogout() + completion() + } + } + + // MARK: SignUpControllerDelegate + + fileprivate func didFinishLogin() { + delegate?.signUpController(self, didFinishLoginWithUserName: userName ?? "") + } + + fileprivate func didCancelLogin() { + delegate?.signUpControllerDidCancelLogin(self) + } + + fileprivate func didLogout() { + delegate?.signUpControllerDidLogout(self) + } + + // MARK: - Properties + + var baseURLString: String { + get { + return requestManager.baseURLString + } + } + + var userName: String? { + get { + return password(forItem: Keychain.userName(forService: baseURLString)) + } + set (value) { + setPassword(value, forItem: Keychain.userName(forService: baseURLString)) + } + } + + var userPassword: String? { + get { + return password(forItem: Keychain.userPassword(forService: baseURLString)) + } + set (value) { + setPassword(value, forItem: Keychain.userPassword(forService: baseURLString)) + } + } + + var userAccessToken: String? { + get { + return password(forItem: Keychain.userAccessToken(forService: baseURLString)) + } + set (value) { + setPassword(value, forItem: Keychain.userAccessToken(forService: baseURLString)) + } + } + + fileprivate func password(forItem item: KeychainPasswordItem) -> String? { + do { + return try item.readPassword() + } catch { + print(error) + return nil + } + } + + fileprivate func setPassword(_ password: String?, forItem item: KeychainPasswordItem) { + do { + if let password = password { + try item.savePassword(password) + } else { + try item.deleteItem() + } + } catch { + print(error) + } + } + + // MARK: - Helper + + fileprivate func showAlert(forError error: Error, andMessage message: String?, withViewController controller: UIViewController) { + let alertController = UIAlertController.init(title: error.localizedDescription, message: message, preferredStyle: .alert) + let okAction = UIAlertAction.init(title: Translation.Common.OK.String, style: .cancel, handler: nil) + alertController.addAction(okAction) + controller.present(alertController, animated: true, completion: nil) + } + +} + +// MARK: - LoginViewControllerDelegate + +extension SignUpController: LoginViewControllerDelegate { + + func loginViewController(_ controller: LoginViewController, willLoginWithUserName userName: String, password: String) { + requestManager.postAccessToken(userName: userName, password: password, success: { [weak self] userName, accessToken in + self?.loginViewController(controller, didFinishLoginWithUserName: userName, password: password, accessToken: accessToken) + }) { [weak self] error, message in + self?.loginViewController(controller, didFailLoginWithError: error, message: message) + } + } + + func loginViewControllerWillCancel(_ controller: LoginViewController) { + controller.presentingViewController?.dismiss(animated: true) + didCancelLogin() + } + + fileprivate func loginViewController(_ controller: LoginViewController, didFinishLoginWithUserName userName: String, password: String, accessToken: String) { + self.userName = userName + self.userPassword = password + self.userAccessToken = accessToken + controller.presentingViewController?.dismiss(animated: true) + didFinishLogin() + } + + fileprivate func loginViewController(_ controller: LoginViewController, didFailLoginWithError error: Error, message: String?) { + showAlert(forError: SignUpController.loginViewControllerError(forError: error), andMessage: message, withViewController: controller) + } + + fileprivate static func loginViewControllerError(forError error: Error) -> Error { + if (error as NSError).domain == NSURLErrorDomain { + return error + } + return SignUpControllerError.loginFailed + } + +} + +// MARK: - SignUpViewControllerDelegate + +extension SignUpController: SignUpViewControllerDelegate { + + func signUpViewController( + _ controller: SignUpViewController, + willSignUpWithUserName userName: String, + email: String, + fullName: String, + company: String, + password: String) + { + requestManager.postCreateUser(userName: userName, email: email, fullName: fullName, company: company, password: password, success: { [weak self] userName, accessToken in + self?.signUpViewController(controller, didFinishSignUpWithUserName: userName, password: password, accessToken: accessToken) + }) { [weak self] error, message in + self?.signUpViewController(controller, didFailSignUpWithError: error, message: message) + } + } + + fileprivate func signUpViewController( + _ controller: SignUpViewController, + didFinishSignUpWithUserName userName: String, + password: String, + accessToken: String) + { + self.userName = userName + self.userPassword = password + self.userAccessToken = accessToken + controller.presentingViewController?.dismiss(animated: true) + didFinishLogin() + } + + fileprivate func signUpViewController(_ controller: SignUpViewController, didFailSignUpWithError error: Error, message: String?) { + showAlert(forError: error, andMessage: message, withViewController: controller) + } + +} + +// MARK: - ForgotPasswordViewControllerDelegate + +extension SignUpController: ForgotPasswordViewControllerDelegate { + + func forgotPasswordViewController(_ controller: ForgotPasswordViewController, willChangePasswordForEmail email: String) { + requestManager.postForgotPassword(email: email, success: { [weak self] in + self?.forgotPasswordViewControllerDidFinishChangePassword(controller) + }) { [weak self] error, message in + self?.forgotPasswordViewController(controller, didFailChangePasswordWithError: error, message: message) + } + } + + func forgotPasswordViewController(_ controller: ForgotPasswordViewController, willChangePasswordForUserName userName: String) { + requestManager.postForgotPassword(userName: userName, success: { [weak self] in + self?.forgotPasswordViewControllerDidFinishChangePassword(controller) + }) { [weak self] error, message in + self?.forgotPasswordViewController(controller, didFailChangePasswordWithError: error, message: message) + } + } + + fileprivate func forgotPasswordViewControllerDidFinishChangePassword(_ controller: ForgotPasswordViewController) { + // TODO + } + + fileprivate func forgotPasswordViewController(_ controller: ForgotPasswordViewController, didFailChangePasswordWithError error: Error, message: String?) { + showAlert(forError: error, andMessage: message, withViewController: controller) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUpRequestManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/SignUpRequestManager.swift new file mode 100644 index 00000000000..d18c7404ba1 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/SignUpRequestManager.swift @@ -0,0 +1,285 @@ +// +// SignUpRequestManager.swift +// SAPTracker +// +// Created by Raimund Wege on 07.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +enum SignUpRequestManagerError: Error { + case percentEncodingError + case invalidResponse +} + +extension SignUpRequestManagerError: LocalizedError { + var errorDescription: String? { + switch self { + case .percentEncodingError: + return "PERCENT ENCODING ERROR" + case .invalidResponse: + return "INVALID RESPONSE" + } + } +} + +class SignUpRequestManager: NSObject { + + fileprivate let basePathString = "/security/api/restsecurity" + + fileprivate enum BodyKeys { + static let AccessToken = "access_token" + static let Authenticated = "authenticated" + static let Company = "company" + static let Email = "email" + static let FullName = "fullName" + static let Password = "password" + static let Principal = "principal" + static let Remembered = "remembered" + static let UserName = "username" + } + + let baseURLString: String + let manager: AFHTTPRequestOperationManager + let sessionManager: AFURLSessionManager + + init(baseURLString: String = "") { + self.baseURLString = baseURLString + manager = AFHTTPRequestOperationManager(baseURL: URL(string: baseURLString)) + manager.requestSerializer = AFJSONRequestSerializer() as AFHTTPRequestSerializer + manager.requestSerializer.timeoutInterval = Application.RequestTimeout + manager.responseSerializer = AFJSONResponseSerializer() as AFHTTPResponseSerializer + sessionManager = AFURLSessionManager(sessionConfiguration: URLSessionConfiguration.default) + sessionManager.responseSerializer = AFJSONResponseSerializer() as AFHTTPResponseSerializer + super.init() + } + + // MARK: - AccessToken + + func postAccessToken( + userName: String, + password: String, + success: @escaping (_ userName: String, _ accessToken: String) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let urlString = "\(basePathString)/access_token" + manager.requestSerializer.setAuthorizationHeaderFieldWithUsername(userName, password: password) + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postAccessTokenSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postAccessTokenFailure(error: error, failure: failure) + } + } + + fileprivate func postAccessTokenSuccess( + responseObject: Any, + success: (_ userName: String, _ accessToken: String) -> Void, + failure: (_ error: Error, _ message: String?) -> Void) + { + manager.requestSerializer.clearAuthorizationHeader() + guard let response = responseObject as? Dictionary else { + postAccessTokenFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let userName = response[BodyKeys.UserName] as? String else { + postAccessTokenFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let accessToken = response[BodyKeys.AccessToken] as? String else { + postAccessTokenFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + manager.requestSerializer.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + logInfo(name: "\(#function)", info: response.description) + success(userName, accessToken) + } + + fileprivate func postAccessTokenFailure(error: Error, failure: (_ error: Error, _ message: String?) -> Void) { + manager.requestSerializer.clearAuthorizationHeader() + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - CreateUser + + func postCreateUser( + userName: String, + email: String, + fullName: String, + company: String, + password: String, + success: @escaping (_ userName: String, _ accessToken: String) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let urlString = "\(basePathString)/create_user?username=\(userName)&email=\(email)&fullName=\(fullName)&company=\(company)&password=\(password)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postCreateUserSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postCreateUserFailure(error: error, failure: failure) + } + } else { + failure(SignUpRequestManagerError.percentEncodingError, nil) + } + } + + fileprivate func postCreateUserSuccess( + responseObject: Any, + success: (_ userName: String, _ accessToken: String) -> Void, + failure: (_ error: Error, _ message: String?) -> Void) + { + guard let response = responseObject as? Dictionary else { + postCreateUserFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let userName = response[BodyKeys.UserName] as? String else { + postCreateUserFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let accessToken = response[BodyKeys.AccessToken] as? String else { + postCreateUserFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + logInfo(name: "\(#function)", info: response.description) + success(userName, accessToken) + } + + fileprivate func postCreateUserFailure(error: Error, failure: (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - ForgotPassword + + func postForgotPassword( + email: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let urlString = "\(basePathString)/forgot_password?email=\(email)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + self.postForgotPassword(urlString: urlString, success: success, failure: failure) + } else { + self.postForgotPasswordFailure(error: SignUpRequestManagerError.percentEncodingError, failure: failure) + } + } + + func postForgotPassword( + userName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let urlString = "\(basePathString)/forgot_password?username=\(userName)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + self.postForgotPassword(urlString: urlString, success: success, failure: failure) + } else { + self.postForgotPasswordFailure(error: SignUpRequestManagerError.percentEncodingError, failure: failure) + } + } + + fileprivate func postForgotPassword( + urlString: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postForgotPasswordSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postForgotPasswordFailure(error: error, failure: failure) + } + } + + fileprivate func postForgotPasswordSuccess( + responseObject: Any, + success: () -> Void, + failure: (_ error: Error, _ message: String?) -> Void) + { + let response = responseObject as AnyObject + logInfo(name: "\(#function)", info: response.description) + success() + } + + fileprivate func postForgotPasswordFailure(error: Error, failure: (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - Hello + + func postHello( + success: @escaping (_ principal: String, _ authenticated: Bool, _ remembered: Bool) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let urlString = "\(basePathString)/hello" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postHelloSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postHelloFailure(error: error, failure: failure) + } + } + + fileprivate func postHelloSuccess( + responseObject: Any, + success: (_ principal: String, _ authenticated: Bool, _ remembered: Bool) -> Void, + failure: (_ error: Error, _ message: String?) -> Void) + { + guard let response = responseObject as? Dictionary else { + postHelloFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let principal = response[BodyKeys.Principal] as? String else { + postHelloFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let authenticated = response[BodyKeys.Authenticated] as? Bool else { + postHelloFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + guard let remembered = response[BodyKeys.Remembered] as? Bool else { + postHelloFailure(error: SignUpRequestManagerError.invalidResponse, failure: failure) + return + } + logInfo(name: "\(#function)", info: response.description) + success(principal, authenticated, remembered) + } + + fileprivate func postHelloFailure(error: Error, failure: (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - Logout + + func postLogout( + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let urlString = "\(basePathString)/logout/" + manager.requestSerializer.clearAuthorizationHeader() + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postLogoutSuccess(success: success) + }) { (requestOperation, error) in + self.postLogoutFailure(error: error, failure: failure) + } + } + + fileprivate func postLogoutSuccess(success: () -> Void) { + logInfo(name: "\(#function)", info: "success") + success() + } + + fileprivate func postLogoutFailure(error: Error, failure: (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - Helper + + fileprivate func stringForError(_ error: Error) -> String? { + guard let data = ((error as NSError).userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? NSData) else { return nil } + return String(data: data as Data, encoding: String.Encoding.utf8) + } + + fileprivate func jsessionCookie() -> HTTPCookie? { + return HTTPCookieStorage.shared.cookies?.first(where: { $0.name == "JSESSIONID" }) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUpTranslation.swift b/ios/SAPTracker/src/xcode/SAPTracker/SignUpTranslation.swift new file mode 100644 index 00000000000..ec68ab89ad3 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/SignUpTranslation.swift @@ -0,0 +1,143 @@ +// +// SignUpTranslation.swift +// SAPTracker +// +// Created by Raimund Wege on 10.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class SignUpTranslation: NSObject { + + fileprivate static let TableName = "SignUp" + + // MARK: - Common + + struct Common { + struct Email { + static let String = NSLocalizedString("Common.Email", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct Password { + static let String = NSLocalizedString("Common.Password", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct Server { + static let String = NSLocalizedString("Common.Server", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct UserName { + static let String = NSLocalizedString("Common.UserName", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + + // MARK: - LoginView + + struct LoginView { + struct Title { + static let String = NSLocalizedString("LoginView.Title", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct LoginButton { + struct Title { + static let String = NSLocalizedString("LoginView.LoginButton.Title", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + } + + // MARK: - ForgotPasswordView + + struct ForgotPasswordView { + struct Title { + static let String = NSLocalizedString("ForgotPasswordView.Title", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct InfoLabel { + struct Text { + static let String = NSLocalizedString("ForgotPasswordView.InfoLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct UserNameLabel { + struct Text { + static let String = NSLocalizedString("ForgotPasswordView.UserNameLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct EmailLabel { + struct Text { + static let String = NSLocalizedString("ForgotPasswordView.EmailLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct ResetPasswordButton { + struct Title { + static let String = NSLocalizedString("ForgotPasswordView.ResetPasswordButton.Title", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + } + + // MARK: - SignUpControllerError + + struct SignUpControllerError { + struct LoginFailed { + static let String = NSLocalizedString("SignUpControllerError.LoginFailed", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct Unauthenticated { + static let String = NSLocalizedString("SignUpControllerError.Unauthenticated", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + + // MARK: - SignUpView + + struct SignUpView { + struct Title { + static let String = NSLocalizedString("SignUpView.Title", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct InfoLabel { + struct Text { + static let String = NSLocalizedString("SignUpView.InfoLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct FullNameLabel { + struct Text { + static let String = NSLocalizedString("SignUpView.FullNameLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct CompanyLabel { + struct Text { + static let String = NSLocalizedString("SignUpView.CompanyLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct PasswordTextField { + struct Placeholder { + static let String = NSLocalizedString("SignUpView.PasswordTextField.Placeholder", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct RepeatPasswordLabel { + struct Text { + static let String = NSLocalizedString("SignUpView.RepeatPasswordLabel.Text", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct RepeatPasswordTextField { + struct Placeholder { + static let String = NSLocalizedString("SignUpView.RepeatPasswordTextField.Placeholder", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + struct SignUpButton { + struct Title { + static let String = NSLocalizedString("SignUpView.SignUpButton.Title", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + } + + // MARK: SignUpViewError + + struct SignUpViewError { + struct UserNameIsToShort { + static let String = NSLocalizedString("SignUpViewError.UserNameIsToShort", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct PasswordIsToShort { + static let String = NSLocalizedString("SignUpViewError.PasswordIsToShort", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + struct PasswordsNotEqual { + static let String = NSLocalizedString("SignUpViewError.PasswordsNotEqual", tableName: TableName, bundle: Bundle.main, value: "", comment: "") + } + } + +} + + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUpViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SignUpViewController.swift new file mode 100644 index 00000000000..7fd05c5fbf4 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/SignUpViewController.swift @@ -0,0 +1,175 @@ +// +// SignUpViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 02.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +enum SignUpViewError: Error { + case userNameIsToShort + case passwordIsToShort + case passwordsNotEqual +} + +extension SignUpViewError: LocalizedError { + var errorDescription: String? { + switch self { + case .userNameIsToShort: + return SignUpTranslation.SignUpViewError.UserNameIsToShort.String + case .passwordIsToShort: + return SignUpTranslation.SignUpViewError.PasswordIsToShort.String + case .passwordsNotEqual: + return SignUpTranslation.SignUpViewError.PasswordsNotEqual.String + } + } +} + +protocol SignUpViewControllerDelegate { + + func signUpViewController( + _ controller: SignUpViewController, + willSignUpWithUserName userName: String, + email: String, + fullName: String, + company: String, + password: String + ) + +} + +class SignUpViewController: FormularViewController { + + weak var signUpController: SignUpController? + + @IBOutlet var infoLabel: UILabel! + @IBOutlet var errorLabel: UILabel! + @IBOutlet var endpointLabel: UILabel! + @IBOutlet var endpointValueLabel: UILabel! + @IBOutlet var emailLabel: UILabel! + @IBOutlet var emailTextField: UITextField! + @IBOutlet var userNameLabel: UILabel! + @IBOutlet var userNameTextField: UITextField! + @IBOutlet var fullNameLabel: UILabel! + @IBOutlet var fullNameTextField: UITextField! + @IBOutlet var companyLabel: UILabel! + @IBOutlet var companyTextField: UITextField! + @IBOutlet var passwordLabel: UILabel! + @IBOutlet var passwordTextField: UITextField! + @IBOutlet var repeatPasswordLabel: UILabel! + @IBOutlet var repeatPasswordTextField: UITextField! + @IBOutlet var signUpButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + textFields.append(contentsOf: [ + emailTextField, + userNameTextField, + fullNameTextField, + companyTextField, + passwordTextField, + repeatPasswordTextField] + ) + setup() + } + + // MARK: - Setup + + fileprivate func setup() { + setupButtons() + setupLocalization() + } + + fileprivate func setupButtons() { + makeBlue(button: signUpButton) + } + + fileprivate func setupLocalization() { + navigationItem.title = SignUpTranslation.SignUpView.Title.String + infoLabel.text = SignUpTranslation.SignUpView.InfoLabel.Text.String + errorLabel.text = "" + endpointLabel.text = SignUpTranslation.Common.Server.String + endpointValueLabel.text = signUpController?.baseURLString + emailLabel.text = SignUpTranslation.Common.Email.String + userNameLabel.text = SignUpTranslation.Common.UserName.String + fullNameLabel.text = SignUpTranslation.SignUpView.FullNameLabel.Text.String + companyLabel.text = SignUpTranslation.SignUpView.CompanyLabel.Text.String + passwordLabel.text = SignUpTranslation.Common.Password.String + passwordTextField.placeholder = SignUpTranslation.SignUpView.PasswordTextField.Placeholder.String + repeatPasswordLabel.text = SignUpTranslation.SignUpView.RepeatPasswordLabel.Text.String + repeatPasswordTextField.placeholder = SignUpTranslation.SignUpView.RepeatPasswordTextField.Placeholder.String + signUpButton.setTitle(SignUpTranslation.SignUpView.SignUpButton.Title.String, for: .normal) + } + + // MARK: - Actions + + @IBAction func signUpButtonTapped(_ sender: Any) { + errorLabel.text = "" + do { + try validateTextFields() + signUpController?.signUpViewController( + self, + willSignUpWithUserName: userNameTextField.text!, + email: emailTextField.text!, + fullName: fullNameTextField.text!, + company: companyTextField.text!, + password: passwordTextField.text! + ) + } catch { + errorLabel.text = error.localizedDescription + let alertController = UIAlertController.init(title: errorLabel.text, message: nil, preferredStyle: .alert) + let okAction = UIAlertAction.init(title: Translation.Common.OK.String, style: .cancel) + alertController.addAction(okAction) + self.present(alertController, animated: true) + } + UIView.animate(withDuration: 0.5) { + self.view.layoutIfNeeded() + } + } + + // MARK: - Validation + + fileprivate func validateTextFields() throws { + + // User name + if let userName = userNameTextField.text { + if userName.characters.count < 3 { + throw SignUpViewError.userNameIsToShort + } + } else { + throw SignUpViewError.userNameIsToShort + } + + // Password + if let password = passwordTextField.text { + if password.characters.count < 5 { + throw SignUpViewError.passwordIsToShort + } + } else { + throw SignUpViewError.passwordIsToShort + } + + // Repeat password + if passwordTextField.text != repeatPasswordTextField.text { + throw SignUpViewError.passwordsNotEqual + } + } + + // MARK: - UITextFieldDelegate + + func textFieldDidEndEditing(_ textField: UITextField) { + errorLabel.text = "" + do { + try validateTextFields() + } catch { + errorLabel.text = error.localizedDescription + } + textField.layoutIfNeeded() + UIView.animate(withDuration: 0.5) { + self.view.layoutIfNeeded() + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TimerViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TimerViewController.swift index 25946f6543c..c9e9c790a77 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/TimerViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/TimerViewController.swift @@ -15,36 +15,53 @@ class TimerViewController: UIViewController { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var timeLabel: UILabel! + weak var timer: Timer? + override func viewDidLoad() { super.viewDidLoad() setup() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + validateTimer() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + invalidateTimer() + } + + // MARK: - Timer + + fileprivate func validateTimer() { + timer = Timer.scheduledTimer( + timeInterval: 0.1, + target: self, + selector: #selector(tickTimer), + userInfo: nil, + repeats: true + ) + tickTimer() + } + + fileprivate func invalidateTimer() { + timer?.invalidate() + } + // MARK: - Setup fileprivate func setup() { setupLocalization() - setupTimer() } fileprivate func setupLocalization() { titleLabel.text = Translation.TimerView.TitleLabel.Text.String } - fileprivate func setupTimer() { - let timer = Timer( - timeInterval: 0.1, - target: self, - selector: #selector(tick), - userInfo: nil, - repeats: true - ) - RunLoop.current.add(timer, forMode: RunLoopMode.commonModes) - } - // MARK: - Timer - @objc fileprivate func tick(_ timer: Timer) { + @objc fileprivate func tickTimer() { let currentDate = Date() let timeInterval = currentDate.timeIntervalSince(startDate) let timerDate = Date(timeIntervalSince1970: timeInterval) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TitleView.swift b/ios/SAPTracker/src/xcode/SAPTracker/TitleView.swift index 24ecf510d4a..77407dbda8e 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/TitleView.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/TitleView.swift @@ -26,13 +26,13 @@ class TitleView: UIView { titleLabel.backgroundColor = .clear titleLabel.textAlignment = .center titleLabel.textColor = Colors.NavigationBarTitleColor - titleLabel.font = Fonts.OpenSansBold13 + titleLabel.font = UIFont.boldSystemFont(ofSize: 13) subtitleLabel = UILabel(frame: CGRect.zero) subtitleLabel.translatesAutoresizingMaskIntoConstraints = false subtitleLabel.backgroundColor = .clear subtitleLabel.textAlignment = .center subtitleLabel.textColor = Colors.NavigationBarTitleColor - subtitleLabel.font = Fonts.OpenSans10 + subtitleLabel.font = UIFont.systemFont(ofSize: 10) super.init(frame: frame) translatesAutoresizingMaskIntoConstraints = true addSubviews() diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewController.swift index 5fd626c6815..aaabfd7243c 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewController.swift @@ -10,8 +10,8 @@ import Foundation class TrackingViewController : UIViewController { - var checkIn: CheckIn! - var sessionController: SessionController! + weak var checkIn: CheckIn! + weak var sessionController: SessionController! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var tableViewHeight: NSLayoutConstraint! @@ -36,7 +36,7 @@ class TrackingViewController : UIViewController { } fileprivate func setupButtons() { - stopTrackingButton.setBackgroundImage(Images.RedHighlighted, for: .highlighted) + makeRed(button: stopTrackingButton) } fileprivate func setupLocalization() { @@ -51,25 +51,34 @@ class TrackingViewController : UIViewController { // MARK: - Actions - @IBAction func stopTrackingButtonTapped(_ sender: AnyObject) { - let alertController = UIAlertController(title: Translation.TrackingView.StopTrackingAlert.Title.String, - message: Translation.TrackingView.StopTrackingAlert.Message.String, - preferredStyle: .alert + @IBAction func stopTrackingButtonTapped(_ sender: Any) { + let alertController = UIAlertController( + title: Translation.TrackingView.StopTrackingAlert.Title.String, + message: Translation.TrackingView.StopTrackingAlert.Message.String, + preferredStyle: .alert ) - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { action in - LocationManager.sharedManager.stopTracking() - SVProgressHUD.show() - self.sessionController.gpsFixController.sendAll(completion: { (withSuccess) in - SVProgressHUD.popActivity() - self.dismiss(animated: true, completion: nil) - }) + let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { [weak self] (action) in + self?.stopTracking() } - let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel) alertController.addAction(okAction) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } + fileprivate func stopTracking() { + LocationManager.sharedManager.stopTracking() + SVProgressHUD.show() + self.sessionController.gpsFixController.sendAll(completion: { (withSuccess) in + SVProgressHUD.popActivity() + self.dismiss(animated: true, completion: nil) + }) + } + + @IBAction func optionButtonTapped(_ sender: Any) { + presentSettingsViewController() + } + } // MARK: - UITableViewDataSourceDelegate diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewGPSFixesCell.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewGPSFixesCell.swift index efedef11813..ac7c1abed7e 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewGPSFixesCell.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrackingViewGPSFixesCell.swift @@ -10,7 +10,7 @@ import UIKit class TrackingViewGPSFixesCell: UITableViewCell { - var checkIn: CheckIn? + weak var checkIn: CheckIn? @IBOutlet weak var gpsFixesTitleLabel: UILabel! @IBOutlet weak var gpsFixesLabel: UILabel! diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingBoatViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingBoatViewController.swift new file mode 100644 index 00000000000..4dc26773b6d --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingBoatViewController.swift @@ -0,0 +1,113 @@ +// +// TrainingBoatViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.03.18. +// Copyright © 2018 com.sap.sailing. All rights reserved. +// + +import UIKit + +class TrainingBoatViewController: BoatSessionViewController { + + struct Segue { + static let EmbedTraining = "EmbedTraining" + } + + weak var trainingViewController: TrainingViewController? + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refresh(false) + } + + // MARK: - Setup + + fileprivate func setup() { + setupNavigationBar() + } + + fileprivate func setupNavigationBar() { + navigationItem.title = boatCheckIn.event.name + } + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == Segue.EmbedTraining) { + if let trainingViewController = segue.destination as? TrainingViewController { + trainingViewController.delegate = self + trainingViewController.trainingCheckIn = boatCheckIn + trainingViewController.trainingCoreDataManager = boatCoreDataManager + self.trainingViewController = trainingViewController + } + } + } + +} + +// MARK: - SessionViewControllerDelegate + +extension TrainingBoatViewController: SessionViewControllerDelegate { + + var checkIn: CheckIn { get { return boatCheckIn } } + + var checkOutActionTitle: String { get { return Translation.TrainingView.OptionSheet.CheckOutAction.Title.String } } + + var checkOutAlertMessage: String { get { return Translation.TrainingView.CheckOutAlert.Message.String } } + + var coreDataManager: CoreDataManager { get { return boatCoreDataManager } } + + var sessionController: SessionController { get { return boatSessionController } } + + func makeOptionSheet() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = self.optionButton + } + alertController.addAction(self.makeActionCheckOut()) + alertController.addAction(self.makeActionSettings()) + alertController.addAction(self.makeActionInfo()) + alertController.addAction(self.makeActionCancel()) + return alertController + } + + func refresh(_ animated: Bool) { + boatViewController?.refresh(animated) + trainingViewController?.refresh(animated) + } + +} + +// MARK: - TrainingViewControllerDelegate + +extension TrainingBoatViewController: TrainingViewControllerDelegate { + + func trainingViewController(_ controller: TrainingViewController, refreshButtonTapped sender: Any) { + super.refreshButtonTapped(sender) + } + + func trainingViewController(_ controller: TrainingViewController, startTrackingButtonTapped sender: Any) { + super.startTrackingButtonTapped(sender) + } + + func trainingViewController(_ controller: TrainingViewController, leaderboardButtonTapped sender: Any) { + super.leaderboardButtonTapped(sender) + } + + func trainingViewControllerDidFinishTraining(_ controller: TrainingViewController) { + updatePessimistic() + } + + func trainingViewControllerDidReactivateTraining(_ controller: TrainingViewController) { + updatePessimistic() + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingCheckInController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingCheckInController.swift new file mode 100644 index 00000000000..770e4c2d5fd --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingCheckInController.swift @@ -0,0 +1,22 @@ +// +// TrainingCheckInController.swift +// SAPTracker +// +// Created by Raimund Wege on 07.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class TrainingCheckInController: CheckInController { + + override init(coreDataManager: CoreDataManager) { + super.init(coreDataManager: coreDataManager) + delegate = self + } + +} + +extension TrainingCheckInController: CheckInControllerDelegate { + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingCompetitorViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingCompetitorViewController.swift new file mode 100644 index 00000000000..4f6bfdf38e9 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingCompetitorViewController.swift @@ -0,0 +1,113 @@ +// +// TrainingCompetitorViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 21.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class TrainingCompetitorViewController: CompetitorSessionViewController { + + struct Segue { + static let EmbedTraining = "EmbedTraining" + } + + weak var trainingViewController: TrainingViewController? + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refresh(false) + } + + // MARK: - Setup + + fileprivate func setup() { + setupNavigationBar() + } + + fileprivate func setupNavigationBar() { + navigationItem.title = competitorCheckIn.event.name + } + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == Segue.EmbedTraining) { + if let trainingViewController = segue.destination as? TrainingViewController { + trainingViewController.delegate = self + trainingViewController.trainingCheckIn = competitorCheckIn + trainingViewController.trainingCoreDataManager = competitorCoreDataManager + self.trainingViewController = trainingViewController + } + } + } + +} + +// MARK: - SessionViewControllerDelegate + +extension TrainingCompetitorViewController: SessionViewControllerDelegate { + + var checkIn: CheckIn { get { return competitorCheckIn } } + + var checkOutActionTitle: String { get { return Translation.TrainingView.OptionSheet.CheckOutAction.Title.String } } + + var checkOutAlertMessage: String { get { return Translation.TrainingView.CheckOutAlert.Message.String } } + + var coreDataManager: CoreDataManager { get { return competitorCoreDataManager } } + + var sessionController: SessionController { get { return competitorSessionController } } + + func makeOptionSheet() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = self.optionButton + } + alertController.addAction(self.makeActionCheckOut()) + alertController.addAction(self.makeActionSettings()) + alertController.addAction(self.makeActionInfo()) + alertController.addAction(self.makeActionCancel()) + return alertController + } + + func refresh(_ animated: Bool) { + competitorViewController?.refresh(animated) + trainingViewController?.refresh(animated) + } + +} + +// MARK: - TrainingViewControllerDelegate + +extension TrainingCompetitorViewController: TrainingViewControllerDelegate { + + func trainingViewController(_ controller: TrainingViewController, refreshButtonTapped sender: Any) { + super.refreshButtonTapped(sender) + } + + func trainingViewController(_ controller: TrainingViewController, startTrackingButtonTapped sender: Any) { + super.startTrackingButtonTapped(sender) + } + + func trainingViewController(_ controller: TrainingViewController, leaderboardButtonTapped sender: Any) { + super.leaderboardButtonTapped(sender) + } + + func trainingViewControllerDidFinishTraining(_ controller: TrainingViewController) { + updatePessimistic() + } + + func trainingViewControllerDidReactivateTraining(_ controller: TrainingViewController) { + updatePessimistic() + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingController.swift new file mode 100644 index 00000000000..98ee94144e7 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingController.swift @@ -0,0 +1,327 @@ +// +// TrainingController.swift +// SAPTracker +// +// Created by Raimund Wege on 24.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +enum TrainingControllerError: Error { + case checkInDataIsIncomplete + case regattaIsMissing +} + +class TrainingController: NSObject { + + fileprivate unowned let coreDataManager: CoreDataManager + + fileprivate let trainingRequestManager: TrainingRequestManager + + fileprivate var trainingRaceData: TrainingRaceData? + fileprivate var sentGPSFixesCount = 0 + + init(coreDataManager: CoreDataManager, baseURLString: String) { + self.coreDataManager = coreDataManager + trainingRequestManager = TrainingRequestManager(baseURLString: baseURLString) + super.init() + } + + // MARK: - Notifications + + fileprivate func subscribeForSentGPSFixesNotifications() { + sentGPSFixesCount = 0 + NotificationCenter.default.addObserver( + self, + selector: #selector(gpsFixControllerSentGPSFixes), + name: NSNotification.Name(rawValue: GPSFixController.NotificationType.SentGPSFixes), + object: nil + ) + } + + fileprivate func unsubscribeFromSentGPSFixesNotifications() { + NotificationCenter.default.removeObserver(self) + } + + @objc fileprivate func gpsFixControllerSentGPSFixes(_ notification: Notification) { + DispatchQueue.main.async(execute: { + guard let sentDict = notification.userInfo?[GPSFixController.UserInfo.Sent] as? [String: Any] else { return } + guard let count = sentDict[GPSFixController.UserInfo.SentKey.Count] as? NSNumber else { return } + self.sentGPSFixesCount += count.intValue + if (self.sentGPSFixesCount >= AutoCourse.MinGPSFixesNeeded) { + if let trainingRaceData = self.trainingRaceData { + self.autoCourseRace(forTrainingRaceData: trainingRaceData, completion: { + self.unsubscribeFromSentGPSFixesNotifications() + }) + } + } + }) + } + + // MARK: - CreateTraining + + func createTraining( + forBoatClassName boatClassName: String, + sailID: String, + nationality: String, + success: @escaping (_ checkInData: CheckInData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + let collector = CreateTrainingData.init(serverURL: trainingRequestManager.baseURLString, boatClassName: boatClassName, sailID: sailID, nationality: nationality) + createTraining_CreateEvent(collector: collector, success: { (createTrainingData) in + if let checkInData = CheckInData.init(createTrainingData: collector) { + success(checkInData) + } else { + failure(TrainingControllerError.checkInDataIsIncomplete) + } + }) { (error) in + failure(error) + } + } + + fileprivate func createTraining_CreateEvent( + collector: CreateTrainingData, + success: @escaping (_ collector: CreateTrainingData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + trainingRequestManager.postCreateEvent(boatClassName: collector.boatClassName, success: { (createEventData) in + collector.createEventData = createEventData + self.createTraining_CreateEventSuccess(collector: collector, success: success, failure: failure) + }) { (error, message) in + failure(error) + } + } + + fileprivate func createTraining_CreateEventSuccess( + collector: CreateTrainingData, + success: @escaping (_ collector: CreateTrainingData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + do { + try createTraining_AddCompetitor(collector: collector, success: success, failure: failure) + } catch { + do { + try createTraining_CreateAndAddCompetitor(collector: collector, success: success, failure: failure) + } catch { + failure(error) + } + } + } + + fileprivate func createTraining_AddCompetitor( + collector: CreateTrainingData, + success: @escaping (_ collector: CreateTrainingData) -> Void, + failure: @escaping (_ error: Error) -> Void) throws + { + guard let regatta = collector.createEventData?.regatta else { + throw TrainingControllerError.regattaIsMissing + } + + let competitorCheckIns = coreDataManager.fetchCompetitorCheckIn(serverURL: collector.serverURL, boatClassName: collector.boatClassName) ?? [] + if let competitorID = competitorCheckIns.first?.competitorID { + trainingRequestManager.postRegattaCompetitorAdd(regattaName: regatta, competitorID: competitorID, success: { + collector.competitorID = competitorID + success(collector) + }) { (error, message) in + self.createTraining_AddCompetitorFailure(collector: collector, success: success, failure: failure) + } + } else { + self.createTraining_AddCompetitorFailure(collector: collector, success: success, failure: failure) + } + } + + fileprivate func createTraining_AddCompetitorFailure( + collector: CreateTrainingData, + success: @escaping (_ collector: CreateTrainingData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + do { + try self.createTraining_CreateAndAddCompetitor(collector: collector, success: success, failure: failure) + } catch { + failure(error) + } + } + + fileprivate func createTraining_CreateAndAddCompetitor( + collector: CreateTrainingData, + success: @escaping (_ collector: CreateTrainingData) -> Void, + failure: @escaping (_ error: Error) -> Void) throws + { + guard let regatta = collector.createEventData?.regatta else { + throw TrainingControllerError.regattaIsMissing + } + + let boatClassName = collector.boatClassName + let sailID = collector.sailID + let nationality = collector.nationality + + trainingRequestManager.postRegattaCompetitorCreateAndAdd(regattaName: regatta, boatClassName: boatClassName, sailID: sailID, nationality: nationality, success: { regattaCompetitorCreateAndAddData in + collector.competitorID = regattaCompetitorCreateAndAddData.competitorID + success(collector) + }) { (error, message) in + failure(error) + } + } + + // MARK: - FinishTraining + + func finishTraining( + forCheckIn checkIn: CheckIn, + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + self.trainingRequestManager.putFinishEventUpdate(eventID: checkIn.event.eventID, success: { + self.trainingRequestManager.postLeaderboardStopTracking(leaderboardName: checkIn.leaderboard.name, fleetName: "Default", success: { + success() + }) { (error, message) in + failure(error) + } + }) { (error, message) in + failure(error) + } + } + + // MARK: - ReactivateTraining + + func reactivateTraining( + forCheckIn checkIn: CheckIn, + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + trainingRequestManager.putReactivateEventUpdate(eventID: checkIn.event.eventID, success: { + success() + }) { (error, message) in + failure(error) + } + } + + // MARK: - StopActiveRace + + func stopActiveRace( + forTrainingRaceData trainingRaceData: TrainingRaceData, + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + self.unsubscribeFromSentGPSFixesNotifications() + self.stopActiveRace_SetStopTrackingTime(forTrainingRaceData: trainingRaceData, success: { + self.autoCourseRace(forTrainingRaceData: trainingRaceData, completion: { + success() + }) + }) { (error) in + failure(error) + } + } + + fileprivate func stopActiveRace_SetStopTrackingTime( + forTrainingRaceData data: TrainingRaceData, + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + stopActiveRace_SetStopTrackingTime( + leaderboardName: data.leaderboardName, + regattaName: data.regattaName, + raceColumnName: data.raceColumnName, + fleetName: data.fleetName, + success: success, + failure: failure + ) + } + + fileprivate func stopActiveRace_SetStopTrackingTime( + leaderboardName: String, + regattaName: String, + raceColumnName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + trainingRequestManager.postLeaderboardRaceSetStopTrackingTime(leaderboardName: leaderboardName, raceColumnName: raceColumnName, fleetName: fleetName, success: { + success() + }) { (error, message) in + failure(error) + } + } + + // MARK: - StartNewRace + + func startNewRace( + forCheckIn checkIn: CheckIn, + success: @escaping (_ trainingRaceData: TrainingRaceData) -> Void, + failure: @escaping (_ error: Error) -> Void) + { + let leaderboardName = checkIn.leaderboard.name + let regattaName = checkIn.event.name + self.trainingRequestManager.postRegattaRaceColumnAdd(regattaName: regattaName, success: { (regattaRaceColumnAddData) in + let raceColumnName = regattaRaceColumnAddData.raceColumnName + let fleetName = regattaRaceColumnAddData.fleetName + self.trainingRequestManager.postLeaderboardRaceSetStartTrackingTime(leaderboardName: leaderboardName, raceColumnName: raceColumnName, fleetName: fleetName, success: { + self.trainingRequestManager.postLeaderboardStartTracking(leaderboardName: leaderboardName, raceColumnName: raceColumnName, fleetName: fleetName, success: { + let trainingRaceData = TrainingRaceData( + leaderboardName: leaderboardName, + regattaName: regattaName, + raceColumnName: raceColumnName, + fleetName: fleetName + ) + self.trainingRaceData = trainingRaceData + self.subscribeForSentGPSFixesNotifications() + success(trainingRaceData) + }, failure: { (error, message) in + failure(error) + }) + }) { (error, message) in + failure(error) + } + }) { (error, message) in + failure(error) + } + } + + // MARK: - AutoCourseRace + + func autoCourseRace( + forTrainingRaceData trainingRaceData: TrainingRaceData, + completion: @escaping () -> Void) + { + let leaderboardName = trainingRaceData.leaderboardName + let regattaName = trainingRaceData.regattaName + let fleetName = trainingRaceData.fleetName + let raceColumnName = trainingRaceData.raceColumnName + + // 1. Get tracked race name for race column + self.trainingRequestManager.getLeaderboardGroup(leaderboardName: leaderboardName, success: { (leaderboardGroupData) in + guard let raceName = leaderboardGroupData.raceName(trainingRaceData: trainingRaceData) else { + completion() + return + } + + // 2. Get course for tracked race name + self.trainingRequestManager.getRegattaRaceCourse(regattaName: regattaName, raceName: raceName, success: { (course) in + guard course.isEmpty() || course.isAutoCourse() else { + completion() + return + } + + // 3. Set auto course + self.trainingRequestManager.postLeaderboardAutoCourse(leaderboardName: leaderboardName, raceColumnName: raceColumnName, fleetName: fleetName, success: { (leaderboardAutoCourseData) in + completion() + }) { (error, message) in + completion() + } + }, failure: { (error, message) in + completion() + }) + }) { (error, message) in + completion() + } + } + + // MARK: - Properties + + var baseURLString: String { + get { + return trainingRequestManager.baseURLString + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingMarkViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingMarkViewController.swift new file mode 100644 index 00000000000..16aa788541f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingMarkViewController.swift @@ -0,0 +1,113 @@ +// +// TrainingMarkViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 11.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class TrainingMarkViewController: MarkSessionViewController { + + struct Segue { + static let EmbedTraining = "EmbedTraining" + } + + weak var trainingViewController: TrainingViewController? + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + refresh(false) + } + + // MARK: - Setup + + fileprivate func setup() { + setupNavigationBar() + } + + fileprivate func setupNavigationBar() { + navigationItem.title = markCheckIn.event.name + } + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + if (segue.identifier == Segue.EmbedTraining) { + if let trainingViewController = segue.destination as? TrainingViewController { + trainingViewController.delegate = self + trainingViewController.trainingCheckIn = markCheckIn + trainingViewController.trainingCoreDataManager = markCoreDataManager + self.trainingViewController = trainingViewController + } + } + } + +} + +// MARK: - SessionViewControllerDelegate + +extension TrainingMarkViewController: SessionViewControllerDelegate { + + var checkIn: CheckIn { get { return markCheckIn } } + + var checkOutActionTitle: String { get { return Translation.TrainingView.OptionSheet.CheckOutAction.Title.String } } + + var checkOutAlertMessage: String { get { return Translation.TrainingView.CheckOutAlert.Message.String } } + + var coreDataManager: CoreDataManager { get { return markCoreDataManager } } + + var sessionController: SessionController { get { return markSessionController } } + + func makeOptionSheet() -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = self.optionButton + } + alertController.addAction(self.makeActionCheckOut()) + alertController.addAction(self.makeActionSettings()) + alertController.addAction(self.makeActionInfo()) + alertController.addAction(self.makeActionCancel()) + return alertController + } + + func refresh(_ animated: Bool) { + markViewController?.refresh(animated) + trainingViewController?.refresh(animated) + } + +} + +// MARK: - TrainingViewControllerDelegate + +extension TrainingMarkViewController: TrainingViewControllerDelegate { + + func trainingViewController(_ controller: TrainingViewController, refreshButtonTapped sender: Any) { + super.refreshButtonTapped(sender) + } + + func trainingViewController(_ controller: TrainingViewController, startTrackingButtonTapped sender: Any) { + super.startTrackingButtonTapped(sender) + } + + func trainingViewController(_ controller: TrainingViewController, leaderboardButtonTapped sender: Any) { + super.leaderboardButtonTapped(sender) + } + + func trainingViewControllerDidFinishTraining(_ controller: TrainingViewController) { + updatePessimistic() + } + + func trainingViewControllerDidReactivateTraining(_ controller: TrainingViewController) { + updatePessimistic() + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingRaceData.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingRaceData.swift new file mode 100644 index 00000000000..b8e9b85516d --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingRaceData.swift @@ -0,0 +1,61 @@ +// +// TrainingRaceTrackingData.swift +// SAPTracker +// +// Created by Raimund Wege on 12.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +class TrainingRaceData: NSObject, NSCoding { + + struct Keys { + static let LeaderboardName = "LeaderboardName" + static let FleetName = "FleetName" + static let RaceColumnName = "RaceColumnName" + static let RegattaName = "RegattaName" + } + + var leaderboardName: String + var regattaName: String + var raceColumnName: String + var fleetName: String + + init( + leaderboardName: String, + regattaName: String, + raceColumnName: String, + fleetName: String) + { + self.leaderboardName = leaderboardName + self.regattaName = regattaName + self.raceColumnName = raceColumnName + self.fleetName = fleetName + } + + // MARK: - NSCoding + + required convenience init?(coder decoder: NSCoder) { + guard + let leaderboardName = decoder.decodeObject(forKey: Keys.LeaderboardName) as? String, + let regattaName = decoder.decodeObject(forKey: Keys.RegattaName) as? String, + let raceColumnName = decoder.decodeObject(forKey: Keys.RaceColumnName) as? String, + let fleetName = decoder.decodeObject(forKey: Keys.FleetName) as? String + else { return nil } + self.init( + leaderboardName: leaderboardName, + regattaName: regattaName, + raceColumnName: raceColumnName, + fleetName: fleetName + ) + } + + func encode(with coder: NSCoder) { + coder.encode(leaderboardName, forKey: Keys.LeaderboardName) + coder.encode(regattaName, forKey: Keys.RegattaName) + coder.encode(raceColumnName, forKey: Keys.RaceColumnName) + coder.encode(fleetName, forKey: Keys.FleetName) + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingRequestManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingRequestManager.swift new file mode 100644 index 00000000000..be93e846a2d --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingRequestManager.swift @@ -0,0 +1,540 @@ +// +// TrainingRequestManager.swift +// SAPTracker +// +// Created by Raimund Wege on 24.08.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +enum TrainingRequestManagerError: Error { + case invalidResponse +} + +class TrainingRequestManager: NSObject { + + static let OneWeekInMillis: Int64 = 7 * 24 * 60 * 60 * 1000 + + fileprivate let basePathString = "/sailingserver/api/v1" + + let baseURLString: String + let manager: AFHTTPRequestOperationManager + let sessionManager: AFURLSessionManager + + init(baseURLString: String = "") { + self.baseURLString = baseURLString + manager = AFHTTPRequestOperationManager(baseURL: URL(string: baseURLString)) + manager.requestSerializer = AFHTTPRequestSerializer() + manager.requestSerializer.timeoutInterval = Application.RequestTimeout + manager.responseSerializer = AFHTTPResponseSerializer() + sessionManager = AFURLSessionManager(sessionConfiguration: URLSessionConfiguration.default) + sessionManager.responseSerializer = AFHTTPResponseSerializer() + super.init() + } + + // MARK: - CreateEvent + + func postCreateEvent( + boatClassName: String, + success: @escaping (_ createEventData: CreateEventData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let urlString = "\(basePathString)/events/createEvent" + var body = [String: AnyObject]() + // body["eventname"] = "raimund" as AnyObject + // body["eventdescription"] = "raimund" as AnyObject + // body["startdate"] = "" as AnyObject + // body["startdateasmillis"] = "" as AnyObject + // body["enddate"] = "" as AnyObject + // body["enddateasmillis"] = "" as AnyObject + body["venuename"] = "hamburg" as AnyObject + // body["venuelat"] = "" as AnyObject + // body["venuelng"] = "" as AnyObject + // body["ispublic"] = "" as AnyObject + // body["officialwebsiteurl"] = "" as AnyObject + // body["baseurl"] = "" as AnyObject + // body["leaderboardgroupids"] = "" as AnyObject + // body["createleaderboardgroup"] = "" as AnyObject + body["createregatta"] = "true" as AnyObject + body["boatclassname"] = boatClassName as AnyObject + body["numberofraces"] = "0" as AnyObject + manager.post(urlString, parameters: body, success: { (requestOperation, responseObject) in + self.postCreateEventSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postCreateEventFailure(error: error, failure: failure) + } + } + + fileprivate func postCreateEventSuccess(responseObject: Any?, success: (_ createEventData: CreateEventData) -> Void, failure: (_ error: Error, _ message: String?) -> Void) { + if let data = responseObject as? Data { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + success(CreateEventData(dictionary: jsonObject as? [String: AnyObject])) + } catch { + logError(name: "\(#function)", error: error) + postCreateEventFailure(error: TrainingRequestManagerError.invalidResponse, failure: failure) + } + } else { + postCreateEventFailure(error: TrainingRequestManagerError.invalidResponse, failure: failure) + } + } + + fileprivate func postCreateEventFailure(error: Error, failure: (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - EventUpdate + + func putFinishEventUpdate( + eventID: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let endDateAsMillis = millisSince1970() + putEventUpdate(eventID: eventID, endDateAsMillis: endDateAsMillis, success: success, failure: failure) + } + + func putReactivateEventUpdate( + eventID: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let endDateAsMillis = NSNumber(value: millisSince1970().int64Value + TrainingRequestManager.OneWeekInMillis) + putEventUpdate(eventID: eventID, endDateAsMillis: endDateAsMillis, success: success, failure: failure) + } + + fileprivate func putEventUpdate( + eventID: String, + endDateAsMillis: NSNumber, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedEventID = eventID.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let urlString = "\(basePathString)/events/\(encodedEventID)/update" + var body = [String: AnyObject]() + body["enddateasmillis"] = endDateAsMillis as AnyObject + manager.put(urlString, parameters: body, success: { (requestOperation, responseObject) in + success() + }) { (requestOperation, error) in + failure(error, self.stringForError(error)) + } + } + + // MARK: - RegattaCompetitorAdd + + func postRegattaCompetitorAdd( + regattaName: String, + competitorID: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedRegattaName = regattaName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedCompetitorID = competitorID.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/regattas/\(encodedRegattaName)/competitors/\(encodedCompetitorID)/add" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postRegattaCompetitorAddSuccess(responseObject: responseObject, success: success) + }) { (requestOperation, error) in + self.postRegattaCompetitorAddFailure(error: error, failure: failure) + } + } + + fileprivate func postRegattaCompetitorAddSuccess(responseObject: Any?, success: @escaping () -> Void) { + logInfo(name: "\(#function)", info: responseObjectToString(responseObject: responseObject)) + success() + } + + fileprivate func postRegattaCompetitorAddFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - RegattaCompetitorCreateAndAdd + + func postRegattaCompetitorCreateAndAdd( + regattaName: String, + boatClassName: String, + sailID: String, + nationality: String, + success: @escaping (_ regattaCompetitorCreateAndAddData: RegattaCompetitorCreateAndAddData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedRegattaName = regattaName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedBoatClassName = boatClassName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedSailID = sailID.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedNationality = nationality.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/regattas/\(encodedRegattaName)/competitors/createandadd?boatclass=\(encodedBoatClassName)&sailid=\(encodedSailID)&nationalityIOC=\(encodedNationality)" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postRegattaCompetitorCreateAndAddSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postRegattaCompetitorCreateAndAddFailure(error: error, failure: failure) + } + } + + fileprivate func postRegattaCompetitorCreateAndAddSuccess( + responseObject: Any?, + success: @escaping (_ regattaCompetitorCreateAndAddData: RegattaCompetitorCreateAndAddData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let data = responseObject as? Data { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + success(RegattaCompetitorCreateAndAddData(dictionary: jsonObject as? [String: AnyObject])) + } catch { + self.postRegattaCompetitorCreateAndAddFailure(error: error, failure: failure) + } + } else { + self.postRegattaCompetitorCreateAndAddFailure(error: TrainingRequestManagerError.invalidResponse, failure: failure) + } + } + + fileprivate func postRegattaCompetitorCreateAndAddFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - RegattaRaceColumnAdd + + func postRegattaRaceColumnAdd( + regattaName: String, + success: @escaping (_ regattaRaceColummAddData: RegattaRaceColumnAddData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedRegattaName = regattaName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let urlString = "\(basePathString)/regattas/\(encodedRegattaName)/addracecolumns" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postRegattaRaceColumnAddSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postRegattaRaceColumnAddAddFailure(error: error, failure: failure) + } + } + + fileprivate func postRegattaRaceColumnAddSuccess( + responseObject: Any?, + success: @escaping (_ regattaRaceColumnAddData: RegattaRaceColumnAddData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let data = responseObject as? Data { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + success(RegattaRaceColumnAddData(dictionary: (jsonObject as? [AnyObject])?.first as? [String: AnyObject])) + } catch { + self.postRegattaRaceColumnAddAddFailure(error: error, failure: failure) + } + } else { + self.postRegattaRaceColumnAddAddFailure(error: TrainingRequestManagerError.invalidResponse, failure: failure) + } + } + + fileprivate func postRegattaRaceColumnAddAddFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - LeaderboardRaceSetStartTrackingTime + + func postLeaderboardRaceSetStartTrackingTime( + leaderboardName: String, + raceColumnName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + postLeaderboardRaceSetTrackingTimeMillis( + forMillisParameter: "startoftrackingasmillis", + leaderboardName: leaderboardName, + raceColumnName: raceColumnName, + fleetName: fleetName, + success: success, + failure: failure + ) + } + + func postLeaderboardRaceSetStopTrackingTime( + leaderboardName: String, + raceColumnName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + postLeaderboardRaceSetTrackingTimeMillis( + forMillisParameter: "endoftrackingasmillis", + leaderboardName: leaderboardName, + raceColumnName: raceColumnName, + fleetName: fleetName, + success: success, + failure: failure + ) + } + + fileprivate func postLeaderboardRaceSetTrackingTimeMillis( + forMillisParameter millisParameter: String, + leaderboardName: String, + raceColumnName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedRaceColumnName = raceColumnName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedFleetName = fleetName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/leaderboards/\(encodedLeaderboardName)/settrackingtimes?race_column=\(encodedRaceColumnName)&fleet=\(encodedFleetName)&\(millisParameter)=\(millisSince1970())" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postLeaderboardRaceSetTrackingTimeMillisSuccess(responseObject: responseObject, success: success) + }) { (requestOperation, error) in + self.postLeaderboardRaceSetTrackingTimeMillisFailure(error: error, failure: failure) + } + } + + fileprivate func postLeaderboardRaceSetTrackingTimeMillisSuccess(responseObject: Any?, success: @escaping () -> Void) { + logInfo(name: "\(#function)", info: responseObjectToString(responseObject: responseObject)) + success() + } + + fileprivate func postLeaderboardRaceSetTrackingTimeMillisFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - LeaderboardStartTracking + + func postLeaderboardStartTracking( + leaderboardName: String, + raceColumnName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedRaceColumnName = raceColumnName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedFleetName = fleetName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/leaderboards/\(encodedLeaderboardName)/starttracking?race_column=\(encodedRaceColumnName)&fleet=\(encodedFleetName)" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postLeaderboardStartTrackingSuccess(responseObject: responseObject, success: success) + }) { (requestOperation, error) in + self.postLeaderboardStartTrackingFailure(error: error, failure: failure) + } + } + + fileprivate func postLeaderboardStartTrackingSuccess(responseObject: Any?, success: @escaping () -> Void) { + logInfo(name: "\(#function)", info: responseObjectToString(responseObject: responseObject)) + success() + } + + fileprivate func postLeaderboardStartTrackingFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - LeaderboardStopTracking + + func postLeaderboardStopTracking( + leaderboardName: String, + raceColumnName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + postLeaderboardStopTracking(leaderboardName: leaderboardName, query: "race_column=\(raceColumnName)", success: success, failure: failure) + } + + func postLeaderboardStopTracking( + leaderboardName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + postLeaderboardStopTracking(leaderboardName: leaderboardName, query: "fleet=\(fleetName)", success: success, failure: failure) + } + + func postLeaderboardStopTracking( + leaderboardName: String, + raceColumnName: String, + fleetName: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + postLeaderboardStopTracking(leaderboardName: leaderboardName, query: "race_column=\(raceColumnName)&fleet=\(fleetName)", success: success, failure: failure) + } + + fileprivate func postLeaderboardStopTracking( + leaderboardName: String, + query: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/leaderboards/\(encodedLeaderboardName)/stoptracking?\(encodedQuery)" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postLeaderboardStopTrackingSuccess(responseObject: responseObject, success: success) + }) { (requestOperation, error) in + self.postLeaderboardStopTrackingFailure(error: error, failure: failure) + } + } + + fileprivate func postLeaderboardStopTrackingSuccess(responseObject: Any?, success: @escaping () -> Void) { + logInfo(name: "\(#function)", info: responseObjectToString(responseObject: responseObject)) + success() + } + + fileprivate func postLeaderboardStopTrackingFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - LeaderboardGroup + + func getLeaderboardGroup( + leaderboardName: String, + success: @escaping (_ leaderboardGroupData: LeaderboardGroupData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let urlString = "\(basePathString)/leaderboardgroups/\(encodedLeaderboardName)" + manager.get(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.getLeaderboardGroupSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.getLeaderboardGroupFailure(error: error, failure: failure) + } + } + + fileprivate func getLeaderboardGroupSuccess( + responseObject: Any?, + success: @escaping (_ leaderboardGroupData: LeaderboardGroupData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let data = responseObject as? Data { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + success(LeaderboardGroupData(dictionary: jsonObject as? [String: AnyObject])) + } catch { + failure(error, nil) + } + } else { + failure(TrainingRequestManagerError.invalidResponse, nil) + } + } + + fileprivate func getLeaderboardGroupFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - RegattaRaceCourse + + func getRegattaRaceCourse( + regattaName: String, + raceName: String, + success: @escaping (_ regattaRaceCourseData: RegattaRaceCourseData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedRegattaName = regattaName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedRaceName = raceName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/regattas/\(encodedRegattaName)/races/\(encodedRaceName)/course" + manager.get(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.getRegattaRaceCourseSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.getRegattaRaceCourseFailure(error: error, failure: failure) + } + } + + fileprivate func getRegattaRaceCourseSuccess( + responseObject: Any?, + success: @escaping (_ regattaRaceCourseData: RegattaRaceCourseData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let data = responseObject as? Data { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + success(RegattaRaceCourseData(dictionary: (jsonObject as? [String: AnyObject]))) + } catch { + failure(error, nil) + } + } else { + failure(TrainingRequestManagerError.invalidResponse, nil) + } + } + + fileprivate func getRegattaRaceCourseFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - LeaderboardAutoCourse + + func postLeaderboardAutoCourse( + leaderboardName: String, + raceColumnName: String, + fleetName: String, + success: @escaping (_ leaderboardAutoCourseData: LeaderboardAutoCourseData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedRaceColumnName = raceColumnName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedFleetName = fleetName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "\(basePathString)/leaderboards/\(encodedLeaderboardName)/autocourse?race_column=\(encodedRaceColumnName)&fleet=\(encodedFleetName)" + manager.post(urlString, parameters: nil, success: { (requestOperation, responseObject) in + self.postLeaderboardAutoCourseSuccess(responseObject: responseObject, success: success, failure: failure) + }) { (requestOperation, error) in + self.postLeaderboardAutoCourseFailure(error: error, failure: failure) + } + } + + fileprivate func postLeaderboardAutoCourseSuccess( + responseObject: Any?, + success: @escaping (_ leaderboardAutoCourseData: LeaderboardAutoCourseData) -> Void, + failure: @escaping (_ error: Error, _ message: String?) -> Void) + { + if let data = responseObject as? Data { + do { + let jsonObject = try JSONSerialization.jsonObject(with: data) + logInfo(name: "\(#function)", info: jsonObject as? String ?? "") + success(LeaderboardAutoCourseData(dictionary: (jsonObject as? [AnyObject])?.first as? [String: AnyObject])) + } catch { + failure(error, nil) + } + } else { + failure(TrainingRequestManagerError.invalidResponse, nil) + } + } + + fileprivate func postLeaderboardAutoCourseFailure(error: Error, failure: @escaping (_ error: Error, _ message: String?) -> Void) { + logError(name: "\(#function)", error: error) + failure(error, stringForError(error)) + } + + // MARK: - Helper + + fileprivate func millisSince1970() -> NSNumber { + return NSNumber(value: Int64(Date().timeIntervalSince1970 * 1000) as Int64) + } + + fileprivate func responseObjectToString(responseObject: Any?) -> String { + return (responseObject as? String) ?? "response object is empty or cannot be casted" + } + + fileprivate func stringForError(_ error: Error) -> String? { + guard let data = ((error as NSError).userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? NSData) else { return nil } + return String(data: data as Data, encoding: String.Encoding.utf8) + } + + func fixJSON(data: Data) -> Data { + guard let json = String(data: data, encoding: String.Encoding.utf8) else { return data } + guard let newData = fixJSON(string: json).data(using: String.Encoding.utf8) else { return data } + return newData + } + + func fixJSON(string: String) -> String { + let pattern = "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}" + let regex = try! NSRegularExpression(pattern: pattern, options: []) + let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.characters.count)) + var newString = string + matches.forEach { (match) in + let uuid = (string as NSString).substring(with: match.range) + newString = newString.replacingOccurrences(of: uuid, with: "\"\(uuid)\"") + } + return newString + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/TrainingViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/TrainingViewController.swift new file mode 100644 index 00000000000..8fcceffa253 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/TrainingViewController.swift @@ -0,0 +1,320 @@ +// +// TrainingViewController.swift +// SAPTracker +// +// Created by Raimund Wege on 13.09.17. +// Copyright © 2017 com.sap.sailing. All rights reserved. +// + +import UIKit + +protocol TrainingViewControllerDelegate: class { + + func trainingViewController(_ controller: TrainingViewController, refreshButtonTapped sender: Any) + + func trainingViewController(_ controller: TrainingViewController, startTrackingButtonTapped sender: Any) + + func trainingViewController(_ controller: TrainingViewController, leaderboardButtonTapped sender: Any) + + func trainingViewControllerDidFinishTraining(_ controller: TrainingViewController) + + func trainingViewControllerDidReactivateTraining(_ controller: TrainingViewController) + +} + +class TrainingViewController: UIViewController { + + weak var delegate: TrainingViewControllerDelegate? + + weak var trainingCheckIn: CheckIn! + weak var trainingCoreDataManager: CoreDataManager! + + @IBOutlet var finishButtonZeroWidth: NSLayoutConstraint! // Strong reference needed to avoid deallocation when constraint is not active + + @IBOutlet weak var finishButton: UIButton! + @IBOutlet weak var trainingNameLabel: UILabel! + @IBOutlet weak var leaderboardButton: UIButton! + @IBOutlet weak var spaceBetweenLeaderboardButtonAndFinishButton: NSLayoutConstraint! + @IBOutlet weak var startTrackingButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + setup() + login() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + stopActiveRace() + } + + // MARK: - Setup + + fileprivate func setup() { + setupButtons() + setupLocalization() + } + + fileprivate func setupButtons() { + makeGray(button: finishButton) + makeGray(button: leaderboardButton) + makeGreen(button: startTrackingButton) + } + + fileprivate func setupLocalization() { + leaderboardButton.setTitle(Translation.TrainingView.LeaderboardButton.Title.String, for: .normal) + startTrackingButton.setTitle(isTrainingActive ? Translation.TrainingView.StartTrackingButton.Title.StringWhenTrainingIsActive : Translation.TrainingView.StartTrackingButton.Title.StringWhenTrainingIsInActive, for: .normal) + finishButton.setTitle(Translation.TrainingView.FinishButton.Title.String, for: .normal) + } + + // MARK: - Login + + fileprivate func login() { + SVProgressHUD.show() + signUpController.login(success: { (userName) in + SVProgressHUD.dismiss() + }) { (error, message) in + SVProgressHUD.dismiss() + } + } + + // MARK: - Refresh + + func refresh(_ animated: Bool) { + refreshFinishTrainingButton(animated) + refreshTrainingNameLabel(animated) + refreshStartTrainingButton(animated) + } + + fileprivate func refreshFinishTrainingButton(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshFinishTrainingButton() } + } else { + refreshFinishTrainingButton() + } + } + + fileprivate func refreshFinishTrainingButton() { + if (isTrainingActive) { + spaceBetweenLeaderboardButtonAndFinishButton.constant = 8 + finishButtonZeroWidth.isActive = false + finishButton.alpha = 1 + } else { + spaceBetweenLeaderboardButtonAndFinishButton.constant = 0 + finishButtonZeroWidth.isActive = true + finishButton.alpha = 0 + } + view.layoutIfNeeded() + } + + fileprivate func refreshTrainingNameLabel(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshTrainingNameLabel() } + } else { + refreshTrainingNameLabel() + } + } + + fileprivate func refreshTrainingNameLabel() { + trainingNameLabel.text = trainingCheckIn.event.name + } + + fileprivate func refreshStartTrainingButton(_ animated: Bool) { + if (animated) { + UIView.animate(withDuration: 0.5) { self.refreshStartTrainingButton() } + } else { + refreshStartTrainingButton() + } + } + + fileprivate func refreshStartTrainingButton() { + if (isTrainingActive) { + startTrackingButton.setTitle(Translation.TrainingView.StartTrackingButton.Title.StringWhenTrainingIsActive, for: .normal) + makeGreen(button: startTrackingButton) + } else { + startTrackingButton.setTitle(Translation.TrainingView.StartTrackingButton.Title.StringWhenTrainingIsInActive, for: .normal) + makeGray(button: startTrackingButton) + } + } + + // MARK: - StartTracking + + fileprivate func startTracking( + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + SVProgressHUD.show() + self.stopActiveRace(completion: { + self.startNewRace(success: { + SVProgressHUD.dismiss() + success() + }) { (error) in + SVProgressHUD.dismiss() + failure(error) + } + }) + } + + fileprivate func stopActiveRace(completion: (() -> Void)? = nil) { + if let trainingRaceData = Preferences.activeTrainingRaceData { + trainingController.stopActiveRace(forTrainingRaceData: trainingRaceData, success: { + Preferences.activeTrainingRaceData = nil + completion?() + }) { (error) in + completion?() + } + } else { + completion?() + } + } + + fileprivate func startNewRace( + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void) + { + trainingController.startNewRace(forCheckIn: self.trainingCheckIn, success: { (trainingRaceData) in + Preferences.activeTrainingRaceData = trainingRaceData + success() + }) { (error) in + failure(error) + } + } + + fileprivate func finishTraining() { + SVProgressHUD.show() + trainingController.finishTraining(forCheckIn: trainingCheckIn, success: { [weak self] in + SVProgressHUD.dismiss() + self?.finishTrainingSuccess() + }) { [weak self] (error) in + SVProgressHUD.dismiss() + self?.handle(error: error) + } + } + + fileprivate func finishTrainingSuccess() { + let alertController = UIAlertController.init(title: Translation.TrainingView.FinishedAlert.Title.String, message: nil, preferredStyle: .alert) + let okAction = UIAlertAction.init(title: Translation.Common.OK.String, style: .default) { [weak self] (action) in + if let strongSelf = self { + strongSelf.delegate?.trainingViewControllerDidFinishTraining(strongSelf) + } + } + alertController.addAction(okAction) + present(alertController, animated: true) + } + + fileprivate func reactivateTraining() { + SVProgressHUD.show() + trainingController.reactivateTraining(forCheckIn: trainingCheckIn, success: { [weak self] in + SVProgressHUD.dismiss() + self?.reactivateTrainingSuccess() + }) { [weak self] (error) in + SVProgressHUD.dismiss() + self?.handle(error: error) + } + } + + fileprivate func reactivateTrainingSuccess() { + let alertController = UIAlertController.init(title: Translation.TrainingView.ReactivatedAlert.Title.String, message: nil, preferredStyle: .alert) + let okAction = UIAlertAction.init(title: Translation.Common.OK.String, style: .default) { [weak self] (action) in + if let strongSelf = self { + strongSelf.delegate?.trainingViewControllerDidReactivateTraining(strongSelf) + } + } + alertController.addAction(okAction) + present(alertController, animated: true) + } + + // MARK: - Actions + + @IBAction func refreshButtonTapped(_ sender: Any) { + delegate?.trainingViewController(self, refreshButtonTapped: sender) + } + + @IBAction func finishTrainingButtonTapped(_ sender: Any) { + showFinishAlert() + } + + @IBAction func leaderboardButtonTapped(_ sender: Any) { + delegate?.trainingViewController(self, leaderboardButtonTapped: sender) + } + + @IBAction func startTrackingButtonTapped(_ sender: Any) { + if isTrainingActive { + startTracking(success: { [weak self] in + if let strongSelf = self { + strongSelf.delegate?.trainingViewController(strongSelf, startTrackingButtonTapped: sender) + } + }) { [weak self] (error) in + self?.handle(error: error) + } + } else { + showReactivateAlert() + } + } + + // MARK: - Alerts + + fileprivate func showFinishAlert() { + let alertController = UIAlertController( + title: Translation.TrainingView.FinishAlert.Title.String, + message: Translation.TrainingView.FinishAlert.Message.String, + preferredStyle: .alert + ) + let yesAction = UIAlertAction(title: Translation.Common.Yes.String, style: .default) { [weak self] action in + self?.finishTraining() + } + let noAction = UIAlertAction(title: Translation.Common.No.String, style: .cancel, handler: nil) + alertController.addAction(yesAction) + alertController.addAction(noAction) + present(alertController, animated: true, completion: nil) + } + + fileprivate func showReactivateAlert() { + let alertController = UIAlertController( + title: Translation.TrainingView.ReactivateAlert.Title.String, + message: Translation.TrainingView.ReactivateAlert.Message.String, + preferredStyle: .alert + ) + let yesAction = UIAlertAction(title: Translation.Common.Yes.String, style: .default) { [weak self] action in + self?.reactivateTraining() + } + let noAction = UIAlertAction(title: Translation.Common.No.String, style: .cancel, handler: nil) + alertController.addAction(yesAction) + alertController.addAction(noAction) + present(alertController, animated: true, completion: nil) + } + + // MARK: - Properties + + fileprivate var isTrainingInactive: Bool { + get { + return !isTrainingActive + } + } + + fileprivate var isTrainingActive: Bool { + get { + print("\(trainingCheckIn.event.endDate) - \(Date().timeIntervalSince1970)") + return trainingCheckIn.event.endDate - Date().timeIntervalSince1970 > 0 + } + } + + fileprivate lazy var signUpController: SignUpController = { + return SignUpController(baseURLString: self.trainingController.baseURLString) + }() + + fileprivate lazy var trainingController: TrainingController = { + return TrainingController(coreDataManager: self.trainingCoreDataManager, baseURLString: self.trainingCheckIn.serverURL) + }() + + // MARK: - Helper + + fileprivate func handle(error: Error) { + if ErrorHelper.isResponseUnauthorized(error: error as NSError) { + self.signUpController.loginWithViewController(self) + } else { + self.showAlert(forError: error) + } + } + +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Translation.swift b/ios/SAPTracker/src/xcode/SAPTracker/Translation.swift index 893c5ff0b4d..7db9b764ab1 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Translation.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/Translation.swift @@ -10,45 +10,71 @@ import UIKit class Translation: NSObject { - struct Common { static let Key = "\(Common.self)" - struct Cancel { static let Key = "\(Common.Key).\(Cancel.self)" + struct Common { private static let Key = "\(Common.self)" + struct Cancel { private static let Key = "\(Common.Key).\(Cancel.self)" static let String = NSLocalizedString(Key, comment: "") } - struct OK { static let Key = "\(Common.Key).\(OK.self)" + struct OK { private static let Key = "\(Common.Key).\(OK.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Info { static let Key = "\(Common.Key).\(Info.self)" + struct Info { private static let Key = "\(Common.Key).\(Info.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Settings { static let Key = "\(Common.Key).\(Settings.self)" + struct Settings { private static let Key = "\(Common.Key).\(Settings.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Warning { static let Key = "\(Common.Key).\(Warning.self)" + struct Warning { private static let Key = "\(Common.Key).\(Warning.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Error { static let Key = "\(Common.Key).\(Error.self)" + struct Error { private static let Key = "\(Common.Key).\(Error.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Yes { static let Key = "\(Common.Key).\(Yes.self)" + struct Yes { private static let Key = "\(Common.Key).\(Yes.self)" static let String = NSLocalizedString(Key, comment: "") } - struct No { static let Key = "\(Common.Key).\(No.self)" + struct No { private static let Key = "\(Common.Key).\(No.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct Logout { private static let Key = "\(Common.Key).\(Logout.self)" static let String = NSLocalizedString(Key, comment: "") } } - - // MARK: - CheckInController - - struct CheckInController { static let Key = "\(CheckInController.self)" - struct WelcomeAlert { static let Key = "\(CheckInController.Key).\(WelcomeAlert.self)" - struct Title { static let Key = "\(WelcomeAlert.Key).\(Title.self)" + + // MARK: Endpoint + + struct Endpoint { private static let Key = "\(Endpoint.self)" + struct Training { private static let Key = "\(Endpoint.Key).\(Training.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + + // MARK: - StartView + + struct StartView { private static let Key = "\(StartView.self)" + struct RegattaButton { private static let Key = "\(StartView.Key).\(RegattaButton.self)" + struct Title { private static let Key = "\(RegattaButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Message { static let Key = "\(WelcomeAlert.Key).\(Message.self)" + } + struct TrainingButton { private static let Key = "\(StartView.Key).\(TrainingButton.self)" + struct Title { private static let Key = "\(TrainingButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct CancelAction { static let Key = "\(WelcomeAlert.Key).\(CancelAction.self)" - struct Title { static let Key = "\(CancelAction.Key).\(Title.self)" + } + } + + // MARK: - RegattaCheckInController + + struct RegattaCheckInController { private static let Key = "\(RegattaCheckInController.self)" + struct WelcomeAlert { private static let Key = "\(RegattaCheckInController.Key).\(WelcomeAlert.self)" + struct Title { private static let Key = "\(WelcomeAlert.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct Message { private static let Key = "\(WelcomeAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct CancelAction { private static let Key = "\(WelcomeAlert.Key).\(CancelAction.self)" + struct Title { private static let Key = "\(CancelAction.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -57,22 +83,22 @@ class Translation: NSObject { // MARK: - GPSFixController - struct GPSFixController { static let Key = "\(GPSFixController.self)" - struct Mode { static let Key = "\(GPSFixController.Key).\(Mode.self)" + struct GPSFixController { private static let Key = "\(GPSFixController.self)" + struct Mode { private static let Key = "\(GPSFixController.Key).\(Mode.self)" static let String = NSLocalizedString(Key, comment: "") - struct Error { static let Key = "\(Mode.Key).\(Error.self)" + struct Error { private static let Key = "\(Mode.Key).\(Error.self)" static let String = NSLocalizedString(Key, comment: "") } - struct BatterySaving { static let Key = "\(Mode.Key).\(BatterySaving.self)" + struct BatterySaving { private static let Key = "\(Mode.Key).\(BatterySaving.self)" static let String = NSLocalizedString(Key, comment: "") } - struct None { static let Key = "\(Mode.Key).\(None.self)" + struct None { private static let Key = "\(Mode.Key).\(None.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Online { static let Key = "\(Mode.Key).\(Online.self)" + struct Online { private static let Key = "\(Mode.Key).\(Online.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Offline { static let Key = "\(Mode.Key).\(Offline.self)" + struct Offline { private static let Key = "\(Mode.Key).\(Offline.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -80,83 +106,91 @@ class Translation: NSObject { // MARK: - LocationManager - struct LocationManager { static let Key = "\(LocationManager.self)" - struct LocationServicesDeniedError { static let Key = "\(LocationManager.Key).\(LocationServicesDeniedError.self)" + struct LocationManager { private static let Key = "\(LocationManager.self)" + struct LocationServicesDeniedError { private static let Key = "\(LocationManager.Key).\(LocationServicesDeniedError.self)" static let String = NSLocalizedString(Key, comment: "") } - struct LocationServicesDisabledError { static let Key = "\(LocationManager.Key).\(LocationServicesDisabledError.self)" + struct LocationServicesDisabledError { private static let Key = "\(LocationManager.Key).\(LocationServicesDisabledError.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Status { static let Key = "\(LocationManager.Key).\(Status.self)" + struct Status { private static let Key = "\(LocationManager.Key).\(Status.self)" static let String = NSLocalizedString(Key, comment: "") - struct Tracking { static let Key = "\(Status.Key).\(Tracking.self)" + struct Tracking { private static let Key = "\(Status.Key).\(Tracking.self)" static let String = NSLocalizedString(Key, comment: "") } - struct NotTracking { static let Key = "\(Status.Key).\(NotTracking.self)" + struct NotTracking { private static let Key = "\(Status.Key).\(NotTracking.self)" static let String = NSLocalizedString(Key, comment: "") } } } - // MARK: - RequestManagerError + // MARK: - CheckInDataCollectorError - struct RequestManagerError { static let Key = "\(RequestManagerError.self)" - struct CommunicationFailed { static let Key = "\(RequestManagerError.Key).\(CommunicationFailed.self)" + struct CheckInDataCollectorError { private static let Key = "\(CheckInDataCollectorError.self)" + struct CheckInDataIsIncomplete { private static let Key = "\(CheckInDataCollectorError.Key).\(CheckInDataIsIncomplete.self)" static let String = NSLocalizedString(Key, comment: "") } - struct CheckInDataIsIncomplete { static let Key = "\(RequestManagerError.Key).\(CheckInDataIsIncomplete.self)" + } + + // MARK: - CheckInRequestManagerError + + struct CheckInRequestManagerError { private static let Key = "\(CheckInRequestManagerError.self)" + struct CommunicationFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(CommunicationFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct GetCompetitorFailed { static let Key = "\(RequestManagerError.Key).\(GetCompetitorFailed.self)" + struct GetBoatFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(GetBoatFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct GetEventFailed { static let Key = "\(RequestManagerError.Key).\(GetEventFailed.self)" + struct GetCompetitorFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(GetCompetitorFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct GetLeaderboardFailed { static let Key = "\(RequestManagerError.Key).\(GetLeaderboardFailed.self)" + struct GetEventFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(GetEventFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct GetMarkFailed { static let Key = "\(RequestManagerError.Key).\(GetMarkFailed.self)" + struct GetLeaderboardFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(GetLeaderboardFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct GetTeamFailed { static let Key = "\(RequestManagerError.Key).\(GetTeamFailed.self)" + struct GetMarkFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(GetMarkFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct PostCheckInFailed { static let Key = "\(RequestManagerError.Key).\(PostCheckInFailed.self)" + struct GetTeamFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(GetTeamFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct PostCheckOutFailed { static let Key = "\(RequestManagerError.Key).\(PostCheckOutFailed.self)" + struct PostCheckInFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(PostCheckInFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct PostGPSFixFailed { static let Key = "\(RequestManagerError.Key).\(PostGPSFixFailed.self)" + struct PostCheckOutFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(PostCheckOutFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct PostTeamImageFailed { static let Key = "\(RequestManagerError.Key).\(PostTeamImageFailed.self)" + struct PostGPSFixFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(PostGPSFixFailed.self)" static let String = NSLocalizedString(Key, comment: "") } - struct TeamImageURLIsInvalid { static let Key = "\(RequestManagerError.Key).\(TeamImageURLIsInvalid.self)" + struct PostTeamImageFailed { private static let Key = "\(CheckInRequestManagerError.Key).\(PostTeamImageFailed.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct TeamImageURLIsInvalid { private static let Key = "\(CheckInRequestManagerError.Key).\(TeamImageURLIsInvalid.self)" static let String = NSLocalizedString(Key, comment: "") } } // MARK: - AboutView - struct AboutView { static let Key = "\(AboutView.self)" - struct Title { static let Key = "\(AboutView.Key).\(Title.self)" + struct AboutView { private static let Key = "\(AboutView.self)" + struct Title { private static let Key = "\(AboutView.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct PartnershipTextView { static let Key = "\(AboutView.Key).\(PartnershipTextView.self)" - struct Text { static let Key = "\(PartnershipTextView.Key).\(Text.self)" + struct PartnershipTextView { private static let Key = "\(AboutView.Key).\(PartnershipTextView.self)" + struct Text { private static let Key = "\(PartnershipTextView.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct TermsButton { static let Key = "\(AboutView.Key).\(TermsButton.self)" - struct Title { static let Key = "\(TermsButton.Key).\(Title.self)" + struct TermsButton { private static let Key = "\(AboutView.Key).\(TermsButton.self)" + struct Title { private static let Key = "\(TermsButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct VersionTitleLabel { static let Key = "\(AboutView.Key).\(VersionTitleLabel.self)" - struct Text { static let Key = "\(VersionTitleLabel.Key).\(Text.self)" + struct VersionTitleLabel { private static let Key = "\(AboutView.Key).\(VersionTitleLabel.self)" + struct Text { private static let Key = "\(VersionTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -164,60 +198,99 @@ class Translation: NSObject { // MARK: - CourseView - struct CourseView { static let Key = "\(CourseView.self)" - struct TitleLabel { static let Key = "\(CourseView.Key).\(TitleLabel.self)" - struct Text { static let Key = "\(TitleLabel.Key).\(Text.self)" + struct CourseView { private static let Key = "\(CourseView.self)" + struct TitleLabel { private static let Key = "\(CourseView.Key).\(TitleLabel.self)" + struct Text { private static let Key = "\(TitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } } - // MARK: - HomeView + // MARK: - RegattaCheckInListView - struct HomeView { static let Key = "\(HomeView.self)" - struct HeaderTitleLabel { static let Key = "\(HomeView.Key).\(HeaderTitleLabel.self)" - struct Text { static let Key = "\(HeaderTitleLabel.Key).\(Text.self)" + struct RegattaCheckInListView { private static let Key = "\(RegattaCheckInListView.self)" + struct Title { private static let Key = "\(RegattaCheckInListView.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct HeaderTitleLabel { private static let Key = "\(RegattaCheckInListView.Key).\(HeaderTitleLabel.self)" + struct Text { private static let Key = "\(HeaderTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct FooterTextView { static let Key = "\(HomeView.Key).\(FooterTextView.self)" - struct Text { static let Key = "\(FooterTextView.Key).\(Text.self)" + struct FooterTextView { private static let Key = "\(RegattaCheckInListView.Key).\(FooterTextView.self)" + struct Text { private static let Key = "\(FooterTextView.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct InfoCodeLabel { static let Key = "\(HomeView.Key).\(InfoCodeLabel.self)" - struct Text { static let Key = "\(InfoCodeLabel.Key).\(Text.self)" + struct InfoCodeLabel { private static let Key = "\(RegattaCheckInListView.Key).\(InfoCodeLabel.self)" + struct Text { private static let Key = "\(InfoCodeLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct TermsAlert { static let Key = "\(HomeView.Key).\(TermsAlert.self)" - struct Title { static let Key = "\(TermsAlert.Key).\(Title.self)" + struct TermsAlert { private static let Key = "\(RegattaCheckInListView.Key).\(TermsAlert.self)" + struct Title { private static let Key = "\(TermsAlert.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Message { static let Key = "\(TermsAlert.Key).\(Message.self)" + struct Message { private static let Key = "\(TermsAlert.Key).\(Message.self)" static let String = NSLocalizedString(Key, comment: "") } - struct ShowTermsAction { static let Key = "\(TermsAlert.Key).\(ShowTermsAction.self)" - struct Title { static let Key = "\(ShowTermsAction.Key).\(Title.self)" + struct ShowTermsAction { private static let Key = "\(TermsAlert.Key).\(ShowTermsAction.self)" + struct Title { private static let Key = "\(ShowTermsAction.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct AcceptTermsAction { static let Key = "\(TermsAlert.Key).\(AcceptTermsAction.self)" - struct Title { static let Key = "\(AcceptTermsAction.Key).\(Title.self)" + struct AcceptTermsAction { private static let Key = "\(TermsAlert.Key).\(AcceptTermsAction.self)" + struct Title { private static let Key = "\(AcceptTermsAction.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } } - struct NoCameraAlert { static let Key = "\(HomeView.Key).\(NoCameraAlert.self)" - struct Message { static let Key = "\(NoCameraAlert.Key).\(Message.self)" + struct NoCameraAlert { private static let Key = "\(RegattaCheckInListView.Key).\(NoCameraAlert.self)" + struct Message { private static let Key = "\(NoCameraAlert.Key).\(Message.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct NoCodeAlert { static let Key = "\(HomeView.Key).\(NoCodeAlert.self)" - struct Title { static let Key = "\(NoCodeAlert.Key).\(Title.self)" + struct NoCodeAlert { private static let Key = "\(RegattaCheckInListView.Key).\(NoCodeAlert.self)" + struct Title { private static let Key = "\(NoCodeAlert.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Message { static let Key = "\(NoCodeAlert.Key).\(Message.self)" + struct Message { private static let Key = "\(NoCodeAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + } + + // MARK: - TrainingCheckInListView + + struct TrainingCheckInListView { private static let Key = "\(TrainingCheckInListView.self)" + struct Title { private static let Key = "\(TrainingCheckInListView.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct HeaderTitleLabel { private static let Key = "\(TrainingCheckInListView.Key).\(HeaderTitleLabel.self)" + struct Text { private static let Key = "\(HeaderTitleLabel.Key).\(Text.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct FooterTextView { private static let Key = "\(TrainingCheckInListView.Key).\(FooterTextView.self)" + struct Text { private static let Key = "\(FooterTextView.Key).\(Text.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + } + + // MARK: - CreateTrainingView + + struct CreateTrainingView { private static let Key = "\(CreateTrainingView.self)" + struct Title { private static let Key = "\(CreateTrainingView.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct BoatClassNameLabel { private static let Key = "\(CreateTrainingView.Key).\(BoatClassNameLabel.self)" + struct Text { private static let Key = "\(BoatClassNameLabel.Key).\(Text.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct CreateTrainingButton { private static let Key = "\(CreateTrainingView.Key).\(CreateTrainingButton.self)" + struct Title { private static let Key = "\(CreateTrainingButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -225,115 +298,110 @@ class Translation: NSObject { // MARK: - LeaderboardView - struct LeaderboardView { static let Key = "\(LeaderboardView.self)" - struct Title { static let Key = "\(LeaderboardView.Key).\(Title.self)" + struct LeaderboardView { private static let Key = "\(LeaderboardView.self)" + struct Title { private static let Key = "\(LeaderboardView.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } // MARK: - LicenseView - struct LicenseView { static let Key = "\(LicenseView.self)" - struct Title { static let Key = "\(LicenseView.Key).\(Title.self)" + struct LicenseView { private static let Key = "\(LicenseView.self)" + struct Title { private static let Key = "\(LicenseView.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - // MARK: - CompetitorView + // MARK: - RegattaView - struct CompetitorView { static let Key = "\(CompetitorView.self)" - struct CountdownDaysTitleLabel { static let Key = "\(CompetitorView.Key).\(CountdownDaysTitleLabel.self)" - struct Text { static let Key = "\(CountdownDaysTitleLabel.Key).\(Text.self)" + struct RegattaView { private static let Key = "\(RegattaView.self)" + struct CountdownDaysTitleLabel { private static let Key = "\(RegattaView.Key).\(CountdownDaysTitleLabel.self)" + struct Text { private static let Key = "\(CountdownDaysTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct CountdownHoursTitleLabel { static let Key = "\(CompetitorView.Key).\(CountdownHoursTitleLabel.self)" - struct Text { static let Key = "\(CountdownHoursTitleLabel.Key).\(Text.self)" + struct CountdownHoursTitleLabel { private static let Key = "\(RegattaView.Key).\(CountdownHoursTitleLabel.self)" + struct Text { private static let Key = "\(CountdownHoursTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct CountdownMinutesTitleLabel { static let Key = "\(CompetitorView.Key).\(CountdownMinutesTitleLabel.self)" - struct Text { static let Key = "\(CountdownMinutesTitleLabel.Key).\(Text.self)" + struct CountdownMinutesTitleLabel { private static let Key = "\(RegattaView.Key).\(CountdownMinutesTitleLabel.self)" + struct Text { private static let Key = "\(CountdownMinutesTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct RegattaStartLabel { static let Key = "\(CompetitorView.Key).\(RegattaStartLabel.self)" - struct Text { static let Key = "\(RegattaStartLabel.Key).\(Text.self)" - struct BeforeRegattaDidStart { static let Key = "\(Text.Key).\(BeforeRegattaDidStart.self)" + struct StartLabel { private static let Key = "\(RegattaView.Key).\(StartLabel.self)" + struct Text { private static let Key = "\(StartLabel.Key).\(Text.self)" + struct BeforeRegattaDidStart { private static let Key = "\(Text.Key).\(BeforeRegattaDidStart.self)" static let String = NSLocalizedString(Key, comment: "") } - struct AfterRegattaDidStart { static let Key = "\(Text.Key).\(AfterRegattaDidStart.self)" + struct AfterRegattaDidStart { private static let Key = "\(Text.Key).\(AfterRegattaDidStart.self)" static let String = NSLocalizedString(Key, comment: "") } } } - struct AnnouncementLabel { static let Key = "\(CompetitorView.Key).\(AnnouncementLabel.self)" - struct Text { static let Key = "\(AnnouncementLabel.Key).\(Text.self)" + struct AnnouncementLabel { private static let Key = "\(RegattaView.Key).\(AnnouncementLabel.self)" + struct Text { private static let Key = "\(AnnouncementLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct EventButton { static let Key = "\(CompetitorView.Key).\(EventButton.self)" - struct Title { static let Key = "\(EventButton.Key).\(Title.self)" + struct EventButton { private static let Key = "\(RegattaView.Key).\(EventButton.self)" + struct Title { private static let Key = "\(EventButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct TeamImageAddButton { static let Key = "\(CompetitorView.Key).\(TeamImageAddButton.self)" - struct Title { static let Key = "\(TeamImageAddButton.Key).\(Title.self)" + struct StartTrackingButton { private static let Key = "\(RegattaView.Key).\(StartTrackingButton.self)" + struct Title { private static let Key = "\(StartTrackingButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct TeamImageUploadRetryButton { static let Key = "\(CompetitorView.Key).\(TeamImageUploadRetryButton.self)" - struct Title { static let Key = "\(TeamImageUploadRetryButton.Key).\(Title.self)" - static let String = NSLocalizedString(Key, comment: "") - } - } - struct StartTrackingButton { static let Key = "\(CompetitorView.Key).\(StartTrackingButton.self)" - struct Title { static let Key = "\(StartTrackingButton.Key).\(Title.self)" - static let String = NSLocalizedString(Key, comment: "") - } - } - struct OptionSheet { static let Key = "\(CompetitorView.Key).\(OptionSheet.self)" - struct CheckOutAction { static let Key = "\(OptionSheet.Key).\(CheckOutAction.self)" - struct Title { static let Key = "\(CheckOutAction.Key).\(Title.self)" - static let String = NSLocalizedString(Key, comment: "") - } - } - struct ReplaceImageAction { static let Key = "\(OptionSheet.Key).\(ReplaceImageAction.self)" - struct Title { static let Key = "\(ReplaceImageAction.Key).\(Title.self)" - static let String = NSLocalizedString(Key, comment: "") - } - } - struct UpdateAction { static let Key = "\(OptionSheet.Key).\(UpdateAction.self)" - struct Title { static let Key = "\(UpdateAction.Key).\(Title.self)" + struct OptionSheet { private static let Key = "\(RegattaView.Key).\(OptionSheet.self)" + struct CheckOutAction { private static let Key = "\(OptionSheet.Key).\(CheckOutAction.self)" + struct Title { private static let Key = "\(CheckOutAction.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } } - struct CheckOutAlert { static let Key = "\(CompetitorView.Key).\(CheckOutAlert.self)" - struct Message { static let Key = "\(CheckOutAlert.Key).\(Message.self)" + struct CheckOutAlert { private static let Key = "\(RegattaView.Key).\(CheckOutAlert.self)" + struct Message { private static let Key = "\(CheckOutAlert.Key).\(Message.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct SelectImageAlert { static let Key = "\(CompetitorView.Key).\(SelectImageAlert.self)" - struct Title { static let Key = "\(SelectImageAlert.Key).\(Title.self)" + } + + // CompetitorView + + struct CompetitorView { private static let Key = "\(CompetitorView.self)" + struct TeamImageAddButton { private static let Key = "\(CompetitorView.Key).\(TeamImageAddButton.self)" + struct Title { private static let Key = "\(TeamImageAddButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Message { static let Key = "\(SelectImageAlert.Key).\(Message.self)" + } + struct TeamImageUploadRetryButton { private static let Key = "\(CompetitorView.Key).\(TeamImageUploadRetryButton.self)" + struct Title { private static let Key = "\(TeamImageUploadRetryButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct CameraAction { static let Key = "\(SelectImageAlert.Key).\(CameraAction.self)" - struct Title { static let Key = "\(CameraAction.Key).\(Title.self)" + } + struct SelectImageAlert { private static let Key = "\(CompetitorView.Key).\(SelectImageAlert.self)" + struct Title { private static let Key = "\(SelectImageAlert.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct Message { private static let Key = "\(SelectImageAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct CameraAction { private static let Key = "\(SelectImageAlert.Key).\(CameraAction.self)" + struct Title { private static let Key = "\(CameraAction.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct PhotoLibraryAction { static let Key = "\(SelectImageAlert.Key).\(PhotoLibraryAction.self)" - struct Title { static let Key = "\(PhotoLibraryAction.Key).\(Title.self)" + struct PhotoLibraryAction { private static let Key = "\(SelectImageAlert.Key).\(PhotoLibraryAction.self)" + struct Title { private static let Key = "\(PhotoLibraryAction.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } } - struct UploadTeamImageFailureAlert { static let Key = "\(CompetitorView.Key).\(UploadTeamImageFailureAlert.self)" - struct Title { static let Key = "\(UploadTeamImageFailureAlert.Key).\(Title.self)" + struct UploadTeamImageFailureAlert { private static let Key = "\(CompetitorView.Key).\(UploadTeamImageFailureAlert.self)" + struct Title { private static let Key = "\(UploadTeamImageFailureAlert.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -341,12 +409,12 @@ class Translation: NSObject { // MARK: - ScanView - struct ScanView { static let Key = "\(ScanView.self)" - struct Title { static let Key = "\(ScanView.Key).\(Title.self)" + struct ScanView { private static let Key = "\(ScanView.self)" + struct Title { private static let Key = "\(ScanView.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct IncorrectCodeAlert { static let Key = "\(ScanView.Key).\(IncorrectCodeAlert.self)" - struct Message { static let Key = "\(IncorrectCodeAlert.Key).\(Message.self)" + struct IncorrectCodeAlert { private static let Key = "\(ScanView.Key).\(IncorrectCodeAlert.self)" + struct Message { private static let Key = "\(IncorrectCodeAlert.Key).\(Message.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -354,34 +422,44 @@ class Translation: NSObject { // MARK: - SettingsView - struct SettingsView { static let Key = "\(SettingsView.self)" - struct Title { static let Key = "\(SettingsView.Key).\(Title.self)" + struct SettingsView { private static let Key = "\(SettingsView.self)" + struct Title { private static let Key = "\(SettingsView.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct TableView { static let Key = "\(SettingsView.Key).\(TableView.self)" - struct BatterySavingSection { static let Key = "\(TableView.Key).\(BatterySavingSection.self)" - struct Title { static let Key = "\(BatterySavingSection.Key).\(Title.self)" + struct TableView { private static let Key = "\(SettingsView.Key).\(TableView.self)" + struct BatterySavingSection { private static let Key = "\(TableView.Key).\(BatterySavingSection.self)" + struct Title { private static let Key = "\(BatterySavingSection.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct OtherSettingsSection { static let Key = "\(TableView.Key).\(OtherSettingsSection.self)" - struct Title { static let Key = "\(OtherSettingsSection.Key).\(Title.self)" + struct DeviceSection { private static let Key = "\(TableView.Key).\(DeviceSection.self)" + struct Title { private static let Key = "\(DeviceSection.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct TrainingSection { private static let Key = "\(TableView.Key).\(TrainingSection.self)" + struct Title { private static let Key = "\(TrainingSection.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } } - struct BatterySavingTitleLabel { static let Key = "\(SettingsView.Key).\(BatterySavingTitleLabel.self)" - struct Text { static let Key = "\(BatterySavingTitleLabel.Key).\(Text.self)" + struct BatterySavingTitleLabel { private static let Key = "\(SettingsView.Key).\(BatterySavingTitleLabel.self)" + struct Text { private static let Key = "\(BatterySavingTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct BatterySavingDescriptionLabel { static let Key = "\(SettingsView.Key).\(BatterySavingDescriptionLabel.self)" - struct Text { static let Key = "\(BatterySavingDescriptionLabel.Key).\(Text.self)" + struct BatterySavingDescriptionLabel { private static let Key = "\(SettingsView.Key).\(BatterySavingDescriptionLabel.self)" + struct Text { private static let Key = "\(BatterySavingDescriptionLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct DeviceIdentifierTitleLabel { static let Key = "\(SettingsView.Key).\(DeviceIdentifierTitleLabel.self)" - struct Text { static let Key = "\(DeviceIdentifierTitleLabel.Key).\(Text.self)" + struct DeviceIdentifierTitleLabel { private static let Key = "\(SettingsView.Key).\(DeviceIdentifierTitleLabel.self)" + struct Text { private static let Key = "\(DeviceIdentifierTitleLabel.Key).\(Text.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct TrainingServerTitleLabel { private static let Key = "\(SettingsView.Key).\(TrainingServerTitleLabel.self)" + struct Text { private static let Key = "\(TrainingServerTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -389,9 +467,9 @@ class Translation: NSObject { // MARK: - SpeedView - struct SpeedView { static let Key = "\(SpeedView.self)" - struct TitleLabel { static let Key = "\(SpeedView.Key).\(TitleLabel.self)" - struct Text { static let Key = "\(TitleLabel.Key).\(Text.self)" + struct SpeedView { private static let Key = "\(SpeedView.self)" + struct TitleLabel { private static let Key = "\(SpeedView.Key).\(TitleLabel.self)" + struct Text { private static let Key = "\(TitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -399,9 +477,9 @@ class Translation: NSObject { // MARK: - TimerView - struct TimerView { static let Key = "\(TimerView.self)" - struct TitleLabel { static let Key = "\(TimerView.Key).\(TitleLabel.self)" - struct Text { static let Key = "\(TitleLabel.Key).\(Text.self)" + struct TimerView { private static let Key = "\(TimerView.self)" + struct TitleLabel { private static let Key = "\(TimerView.Key).\(TitleLabel.self)" + struct Text { private static let Key = "\(TitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } @@ -409,40 +487,101 @@ class Translation: NSObject { // MARK: - TrackingView - struct TrackingView { static let Key = "\(TrackingView.self)" - struct TableView { static let Key = "\(TrackingView.Key).\(TableView.self)" - struct GPSAccuracyCell { static let Key = "\(TableView.Key).\(GPSAccuracyCell.self)" - struct GPSAccuracyTitleLabel { static let Key = "\(GPSAccuracyCell.Key).\(GPSAccuracyTitleLabel.self)" - struct Text { static let Key = "\(GPSAccuracyTitleLabel.Key).\(Text.self)" + struct TrackingView { private static let Key = "\(TrackingView.self)" + struct TableView { private static let Key = "\(TrackingView.Key).\(TableView.self)" + struct GPSAccuracyCell { private static let Key = "\(TableView.Key).\(GPSAccuracyCell.self)" + struct GPSAccuracyTitleLabel { private static let Key = "\(GPSAccuracyCell.Key).\(GPSAccuracyTitleLabel.self)" + struct Text { private static let Key = "\(GPSAccuracyTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct GPSAccuracyLabel { static let Key = "\(GPSAccuracyCell.Key).\(GPSAccuracyLabel.self)" - struct Text { static let Key = "\(GPSAccuracyLabel.Key).\(Text.self)" - struct NoGPS { static let Key = "\(Text.Key).\(NoGPS.self)" + struct GPSAccuracyLabel { private static let Key = "\(GPSAccuracyCell.Key).\(GPSAccuracyLabel.self)" + struct Text { private static let Key = "\(GPSAccuracyLabel.Key).\(Text.self)" + struct NoGPS { private static let Key = "\(Text.Key).\(NoGPS.self)" static let String = NSLocalizedString(Key, comment: "") } } } } - struct GPSFixesCell { static let Key = "\(TableView.Key).\(GPSFixesCell.self)" - struct GPSFixesTitleLabel { static let Key = "\(GPSFixesCell.Key).\(GPSFixesTitleLabel.self)" - struct Text { static let Key = "\(GPSFixesTitleLabel.Key).\(Text.self)" + struct GPSFixesCell { private static let Key = "\(TableView.Key).\(GPSFixesCell.self)" + struct GPSFixesTitleLabel { private static let Key = "\(GPSFixesCell.Key).\(GPSFixesTitleLabel.self)" + struct Text { private static let Key = "\(GPSFixesTitleLabel.Key).\(Text.self)" static let String = NSLocalizedString(Key, comment: "") } } } } - struct StopTrackingButton { static let Key = "\(TrackingView.Key).\(StopTrackingButton.self)" - struct Title { static let Key = "\(StopTrackingButton.Key).\(Title.self)" + struct StopTrackingButton { private static let Key = "\(TrackingView.Key).\(StopTrackingButton.self)" + struct Title { private static let Key = "\(StopTrackingButton.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } - struct StopTrackingAlert { static let Key = "\(TrackingView.Key).\(StopTrackingAlert.self)" - struct Title { static let Key = "\(StopTrackingAlert.Key).\(Title.self)" + struct StopTrackingAlert { private static let Key = "\(TrackingView.Key).\(StopTrackingAlert.self)" + struct Title { private static let Key = "\(StopTrackingAlert.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } - struct Message { static let Key = "\(StopTrackingAlert.Key).\(Message.self)" + struct Message { private static let Key = "\(StopTrackingAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + } + + // MARK: - TrainingView + + struct TrainingView { private static let Key = "\(TrainingView.self)" + struct LeaderboardButton { private static let Key = "\(TrainingView.Key).\(LeaderboardButton.self)" + struct Title { private static let Key = "\(LeaderboardButton.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct CheckOutAlert { private static let Key = "\(TrainingView.Key).\(CheckOutAlert.self)" + struct Message { private static let Key = "\(CheckOutAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct OptionSheet { private static let Key = "\(TrainingView.Key).\(OptionSheet.self)" + struct CheckOutAction { private static let Key = "\(OptionSheet.Key).\(CheckOutAction.self)" + struct Title { private static let Key = "\(CheckOutAction.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + } + struct ReactivateAlert { private static let Key = "\(TrainingView.Key).\(ReactivateAlert.self)" + struct Title { private static let Key = "\(ReactivateAlert.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct Message { private static let Key = "\(ReactivateAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct ReactivatedAlert { + static let Key = "\(TrainingView.Key).\(ReactivatedAlert.self)" + struct Title { private static let Key = "\(ReactivatedAlert.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct StartTrackingButton { private static let Key = "\(TrainingView.Key).\(StartTrackingButton.self)" + struct Title { private static let Key = "\(StartTrackingButton.Key).\(Title.self)" + static let StringWhenTrainingIsActive = RegattaView.StartTrackingButton.Title.String + static let StringWhenTrainingIsInActive = TrainingView.ReactivateAlert.Title.String + } + } + struct FinishButton { private static let Key = "\(TrainingView.Key).\(FinishButton.self)" + struct Title { private static let Key = "\(FinishButton.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct FinishAlert { + static let Key = "\(TrainingView.Key).\(FinishAlert.self)" + struct Title { private static let Key = "\(FinishAlert.Key).\(Title.self)" + static let String = NSLocalizedString(Key, comment: "") + } + struct Message { private static let Key = "\(FinishAlert.Key).\(Message.self)" + static let String = NSLocalizedString(Key, comment: "") + } + } + struct FinishedAlert { private static let Key = "\(TrainingView.Key).\(FinishedAlert.self)" + struct Title { private static let Key = "\(FinishedAlert.Key).\(Title.self)" static let String = NSLocalizedString(Key, comment: "") } } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings index d9c04bd623d..e3fca47d2f6 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings +++ b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings @@ -6,6 +6,7 @@ // Copyright © 2016 com.sap.sailing. All rights reserved. // -"NSLocationAlwaysUsageDescription" = "Um das ganze Rennen mit Deinem Gerät zu tracken, muss das Tracking auch dann fortgesetzt werden, wenn der Bildschirm abgeschaltet oder gesperrt wird. Das hilft, die Batterielaufzeit zu verlängern und versehentlich das Tracking zu stoppen."; +"NSLocationWhenInUseUsageDescription" = "Um das ganze Rennen mit Deinem Gerät zu tracken, muss das Tracking auch dann fortgesetzt werden, wenn der Bildschirm abgeschaltet oder gesperrt wird. Das hilft, die Batterielaufzeit zu verlängern und versehentlich das Tracking zu stoppen."; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Um das ganze Rennen mit Deinem Gerät zu tracken, muss das Tracking auch dann fortgesetzt werden, wenn der Bildschirm abgeschaltet oder gesperrt wird. Das hilft, die Batterielaufzeit zu verlängern und versehentlich das Tracking zu stoppen."; "NSCameraUsageDescription" = "Die Kamera wird benötigt, um per QR-Code einen Check-In auszuführen."; "NSPhotoLibraryUsageDescription" = "Um das Team Photo zu ersetzen, benötigt die App den Zugriff auf ihr Fotoalbum."; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings index 3aa04fba3c3..e3645843144 100644 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings and b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings differ