Merge branch 'master' into bug4478

This commit is contained in:
Kai Boernert committed 2018-03-15 13:21:54 +01:00
commit 0311bfe8c5
122 files changed
+8766 -2146

No files matched your search

@@ -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 = "<group>"; };
7F83F15519F154F000C53328 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7F83F15719F154F000C53328 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
7F83F16B19F154F000C53328 /* SAPTrackerAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPTrackerAPITests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
7F87CA9E1A2CCE8B0076C617 /* OpenSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Light.ttf"; sourceTree = "<group>"; };
7F87CA9F1A2CCE8B0076C617 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Regular.ttf"; sourceTree = "<group>"; };
7F87CAA01A2CCE8B0076C617 /* OpenSans-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Semibold.ttf"; sourceTree = "<group>"; };
7F8B8D3E1A37356C00C34B88 /* TrackingPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingPageViewController.swift; sourceTree = "<group>"; };
7F8B8D401A373B0200C34B88 /* SpeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeedViewController.swift; sourceTree = "<group>"; };
7F8B8D421A373D4400C34B88 /* CourseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseViewController.swift; sourceTree = "<group>"; };
7F8CC2071A10D47A00C56D7F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
7F950A901A35E967003C8C5C /* LeaderboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeaderboardViewController.swift; sourceTree = "<group>"; };
7F99DE3B1A126554009B3619 /* CompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorViewController.swift; sourceTree = "<group>"; };
7F99DE3B1A126554009B3619 /* RegattaCompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaCompetitorViewController.swift; sourceTree = "<group>"; };
7FA6551919F6661900A1B92D /* CoreDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CoreDataManager.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
7FAFA7941A126B5700D230F1 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
C8150BA61DCC6FDB0087C9C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = "<group>"; };
C8150BA81DCC6FDD0087C9C2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
C8289F391F386D8300A76953 /* SignUpRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpRequestManager.swift; sourceTree = "<group>"; };
C843D4791EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CompetitorCheckIn+CoreDataProperties.swift"; sourceTree = "<group>"; };
C843D47A1EBC54A300250F01 /* CompetitorCheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorCheckIn.swift; sourceTree = "<group>"; };
C843D47B1EBC54A300250F01 /* MarkCheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MarkCheckIn+CoreDataProperties.swift"; sourceTree = "<group>"; };
C843D47C1EBC54A300250F01 /* MarkCheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkCheckIn.swift; sourceTree = "<group>"; };
C843D47D1EBC54A300250F01 /* CheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CheckIn+CoreDataProperties.swift"; sourceTree = "<group>"; };
C843D47E1EBC54A300250F01 /* CheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckIn.swift; sourceTree = "<group>"; };
C843D4851EBC927800250F01 /* MarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkViewController.swift; sourceTree = "<group>"; };
C843D4851EBC927800250F01 /* RegattaMarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaMarkViewController.swift; sourceTree = "<group>"; };
C843D4871EBCADDC00250F01 /* CompetitorSessionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorSessionController.swift; sourceTree = "<group>"; };
C843D4891EBCADF500250F01 /* MarkSessionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkSessionController.swift; sourceTree = "<group>"; };
C843D48B1EBCB36900250F01 /* SessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionViewController.swift; sourceTree = "<group>"; };
C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = "CoreData2-3.xcmappingmodel"; sourceTree = "<group>"; };
C843D4901EC0A93600250F01 /* CoreData1-3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = "CoreData1-3.xcmappingmodel"; sourceTree = "<group>"; };
C843D522201F1F17003725F0 /* CoreData 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 4.xcdatamodel"; sourceTree = "<group>"; };
C843D531201F3050003725F0 /* NewCheckInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCheckInViewController.swift; sourceTree = "<group>"; };
C84936171D40D295000F30E8 /* GPSFixController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPSFixController.swift; sourceTree = "<group>"; };
C84936281D47645F000F30E8 /* HomeViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewCell.swift; sourceTree = "<group>"; };
C84936281D47645F000F30E8 /* CheckInTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInTableViewCell.swift; sourceTree = "<group>"; };
C85291501D48FB91009E9A9D /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; };
C852BAA11CF5E1EA0093B0C5 /* RequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestManager.swift; sourceTree = "<group>"; };
C852BAA11CF5E1EA0093B0C5 /* CheckInRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInRequestManager.swift; sourceTree = "<group>"; };
C852BAAA1CF736680093B0C5 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
C85DB8241F30B680009559CE /* SignUp.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SignUp.storyboard; sourceTree = "<group>"; };
C85DB8271F31AAE0009559CE /* SignUpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = "<group>"; };
C85DB8291F31AAFD009559CE /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
C85DB82B1F31AB1A009559CE /* ForgotPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForgotPasswordViewController.swift; sourceTree = "<group>"; };
C85DB82D1F31D804009559CE /* SignUpController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpController.swift; sourceTree = "<group>"; };
C85FAA501D4F84460087C2A2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
C8603D491C528832002B2F28 /* Acknowledgements.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Acknowledgements.markdown; sourceTree = "<group>"; };
C867BD081F4B0274007E78E0 /* TrainingCompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingCompetitorViewController.swift; sourceTree = "<group>"; };
C867BD0A1F4B0290007E78E0 /* CreateTrainingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateTrainingViewController.swift; sourceTree = "<group>"; };
C8739D9A1D25317100A89F3C /* CoreData 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 2.xcdatamodel"; sourceTree = "<group>"; };
C8739DFC1D2AA2A500A89F3C /* GPSFix+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GPSFix+CoreDataProperties.swift"; sourceTree = "<group>"; };
C8739DFD1D2AA2A500A89F3C /* GPSFix.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPSFix.swift; sourceTree = "<group>"; };
@@ -718,17 +761,51 @@
C8739E151D2BF17900A89F3C /* CompetitorData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorData.swift; sourceTree = "<group>"; };
C8739E171D2BF2DC00A89F3C /* BaseData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseData.swift; sourceTree = "<group>"; };
C8739E1B1D2D5F1700A89F3C /* LocationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationData.swift; sourceTree = "<group>"; };
C87A74E01F4EE570003DE396 /* TrainingRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingRequestManager.swift; sourceTree = "<group>"; };
C87A74E21F4F0E34003DE396 /* TrainingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingController.swift; sourceTree = "<group>"; };
C87A74E71F504EBB003DE396 /* CreateEventData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateEventData.swift; sourceTree = "<group>"; };
C87A74E91F5053AE003DE396 /* RegattaCompetitorCreateAndAddData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaCompetitorCreateAndAddData.swift; sourceTree = "<group>"; };
C87A74EB1F5068D0003DE396 /* CreateTrainingData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateTrainingData.swift; sourceTree = "<group>"; };
C88F6D2A1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardAutoCourseData.swift; sourceTree = "<group>"; };
C88F6D2C1FEACC5700A616BE /* LeaderboardGroupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardGroupData.swift; sourceTree = "<group>"; };
C88F6D2E1FEACD7400A616BE /* RegattaRaceCourseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegattaRaceCourseData.swift; sourceTree = "<group>"; };
C88F6D301FFBC35F00A616BE /* ErrorHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHelper.swift; sourceTree = "<group>"; };
C891886D2057DB4A00E37CC3 /* BoatData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoatData.swift; sourceTree = "<group>"; };
C891886F2057E7D700E37CC3 /* BoatCheckIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatCheckIn.swift; sourceTree = "<group>"; };
C89188702057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoatCheckIn+CoreDataProperties.swift"; sourceTree = "<group>"; };
C89188732057F32300E37CC3 /* RegattaBoatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegattaBoatViewController.swift; sourceTree = "<group>"; };
C89188752057F36300E37CC3 /* BoatSessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatSessionViewController.swift; sourceTree = "<group>"; };
C89188772057F44200E37CC3 /* BoatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatViewController.swift; sourceTree = "<group>"; };
C89188792057F55100E37CC3 /* BoatSessionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoatSessionController.swift; sourceTree = "<group>"; };
C891887B20580A4100E37CC3 /* TrainingBoatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainingBoatViewController.swift; sourceTree = "<group>"; };
C89C5FC41D06A289002AAAD2 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = "<group>"; };
C89C5FC91D06CECD002AAAD2 /* TrackingViewStatusCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewStatusCell.swift; sourceTree = "<group>"; };
C89C5FCB1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewGPSAccuracyCell.swift; sourceTree = "<group>"; };
C89C5FCD1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewGPSFixesCell.swift; sourceTree = "<group>"; };
C89C5FCF1D06CF46002AAAD2 /* TrackingViewModeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewModeCell.swift; sourceTree = "<group>"; };
C8A4400A1C18153600DF70A0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = SignUp.strings; sourceTree = "<group>"; };
C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpTranslation.swift; sourceTree = "<group>"; };
C8BBAC781C52649E004CA551 /* LicenseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LicenseViewController.swift; sourceTree = "<group>"; };
C8BFCCC31D51C53500FC1A27 /* TeamData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamData.swift; sourceTree = "<group>"; };
C8BFCCCB1D523C5D00FC1A27 /* TitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleView.swift; sourceTree = "<group>"; };
C8CF22A21F6194C600A01E0A /* RegattaCheckInController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaCheckInController.swift; sourceTree = "<group>"; };
C8CF22A41F61965C00A01E0A /* TrainingCheckInController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingCheckInController.swift; sourceTree = "<group>"; };
C8CF22A81F62780C00A01E0A /* CheckInDataCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInDataCollector.swift; sourceTree = "<group>"; };
C8CF22AC1F668BAB00A01E0A /* TrainingMarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingMarkViewController.swift; sourceTree = "<group>"; };
C8CF22B31F67C4D800A01E0A /* RegattaRaceColumnAddData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegattaRaceColumnAddData.swift; sourceTree = "<group>"; };
C8CF22B71F68133000A01E0A /* TrainingRaceData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingRaceData.swift; sourceTree = "<group>"; };
C8CF22BD1F69187800A01E0A /* CompetitorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorViewController.swift; sourceTree = "<group>"; };
C8CF22BF1F6926E500A01E0A /* CompetitorSessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompetitorSessionViewController.swift; sourceTree = "<group>"; };
C8CF22C11F692B5300A01E0A /* MarkSessionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkSessionViewController.swift; sourceTree = "<group>"; };
C8CF22C31F692B7700A01E0A /* MarkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkViewController.swift; sourceTree = "<group>"; };
C8CF22C51F694EB900A01E0A /* TrainingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrainingViewController.swift; sourceTree = "<group>"; };
C8D5E81C1D479FA800DA0D61 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
C8D938B11EB9AFDB00B1B4BC /* CoreData 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 3.xcdatamodel"; sourceTree = "<group>"; };
C8E0BD091F39A08B002FCCA5 /* FormularViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormularViewController.swift; sourceTree = "<group>"; };
C8E0BD0C1F39ADAD002FCCA5 /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = "<group>"; };
C8E0D3121F59475B001EEF5C /* CheckInTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckInTableViewController.swift; sourceTree = "<group>"; };
C8F1B1E01D488B6D004C3379 /* Translation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Translation.swift; sourceTree = "<group>"; };
C8F71EA51D8691F700826DE4 /* AFHTTPRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AFHTTPRequestOperation.h; sourceTree = "<group>"; };
C8F71EA61D8691F700826DE4 /* AFHTTPRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AFHTTPRequestOperation.m; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -1138,10 +1216,10 @@
7F83F14719F154F000C53328 = {
isa = PBXGroup;
children = (
7AC6457280AEA3EC18F5E846 /* Frameworks */,
7F83F15119F154F000C53328 /* Products */,
7F83F15219F154F000C53328 /* SAPTracker */,
7F83F16819F154F000C53328 /* SAPTrackerTests */,
7F83F15119F154F000C53328 /* Products */,
7AC6457280AEA3EC18F5E846 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -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 = "<group>";
@@ -1216,8 +1298,8 @@
C843D48D1EC0999D00250F01 /* Migration */ = {
isa = PBXGroup;
children = (
C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */,
C843D4901EC0A93600250F01 /* CoreData1-3.xcmappingmodel */,
C843D48E1EC099EB00250F01 /* CoreData2-3.xcmappingmodel */,
);
name = Migration;
sourceTree = "<group>";
@@ -1225,17 +1307,30 @@
C852BAA91CF736440093B0C5 /* Preferences */ = {
isa = PBXGroup;
children = (
C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */,
C8E0BD0C1F39ADAD002FCCA5 /* KeychainPasswordItem.swift */,
C852BAAA1CF736680093B0C5 /* Preferences.swift */,
);
name = Preferences;
sourceTree = "<group>";
};
C85DB8261F31AAAA009559CE /* SignUp */ = {
isa = PBXGroup;
children = (
C85DB82B1F31AB1A009559CE /* ForgotPasswordViewController.swift */,
C8E0BD091F39A08B002FCCA5 /* FormularViewController.swift */,
C85DB8291F31AAFD009559CE /* LoginViewController.swift */,
C85DB8271F31AAE0009559CE /* SignUpViewController.swift */,
);
name = SignUp;
sourceTree = "<group>";
};
C8739D8B1D23C01500A89F3C /* Managers */ = {
isa = PBXGroup;
children = (
7F2C605919F160E400C32D93 /* LocationManager.swift */,
7F1B66F81A0CDCF600904A41 /* BatteryManager.swift */,
C852BAA11CF5E1EA0093B0C5 /* RequestManager.swift */,
7F2C605919F160E400C32D93 /* LocationManager.swift */,
C8CF22AE1F669C1A00A01E0A /* Request */,
);
name = Managers;
sourceTree = "<group>";
@@ -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 = "<group>";
};
C87A74E41F504C97003DE396 /* Training */ = {
isa = PBXGroup;
children = (
C87A74E21F4F0E34003DE396 /* TrainingController.swift */,
);
name = Training;
sourceTree = "<group>";
};
C87A74E51F504CA7003DE396 /* CheckIn */ = {
isa = PBXGroup;
children = (
7F23F0871A1A3DEF00F03F1C /* CheckInController.swift */,
C8CF22A81F62780C00A01E0A /* CheckInDataCollector.swift */,
C8CF22A21F6194C600A01E0A /* RegattaCheckInController.swift */,
C8CF22A41F61965C00A01E0A /* TrainingCheckInController.swift */,
);
name = CheckIn;
sourceTree = "<group>";
};
C87A74E61F504CB4003DE396 /* SignUp */ = {
isa = PBXGroup;
children = (
C85DB82D1F31D804009559CE /* SignUpController.swift */,
);
name = SignUp;
sourceTree = "<group>";
};
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 = "<group>";
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -1326,22 +1463,10 @@
isa = PBXGroup;
children = (
7F14A0031A13B9A6001C1ECB /* Flags */,
C89C60141D0FF63C002AAAD2 /* Fonts */,
);
name = Resources;
sourceTree = "<group>";
};
C89C60141D0FF63C002AAAD2 /* Fonts */ = {
isa = PBXGroup;
children = (
7F87CA9D1A2CCE8B0076C617 /* OpenSans-Bold.ttf */,
7F87CA9E1A2CCE8B0076C617 /* OpenSans-Light.ttf */,
7F87CA9F1A2CCE8B0076C617 /* OpenSans-Regular.ttf */,
7F87CAA01A2CCE8B0076C617 /* OpenSans-Semibold.ttf */,
);
name = Fonts;
sourceTree = "<group>";
};
C8BFCC1B1D50F17800FC1A27 /* SMTWiFiStatus */ = {
isa = PBXGroup;
children = (
@@ -1352,6 +1477,76 @@
path = ..;
sourceTree = "<group>";
};
C8CF22A61F619B4800A01E0A /* Session */ = {
isa = PBXGroup;
children = (
C843D4871EBCADDC00250F01 /* CompetitorSessionController.swift */,
C843D4891EBCADF500250F01 /* MarkSessionController.swift */,
C8739E0E1D2BEE7900A89F3C /* SessionController.swift */,
C89188792057F55100E37CC3 /* BoatSessionController.swift */,
);
name = Session;
sourceTree = "<group>";
};
C8CF22A71F619B5F00A01E0A /* GPSFix */ = {
isa = PBXGroup;
children = (
C84936171D40D295000F30E8 /* GPSFixController.swift */,
);
name = GPSFix;
sourceTree = "<group>";
};
C8CF22AE1F669C1A00A01E0A /* Request */ = {
isa = PBXGroup;
children = (
C852BAA11CF5E1EA0093B0C5 /* CheckInRequestManager.swift */,
C8289F391F386D8300A76953 /* SignUpRequestManager.swift */,
C87A74E01F4EE570003DE396 /* TrainingRequestManager.swift */,
);
name = Request;
sourceTree = "<group>";
};
C8CF22AF1F669C8F00A01E0A /* New */ = {
isa = PBXGroup;
children = (
C843D531201F3050003725F0 /* NewCheckInViewController.swift */,
C867BD0A1F4B0290007E78E0 /* CreateTrainingViewController.swift */,
7F0658FC19F7D6700077FF95 /* ScanViewController.swift */,
);
name = New;
sourceTree = "<group>";
};
C8CF22B01F669CA700A01E0A /* CheckIn */ = {
isa = PBXGroup;
children = (
C84936281D47645F000F30E8 /* CheckInTableViewCell.swift */,
C8E0D3121F59475B001EEF5C /* CheckInTableViewController.swift */,
C8CF22AF1F669C8F00A01E0A /* New */,
);
name = CheckIn;
sourceTree = "<group>";
};
C8CF22B11F669DA700A01E0A /* Regatta */ = {
isa = PBXGroup;
children = (
7F99DE3B1A126554009B3619 /* RegattaCompetitorViewController.swift */,
C843D4851EBC927800250F01 /* RegattaMarkViewController.swift */,
C89188732057F32300E37CC3 /* RegattaBoatViewController.swift */,
);
name = Regatta;
sourceTree = "<group>";
};
C8CF22B21F669DB100A01E0A /* Training */ = {
isa = PBXGroup;
children = (
C867BD081F4B0274007E78E0 /* TrainingCompetitorViewController.swift */,
C8CF22AC1F668BAB00A01E0A /* TrainingMarkViewController.swift */,
C8CF22C51F694EB900A01E0A /* TrainingViewController.swift */,
C891887B20580A4100E37CC3 /* TrainingBoatViewController.swift */,
);
name = Training;
sourceTree = "<group>";
};
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 = "<group>";
versionGroupType = wrapper.xcdatamodel;
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -73,6 +73,10 @@
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
<LocationScenarioReference
identifier = "London, England"
referenceType = "1">
</LocationScenarioReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -29,11 +29,17 @@ class AboutViewController: UIViewController {
// MARK: - Setup
fileprivate func setup() {
setupButtons()
setupLocalization()
setupNavigationBar()
setupVersion()
}
fileprivate func setupButtons() {
makeBlue(button: licenseInformationButton)
makeBlue(button: termsButton)
}
fileprivate func setupLocalization() {
navigationItem.title = Translation.AboutView.Title.String
partnershipTextView.text = Translation.AboutView.PartnershipTextView.Text.String
@@ -52,11 +58,11 @@ class AboutViewController: UIViewController {
// MARK: - Actions
@IBAction func doneButtonTapped(_ sender: AnyObject) {
@IBAction func doneButtonTapped(_ sender: Any) {
presentingViewController!.dismiss(animated: true, completion: nil)
}
@IBAction func termsButtonTapped(_ sender: AnyObject) {
@IBAction func termsButtonTapped(_ sender: Any) {
UIApplication.shared.openURL(URLs.Terms)
}
@@ -18,12 +18,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
logInfo(name: "\(#function)", info: "Background fetch started...")
var noData = true
var allSuccess = true
if let checkIns = CoreDataManager.sharedManager.fetchCheckIns() {
if let checkIns = CoreDataManager.shared.fetchCheckIns() {
checkIns.forEach({ (checkIn) in
if let gpsFixes = checkIn.gpsFixes {
if gpsFixes.count > 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
@@ -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.";
@@ -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
}
@@ -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
}
}
@@ -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) } }
}
@@ -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 {
}
@@ -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)
}()
}
@@ -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)
}
}
}
@@ -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
@@ -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)
@@ -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)
}
}
@@ -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? {
@@ -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)
}
}
}
@@ -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
}
@@ -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!
}
@@ -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..<checkIns.count]), completion: completion)
})
}
fileprivate func reviewGPSFixesCompleted(completion: () -> 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<CheckIn>? = {
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<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
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<NSFetchRequestResult>) {
tableView.endUpdates()
setupTableViewHeader()
setupTableViewFooter()
}
}
// MARK: - NewCheckInViewControllerDelegate
extension CheckInTableViewController: NewCheckInViewControllerDelegate {
func newCheckInViewController(_ controller: NewCheckInViewController, didCheckIn checkIn: CheckIn) {
performSegue(forCheckIn: checkIn)
}
}
@@ -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,
@@ -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)
}()
}
@@ -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()
}
}
@@ -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 }
@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>CoreData 3.xcdatamodel</string>
<string>CoreData 4.xcdatamodel</string>
</dict>
</plist>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="16G1212" minimumToolsVersion="Xcode 4.3" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="BoatCheckIn" representedClassName=".BoatCheckIn" parentEntity="CheckIn" syncable="YES">
<attribute name="boatID" attributeType="String" syncable="YES"/>
<attribute name="color" attributeType="String" syncable="YES"/>
<attribute name="sailID" attributeType="String" syncable="YES"/>
</entity>
<entity name="CheckIn" representedClassName="CheckIn" elementID="Regatta" syncable="YES">
<attribute name="isTraining" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="serverURL" attributeType="String" elementID="serverUrl" syncable="YES"/>
<relationship name="event" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Event" inverseName="checkIn" inverseEntity="Event" syncable="YES"/>
<relationship name="gpsFixes" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="GPSFix" inverseName="checkIn" inverseEntity="GPSFix" syncable="YES"/>
<relationship name="leaderboard" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Leaderboard" inverseName="checkIn" inverseEntity="Leaderboard" elementID="leaderBoard" syncable="YES"/>
</entity>
<entity name="CompetitorCheckIn" representedClassName=".CompetitorCheckIn" parentEntity="CheckIn" syncable="YES">
<attribute name="boatClassName" attributeType="String" syncable="YES"/>
<attribute name="competitorID" attributeType="String" indexed="YES" elementID="competitorId" syncable="YES"/>
<attribute name="countryCode" attributeType="String" syncable="YES"/>
<attribute name="nationality" attributeType="String" syncable="YES"/>
<attribute name="sailID" attributeType="String" elementID="sailId" syncable="YES"/>
<attribute name="teamImageData" optional="YES" attributeType="Binary" elementID="userImage" syncable="YES"/>
<attribute name="teamImageRetry" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="teamImageURL" optional="YES" attributeType="String" elementID="imageUrl" syncable="YES"/>
</entity>
<entity name="Event" representedClassName="Event" syncable="YES">
<attribute name="endDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="eventID" attributeType="String" indexed="YES" elementID="eventId" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="startDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="checkIn" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CheckIn" inverseName="event" inverseEntity="CheckIn" elementID="regatta" syncable="YES"/>
</entity>
<entity name="GPSFix" representedClassName="GPSFix" syncable="YES">
<attribute name="course" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="latitude" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="longitude" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="speed" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="timestamp" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
<relationship name="checkIn" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CheckIn" inverseName="gpsFixes" inverseEntity="CheckIn" elementID="regatta" syncable="YES"/>
</entity>
<entity name="Leaderboard" representedClassName="Leaderboard" elementID="LeaderBoard" syncable="YES">
<attribute name="name" attributeType="String" indexed="YES" syncable="YES"/>
<relationship name="checkIn" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CheckIn" inverseName="leaderboard" inverseEntity="CheckIn" elementID="regatta" syncable="YES"/>
</entity>
<entity name="MarkCheckIn" representedClassName=".MarkCheckIn" parentEntity="CheckIn" syncable="YES">
<attribute name="markID" attributeType="String" indexed="YES" syncable="YES"/>
</entity>
<elements>
<element name="BoatCheckIn" positionX="-164" positionY="216" width="128" height="90"/>
<element name="CheckIn" positionX="-299" positionY="6" width="128" height="135"/>
<element name="CompetitorCheckIn" positionX="-299" positionY="215" width="128" height="163"/>
<element name="Event" positionX="-137" positionY="-63" width="128" height="118"/>
<element name="GPSFix" positionX="-470" positionY="0" width="128" height="133"/>
<element name="Leaderboard" positionX="-137" positionY="69" width="128" height="73"/>
<element name="MarkCheckIn" positionX="-434" positionY="216" width="128" height="58"/>
</elements>
</model>
@@ -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<BoatCheckIn>()
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<NSFetchRequestResult>()
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<CompetitorCheckIn>()
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...")
@@ -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 } }
}
@@ -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()
}
}
@@ -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]
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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
}
}
}
}
@@ -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
}
}
@@ -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<GPSFix>) -> 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
@@ -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..<checkIns.count]), completion: completion)
})
}
fileprivate func reviewGPSFixesCompleted(completion: () -> 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<CheckIn> = {
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<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
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<NSFetchRequestResult>) {
tableView.endUpdates()
setupTableViewHeader()
}
}
// MARK: - CheckInControllerDelegate
extension HomeViewController: CheckInControllerDelegate {
func checkInController(_ sender: CheckInController, show alertController: UIAlertController) {
present(alertController, animated: true, completion: nil)
}
}
@@ -97,6 +97,11 @@
"idiom" : "ipad",
"filename" : "167_iPhone_6Plus_App_Icon@2x.png",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -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"
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -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"
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -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"
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -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"
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -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" : {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 B

@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

@@ -48,13 +48,6 @@
</dict>
<key>NSLocationAlwaysUsageDescription</key>
<string></string>
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>
<string>OpenSans-Light.ttf</string>
<string>OpenSans-Regular.ttf</string>
<string>OpenSans-Semibold.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
@@ -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)
}
}
@@ -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
}
}
@@ -1,19 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="15G1421" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<customFonts key="customFonts">
<array key="OpenSans-Regular.ttf">
<string>OpenSans</string>
</array>
</customFonts>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
@@ -28,7 +23,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ct1-2l-YCw" userLabel="Center View">
<rect key="frame" x="33.5" y="256" width="308" height="155"/>
<rect key="frame" x="33.5" y="259" width="308" height="149.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="applogo" translatesAutoresizingMaskIntoConstraints="NO" id="XA8-DM-z3j">
<rect key="frame" x="0.0" y="0.0" width="308" height="81"/>
@@ -37,8 +32,8 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SAP Sail InSight" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZGa-Zx-evx">
<rect key="frame" x="0.0" y="111" width="308" height="44"/>
<fontDescription key="fontDescription" name="OpenSans" family="Open Sans" pointSize="32"/>
<rect key="frame" x="0.0" y="111" width="308" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="32"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
@@ -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 {
}
@@ -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
}
}
@@ -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)
@@ -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
}
}
}
File diff suppressed because it is too large. Load diff
@@ -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)
}()
}
@@ -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()
}
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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) } }
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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) } }
}
@@ -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"
}
}
@@ -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()
}
}
@@ -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)
}()
}
@@ -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)
}
}
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more