diff --git a/.gitignore b/.gitignore index 39904b2..084ef6c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ profile *.moved-aside Pods + +# Protect secret keys +UGentOAuthConfig.plist diff --git a/External/SORelativeDateTransformer.bundle/nl.lproj/SORelativeDateTransformer.strings b/External/SORelativeDateTransformer.bundle/nl.lproj/SORelativeDateTransformer.strings index fdc7381..3efdc62 100755 Binary files a/External/SORelativeDateTransformer.bundle/nl.lproj/SORelativeDateTransformer.strings and b/External/SORelativeDateTransformer.bundle/nl.lproj/SORelativeDateTransformer.strings differ diff --git a/External/SORelativeDateTransformer.m b/External/SORelativeDateTransformer.m index e559e73..3fe424b 100755 --- a/External/SORelativeDateTransformer.m +++ b/External/SORelativeDateTransformer.m @@ -40,8 +40,8 @@ - (id) init [__calendar retain]; #endif - __unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSWeekCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; - __dateComponentSelectorNames = [[NSArray alloc] initWithObjects:@"year", @"month", @"week", @"day", @"hour", @"minute", @"second", nil]; + __unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitWeekOfMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + __dateComponentSelectorNames = [[NSArray alloc] initWithObjects:@"year", @"month", @"weekOfMonth", @"day", @"hour", @"minute", @"second", nil]; return self; } diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist new file mode 100644 index 0000000..ac34e7b --- /dev/null +++ b/GoogleService-Info.plist @@ -0,0 +1,42 @@ + + + + + TRACKING_ID + UA-25444917-3 + AD_UNIT_ID_FOR_BANNER_TEST + ca-app-pub-3940256099942544/2934735716 + AD_UNIT_ID_FOR_INTERSTITIAL_TEST + ca-app-pub-3940256099942544/4411468910 + CLIENT_ID + 758079297428-en5q4eupkqkqci5ojbcshqs22dcm2dk1.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.758079297428-en5q4eupkqkqci5ojbcshqs22dcm2dk1 + API_KEY + AIzaSyASj-xXi7PIwJxZrY8JgESgw3_AhiKR4bA + GCM_SENDER_ID + 758079297428 + PLIST_VERSION + 1 + BUNDLE_ID + be.ugent.zeus.Hydra + PROJECT_ID + hydra-142516 + STORAGE_BUCKET + hydra-142516.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:758079297428:ios:9a4bdee4f64f1ef8 + DATABASE_URL + https://hydra-142516.firebaseio.com + + diff --git a/Hydra-Info.plist b/Hydra-Info.plist index bf2fcd0..c748004 100644 --- a/Hydra-Info.plist +++ b/Hydra-Info.plist @@ -19,7 +19,7 @@ CFBundleIdentifier - be.ugent.zeus.${PRODUCT_NAME:rfc1034identifier}${CUSTOM_IDENTIFIER_SUFFIX} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -27,7 +27,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.0.1 + 2.1 CFBundleSignature ???? CFBundleURLTypes @@ -38,11 +38,30 @@ fb146947948791011 + + CFBundleTypeRole + Editor + CFBundleURLName + hydra-ugent + CFBundleURLSchemes + + hydra-ugent + + CFBundleVersion - 2.0.15 + 2.1.9 FacebookAppID 146947948791011 + FacebookDisplayName + Hydra + LSApplicationQueriesSchemes + + fbapi + fb-messenger-api + fbauth2 + fbshareextension + LSRequiresIPhoneOS NSAppTransportSecurity @@ -59,12 +78,16 @@ NSLocationWhenInUseUsageDescription We hebben uw locatie nodig om deze te kunnen tonen op de kaarten. + UIAppFonts + + LeagueGothic-Regular.otf + UIBackgroundModes audio UILaunchStoryboardName - MainStoryboard + LaunchScreen UIPrerenderedIcon UIRequiresFullScreen diff --git a/Hydra.xcodeproj/project.pbxproj b/Hydra.xcodeproj/project.pbxproj index 0adbed2..9614c07 100644 --- a/Hydra.xcodeproj/project.pbxproj +++ b/Hydra.xcodeproj/project.pbxproj @@ -3,31 +3,37 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 47; objects = { /* Begin PBXBuildFile section */ 395C117A16272D8000B99F9B /* NewsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 395C117916272D8000B99F9B /* NewsViewController.m */; }; 395C117D1627437000B99F9B /* NewsDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 395C117C1627437000B99F9B /* NewsDetailViewController.m */; }; - 3B436DF815B8915300984D3F /* InfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B436DF715B8915200984D3F /* InfoViewController.m */; }; 3B93FB9F1630939200C962DC /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B93FB9E1630939200C962DC /* AudioToolbox.framework */; }; 3B93FBA7163095F900C962DC /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B93FB9E1630939200C962DC /* AudioToolbox.framework */; }; 3B93FBAA1630969900C962DC /* UrgentPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B93FBA91630969900C962DC /* UrgentPlayer.m */; }; 3B93FBAC16309E2200C962DC /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B93FBAB16309E2100C962DC /* AVFoundation.framework */; }; - 445C81F516C3DB0B0080819C /* AssociationPreferenceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 445C81F416C3DB0B0080819C /* AssociationPreferenceController.m */; }; + 3FAEE3F08BA5B665F18C8B3D /* Pods_Hydra.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FA3342E7471DF2C095F6AFD /* Pods_Hydra.framework */; }; 44A29C58162DA88E001A573C /* EventKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44A29C57162DA88E001A573C /* EventKitUI.framework */; }; + 900295EE1D2EB8F8005F86E6 /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900295E91D2EB8F8005F86E6 /* Announcement.swift */; }; + 900295F01D2EB8F8005F86E6 /* Course.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900295EB1D2EB8F8005F86E6 /* Course.swift */; }; + 900295F11D2EB8F8005F86E6 /* OAuthTokenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900295EC1D2EB8F8005F86E6 /* OAuthTokenInfo.swift */; }; + 900295F21D2EB8F8005F86E6 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900295ED1D2EB8F8005F86E6 /* User.swift */; }; + 900295F41D2EBA52005F86E6 /* UGentOAuthConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = 900295F31D2EBA52005F86E6 /* UGentOAuthConfig.plist */; }; + 900295F61D2EBA98005F86E6 /* UGentOAuth2Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900295F51D2EBA98005F86E6 /* UGentOAuth2Service.swift */; }; 9004F58F1B7F2FB60041FAA6 /* resto-map-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9004F58D1B7F2FB60041FAA6 /* resto-map-icon@2x.png */; }; 9004F5901B7F2FB60041FAA6 /* resto-map-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9004F58E1B7F2FB60041FAA6 /* resto-map-icon.png */; }; 9015D0FA1A8ABA4B00228ADC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 908E536419BE4F3900F1DA57 /* Images.xcassets */; }; 901603DC1B716DED002E0D60 /* HomeNewsItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901603DB1B716DED002E0D60 /* HomeNewsItemCollectionViewCell.swift */; }; 902939441A8C22EB00868221 /* info-bloklocaties.html in Resources */ = {isa = PBXBuildFile; fileRef = 902939431A8C22EB00868221 /* info-bloklocaties.html */; }; + 902B7CAB1D5FA83000B53B98 /* HomeMinervaAnnouncementCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902B7CAA1D5FA83000B53B98 /* HomeMinervaAnnouncementCell.swift */; }; + 902B7CAD1D5FCC6D00B53B98 /* HomeMinervaCalendarItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902B7CAC1D5FCC6D00B53B98 /* HomeMinervaCalendarItemCell.swift */; }; + 902C9FE21D4D437E00A4BE86 /* CalendarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902C9FE11D4D437E00A4BE86 /* CalendarItem.swift */; }; 9035BF121B6C1753008E7875 /* schamper.png in Resources */ = {isa = PBXBuildFile; fileRef = 9035BF111B6C1753008E7875 /* schamper.png */; }; 9035BF141B6C1F5D008E7875 /* HomeSchamperCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9035BF131B6C1F5D008E7875 /* HomeSchamperCollectionViewCell.swift */; }; 9035BF161B6C37FE008E7875 /* home-zeus.png in Resources */ = {isa = PBXBuildFile; fileRef = 9035BF151B6C37FE008E7875 /* home-zeus.png */; }; 9035BF181B6CB52A008E7875 /* HomeActivityCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9035BF171B6CB52A008E7875 /* HomeActivityCollectionViewCell.swift */; }; 9035BF1A1B6CF663008E7875 /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9035BF191B6CF663008E7875 /* LocationService.swift */; }; - 903A8AF61C04F369002DAF5B /* ActivityOverviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 903A8AF51C04F369002DAF5B /* ActivityOverviewCell.swift */; }; - 903A8AF81C04FBF7002DAF5B /* ActivityOverviewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 903A8AF71C04FBF7002DAF5B /* ActivityOverviewCell.xib */; }; 90410B101848DF9100331F6C /* info-guide.png in Resources */ = {isa = PBXBuildFile; fileRef = 90410B0E1848DF9100331F6C /* info-guide.png */; }; 90410B111848DF9100331F6C /* info-guide@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90410B0F1848DF9100331F6C /* info-guide@2x.png */; }; 904166231B7A689500D231EF /* tabbar-urgent@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 904166211B7A689500D231EF /* tabbar-urgent@2x.png */; }; @@ -35,23 +41,88 @@ 904166261B7A837C00D231EF /* tabbar-settings.png in Resources */ = {isa = PBXBuildFile; fileRef = 904166251B7A837C00D231EF /* tabbar-settings.png */; }; 9041C3681B7E67B600E4A50C /* RestoMenuCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9041C3671B7E67B600E4A50C /* RestoMenuCollectionCell.swift */; }; 9041C36D1B7E801B00E4A50C /* RestoMenuHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9041C36C1B7E801B00E4A50C /* RestoMenuHeader.swift */; }; + 9042D8711D89AC22001F3E12 /* tabbar-sko.png in Resources */ = {isa = PBXBuildFile; fileRef = 9042D8701D89AC22001F3E12 /* tabbar-sko.png */; }; + 9047505C1D64FCD00091E2EF /* MinervaCalendarDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9047505B1D64FCD00091E2EF /* MinervaCalendarDetailViewController.swift */; }; + 904A58211CB418CE0079ED25 /* NSDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 904A58201CB418CE0079ED25 /* NSDate.swift */; }; + 904A58231CB505F00079ED25 /* HomeSpecialEventBasicCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 904A58221CB505F00079ED25 /* HomeSpecialEventBasicCollectionViewCell.swift */; }; + 904AD4EE1C88B845008F88E8 /* FacebookEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 904AD4ED1C88B845008F88E8 /* FacebookEvent.swift */; }; + 904AD4F01C88B858008F88E8 /* FacebookSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 904AD4EF1C88B858008F88E8 /* FacebookSession.swift */; }; 904BC46C17E2014A0000FED6 /* dot-question.png in Resources */ = {isa = PBXBuildFile; fileRef = 904BC46817E2014A0000FED6 /* dot-question.png */; }; 904BC46D17E2014A0000FED6 /* dot-question@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 904BC46917E2014A0000FED6 /* dot-question@2x.png */; }; + 90530DBB1D2D889400A0CC8A /* PreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90530DBA1D2D889400A0CC8A /* PreferencesController.swift */; }; + 90530DBD1D2D98D300A0CC8A /* PreferencesExtraTableViewCells.xib in Resources */ = {isa = PBXBuildFile; fileRef = 90530DBC1D2D98D300A0CC8A /* PreferencesExtraTableViewCells.xib */; }; + 90530DBF1D2D9CDD00A0CC8A /* PreferenceTableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90530DBE1D2D9CDD00A0CC8A /* PreferenceTableViewCells.swift */; }; + 90530DC11D2DA5AA00A0CC8A /* PreferencesSwitchTableViewCells.xib in Resources */ = {isa = PBXBuildFile; fileRef = 90530DC01D2DA5AA00A0CC8A /* PreferencesSwitchTableViewCells.xib */; }; + 90530DC31D2DB30200A0CC8A /* PreferencesTextTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 90530DC21D2DB30200A0CC8A /* PreferencesTextTableViewCell.xib */; }; + 90530DC51D2DC10E00A0CC8A /* AssociationPreferenceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 90530DC41D2DC10E00A0CC8A /* AssociationPreferenceController.m */; }; + 90530DC81D2E51B800A0CC8A /* PreferencesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90530DC71D2E51B800A0CC8A /* PreferencesService.swift */; }; 905EB0131BC809C600F38679 /* tabbar-settings@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 905EB0121BC809C600F38679 /* tabbar-settings@2x.png */; }; 905EB0151BC80A3800F38679 /* tabbar-news@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 905EB0141BC80A3800F38679 /* tabbar-news@2x.png */; }; 905EB0171BC80B2100F38679 /* tabbar-activities@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 905EB0161BC80B2000F38679 /* tabbar-activities@2x.png */; }; + 9063FC601D5D079100BD01F6 /* InfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9063FC5F1D5D079100BD01F6 /* InfoItem.swift */; }; + 9063FC621D5E277400BD01F6 /* InfoStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9063FC611D5E277400BD01F6 /* InfoStore.swift */; }; + 9063FC641D5E2BB900BD01F6 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9063FC631D5E2BB900BD01F6 /* InfoViewController.swift */; }; + 90677EEF1D91C7CE003F7135 /* tabbar-sko-student-village.png in Resources */ = {isa = PBXBuildFile; fileRef = 90677EED1D91C7CE003F7135 /* tabbar-sko-student-village.png */; }; + 90677EF01D91C7CE003F7135 /* tabbar-sko-student-village@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90677EEE1D91C7CE003F7135 /* tabbar-sko-student-village@2x.png */; }; + 90677EF31D91C853003F7135 /* tabbar-sko-lineup.png in Resources */ = {isa = PBXBuildFile; fileRef = 90677EF11D91C853003F7135 /* tabbar-sko-lineup.png */; }; + 90677EF41D91C853003F7135 /* tabbar-sko-lineup@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90677EF21D91C853003F7135 /* tabbar-sko-lineup@2x.png */; }; + 90677EF71D91C8C1003F7135 /* tabbar-sko-map@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90677EF51D91C8C1003F7135 /* tabbar-sko-map@2x.png */; }; + 90677EF81D91C8C1003F7135 /* tabbar-sko-map.png in Resources */ = {isa = PBXBuildFile; fileRef = 90677EF61D91C8C1003F7135 /* tabbar-sko-map.png */; }; + 90677EFA1D92B581003F7135 /* SKOStudentVillageDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90677EF91D92B581003F7135 /* SKOStudentVillageDetailViewController.swift */; }; 9068BA781B7BB437005F79FA /* tabbar-resto@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9068BA771B7BB437005F79FA /* tabbar-resto@2x.png */; }; 9068BA7A1B7BB560005F79FA /* tabbar-home@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9068BA791B7BB560005F79FA /* tabbar-home@2x.png */; }; 9068BA7C1B7BB684005F79FA /* tabbar-info@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9068BA7B1B7BB684005F79FA /* tabbar-info@2x.png */; }; + 90692EE71D6399C600F607D8 /* CalendarSingleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90692EE61D6399C600F607D8 /* CalendarSingleTableViewCell.swift */; }; 906C1D8917C8A4A600145CD3 /* MapViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 906C1D8817C8A4A600145CD3 /* MapViewController.m */; }; 907CD3CA1B7E52B600DD7539 /* RestoMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907CD3C91B7E52B600DD7539 /* RestoMenuViewController.swift */; }; + 90869A001CACA038004C9FF8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 908699FF1CACA038004C9FF8 /* GoogleService-Info.plist */; }; + 90869A011CACA04D004C9FF8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 908699FF1CACA038004C9FF8 /* GoogleService-Info.plist */; }; + 90869A021CACA04E004C9FF8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 908699FF1CACA038004C9FF8 /* GoogleService-Info.plist */; }; + 90869A041CACA116004C9FF8 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90869A031CACA116004C9FF8 /* UIViewController.swift */; }; + 9086FD8B1C84E8BF0053493D /* Association.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD871C84E8BF0053493D /* Association.swift */; }; + 9086FD8C1C84E8BF0053493D /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD881C84E8BF0053493D /* Activity.swift */; }; + 9086FD8D1C84E8BF0053493D /* NewsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD891C84E8BF0053493D /* NewsItem.swift */; }; + 9086FD8E1C84E8BF0053493D /* SchamperArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD8A1C84E8BF0053493D /* SchamperArticle.swift */; }; + 9086FD921C84E9900053493D /* AssociationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD8F1C84E9900053493D /* AssociationStore.swift */; }; + 9086FD931C84E9900053493D /* SchamperStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD901C84E9900053493D /* SchamperStore.swift */; }; + 9086FD941C84E9900053493D /* SavableStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD911C84E9900053493D /* SavableStore.swift */; }; + 9086FD961C84EEC50053493D /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD951C84EEC50053493D /* Config.swift */; }; + 9086FD9A1C84F0750053493D /* p2_OAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD981C84F0750053493D /* p2_OAuth2.swift */; }; + 9086FD9B1C84F0750053493D /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9086FD991C84F0750053493D /* String.swift */; }; 908CA4DD1A8D439C00F8C31F /* info-mapmarker.png in Resources */ = {isa = PBXBuildFile; fileRef = 908CA4DB1A8D439C00F8C31F /* info-mapmarker.png */; }; 908CA4DE1A8D439C00F8C31F /* info-mapmarker@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 908CA4DC1A8D439C00F8C31F /* info-mapmarker@2x.png */; }; 908DFF03188E932D00D526DC /* Pods-acknowledgements.plist in Resources */ = {isa = PBXBuildFile; fileRef = 908DFF02188E932D00D526DC /* Pods-acknowledgements.plist */; }; 908E536519BE4F3900F1DA57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 908E536419BE4F3900F1DA57 /* Images.xcassets */; }; - 90969AD21BFF5D8D006EDD9D /* AssociationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90969AD11BFF5D8D006EDD9D /* AssociationStore.swift */; }; + 9092D6AA1D5A358F00B75CEE /* MinervaAnnounceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9092D6A91D5A358F00B75CEE /* MinervaAnnounceDetailViewController.swift */; }; + 9093B0241D50F5EC00EB00E2 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9093B0231D50F5EC00EB00E2 /* UIColor.swift */; }; + 9093B0261D512BDD00EB00E2 /* Whatsnew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9093B0251D512BDD00EB00E2 /* Whatsnew.swift */; }; 90969AD41BFF5F90006EDD9D /* RestoStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90969AD31BFF5F90006EDD9D /* RestoStore.swift */; }; - 90969AD61BFF6014006EDD9D /* SchamperStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90969AD51BFF6014006EDD9D /* SchamperStore.swift */; }; + 909E46F51D5A17770072AFEC /* tabbar-minerva.png in Resources */ = {isa = PBXBuildFile; fileRef = 909E46F21D5A17770072AFEC /* tabbar-minerva.png */; }; + 909E46F61D5A17770072AFEC /* tabbar-minerva@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 909E46F31D5A17770072AFEC /* tabbar-minerva@2x.png */; }; + 909E46F71D5A17770072AFEC /* tabbar-minerva@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 909E46F41D5A17770072AFEC /* tabbar-minerva@3x.png */; }; + 909E46FB1D5A2CF90072AFEC /* minerva-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 909E46F81D5A2CF80072AFEC /* minerva-icon.png */; }; + 909E46FC1D5A2CF90072AFEC /* minerva-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 909E46F91D5A2CF90072AFEC /* minerva-icon@2x.png */; }; + 909E46FD1D5A2CF90072AFEC /* minerva-icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 909E46FA1D5A2CF90072AFEC /* minerva-icon@3x.png */; }; + 90AA4BA21D831C86003303FE /* SKOHydraTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BA11D831C86003303FE /* SKOHydraTabBarController.swift */; }; + 90AA4BA41D831D3A003303FE /* sko.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90AA4BA31D831D3A003303FE /* sko.storyboard */; }; + 90AA4BA61D831F1B003303FE /* SKOBackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BA51D831F1B003303FE /* SKOBackViewController.swift */; }; + 90AA4BA91D833347003303FE /* logo-sko.png in Resources */ = {isa = PBXBuildFile; fileRef = 90AA4BA81D833347003303FE /* logo-sko.png */; }; + 90AA4BAB1D8335BB003303FE /* SKOTimelineCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BAA1D8335BB003303FE /* SKOTimelineCollectionViewController.swift */; }; + 90AA4BAE1D835919003303FE /* LeagueGothic-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 90AA4BAD1D83587C003303FE /* LeagueGothic-Regular.otf */; }; + 90AA4BB01D835B67003303FE /* SKOLineupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BAF1D835B67003303FE /* SKOLineupViewController.swift */; }; + 90AA4BB21D835BB0003303FE /* SKOMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BB11D835BB0003303FE /* SKOMapViewController.swift */; }; + 90AA4BB41D835C08003303FE /* SKOStudentVillageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BB31D835C08003303FE /* SKOStudentVillageViewController.swift */; }; + 90AA4BB71D83601B003303FE /* tabbar-hydra.png in Resources */ = {isa = PBXBuildFile; fileRef = 90AA4BB51D83601B003303FE /* tabbar-hydra.png */; }; + 90AA4BB81D83601B003303FE /* tabbar-hydra@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90AA4BB61D83601B003303FE /* tabbar-hydra@2x.png */; }; + 90AA4BBC1D840097003303FE /* SKOStageHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BBB1D840097003303FE /* SKOStageHeaderCollectionReusableView.swift */; }; + 90AA4BBE1D840864003303FE /* SKOLineupStageCollectionReusableView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 90AA4BBD1D840864003303FE /* SKOLineupStageCollectionReusableView.xib */; }; + 90AA4BC11D840F65003303FE /* SKOLineUpCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AA4BC01D840F65003303FE /* SKOLineUpCollectionViewCell.swift */; }; + 90AC8E841D87347F00A16082 /* onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90AC8E831D87347F00A16082 /* onboarding.storyboard */; }; + 90AC8E861D87380B00A16082 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 90AC8E851D87380B00A16082 /* Settings.bundle */; }; + 90AC8E881D8745A600A16082 /* TimelineOnboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AC8E871D8745A600A16082 /* TimelineOnboardViewController.swift */; }; + 90AC8E8A1D874F1500A16082 /* InitialOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AC8E891D874F1500A16082 /* InitialOnboardingViewController.swift */; }; + 90AC8E8C1D8751A300A16082 /* MinervaOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AC8E8B1D8751A300A16082 /* MinervaOnboardingViewController.swift */; }; + 90AC8E8E1D87696C00A16082 /* TimeLineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90AC8E8D1D87696C00A16082 /* TimeLineTableViewCell.swift */; }; 90B010A31C305F73000DE080 /* RestoSandwich.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B010A21C305F73000DE080 /* RestoSandwich.swift */; }; 90B010A51C308CB2000DE080 /* NSCalendar+HydraCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B010A41C308CB2000DE080 /* NSCalendar+HydraCalendar.swift */; }; 90B1029717A85E9200669CCD /* kalender.css in Resources */ = {isa = PBXBuildFile; fileRef = 90B1029617A85E9200669CCD /* kalender.css */; }; @@ -62,31 +133,46 @@ 90B1EF141B7A60B000E733C1 /* tabbar-news.png in Resources */ = {isa = PBXBuildFile; fileRef = 90B1EF131B7A60B000E733C1 /* tabbar-news.png */; }; 90B1EF161B7A60FC00E733C1 /* tabbar-schamper@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90B1EF151B7A60FC00E733C1 /* tabbar-schamper@2x.png */; }; 90B1EF181B7A61DD00E733C1 /* tabbar-schamper.png in Resources */ = {isa = PBXBuildFile; fileRef = 90B1EF171B7A61DD00E733C1 /* tabbar-schamper.png */; }; + 90B3D7C31CB1CCF200A7E25E /* SpecialEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B3D7C21CB1CCF200A7E25E /* SpecialEvent.swift */; }; + 90B3D7C51CB1D22400A7E25E /* SpecialEventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B3D7C41CB1D22400A7E25E /* SpecialEventStore.swift */; }; 90BAB266162749BD0043BA92 /* SchamperDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 90BAB265162749BC0043BA92 /* SchamperDetailViewController.m */; }; 90BE1F891B6AB3390061888C /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BE1F881B6AB3390061888C /* HomeViewController.swift */; }; 90BE1F8D1B6AB61D0061888C /* MainStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90BE1F8C1B6AB61D0061888C /* MainStoryboard.storyboard */; }; 90BE1F901B6AB8780061888C /* home-header.png in Resources */ = {isa = PBXBuildFile; fileRef = 90BE1F8E1B6AB8780061888C /* home-header.png */; }; 90BE1F911B6AB8780061888C /* home-background.png in Resources */ = {isa = PBXBuildFile; fileRef = 90BE1F8F1B6AB8780061888C /* home-background.png */; }; - 90C532AB16A325A200857EB0 /* FacebookEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 90C532AA16A325A200857EB0 /* FacebookEvent.m */; }; + 90C288A51D47A69000231462 /* MinervaCourseAnnouncementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90C288A41D47A69000231462 /* MinervaCourseAnnouncementViewController.swift */; }; + 90C288A71D47A6FA00231462 /* MinervaCoursePreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90C288A61D47A6FA00231462 /* MinervaCoursePreferenceViewController.swift */; }; + 90C288A91D47AD1500231462 /* MinervaStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90C288A81D47AD1500231462 /* MinervaStore.swift */; }; 90C8CA2E1B6AE3E80073E026 /* HomeFeedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90C8CA2D1B6AE3E80073E026 /* HomeFeedService.swift */; }; + 90CA18341D8BE62300309815 /* tabbar-sko@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90CA18331D8BE62300309815 /* tabbar-sko@2x.png */; }; + 90CA18371D8BF33200309815 /* TimelinePost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CA18361D8BF33200309815 /* TimelinePost.swift */; }; + 90CA18391D8C1BFE00309815 /* SKOTimelineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CA18381D8C1BFE00309815 /* SKOTimelineCollectionViewCell.swift */; }; 90CDD89D1C0A2A1300394F42 /* home-location.png in Resources */ = {isa = PBXBuildFile; fileRef = 90CDD89C1C0A2A1300394F42 /* home-location.png */; }; 90CDD89F1C0A2A1800394F42 /* home-location@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90CDD89E1C0A2A1800394F42 /* home-location@2x.png */; }; 90CDD8A11C0A2BE200394F42 /* home-location@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90CDD8A01C0A2BE200394F42 /* home-location@3x.png */; }; 90CDD8A31C0A2CEE00394F42 /* home-clock.png in Resources */ = {isa = PBXBuildFile; fileRef = 90CDD8A21C0A2CEE00394F42 /* home-clock.png */; }; 90CDD8A51C0A2D0F00394F42 /* home-clock@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90CDD8A41C0A2D0F00394F42 /* home-clock@2x.png */; }; + 90CDDA2F1D85CDFE00CCF8FB /* SKOStudentVillageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CDDA2E1D85CDFE00CCF8FB /* SKOStudentVillageTableViewCell.swift */; }; + 90CDDA331D85E69A00CCF8FB /* Exihibitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CDDA321D85E69A00CCF8FB /* Exihibitor.swift */; }; + 90CEB35D1D81AAB700833477 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CEB35C1D81AAB700833477 /* NotificationService.swift */; }; 90D55B9B1B7A4CE800813179 /* HydraTabbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90D55B9A1B7A4CE800813179 /* HydraTabbarController.swift */; }; 90DDFB791B6BB3D300DDAFA0 /* HomeRestoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DDFB781B6BB3D300DDAFA0 /* HomeRestoCollectionViewCell.swift */; }; + 90E3F5601C86585600343DF2 /* RestoMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E3F55F1C86585600343DF2 /* RestoMenu.swift */; }; + 90E4BBB81D84466300582586 /* SKOStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E4BBB71D84466300582586 /* SKOStore.swift */; }; + 90E4BBBB1D8446C500582586 /* Stage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E4BBBA1D8446C500582586 /* Stage.swift */; }; + 90E4BBBD1D84470800582586 /* Artist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E4BBBC1D84470800582586 /* Artist.swift */; }; 90E814F31854C76C00075F96 /* ios7-Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90E814F11854C76C00075F96 /* ios7-Default-568h@2x.png */; }; 90E814F41854C76C00075F96 /* ios7-Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 90E814F21854C76C00075F96 /* ios7-Default@2x.png */; }; 90E8DDC11B6AD45C0059F71B /* home-button-bar.png in Resources */ = {isa = PBXBuildFile; fileRef = 90E8DDC01B6AD45C0059F71B /* home-button-bar.png */; }; - 90EB7BEA1688ECE300A9582D /* RestoLegendItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 90EB7BE91688ECE300A9582D /* RestoLegendItem.m */; }; 90EE4CD01BB7193900C2DD32 /* RestoMenuInfoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90EE4CCF1BB7193900C2DD32 /* RestoMenuInfoCollectionViewCell.swift */; }; + 90EE815E1D8C58A200EE0F4F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90EE815D1D8C58A200EE0F4F /* LaunchScreen.storyboard */; }; 90EF4A591B6D8AE40034AD8F /* HomeUrgentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90EF4A581B6D8AE40034AD8F /* HomeUrgentCollectionViewCell.swift */; }; 90F0525E168C663F004CAFFC /* RestoMapController.m in Sources */ = {isa = PBXBuildFile; fileRef = 90F0525C168C663F004CAFFC /* RestoMapController.m */; }; 90F05261168C7022004CAFFC /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90F05260168C7022004CAFFC /* CoreLocation.framework */; }; 90F05263168C7726004CAFFC /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90F05262168C7726004CAFFC /* MapKit.framework */; }; - 90F05266168C7D19004CAFFC /* RestoLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 90F05265168C7D19004CAFFC /* RestoLocation.m */; }; - B50ED58F220549D98F92E254 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E747F0E311D74638A7FC212F /* libPods.a */; }; + 90F37DD31C8B9A1200A380F0 /* RestoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90F37DD21C8B9A1200A380F0 /* RestoLocation.swift */; }; + 90FFEA061D610D8900C18FDE /* CalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90FFEA051D610D8900C18FDE /* CalendarViewController.swift */; }; + ED20E64B1D2D809000255C57 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED20E64A1D2D809000255C57 /* UIViewExtension.swift */; }; F49F792E1A1671950040987F /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F49F792D1A1671950040987F /* NotificationCenter.framework */; }; F49F79331A1671950040987F /* TodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49F79321A1671950040987F /* TodayViewController.swift */; }; F49F79381A1671950040987F /* RestoMenuToday.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F49F792C1A1671950040987F /* RestoMenuToday.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -98,7 +184,6 @@ F49F794B1A1672410040987F /* RestoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49F79431A1672410040987F /* RestoManager.swift */; }; F49F79531A16748B0040987F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F49F79551A16748B0040987F /* Localizable.strings */; }; F4BE9FD01A168C5B00BFB8F8 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BE9FCF1A168C5B00BFB8F8 /* String.swift */; }; - F5016BA6162756F300BBDB0A /* ActivitiesController.m in Sources */ = {isa = PBXBuildFile; fileRef = F5016BA5162756F300BBDB0A /* ActivitiesController.m */; }; F5016BA91627665800BBDB0A /* ActivityDetailController.m in Sources */ = {isa = PBXBuildFile; fileRef = F5016BA81627665800BBDB0A /* ActivityDetailController.m */; }; F50527CE15BDC1E600A697F8 /* info-kalender.html in Resources */ = {isa = PBXBuildFile; fileRef = F50527CD15BDC1E600A697F8 /* info-kalender.html */; }; F50527D015BDC5F600A697F8 /* info-studentenartsen.html in Resources */ = {isa = PBXBuildFile; fileRef = F50527CF15BDC5F600A697F8 /* info-studentenartsen.html */; }; @@ -106,7 +191,6 @@ F50EE09715B5F587000F1992 /* WebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F50EE09515B5F587000F1992 /* WebViewController.m */; }; F520A2DD166E933500EE340F /* icon-calendar.png in Resources */ = {isa = PBXBuildFile; fileRef = F520A2DB166E933500EE340F /* icon-calendar.png */; }; F520A2E0166E999400EE340F /* icon-calendar@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F520A2DF166E999400EE340F /* icon-calendar@2x.png */; }; - F521D11616C53A1200B686B2 /* PreferencesController.m in Sources */ = {isa = PBXBuildFile; fileRef = F521D11516C53A1200B686B2 /* PreferencesController.m */; }; F52A8B7917CD366E00C3379C /* ActivityMapController.m in Sources */ = {isa = PBXBuildFile; fileRef = F52A8B7817CD366E00C3379C /* ActivityMapController.m */; }; F52B9B77168507BD000B71D6 /* NSDateFormatter+AppLocale.m in Sources */ = {isa = PBXBuildFile; fileRef = F52B9B76168507BD000B71D6 /* NSDateFormatter+AppLocale.m */; }; F531DE6516B2E41B0050D88B /* button-route@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F531DE6416B2E41B0050D88B /* button-route@2x.png */; }; @@ -134,15 +218,11 @@ F54DC88C16A1A97100FD212E /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F54DC88B16A1A97100FD212E /* SystemConfiguration.framework */; }; F54DC88E16A1A97C00FD212E /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F54DC88D16A1A97C00FD212E /* libxml2.dylib */; }; F557EA46163431FC00635CDD /* schamper-bg@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F557EA45163431FC00635CDD /* schamper-bg@2x.png */; }; - F55895C415B60F450001B399 /* RestoMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = F55895C315B60F450001B399 /* RestoMenu.m */; }; - F55895C715B610140001B399 /* RestoStore.m in Sources */ = {isa = PBXBuildFile; fileRef = F55895C615B610130001B399 /* RestoStore.m */; }; - F55E89EC16A4AF8E008595CB /* ShareKitConfigurator.m in Sources */ = {isa = PBXBuildFile; fileRef = F55E89EB16A4AF8E008595CB /* ShareKitConfigurator.m */; }; F567044716BD5D8100C6B00D /* info-minerva.png in Resources */ = {isa = PBXBuildFile; fileRef = F567044316BD5D8100C6B00D /* info-minerva.png */; }; F569582716AA095800C45D00 /* UINavigationController+ReplaceController.m in Sources */ = {isa = PBXBuildFile; fileRef = F569582616AA095800C45D00 /* UINavigationController+ReplaceController.m */; }; F56B177116C9069300AB59D6 /* NSMutableArray+Shuffling.m in Sources */ = {isa = PBXBuildFile; fileRef = F56B177016C9069300AB59D6 /* NSMutableArray+Shuffling.m */; }; F56F139C169731FE003D4447 /* UrgentViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F56F139E169731FE003D4447 /* UrgentViewController.xib */; }; F57CB9DA15B84C4B00D87CB3 /* NSDate+Utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = F57CB9D915B84C4B00D87CB3 /* NSDate+Utilities.m */; }; - F59CEE8A16A188050038FD24 /* FacebookSession.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CEE8216A188050038FD24 /* FacebookSession.m */; }; F59D16F415BD693F00B6F892 /* info-academiccalendar.png in Resources */ = {isa = PBXBuildFile; fileRef = F59D16E415BD693F00B6F892 /* info-academiccalendar.png */; }; F59D16F515BD693F00B6F892 /* info-academiccalendar@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F59D16E515BD693F00B6F892 /* info-academiccalendar@2x.png */; }; F59D16F615BD693F00B6F892 /* info-bicycle.png in Resources */ = {isa = PBXBuildFile; fileRef = F59D16E615BD693F00B6F892 /* info-bicycle.png */; }; @@ -197,12 +277,10 @@ F5C46190166E442000874D79 /* navigation-down@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F5C4618C166E442000874D79 /* navigation-down@2x.png */; }; F5C46191166E442000874D79 /* navigation-up.png in Resources */ = {isa = PBXBuildFile; fileRef = F5C4618D166E442000874D79 /* navigation-up.png */; }; F5C46192166E442000874D79 /* navigation-up@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F5C4618E166E442000874D79 /* navigation-up@2x.png */; }; - F5C6093816D2DFCB0043BD44 /* PreferencesService.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C6093716D2DFCB0043BD44 /* PreferencesService.m */; }; F5CE522216C2BDA70033A52B /* Errors.strings in Resources */ = {isa = PBXBuildFile; fileRef = F5CE522016C2BDA70033A52B /* Errors.strings */; }; F5CFAA3D15BD9B87008E75B0 /* info-fietsen.html in Resources */ = {isa = PBXBuildFile; fileRef = F5CFAA3C15BD9B87008E75B0 /* info-fietsen.html */; }; F5D36BB715B5BD2A00B6E017 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = F5D36BB315B5BD2A00B6E017 /* Default.png */; }; F5D36BB815B5BD2A00B6E017 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F5D36BB415B5BD2A00B6E017 /* Default@2x.png */; }; - F5D36BC115B5BDA500B6E017 /* SchamperStore.m in Sources */ = {isa = PBXBuildFile; fileRef = F5D36BC015B5BDA500B6E017 /* SchamperStore.m */; }; F5D51CF716B6D44800826B51 /* info-minerva@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F5D51CF616B6D44800826B51 /* info-minerva@2x.png */; }; F5DF2FB916C41DDE003B05EC /* MarqueeLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F5DF2FB816C41DDE003B05EC /* MarqueeLabel.m */; }; F5E7F06C15EF9408004024AA /* header-bg@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F5E7F06B15EF9407004024AA /* header-bg@2x.png */; }; @@ -228,11 +306,6 @@ F5EBF11D159E092300EB5D26 /* SchamperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F5EBF11B159E092300EB5D26 /* SchamperViewController.m */; }; F5EBF13D159E13E600EB5D26 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBF13C159E13E600EB5D26 /* MobileCoreServices.framework */; }; F5EBF143159E13FD00EB5D26 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBF142159E13FD00EB5D26 /* QuartzCore.framework */; }; - F5EBF14C159E1E3600EB5D26 /* SchamperArticle.m in Sources */ = {isa = PBXBuildFile; fileRef = F5EBF14B159E1E3600EB5D26 /* SchamperArticle.m */; }; - F5F43C3E15BAD246003C2AAE /* AssociationNewsItem.m in Sources */ = {isa = PBXBuildFile; fileRef = F5F43C3D15BAD246003C2AAE /* AssociationNewsItem.m */; }; - F5F43C4115BAD2CC003C2AAE /* AssociationActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = F5F43C4015BAD2CC003C2AAE /* AssociationActivity.m */; }; - F5F43C4415BAD2EB003C2AAE /* AssociationStore.m in Sources */ = {isa = PBXBuildFile; fileRef = F5F43C4315BAD2EB003C2AAE /* AssociationStore.m */; }; - F5F43C4715BAD42E003C2AAE /* Association.m in Sources */ = {isa = PBXBuildFile; fileRef = F5F43C4615BAD42E003C2AAE /* Association.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -267,31 +340,36 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1FA3342E7471DF2C095F6AFD /* Pods_Hydra.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Hydra.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 395C117816272D8000B99F9B /* NewsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewsViewController.h; sourceTree = ""; }; 395C117916272D8000B99F9B /* NewsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewsViewController.m; sourceTree = ""; }; 395C117B1627437000B99F9B /* NewsDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewsDetailViewController.h; sourceTree = ""; }; 395C117C1627437000B99F9B /* NewsDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewsDetailViewController.m; sourceTree = ""; }; - 39B9A4055F5F374AA56058ED /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; - 3B436DF615B8915200984D3F /* InfoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InfoViewController.h; sourceTree = ""; }; - 3B436DF715B8915200984D3F /* InfoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InfoViewController.m; sourceTree = ""; }; + 3A8CEDFFE59A2DEF914797B2 /* Pods-Hydra.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Hydra.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Hydra/Pods-Hydra.debug.xcconfig"; sourceTree = ""; }; 3B93FB9E1630939200C962DC /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 3B93FBA81630969900C962DC /* UrgentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UrgentPlayer.h; sourceTree = ""; }; 3B93FBA91630969900C962DC /* UrgentPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UrgentPlayer.m; sourceTree = ""; }; 3B93FBAB16309E2100C962DC /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; 445C81F316C3DB0B0080819C /* AssociationPreferenceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AssociationPreferenceController.h; sourceTree = ""; }; - 445C81F416C3DB0B0080819C /* AssociationPreferenceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AssociationPreferenceController.m; sourceTree = ""; }; 44A29C57162DA88E001A573C /* EventKitUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EventKitUI.framework; path = System/Library/Frameworks/EventKitUI.framework; sourceTree = SDKROOT; }; + 900295E91D2EB8F8005F86E6 /* Announcement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = ""; }; + 900295EB1D2EB8F8005F86E6 /* Course.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Course.swift; sourceTree = ""; }; + 900295EC1D2EB8F8005F86E6 /* OAuthTokenInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthTokenInfo.swift; sourceTree = ""; }; + 900295ED1D2EB8F8005F86E6 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 900295F31D2EBA52005F86E6 /* UGentOAuthConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = UGentOAuthConfig.plist; sourceTree = ""; }; + 900295F51D2EBA98005F86E6 /* UGentOAuth2Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UGentOAuth2Service.swift; sourceTree = ""; }; 9004F58D1B7F2FB60041FAA6 /* resto-map-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "resto-map-icon@2x.png"; sourceTree = ""; }; 9004F58E1B7F2FB60041FAA6 /* resto-map-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "resto-map-icon.png"; sourceTree = ""; }; 901603DB1B716DED002E0D60 /* HomeNewsItemCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeNewsItemCollectionViewCell.swift; sourceTree = ""; }; 902939431A8C22EB00868221 /* info-bloklocaties.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "info-bloklocaties.html"; sourceTree = ""; }; + 902B7CAA1D5FA83000B53B98 /* HomeMinervaAnnouncementCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeMinervaAnnouncementCell.swift; sourceTree = ""; }; + 902B7CAC1D5FCC6D00B53B98 /* HomeMinervaCalendarItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeMinervaCalendarItemCell.swift; sourceTree = ""; }; + 902C9FE11D4D437E00A4BE86 /* CalendarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarItem.swift; sourceTree = ""; }; 9035BF111B6C1753008E7875 /* schamper.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = schamper.png; sourceTree = ""; }; 9035BF131B6C1F5D008E7875 /* HomeSchamperCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeSchamperCollectionViewCell.swift; sourceTree = ""; }; 9035BF151B6C37FE008E7875 /* home-zeus.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-zeus.png"; sourceTree = ""; }; 9035BF171B6CB52A008E7875 /* HomeActivityCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeActivityCollectionViewCell.swift; sourceTree = ""; }; 9035BF191B6CF663008E7875 /* LocationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = ""; }; - 903A8AF51C04F369002DAF5B /* ActivityOverviewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityOverviewCell.swift; sourceTree = ""; }; - 903A8AF71C04FBF7002DAF5B /* ActivityOverviewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ActivityOverviewCell.xib; sourceTree = ""; }; 90410B0E1848DF9100331F6C /* info-guide.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-guide.png"; sourceTree = ""; }; 90410B0F1848DF9100331F6C /* info-guide@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-guide@2x.png"; sourceTree = ""; }; 904166211B7A689500D231EF /* tabbar-urgent@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-urgent@2x.png"; sourceTree = ""; }; @@ -299,24 +377,87 @@ 904166251B7A837C00D231EF /* tabbar-settings.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-settings.png"; sourceTree = ""; }; 9041C3671B7E67B600E4A50C /* RestoMenuCollectionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoMenuCollectionCell.swift; sourceTree = ""; }; 9041C36C1B7E801B00E4A50C /* RestoMenuHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoMenuHeader.swift; sourceTree = ""; }; + 9042D8701D89AC22001F3E12 /* tabbar-sko.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko.png"; sourceTree = ""; }; + 9047505B1D64FCD00091E2EF /* MinervaCalendarDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinervaCalendarDetailViewController.swift; sourceTree = ""; }; + 904A58201CB418CE0079ED25 /* NSDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDate.swift; sourceTree = ""; }; + 904A58221CB505F00079ED25 /* HomeSpecialEventBasicCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeSpecialEventBasicCollectionViewCell.swift; sourceTree = ""; }; + 904AD4ED1C88B845008F88E8 /* FacebookEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FacebookEvent.swift; sourceTree = ""; }; + 904AD4EF1C88B858008F88E8 /* FacebookSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FacebookSession.swift; sourceTree = ""; }; 904BC46817E2014A0000FED6 /* dot-question.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "dot-question.png"; sourceTree = ""; }; 904BC46917E2014A0000FED6 /* dot-question@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "dot-question@2x.png"; sourceTree = ""; }; + 90530DBA1D2D889400A0CC8A /* PreferencesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesController.swift; sourceTree = ""; }; + 90530DBC1D2D98D300A0CC8A /* PreferencesExtraTableViewCells.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreferencesExtraTableViewCells.xib; sourceTree = ""; }; + 90530DBE1D2D9CDD00A0CC8A /* PreferenceTableViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceTableViewCells.swift; sourceTree = ""; }; + 90530DC01D2DA5AA00A0CC8A /* PreferencesSwitchTableViewCells.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreferencesSwitchTableViewCells.xib; sourceTree = ""; }; + 90530DC21D2DB30200A0CC8A /* PreferencesTextTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PreferencesTextTableViewCell.xib; sourceTree = ""; }; + 90530DC41D2DC10E00A0CC8A /* AssociationPreferenceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AssociationPreferenceController.m; sourceTree = ""; }; + 90530DC71D2E51B800A0CC8A /* PreferencesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesService.swift; sourceTree = ""; }; 905EB0121BC809C600F38679 /* tabbar-settings@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-settings@2x.png"; sourceTree = ""; }; 905EB0141BC80A3800F38679 /* tabbar-news@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-news@2x.png"; sourceTree = ""; }; 905EB0161BC80B2000F38679 /* tabbar-activities@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-activities@2x.png"; sourceTree = ""; }; + 9063FC5F1D5D079100BD01F6 /* InfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoItem.swift; sourceTree = ""; }; + 9063FC611D5E277400BD01F6 /* InfoStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoStore.swift; sourceTree = ""; }; + 9063FC631D5E2BB900BD01F6 /* InfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; + 90677EED1D91C7CE003F7135 /* tabbar-sko-student-village.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko-student-village.png"; sourceTree = ""; }; + 90677EEE1D91C7CE003F7135 /* tabbar-sko-student-village@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko-student-village@2x.png"; sourceTree = ""; }; + 90677EF11D91C853003F7135 /* tabbar-sko-lineup.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko-lineup.png"; sourceTree = ""; }; + 90677EF21D91C853003F7135 /* tabbar-sko-lineup@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko-lineup@2x.png"; sourceTree = ""; }; + 90677EF51D91C8C1003F7135 /* tabbar-sko-map@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko-map@2x.png"; sourceTree = ""; }; + 90677EF61D91C8C1003F7135 /* tabbar-sko-map.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko-map.png"; sourceTree = ""; }; + 90677EF91D92B581003F7135 /* SKOStudentVillageDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOStudentVillageDetailViewController.swift; sourceTree = ""; }; 9068BA771B7BB437005F79FA /* tabbar-resto@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-resto@2x.png"; sourceTree = ""; }; 9068BA791B7BB560005F79FA /* tabbar-home@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-home@2x.png"; sourceTree = ""; }; 9068BA7B1B7BB684005F79FA /* tabbar-info@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-info@2x.png"; sourceTree = ""; }; + 90692EE61D6399C600F607D8 /* CalendarSingleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarSingleTableViewCell.swift; sourceTree = ""; }; 906C1D8717C8A4A600145CD3 /* MapViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapViewController.h; sourceTree = ""; }; 906C1D8817C8A4A600145CD3 /* MapViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapViewController.m; sourceTree = ""; }; 907CD3C91B7E52B600DD7539 /* RestoMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoMenuViewController.swift; sourceTree = ""; }; + 908699FF1CACA038004C9FF8 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 90869A031CACA116004C9FF8 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; + 9086FD871C84E8BF0053493D /* Association.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Association.swift; sourceTree = ""; }; + 9086FD881C84E8BF0053493D /* Activity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Activity.swift; sourceTree = ""; }; + 9086FD891C84E8BF0053493D /* NewsItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsItem.swift; sourceTree = ""; }; + 9086FD8A1C84E8BF0053493D /* SchamperArticle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchamperArticle.swift; sourceTree = ""; }; + 9086FD8F1C84E9900053493D /* AssociationStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationStore.swift; sourceTree = ""; }; + 9086FD901C84E9900053493D /* SchamperStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchamperStore.swift; sourceTree = ""; }; + 9086FD911C84E9900053493D /* SavableStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SavableStore.swift; sourceTree = ""; }; + 9086FD951C84EEC50053493D /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 9086FD981C84F0750053493D /* p2_OAuth2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = p2_OAuth2.swift; sourceTree = ""; }; + 9086FD991C84F0750053493D /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 908CA4DB1A8D439C00F8C31F /* info-mapmarker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-mapmarker.png"; sourceTree = ""; }; 908CA4DC1A8D439C00F8C31F /* info-mapmarker@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-mapmarker@2x.png"; sourceTree = ""; }; 908DFF02188E932D00D526DC /* Pods-acknowledgements.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Pods-acknowledgements.plist"; sourceTree = ""; }; - 908E536419BE4F3900F1DA57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 90969AD11BFF5D8D006EDD9D /* AssociationStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationStore.swift; sourceTree = ""; }; + 908E536419BE4F3900F1DA57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Hydra/Images.xcassets; sourceTree = ""; }; + 9092D6A91D5A358F00B75CEE /* MinervaAnnounceDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinervaAnnounceDetailViewController.swift; sourceTree = ""; }; + 9093B0231D50F5EC00EB00E2 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; + 9093B0251D512BDD00EB00E2 /* Whatsnew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Whatsnew.swift; sourceTree = ""; }; 90969AD31BFF5F90006EDD9D /* RestoStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoStore.swift; sourceTree = ""; }; - 90969AD51BFF6014006EDD9D /* SchamperStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchamperStore.swift; sourceTree = ""; }; + 909E46F21D5A17770072AFEC /* tabbar-minerva.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-minerva.png"; sourceTree = ""; }; + 909E46F31D5A17770072AFEC /* tabbar-minerva@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-minerva@2x.png"; sourceTree = ""; }; + 909E46F41D5A17770072AFEC /* tabbar-minerva@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-minerva@3x.png"; sourceTree = ""; }; + 909E46F81D5A2CF80072AFEC /* minerva-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "minerva-icon.png"; sourceTree = ""; }; + 909E46F91D5A2CF90072AFEC /* minerva-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "minerva-icon@2x.png"; sourceTree = ""; }; + 909E46FA1D5A2CF90072AFEC /* minerva-icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "minerva-icon@3x.png"; sourceTree = ""; }; + 90AA4BA11D831C86003303FE /* SKOHydraTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOHydraTabBarController.swift; sourceTree = ""; }; + 90AA4BA31D831D3A003303FE /* sko.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = sko.storyboard; sourceTree = ""; }; + 90AA4BA51D831F1B003303FE /* SKOBackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOBackViewController.swift; sourceTree = ""; }; + 90AA4BA81D833347003303FE /* logo-sko.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-sko.png"; sourceTree = ""; }; + 90AA4BAA1D8335BB003303FE /* SKOTimelineCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOTimelineCollectionViewController.swift; sourceTree = ""; }; + 90AA4BAD1D83587C003303FE /* LeagueGothic-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "LeagueGothic-Regular.otf"; sourceTree = ""; }; + 90AA4BAF1D835B67003303FE /* SKOLineupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOLineupViewController.swift; sourceTree = ""; }; + 90AA4BB11D835BB0003303FE /* SKOMapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOMapViewController.swift; sourceTree = ""; }; + 90AA4BB31D835C08003303FE /* SKOStudentVillageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOStudentVillageViewController.swift; sourceTree = ""; }; + 90AA4BB51D83601B003303FE /* tabbar-hydra.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-hydra.png"; sourceTree = ""; }; + 90AA4BB61D83601B003303FE /* tabbar-hydra@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-hydra@2x.png"; sourceTree = ""; }; + 90AA4BBB1D840097003303FE /* SKOStageHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOStageHeaderCollectionReusableView.swift; sourceTree = ""; }; + 90AA4BBD1D840864003303FE /* SKOLineupStageCollectionReusableView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SKOLineupStageCollectionReusableView.xib; sourceTree = ""; }; + 90AA4BC01D840F65003303FE /* SKOLineUpCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOLineUpCollectionViewCell.swift; sourceTree = ""; }; + 90AC8E831D87347F00A16082 /* onboarding.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = onboarding.storyboard; sourceTree = ""; }; + 90AC8E851D87380B00A16082 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 90AC8E871D8745A600A16082 /* TimelineOnboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineOnboardViewController.swift; sourceTree = ""; }; + 90AC8E891D874F1500A16082 /* InitialOnboardingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialOnboardingViewController.swift; sourceTree = ""; }; + 90AC8E8B1D8751A300A16082 /* MinervaOnboardingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinervaOnboardingViewController.swift; sourceTree = ""; }; + 90AC8E8D1D87696C00A16082 /* TimeLineTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeLineTableViewCell.swift; sourceTree = ""; }; 90B010A21C305F73000DE080 /* RestoSandwich.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoSandwich.swift; sourceTree = ""; }; 90B010A41C308CB2000DE080 /* NSCalendar+HydraCalendar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSCalendar+HydraCalendar.swift"; sourceTree = ""; }; 90B1029617A85E9200669CCD /* kalender.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = kalender.css; sourceTree = ""; }; @@ -327,6 +468,8 @@ 90B1EF131B7A60B000E733C1 /* tabbar-news.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-news.png"; sourceTree = ""; }; 90B1EF151B7A60FC00E733C1 /* tabbar-schamper@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-schamper@2x.png"; sourceTree = ""; }; 90B1EF171B7A61DD00E733C1 /* tabbar-schamper.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-schamper.png"; sourceTree = ""; }; + 90B3D7C21CB1CCF200A7E25E /* SpecialEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecialEvent.swift; sourceTree = ""; }; + 90B3D7C41CB1D22400A7E25E /* SpecialEventStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecialEventStore.swift; sourceTree = ""; }; 90BAB264162749BC0043BA92 /* SchamperDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SchamperDetailViewController.h; sourceTree = ""; }; 90BAB265162749BC0043BA92 /* SchamperDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SchamperDetailViewController.m; sourceTree = ""; }; 90BE1F871B6AB3390061888C /* Hydra-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Hydra-Bridging-Header.h"; path = "Hydra/Hydra-Bridging-Header.h"; sourceTree = ""; }; @@ -334,31 +477,42 @@ 90BE1F8C1B6AB61D0061888C /* MainStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MainStoryboard.storyboard; sourceTree = ""; }; 90BE1F8E1B6AB8780061888C /* home-header.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-header.png"; sourceTree = ""; }; 90BE1F8F1B6AB8780061888C /* home-background.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-background.png"; sourceTree = ""; }; - 90C532A916A325A200857EB0 /* FacebookEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FacebookEvent.h; sourceTree = ""; }; - 90C532AA16A325A200857EB0 /* FacebookEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FacebookEvent.m; sourceTree = ""; }; + 90C288A41D47A69000231462 /* MinervaCourseAnnouncementViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinervaCourseAnnouncementViewController.swift; sourceTree = ""; }; + 90C288A61D47A6FA00231462 /* MinervaCoursePreferenceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinervaCoursePreferenceViewController.swift; sourceTree = ""; }; + 90C288A81D47AD1500231462 /* MinervaStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinervaStore.swift; sourceTree = ""; }; 90C8CA2D1B6AE3E80073E026 /* HomeFeedService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeFeedService.swift; sourceTree = ""; }; + 90CA18331D8BE62300309815 /* tabbar-sko@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tabbar-sko@2x.png"; sourceTree = ""; }; + 90CA18361D8BF33200309815 /* TimelinePost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelinePost.swift; sourceTree = ""; }; + 90CA18381D8C1BFE00309815 /* SKOTimelineCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOTimelineCollectionViewCell.swift; sourceTree = ""; }; 90CDD89C1C0A2A1300394F42 /* home-location.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-location.png"; sourceTree = ""; }; 90CDD89E1C0A2A1800394F42 /* home-location@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-location@2x.png"; sourceTree = ""; }; 90CDD8A01C0A2BE200394F42 /* home-location@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-location@3x.png"; sourceTree = ""; }; 90CDD8A21C0A2CEE00394F42 /* home-clock.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-clock.png"; sourceTree = ""; }; 90CDD8A41C0A2D0F00394F42 /* home-clock@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-clock@2x.png"; sourceTree = ""; }; + 90CDDA2E1D85CDFE00CCF8FB /* SKOStudentVillageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOStudentVillageTableViewCell.swift; sourceTree = ""; }; + 90CDDA321D85E69A00CCF8FB /* Exihibitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Exihibitor.swift; sourceTree = ""; }; + 90CEB35C1D81AAB700833477 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 90D55B9A1B7A4CE800813179 /* HydraTabbarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HydraTabbarController.swift; sourceTree = ""; }; 90DDFB781B6BB3D300DDAFA0 /* HomeRestoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeRestoCollectionViewCell.swift; sourceTree = ""; }; + 90E3F55F1C86585600343DF2 /* RestoMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoMenu.swift; sourceTree = ""; }; + 90E4BBB71D84466300582586 /* SKOStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKOStore.swift; sourceTree = ""; }; + 90E4BBBA1D8446C500582586 /* Stage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stage.swift; sourceTree = ""; }; + 90E4BBBC1D84470800582586 /* Artist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Artist.swift; sourceTree = ""; }; 90E814F11854C76C00075F96 /* ios7-Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ios7-Default-568h@2x.png"; sourceTree = ""; }; 90E814F21854C76C00075F96 /* ios7-Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ios7-Default@2x.png"; sourceTree = ""; }; 90E8DDC01B6AD45C0059F71B /* home-button-bar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "home-button-bar.png"; sourceTree = ""; }; - 90EB7BE81688ECE300A9582D /* RestoLegendItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestoLegendItem.h; sourceTree = ""; }; - 90EB7BE91688ECE300A9582D /* RestoLegendItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestoLegendItem.m; sourceTree = ""; }; 90EE4CCF1BB7193900C2DD32 /* RestoMenuInfoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoMenuInfoCollectionViewCell.swift; sourceTree = ""; }; + 90EE815D1D8C58A200EE0F4F /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 90EF4A581B6D8AE40034AD8F /* HomeUrgentCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeUrgentCollectionViewCell.swift; sourceTree = ""; }; 90F0525B168C663F004CAFFC /* RestoMapController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestoMapController.h; sourceTree = ""; }; 90F0525C168C663F004CAFFC /* RestoMapController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestoMapController.m; sourceTree = ""; }; 90F05260168C7022004CAFFC /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; 90F05262168C7726004CAFFC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; - 90F05264168C7D19004CAFFC /* RestoLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestoLocation.h; sourceTree = ""; }; - 90F05265168C7D19004CAFFC /* RestoLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestoLocation.m; sourceTree = ""; }; - E747F0E311D74638A7FC212F /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; - F2E1004E081A013343FCA87A /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 90F37DD21C8B9A1200A380F0 /* RestoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoLocation.swift; sourceTree = ""; }; + 90FFEA051D610D8900C18FDE /* CalendarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarViewController.swift; sourceTree = ""; }; + 9CC44FFCB3066F968CB87B10 /* Pods-Hydra.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Hydra.release.xcconfig"; path = "Pods/Target Support Files/Pods-Hydra/Pods-Hydra.release.xcconfig"; sourceTree = ""; }; + D8104672AF9CD02CDBDA395E /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ED20E64A1D2D809000255C57 /* UIViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; F49F792C1A1671950040987F /* RestoMenuToday.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RestoMenuToday.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F49F792D1A1671950040987F /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; F49F79311A1671950040987F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -371,8 +525,6 @@ F49F79431A1672410040987F /* RestoManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoManager.swift; sourceTree = ""; }; F49F79541A16748B0040987F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F4BE9FCF1A168C5B00BFB8F8 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - F5016BA4162756F300BBDB0A /* ActivitiesController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActivitiesController.h; sourceTree = ""; }; - F5016BA5162756F300BBDB0A /* ActivitiesController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ActivitiesController.m; sourceTree = ""; }; F5016BA71627665800BBDB0A /* ActivityDetailController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActivityDetailController.h; sourceTree = ""; }; F5016BA81627665800BBDB0A /* ActivityDetailController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ActivityDetailController.m; sourceTree = ""; }; F50527CD15BDC1E600A697F8 /* info-kalender.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "info-kalender.html"; sourceTree = ""; }; @@ -382,8 +534,6 @@ F50EE09515B5F587000F1992 /* WebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewController.m; sourceTree = ""; }; F520A2DB166E933500EE340F /* icon-calendar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-calendar.png"; sourceTree = ""; }; F520A2DF166E999400EE340F /* icon-calendar@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-calendar@2x.png"; sourceTree = ""; }; - F521D11416C53A1200B686B2 /* PreferencesController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreferencesController.h; sourceTree = ""; }; - F521D11516C53A1200B686B2 /* PreferencesController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreferencesController.m; sourceTree = ""; }; F52A8B7717CD366E00C3379C /* ActivityMapController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActivityMapController.h; sourceTree = ""; }; F52A8B7817CD366E00C3379C /* ActivityMapController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ActivityMapController.m; sourceTree = ""; }; F52B9B75168507BD000B71D6 /* NSDateFormatter+AppLocale.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDateFormatter+AppLocale.h"; sourceTree = ""; }; @@ -416,12 +566,6 @@ F54DC88B16A1A97100FD212E /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; F54DC88D16A1A97C00FD212E /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; F557EA45163431FC00635CDD /* schamper-bg@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "schamper-bg@2x.png"; sourceTree = ""; }; - F55895C215B60F450001B399 /* RestoMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestoMenu.h; sourceTree = ""; }; - F55895C315B60F450001B399 /* RestoMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestoMenu.m; sourceTree = ""; }; - F55895C515B6100C0001B399 /* RestoStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RestoStore.h; sourceTree = ""; }; - F55895C615B610130001B399 /* RestoStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RestoStore.m; sourceTree = ""; }; - F55E89EA16A4AF8E008595CB /* ShareKitConfigurator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareKitConfigurator.h; sourceTree = ""; }; - F55E89EB16A4AF8E008595CB /* ShareKitConfigurator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareKitConfigurator.m; sourceTree = ""; }; F567044316BD5D8100C6B00D /* info-minerva.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-minerva.png"; sourceTree = ""; }; F569582516AA095800C45D00 /* UINavigationController+ReplaceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationController+ReplaceController.h"; sourceTree = ""; }; F569582616AA095800C45D00 /* UINavigationController+ReplaceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationController+ReplaceController.m"; sourceTree = ""; }; @@ -430,8 +574,6 @@ F56F139D169731FE003D4447 /* nl */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = nl; path = nl.lproj/UrgentViewController.xib; sourceTree = ""; }; F57CB9D815B84C4B00D87CB3 /* NSDate+Utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Utilities.h"; sourceTree = ""; }; F57CB9D915B84C4B00D87CB3 /* NSDate+Utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Utilities.m"; sourceTree = ""; }; - F59CEE8116A188050038FD24 /* FacebookSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FacebookSession.h; sourceTree = ""; }; - F59CEE8216A188050038FD24 /* FacebookSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FacebookSession.m; sourceTree = ""; }; F59D16E415BD693F00B6F892 /* info-academiccalendar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-academiccalendar.png"; sourceTree = ""; }; F59D16E515BD693F00B6F892 /* info-academiccalendar@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-academiccalendar@2x.png"; sourceTree = ""; }; F59D16E615BD693F00B6F892 /* info-bicycle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-bicycle.png"; sourceTree = ""; }; @@ -491,14 +633,10 @@ F5C4618C166E442000874D79 /* navigation-down@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "navigation-down@2x.png"; sourceTree = ""; }; F5C4618D166E442000874D79 /* navigation-up.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "navigation-up.png"; sourceTree = ""; }; F5C4618E166E442000874D79 /* navigation-up@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "navigation-up@2x.png"; sourceTree = ""; }; - F5C6093616D2DFCB0043BD44 /* PreferencesService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreferencesService.h; sourceTree = ""; }; - F5C6093716D2DFCB0043BD44 /* PreferencesService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreferencesService.m; sourceTree = ""; }; F5CE522116C2BDA70033A52B /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Hydra/nl.lproj/Errors.strings; sourceTree = ""; }; F5CFAA3C15BD9B87008E75B0 /* info-fietsen.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "info-fietsen.html"; sourceTree = ""; }; F5D36BB315B5BD2A00B6E017 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; F5D36BB415B5BD2A00B6E017 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; - F5D36BBF15B5BDA500B6E017 /* SchamperStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SchamperStore.h; sourceTree = ""; }; - F5D36BC015B5BDA500B6E017 /* SchamperStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SchamperStore.m; sourceTree = ""; }; F5D36BD815B5CD1500B6E017 /* Hydra-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Hydra-Prefix.pch"; path = "Hydra/Hydra-Prefix.pch"; sourceTree = ""; }; F5D51CF616B6D44800826B51 /* info-minerva@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "info-minerva@2x.png"; sourceTree = ""; }; F5DF2FB716C41DDE003B05EC /* MarqueeLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MarqueeLabel.h; sourceTree = ""; }; @@ -528,16 +666,6 @@ F5EBF11B159E092300EB5D26 /* SchamperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SchamperViewController.m; sourceTree = ""; }; F5EBF13C159E13E600EB5D26 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; F5EBF142159E13FD00EB5D26 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; - F5EBF14A159E1E3600EB5D26 /* SchamperArticle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SchamperArticle.h; sourceTree = ""; }; - F5EBF14B159E1E3600EB5D26 /* SchamperArticle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SchamperArticle.m; sourceTree = ""; }; - F5F43C3C15BAD246003C2AAE /* AssociationNewsItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AssociationNewsItem.h; sourceTree = ""; }; - F5F43C3D15BAD246003C2AAE /* AssociationNewsItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AssociationNewsItem.m; sourceTree = ""; }; - F5F43C3F15BAD2CC003C2AAE /* AssociationActivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AssociationActivity.h; sourceTree = ""; }; - F5F43C4015BAD2CC003C2AAE /* AssociationActivity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AssociationActivity.m; sourceTree = ""; }; - F5F43C4215BAD2EB003C2AAE /* AssociationStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AssociationStore.h; sourceTree = ""; }; - F5F43C4315BAD2EB003C2AAE /* AssociationStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AssociationStore.m; sourceTree = ""; }; - F5F43C4515BAD42E003C2AAE /* Association.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Association.h; sourceTree = ""; }; - F5F43C4615BAD42E003C2AAE /* Association.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Association.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -571,7 +699,7 @@ F5EBF13D159E13E600EB5D26 /* MobileCoreServices.framework in Frameworks */, F5EBF143159E13FD00EB5D26 /* QuartzCore.framework in Frameworks */, F5BAEC8B1518868200F6A1B1 /* UIKit.framework in Frameworks */, - B50ED58F220549D98F92E254 /* libPods.a in Frameworks */, + 3FAEE3F08BA5B665F18C8B3D /* Pods_Hydra.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -588,6 +716,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 389B4F95A3FF8290C34C4FDE /* Pods */ = { + isa = PBXGroup; + children = ( + 3A8CEDFFE59A2DEF914797B2 /* Pods-Hydra.debug.xcconfig */, + 9CC44FFCB3066F968CB87B10 /* Pods-Hydra.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; 3B059BF215B99B4800B4C1DA /* Custom views */ = { isa = PBXGroup; children = ( @@ -614,25 +751,125 @@ 3B93FBA01630942500C962DC /* Services */ = { isa = PBXGroup; children = ( - F5F43C4215BAD2EB003C2AAE /* AssociationStore.h */, - F5F43C4315BAD2EB003C2AAE /* AssociationStore.m */, - 90969AD11BFF5D8D006EDD9D /* AssociationStore.swift */, + 9086FD8F1C84E9900053493D /* AssociationStore.swift */, 90C8CA2D1B6AE3E80073E026 /* HomeFeedService.swift */, + 9063FC611D5E277400BD01F6 /* InfoStore.swift */, 9035BF191B6CF663008E7875 /* LocationService.swift */, - F5C6093616D2DFCB0043BD44 /* PreferencesService.h */, - F5C6093716D2DFCB0043BD44 /* PreferencesService.m */, - F55895C515B6100C0001B399 /* RestoStore.h */, - F55895C615B610130001B399 /* RestoStore.m */, + 90C288A81D47AD1500231462 /* MinervaStore.swift */, + 90530DC71D2E51B800A0CC8A /* PreferencesService.swift */, 90969AD31BFF5F90006EDD9D /* RestoStore.swift */, - F5D36BBF15B5BDA500B6E017 /* SchamperStore.h */, - F5D36BC015B5BDA500B6E017 /* SchamperStore.m */, - 90969AD51BFF6014006EDD9D /* SchamperStore.swift */, + 9086FD911C84E9900053493D /* SavableStore.swift */, + 9086FD901C84E9900053493D /* SchamperStore.swift */, + 90B3D7C41CB1D22400A7E25E /* SpecialEventStore.swift */, + 900295F51D2EBA98005F86E6 /* UGentOAuth2Service.swift */, 3B93FBA81630969900C962DC /* UrgentPlayer.h */, 3B93FBA91630969900C962DC /* UrgentPlayer.m */, + 90CEB35C1D81AAB700833477 /* NotificationService.swift */, + 90E4BBB71D84466300582586 /* SKOStore.swift */, ); name = Services; sourceTree = ""; }; + 900295E81D2EB895005F86E6 /* Minerva */ = { + isa = PBXGroup; + children = ( + 900295E91D2EB8F8005F86E6 /* Announcement.swift */, + 900295EB1D2EB8F8005F86E6 /* Course.swift */, + 900295EC1D2EB8F8005F86E6 /* OAuthTokenInfo.swift */, + 900295ED1D2EB8F8005F86E6 /* User.swift */, + 902C9FE11D4D437E00A4BE86 /* CalendarItem.swift */, + 9093B0251D512BDD00EB00E2 /* Whatsnew.swift */, + ); + name = Minerva; + sourceTree = ""; + }; + 9063FC5D1D5D06CD00BD01F6 /* Associations */ = { + isa = PBXGroup; + children = ( + 9086FD881C84E8BF0053493D /* Activity.swift */, + 9086FD871C84E8BF0053493D /* Association.swift */, + 9086FD891C84E8BF0053493D /* NewsItem.swift */, + ); + name = Associations; + sourceTree = ""; + }; + 9063FC5E1D5D06ED00BD01F6 /* Resto */ = { + isa = PBXGroup; + children = ( + 90F37DD21C8B9A1200A380F0 /* RestoLocation.swift */, + 90E3F55F1C86585600343DF2 /* RestoMenu.swift */, + 90B010A21C305F73000DE080 /* RestoSandwich.swift */, + ); + name = Resto; + sourceTree = ""; + }; + 9086FD971C84F0510053493D /* Extensions */ = { + isa = PBXGroup; + children = ( + 90B010A41C308CB2000DE080 /* NSCalendar+HydraCalendar.swift */, + 9086FD981C84F0750053493D /* p2_OAuth2.swift */, + 9086FD991C84F0750053493D /* String.swift */, + 90869A031CACA116004C9FF8 /* UIViewController.swift */, + 904A58201CB418CE0079ED25 /* NSDate.swift */, + ED20E64A1D2D809000255C57 /* UIViewExtension.swift */, + 9093B0231D50F5EC00EB00E2 /* UIColor.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 90AA4BA01D831A6C003303FE /* SKO */ = { + isa = PBXGroup; + children = ( + 90CA18351D8BF03300309815 /* TimeLine */, + 90AA4BBF1D840D7B003303FE /* LineUp */, + 90CDDA2D1D85CDD500CCF8FB /* StudentVillage */, + 90AA4BA11D831C86003303FE /* SKOHydraTabBarController.swift */, + 90AA4BA31D831D3A003303FE /* sko.storyboard */, + 90AA4BA51D831F1B003303FE /* SKOBackViewController.swift */, + 90AA4BB11D835BB0003303FE /* SKOMapViewController.swift */, + ); + name = SKO; + sourceTree = ""; + }; + 90AA4BA71D83333C003303FE /* sko */ = { + isa = PBXGroup; + children = ( + 90AA4BA81D833347003303FE /* logo-sko.png */, + ); + name = sko; + sourceTree = ""; + }; + 90AA4BAC1D835868003303FE /* Fonts */ = { + isa = PBXGroup; + children = ( + 90AA4BAD1D83587C003303FE /* LeagueGothic-Regular.otf */, + ); + name = Fonts; + sourceTree = ""; + }; + 90AA4BBF1D840D7B003303FE /* LineUp */ = { + isa = PBXGroup; + children = ( + 90AA4BAF1D835B67003303FE /* SKOLineupViewController.swift */, + 90AA4BBB1D840097003303FE /* SKOStageHeaderCollectionReusableView.swift */, + 90AA4BBD1D840864003303FE /* SKOLineupStageCollectionReusableView.xib */, + 90AA4BC01D840F65003303FE /* SKOLineUpCollectionViewCell.swift */, + ); + name = LineUp; + sourceTree = ""; + }; + 90AC8E821D87345400A16082 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 90AC8E831D87347F00A16082 /* onboarding.storyboard */, + 90AC8E871D8745A600A16082 /* TimelineOnboardViewController.swift */, + 90AC8E891D874F1500A16082 /* InitialOnboardingViewController.swift */, + 90AC8E8B1D8751A300A16082 /* MinervaOnboardingViewController.swift */, + 90AC8E8D1D87696C00A16082 /* TimeLineTableViewCell.swift */, + ); + name = Onboarding; + sourceTree = ""; + }; 90BE1F861B6AB30F0061888C /* Home */ = { isa = PBXGroup; children = ( @@ -641,25 +878,61 @@ name = Home; sourceTree = ""; }; + 90C288A31D47A66300231462 /* Minerva */ = { + isa = PBXGroup; + children = ( + 90C288A41D47A69000231462 /* MinervaCourseAnnouncementViewController.swift */, + 9092D6A91D5A358F00B75CEE /* MinervaAnnounceDetailViewController.swift */, + 90FFEA051D610D8900C18FDE /* CalendarViewController.swift */, + 90692EE61D6399C600F607D8 /* CalendarSingleTableViewCell.swift */, + 9047505B1D64FCD00091E2EF /* MinervaCalendarDetailViewController.swift */, + ); + name = Minerva; + sourceTree = ""; + }; + 90CA18351D8BF03300309815 /* TimeLine */ = { + isa = PBXGroup; + children = ( + 90AA4BAA1D8335BB003303FE /* SKOTimelineCollectionViewController.swift */, + 90CA18381D8C1BFE00309815 /* SKOTimelineCollectionViewCell.swift */, + ); + name = TimeLine; + sourceTree = ""; + }; + 90CDDA2D1D85CDD500CCF8FB /* StudentVillage */ = { + isa = PBXGroup; + children = ( + 90AA4BB31D835C08003303FE /* SKOStudentVillageViewController.swift */, + 90CDDA2E1D85CDFE00CCF8FB /* SKOStudentVillageTableViewCell.swift */, + 90677EF91D92B581003F7135 /* SKOStudentVillageDetailViewController.swift */, + ); + name = StudentVillage; + sourceTree = ""; + }; 90DDFB7B1B6BB40100DDAFA0 /* Home */ = { isa = PBXGroup; children = ( 9035BF171B6CB52A008E7875 /* HomeActivityCollectionViewCell.swift */, + 901603DB1B716DED002E0D60 /* HomeNewsItemCollectionViewCell.swift */, 90DDFB781B6BB3D300DDAFA0 /* HomeRestoCollectionViewCell.swift */, 9035BF131B6C1F5D008E7875 /* HomeSchamperCollectionViewCell.swift */, + 904A58221CB505F00079ED25 /* HomeSpecialEventBasicCollectionViewCell.swift */, 90EF4A581B6D8AE40034AD8F /* HomeUrgentCollectionViewCell.swift */, - 901603DB1B716DED002E0D60 /* HomeNewsItemCollectionViewCell.swift */, + 902B7CAA1D5FA83000B53B98 /* HomeMinervaAnnouncementCell.swift */, + 902B7CAC1D5FCC6D00B53B98 /* HomeMinervaCalendarItemCell.swift */, ); name = Home; sourceTree = ""; }; - B007FEAAB912E2F80EF4FE3C /* Pods */ = { + 90E4BBB91D8446B000582586 /* SKO */ = { isa = PBXGroup; children = ( - F2E1004E081A013343FCA87A /* Pods.debug.xcconfig */, - 39B9A4055F5F374AA56058ED /* Pods.release.xcconfig */, + 90E4BBBA1D8446C500582586 /* Stage.swift */, + 90E4BBBC1D84470800582586 /* Artist.swift */, + 90CDDA321D85E69A00CCF8FB /* Exihibitor.swift */, + 90CA18361D8BF33200309815 /* TimelinePost.swift */, ); - name = Pods; + name = SKO; sourceTree = ""; }; F49F792F1A1671950040987F /* RestoMenuToday */ = { @@ -723,9 +996,13 @@ isa = PBXGroup; children = ( 445C81F316C3DB0B0080819C /* AssociationPreferenceController.h */, - 445C81F416C3DB0B0080819C /* AssociationPreferenceController.m */, - F521D11416C53A1200B686B2 /* PreferencesController.h */, - F521D11516C53A1200B686B2 /* PreferencesController.m */, + 90530DC41D2DC10E00A0CC8A /* AssociationPreferenceController.m */, + 90530DBA1D2D889400A0CC8A /* PreferencesController.swift */, + 90530DBC1D2D98D300A0CC8A /* PreferencesExtraTableViewCells.xib */, + 90530DC01D2DA5AA00A0CC8A /* PreferencesSwitchTableViewCells.xib */, + 90530DC21D2DB30200A0CC8A /* PreferencesTextTableViewCell.xib */, + 90530DBE1D2D9CDD00A0CC8A /* PreferenceTableViewCells.swift */, + 90C288A61D47A6FA00231462 /* MinervaCoursePreferenceViewController.swift */, ); name = Preferences; sourceTree = ""; @@ -733,14 +1010,10 @@ F536359C16A69C86000A50C4 /* Activities */ = { isa = PBXGroup; children = ( - F5016BA4162756F300BBDB0A /* ActivitiesController.h */, - F5016BA5162756F300BBDB0A /* ActivitiesController.m */, F5016BA71627665800BBDB0A /* ActivityDetailController.h */, F5016BA81627665800BBDB0A /* ActivityDetailController.m */, F52A8B7717CD366E00C3379C /* ActivityMapController.h */, F52A8B7817CD366E00C3379C /* ActivityMapController.m */, - 903A8AF51C04F369002DAF5B /* ActivityOverviewCell.swift */, - 903A8AF71C04FBF7002DAF5B /* ActivityOverviewCell.xib */, ); name = Activities; sourceTree = ""; @@ -839,6 +1112,9 @@ F59D16ED15BD693F00B6F892 /* info-more@2x.png */, F59D16EE15BD693F00B6F892 /* info-sports.png */, F59D16EF15BD693F00B6F892 /* info-sports@2x.png */, + 909E46F81D5A2CF80072AFEC /* minerva-icon.png */, + 909E46F91D5A2CF90072AFEC /* minerva-icon@2x.png */, + 909E46FA1D5A2CF90072AFEC /* minerva-icon@3x.png */, F5C4618B166E442000874D79 /* navigation-down.png */, F5C4618C166E442000874D79 /* navigation-down@2x.png */, F5C4618D166E442000874D79 /* navigation-up.png */, @@ -850,12 +1126,18 @@ F533DABB163163E2001269A8 /* schamper-bg.png */, F557EA45163431FC00635CDD /* schamper-bg@2x.png */, 9035BF111B6C1753008E7875 /* schamper.png */, + 90AA4BA71D83333C003303FE /* sko */, 90B1EF111B7A606E00E733C1 /* tabbar-activities.png */, 905EB0161BC80B2000F38679 /* tabbar-activities@2x.png */, 90B1EF0B1B7A5DC300E733C1 /* tabbar-home.png */, 9068BA791B7BB560005F79FA /* tabbar-home@2x.png */, + 90AA4BB51D83601B003303FE /* tabbar-hydra.png */, + 90AA4BB61D83601B003303FE /* tabbar-hydra@2x.png */, 90B1EF0F1B7A5FDB00E733C1 /* tabbar-info.png */, 9068BA7B1B7BB684005F79FA /* tabbar-info@2x.png */, + 909E46F21D5A17770072AFEC /* tabbar-minerva.png */, + 909E46F31D5A17770072AFEC /* tabbar-minerva@2x.png */, + 909E46F41D5A17770072AFEC /* tabbar-minerva@3x.png */, 90B1EF131B7A60B000E733C1 /* tabbar-news.png */, 905EB0141BC80A3800F38679 /* tabbar-news@2x.png */, 90B1EF0D1B7A5EF000E733C1 /* tabbar-resto.png */, @@ -864,6 +1146,14 @@ 90B1EF151B7A60FC00E733C1 /* tabbar-schamper@2x.png */, 904166251B7A837C00D231EF /* tabbar-settings.png */, 905EB0121BC809C600F38679 /* tabbar-settings@2x.png */, + 90677EF11D91C853003F7135 /* tabbar-sko-lineup.png */, + 90677EF21D91C853003F7135 /* tabbar-sko-lineup@2x.png */, + 90677EF61D91C8C1003F7135 /* tabbar-sko-map.png */, + 90677EF51D91C8C1003F7135 /* tabbar-sko-map@2x.png */, + 90677EED1D91C7CE003F7135 /* tabbar-sko-student-village.png */, + 90677EEE1D91C7CE003F7135 /* tabbar-sko-student-village@2x.png */, + 9042D8701D89AC22001F3E12 /* tabbar-sko.png */, + 90CA18331D8BE62300309815 /* tabbar-sko@2x.png */, 904166221B7A689500D231EF /* tabbar-urgent.png */, 904166211B7A689500D231EF /* tabbar-urgent@2x.png */, F5E851CE16C44B47003DF993 /* urgent-bg.jpg */, @@ -905,7 +1195,6 @@ F57CB9DB15B84C4E00D87CB3 /* Categories */ = { isa = PBXGroup; children = ( - 90B010A41C308CB2000DE080 /* NSCalendar+HydraCalendar.swift */, F57CB9D815B84C4B00D87CB3 /* NSDate+Utilities.h */, F57CB9D915B84C4B00D87CB3 /* NSDate+Utilities.m */, F52B9B75168507BD000B71D6 /* NSDateFormatter+AppLocale.h */, @@ -923,12 +1212,8 @@ F59CEE9716A189290038FD24 /* Social */ = { isa = PBXGroup; children = ( - 90C532A916A325A200857EB0 /* FacebookEvent.h */, - 90C532AA16A325A200857EB0 /* FacebookEvent.m */, - F59CEE8116A188050038FD24 /* FacebookSession.h */, - F59CEE8216A188050038FD24 /* FacebookSession.m */, - F55E89EA16A4AF8E008595CB /* ShareKitConfigurator.h */, - F55E89EB16A4AF8E008595CB /* ShareKitConfigurator.m */, + 904AD4ED1C88B845008F88E8 /* FacebookEvent.swift */, + 904AD4EF1C88B858008F88E8 /* FacebookSession.swift */, ); name = Social; sourceTree = ""; @@ -961,7 +1246,7 @@ F5CFDF851631597E008CBF98 /* External */, F5BAEC891518868200F6A1B1 /* Frameworks */, F5BAEC871518868200F6A1B1 /* Products */, - B007FEAAB912E2F80EF4FE3C /* Pods */, + 389B4F95A3FF8290C34C4FDE /* Pods */, ); sourceTree = ""; }; @@ -978,7 +1263,6 @@ F5BAEC891518868200F6A1B1 /* Frameworks */ = { isa = PBXGroup; children = ( - E747F0E311D74638A7FC212F /* libPods.a */, F54DC88D16A1A97C00FD212E /* libxml2.dylib */, 3B93FB9E1630939200C962DC /* AudioToolbox.framework */, 3B93FBAB16309E2100C962DC /* AVFoundation.framework */, @@ -998,6 +1282,8 @@ F54DC88B16A1A97100FD212E /* SystemConfiguration.framework */, F5BAEC8A1518868200F6A1B1 /* UIKit.framework */, F49F792D1A1671950040987F /* NotificationCenter.framework */, + D8104672AF9CD02CDBDA395E /* Pods.framework */, + 1FA3342E7471DF2C095F6AFD /* Pods_Hydra.framework */, ); name = Frameworks; sourceTree = ""; @@ -1010,8 +1296,9 @@ F53A5E8216700B28009EA4CC /* ApplicationWithRemoteSupport.h */, F53A5E8316700B28009EA4CC /* ApplicationWithRemoteSupport.m */, F57CB9DB15B84C4E00D87CB3 /* Categories */, + 9086FD951C84EEC50053493D /* Config.swift */, F5EBF119159E04B500EB5D26 /* Controllers */, - 908E536419BE4F3900F1DA57 /* Images.xcassets */, + 9086FD971C84F0510053493D /* Extensions */, F5EBF11F159E09C700EB5D26 /* Models */, 3B93FBA01630942500C962DC /* Services */, F59CEE9716A189290038FD24 /* Social */, @@ -1022,19 +1309,25 @@ F5BAEC911518868200F6A1B1 /* Resources */ = { isa = PBXGroup; children = ( - F5BAEC961518868200F6A1B1 /* main.m */, F5BC3D8D162EBB5100E4A902 /* Default-568h@2x.png */, F5D36BB315B5BD2A00B6E017 /* Default.png */, F5D36BB415B5BD2A00B6E017 /* Default@2x.png */, - 90E814F11854C76C00075F96 /* ios7-Default-568h@2x.png */, - 90E814F21854C76C00075F96 /* ios7-Default@2x.png */, + F5CE522016C2BDA70033A52B /* Errors.strings */, + 90AA4BAC1D835868003303FE /* Fonts */, + 908699FF1CACA038004C9FF8 /* GoogleService-Info.plist */, + 90BE1F871B6AB3390061888C /* Hydra-Bridging-Header.h */, F5BAEC921518868200F6A1B1 /* Hydra-Info.plist */, F5D36BD815B5CD1500B6E017 /* Hydra-Prefix.pch */, - 90BE1F871B6AB3390061888C /* Hydra-Bridging-Header.h */, - F5CE522016C2BDA70033A52B /* Errors.strings */, F56D76701699ED4D002CF245 /* Icons */, F53D2488159DB4DB00F408AB /* Images */, + 908E536419BE4F3900F1DA57 /* Images.xcassets */, + 90E814F11854C76C00075F96 /* ios7-Default-568h@2x.png */, + 90E814F21854C76C00075F96 /* ios7-Default@2x.png */, + F5BAEC961518868200F6A1B1 /* main.m */, F59D170615BD7CE500B6F892 /* Resources */, + 900295F31D2EBA52005F86E6 /* UGentOAuthConfig.plist */, + 90AC8E851D87380B00A16082 /* Settings.bundle */, + 90EE815D1D8C58A200EE0F4F /* LaunchScreen.storyboard */, ); name = Resources; sourceTree = ""; @@ -1073,20 +1366,22 @@ isa = PBXGroup; children = ( F536359C16A69C86000A50C4 /* Activities */, + 3B059BF215B99B4800B4C1DA /* Custom views */, 90BE1F861B6AB30F0061888C /* Home */, 90D55B9A1B7A4CE800813179 /* HydraTabbarController.swift */, - 3B436DF615B8915200984D3F /* InfoViewController.h */, - 3B436DF715B8915200984D3F /* InfoViewController.m */, + 9063FC631D5E2BB900BD01F6 /* InfoViewController.swift */, + 90BE1F8C1B6AB61D0061888C /* MainStoryboard.storyboard */, + 90C288A31D47A66300231462 /* Minerva */, F536359E16A69C94000A50C4 /* News */, + 90AC8E821D87345400A16082 /* Onboarding */, F521D11316C5386F00B686B2 /* Preferences */, 3B7AD3B615BC82EB0026BB62 /* Resto */, F536359F16A69C9D000A50C4 /* Schamper */, F55CD7FC17CD2296006C5408 /* Shared */, + 90AA4BA01D831A6C003303FE /* SKO */, F53A5E7416700242009EA4CC /* UrgentViewController.h */, F53A5E7516700242009EA4CC /* UrgentViewController.m */, F56F139E169731FE003D4447 /* UrgentViewController.xib */, - 90BE1F8C1B6AB61D0061888C /* MainStoryboard.storyboard */, - 3B059BF215B99B4800B4C1DA /* Custom views */, ); name = Controllers; sourceTree = ""; @@ -1094,21 +1389,13 @@ F5EBF11F159E09C700EB5D26 /* Models */ = { isa = PBXGroup; children = ( - F5F43C4515BAD42E003C2AAE /* Association.h */, - F5F43C4615BAD42E003C2AAE /* Association.m */, - F5F43C3F15BAD2CC003C2AAE /* AssociationActivity.h */, - F5F43C4015BAD2CC003C2AAE /* AssociationActivity.m */, - F5F43C3C15BAD246003C2AAE /* AssociationNewsItem.h */, - F5F43C3D15BAD246003C2AAE /* AssociationNewsItem.m */, - 90EB7BE81688ECE300A9582D /* RestoLegendItem.h */, - 90EB7BE91688ECE300A9582D /* RestoLegendItem.m */, - 90F05264168C7D19004CAFFC /* RestoLocation.h */, - 90F05265168C7D19004CAFFC /* RestoLocation.m */, - F55895C215B60F450001B399 /* RestoMenu.h */, - F55895C315B60F450001B399 /* RestoMenu.m */, - 90B010A21C305F73000DE080 /* RestoSandwich.swift */, - F5EBF14A159E1E3600EB5D26 /* SchamperArticle.h */, - F5EBF14B159E1E3600EB5D26 /* SchamperArticle.m */, + 9063FC5D1D5D06CD00BD01F6 /* Associations */, + 9063FC5F1D5D079100BD01F6 /* InfoItem.swift */, + 900295E81D2EB895005F86E6 /* Minerva */, + 9063FC5E1D5D06ED00BD01F6 /* Resto */, + 9086FD8A1C84E8BF0053493D /* SchamperArticle.swift */, + 90E4BBB91D8446B000582586 /* SKO */, + 90B3D7C21CB1CCF200A7E25E /* SpecialEvent.swift */, ); name = Models; sourceTree = ""; @@ -1138,13 +1425,14 @@ isa = PBXNativeTarget; buildConfigurationList = F5BAECBF1518868200F6A1B1 /* Build configuration list for PBXNativeTarget "Hydra" */; buildPhases = ( + F894F1D6500056E00817771A /* Check Pods Manifest.lock */, F5BAEC821518868200F6A1B1 /* Sources */, F5BAEC831518868200F6A1B1 /* Frameworks */, F5D3B621162EFBD400698C3E /* ShellScript */, F5BAEC841518868200F6A1B1 /* Resources */, - 7DF2C18BBE7D41B8B3A020A6 /* Copy Pods Resources */, F49F793C1A1671960040987F /* Embed App Extensions */, - 508E1E6ECE6AEAB9A96A68C3 /* Embed Pods Frameworks */, + 160F8BF902D95BA643DFF470 /* Embed Pods Frameworks */, + 1D54761D21BFBF2D308C5D54 /* Copy Pods Resources */, ); buildRules = ( ); @@ -1182,7 +1470,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0500; + LastUpgradeCheck = 0730; ORGANIZATIONNAME = "Zeus WPI"; TargetAttributes = { F49F792B1A1671950040987F = { @@ -1195,7 +1483,7 @@ }; }; buildConfigurationList = F5BAEC801518868100F6A1B1 /* Build configuration list for PBXProject "Hydra" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 6.3"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( @@ -1220,6 +1508,7 @@ buildActionMask = 2147483647; files = ( 9015D0FA1A8ABA4B00228ADC /* Images.xcassets in Resources */, + 90869A011CACA04D004C9FF8 /* GoogleService-Info.plist in Resources */, F49F79461A1672410040987F /* MainInterface.storyboard in Resources */, F49F79531A16748B0040987F /* Localizable.strings in Resources */, ); @@ -1229,6 +1518,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 90AA4BAE1D835919003303FE /* LeagueGothic-Regular.otf in Resources */, F53D248A159DB50800F408AB /* header-bg.png in Resources */, 90CDD89F1C0A2A1800394F42 /* home-location@2x.png in Resources */, 908CA4DD1A8D439C00F8C31F /* info-mapmarker.png in Resources */, @@ -1236,29 +1526,39 @@ 905EB0131BC809C600F38679 /* tabbar-settings@2x.png in Resources */, 90BE1F8D1B6AB61D0061888C /* MainStoryboard.storyboard in Resources */, F5D36BB715B5BD2A00B6E017 /* Default.png in Resources */, + 90677EF31D91C853003F7135 /* tabbar-sko-lineup.png in Resources */, F5D36BB815B5BD2A00B6E017 /* Default@2x.png in Resources */, + 90677EF81D91C8C1003F7135 /* tabbar-sko-map.png in Resources */, 904166261B7A837C00D231EF /* tabbar-settings.png in Resources */, F54B160215B7FFE6000A4407 /* resto-logo.png in Resources */, F54B160315B7FFE6000A4407 /* resto-logo@2x.png in Resources */, F5B9F2F117CEAA040042C8A4 /* Icon-Small-iOS6@2x.png in Resources */, - 903A8AF81C04FBF7002DAF5B /* ActivityOverviewCell.xib in Resources */, 90CDD89D1C0A2A1300394F42 /* home-location.png in Resources */, + 909E46FC1D5A2CF90072AFEC /* minerva-icon@2x.png in Resources */, 904BC46C17E2014A0000FED6 /* dot-question.png in Resources */, + 90677EF71D91C8C1003F7135 /* tabbar-sko-map@2x.png in Resources */, F59D16F415BD693F00B6F892 /* info-academiccalendar.png in Resources */, + 90677EF01D91C7CE003F7135 /* tabbar-sko-student-village@2x.png in Resources */, 90B1EF141B7A60B000E733C1 /* tabbar-news.png in Resources */, F59D16F515BD693F00B6F892 /* info-academiccalendar@2x.png in Resources */, 904BC46D17E2014A0000FED6 /* dot-question@2x.png in Resources */, F59D16F615BD693F00B6F892 /* info-bicycle.png in Resources */, F59D16F715BD693F00B6F892 /* info-bicycle@2x.png in Resources */, + 90AC8E861D87380B00A16082 /* Settings.bundle in Resources */, F59D16FA15BD693F00B6F892 /* info-library.png in Resources */, + 909E46FD1D5A2CF90072AFEC /* minerva-icon@3x.png in Resources */, + 909E46FB1D5A2CF90072AFEC /* minerva-icon.png in Resources */, F59D16FB15BD693F00B6F892 /* info-library@2x.png in Resources */, F59D16FC15BD693F00B6F892 /* info-more.png in Resources */, F59D16FD15BD693F00B6F892 /* info-more@2x.png in Resources */, F59D16FE15BD693F00B6F892 /* info-sports.png in Resources */, 908DFF03188E932D00D526DC /* Pods-acknowledgements.plist in Resources */, + 9042D8711D89AC22001F3E12 /* tabbar-sko.png in Resources */, F59D16FF15BD693F00B6F892 /* info-sports@2x.png in Resources */, F59D170015BD693F00B6F892 /* info-doctors.png in Resources */, F59D170115BD693F00B6F892 /* info-doctors@2x.png in Resources */, + 909E46F71D5A17770072AFEC /* tabbar-minerva@3x.png in Resources */, + 90CA18341D8BE62300309815 /* tabbar-sko@2x.png in Resources */, F59D170915BD7CE500B6F892 /* info-content.plist in Resources */, F59D170A15BD7CE500B6F892 /* info-sport-openingsuren.html in Resources */, F5B9F2F017CEAA040042C8A4 /* Icon-Small-iOS6.png in Resources */, @@ -1268,6 +1568,7 @@ F50527CE15BDC1E600A697F8 /* info-kalender.html in Resources */, F50527D015BDC5F600A697F8 /* info-studentenartsen.html in Resources */, 90E814F31854C76C00075F96 /* ios7-Default-568h@2x.png in Resources */, + 90AA4BA91D833347003303FE /* logo-sko.png in Resources */, 90410B101848DF9100331F6C /* info-guide.png in Resources */, F5A81CBE15CF1BA700FE033B /* external-link.png in Resources */, F5A81CC215CF1CC500FE033B /* external-link-active.png in Resources */, @@ -1276,8 +1577,12 @@ F5A81CC415CF1CC500FE033B /* external-link@2x.png in Resources */, F5E7F06C15EF9408004024AA /* header-bg@2x.png in Resources */, 90B1EF101B7A5FDB00E733C1 /* tabbar-info.png in Resources */, + 90AA4BA41D831D3A003303FE /* sko.storyboard in Resources */, + 90AA4BB81D83601B003303FE /* tabbar-hydra@2x.png in Resources */, + 90677EF41D91C853003F7135 /* tabbar-sko-lineup@2x.png in Resources */, 90B1EF0C1B7A5DC300E733C1 /* tabbar-home.png in Resources */, 90CDD8A51C0A2D0F00394F42 /* home-clock@2x.png in Resources */, + 90AA4BBE1D840864003303FE /* SKOLineupStageCollectionReusableView.xib in Resources */, 905EB0171BC80B2100F38679 /* tabbar-activities@2x.png in Resources */, 9068BA7C1B7BB684005F79FA /* tabbar-info@2x.png in Resources */, 90B1EF181B7A61DD00E733C1 /* tabbar-schamper.png in Resources */, @@ -1291,6 +1596,8 @@ F557EA46163431FC00635CDD /* schamper-bg@2x.png in Resources */, 90BE1F911B6AB8780061888C /* home-background.png in Resources */, 90E814F41854C76C00075F96 /* ios7-Default@2x.png in Resources */, + 909E46F51D5A17770072AFEC /* tabbar-minerva.png in Resources */, + 900295F41D2EBA52005F86E6 /* UGentOAuthConfig.plist in Resources */, 9068BA781B7BB437005F79FA /* tabbar-resto@2x.png in Resources */, 9035BF161B6C37FE008E7875 /* home-zeus.png in Resources */, F5EBF0E0164BE583002C14BF /* SORelativeDateTransformer.bundle in Resources */, @@ -1310,8 +1617,10 @@ F531DE6D16B30A0F0050D88B /* button-track-selected-highlighted@2x.png in Resources */, 90410B111848DF9100331F6C /* info-guide@2x.png in Resources */, F531DE6E16B30A0F0050D88B /* button-track-highlighted@2x.png in Resources */, + 90AC8E841D87347F00A16082 /* onboarding.storyboard in Resources */, F531DE6F16B30A0F0050D88B /* button-track-selected@2x.png in Resources */, F531DE7016B30A0F0050D88B /* button-track@2x.png in Resources */, + 90530DBD1D2D98D300A0CC8A /* PreferencesExtraTableViewCells.xib in Resources */, F5B9F2E917CEA96F0042C8A4 /* Icon-iOS6@2x.png in Resources */, F531DE9016B31FFC0050D88B /* button-track-highlighted.png in Resources */, F531DE9116B31FFC0050D88B /* button-track-selected-highlighted.png in Resources */, @@ -1326,6 +1635,7 @@ F5B6509E16C129B700989ADB /* btn-urgent-bg.png in Resources */, F5B6509F16C129B700989ADB /* btn-urgent-bg@2x.png in Resources */, F5B9F2F317CEAA040042C8A4 /* iTunesArtwork.png in Resources */, + 90677EEF1D91C7CE003F7135 /* tabbar-sko-student-village.png in Resources */, F5B650A416C12A9300989ADB /* btn-urgent-pause.png in Resources */, F5B650A516C12A9300989ADB /* btn-urgent-pause@2x.png in Resources */, 9035BF121B6C1753008E7875 /* schamper.png in Resources */, @@ -1340,6 +1650,7 @@ F5E851BF16C43174003DF993 /* btn-urgent-facebook@2x.png in Resources */, 904166231B7A689500D231EF /* tabbar-urgent@2x.png in Resources */, F5E851C016C43174003DF993 /* btn-urgent-home.png in Resources */, + 90EE815E1D8C58A200EE0F4F /* LaunchScreen.storyboard in Resources */, F5E851C116C43174003DF993 /* btn-urgent-home@2x.png in Resources */, F5E851C216C43174003DF993 /* btn-urgent-mail.png in Resources */, F5E851C316C43174003DF993 /* btn-urgent-mail@2x.png in Resources */, @@ -1354,13 +1665,18 @@ F5E851CC16C447C4003DF993 /* urgent-nowplaying.jpg in Resources */, F5E851CD16C447C4003DF993 /* urgent-nowplaying@2x.jpg in Resources */, 90B1EF161B7A60FC00E733C1 /* tabbar-schamper@2x.png in Resources */, + 90530DC31D2DB30200A0CC8A /* PreferencesTextTableViewCell.xib in Resources */, + 90869A001CACA038004C9FF8 /* GoogleService-Info.plist in Resources */, F5E851D016C44B47003DF993 /* urgent-bg.jpg in Resources */, F5E851D116C44B47003DF993 /* urgent-bg@2x.jpg in Resources */, F5B9F2EA17CEA96F0042C8A4 /* Icon-iOS7@2x.png in Resources */, F5E851D416C44BD7003DF993 /* hydra-logo.png in Resources */, + 90AA4BB71D83601B003303FE /* tabbar-hydra.png in Resources */, + 90530DC11D2DA5AA00A0CC8A /* PreferencesSwitchTableViewCells.xib in Resources */, F5E851D516C44BD7003DF993 /* hydra-logo@2x.png in Resources */, 904166241B7A689500D231EF /* tabbar-urgent.png in Resources */, 90B1029717A85E9200669CCD /* kalender.css in Resources */, + 909E46F61D5A17770072AFEC /* tabbar-minerva@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1368,13 +1684,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 90869A021CACA04E004C9FF8 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 508E1E6ECE6AEAB9A96A68C3 /* Embed Pods Frameworks */ = { + 160F8BF902D95BA643DFF470 /* Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1386,10 +1703,10 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Hydra/Pods-Hydra-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 7DF2C18BBE7D41B8B3A020A6 /* Copy Pods Resources */ = { + 1D54761D21BFBF2D308C5D54 /* Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1401,7 +1718,8 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Hydra/Pods-Hydra-resources.sh\"\n"; + showEnvVarsInLog = 0; }; 90EE4CD11BB804F600C2DD32 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1429,6 +1747,21 @@ shellPath = /bin/bash; shellScript = "src_plist=${PROJECT_DIR}/${INFOPLIST_FILE}\nbuild_plist=${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\n\nif [ $CONFIGURATION == Release ]; then\n # increment the build number (ie 1.0.115 to 1.0.116)\n echo \"Bumping build number...\"\n\n full_version=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleVersion\" \"${src_plist}\")\n if [[ \"${full_version}\" == \"\" ]]; then\n echo \"No build number in ${src_plist}\"\n exit 2\n fi\n\n version=${full_version%.*}\n build=${full_version##*.}\n\n new_version=\"$version.$(($build+1))\"\n /usr/libexec/Plistbuddy -c \"Set :CFBundleVersion $new_version\" \"${src_plist}\"\n echo \"Bumped build number to $new_version\" \nfi"; }; + F894F1D6500056E00817771A /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1452,63 +1785,107 @@ files = ( F5BAEC971518868200F6A1B1 /* main.m in Sources */, 907CD3CA1B7E52B600DD7539 /* RestoMenuViewController.swift in Sources */, + 90AA4BB41D835C08003303FE /* SKOStudentVillageViewController.swift in Sources */, + 90677EFA1D92B581003F7135 /* SKOStudentVillageDetailViewController.swift in Sources */, 90969AD41BFF5F90006EDD9D /* RestoStore.swift in Sources */, F5BAEC9B1518868200F6A1B1 /* AppDelegate.m in Sources */, + 902C9FE21D4D437E00A4BE86 /* CalendarItem.swift in Sources */, + 90530DC51D2DC10E00A0CC8A /* AssociationPreferenceController.m in Sources */, + 900295F11D2EB8F8005F86E6 /* OAuthTokenInfo.swift in Sources */, + 904A58211CB418CE0079ED25 /* NSDate.swift in Sources */, + 900295EE1D2EB8F8005F86E6 /* Announcement.swift in Sources */, + 9093B0261D512BDD00EB00E2 /* Whatsnew.swift in Sources */, + 9086FD941C84E9900053493D /* SavableStore.swift in Sources */, + 902B7CAD1D5FCC6D00B53B98 /* HomeMinervaCalendarItemCell.swift in Sources */, + 90E4BBB81D84466300582586 /* SKOStore.swift in Sources */, F5EBF11D159E092300EB5D26 /* SchamperViewController.m in Sources */, + 9086FD8D1C84E8BF0053493D /* NewsItem.swift in Sources */, + 90AA4BA21D831C86003303FE /* SKOHydraTabBarController.swift in Sources */, 90EE4CD01BB7193900C2DD32 /* RestoMenuInfoCollectionViewCell.swift in Sources */, - F5EBF14C159E1E3600EB5D26 /* SchamperArticle.m in Sources */, - F5D36BC115B5BDA500B6E017 /* SchamperStore.m in Sources */, + 9063FC641D5E2BB900BD01F6 /* InfoViewController.swift in Sources */, + 9063FC601D5D079100BD01F6 /* InfoItem.swift in Sources */, + 90530DBF1D2D9CDD00A0CC8A /* PreferenceTableViewCells.swift in Sources */, + 90C288A71D47A6FA00231462 /* MinervaCoursePreferenceViewController.swift in Sources */, F50EE09715B5F587000F1992 /* WebViewController.m in Sources */, - F55895C415B60F450001B399 /* RestoMenu.m in Sources */, - F55895C715B610140001B399 /* RestoStore.m in Sources */, + 9086FD8C1C84E8BF0053493D /* Activity.swift in Sources */, + 90E4BBBD1D84470800582586 /* Artist.swift in Sources */, + 90530DBB1D2D889400A0CC8A /* PreferencesController.swift in Sources */, F54B160715B8118C000A4407 /* UIColor+AppColors.m in Sources */, F57CB9DA15B84C4B00D87CB3 /* NSDate+Utilities.m in Sources */, - 3B436DF815B8915300984D3F /* InfoViewController.m in Sources */, - F5F43C3E15BAD246003C2AAE /* AssociationNewsItem.m in Sources */, - F5F43C4115BAD2CC003C2AAE /* AssociationActivity.m in Sources */, - F5F43C4415BAD2EB003C2AAE /* AssociationStore.m in Sources */, - F5F43C4715BAD42E003C2AAE /* Association.m in Sources */, + 90B3D7C31CB1CCF200A7E25E /* SpecialEvent.swift in Sources */, 395C117A16272D8000B99F9B /* NewsViewController.m in Sources */, + 900295F21D2EB8F8005F86E6 /* User.swift in Sources */, + 90CA18391D8C1BFE00309815 /* SKOTimelineCollectionViewCell.swift in Sources */, + 90869A041CACA116004C9FF8 /* UIViewController.swift in Sources */, + 90F37DD31C8B9A1200A380F0 /* RestoLocation.swift in Sources */, + 90CEB35D1D81AAB700833477 /* NotificationService.swift in Sources */, + 90CDDA331D85E69A00CCF8FB /* Exihibitor.swift in Sources */, + 90AC8E8C1D8751A300A16082 /* MinervaOnboardingViewController.swift in Sources */, 395C117D1627437000B99F9B /* NewsDetailViewController.m in Sources */, 90BAB266162749BD0043BA92 /* SchamperDetailViewController.m in Sources */, - F5016BA6162756F300BBDB0A /* ActivitiesController.m in Sources */, - 90969AD21BFF5D8D006EDD9D /* AssociationStore.swift in Sources */, 9041C3681B7E67B600E4A50C /* RestoMenuCollectionCell.swift in Sources */, F5016BA91627665800BBDB0A /* ActivityDetailController.m in Sources */, 3B93FBAA1630969900C962DC /* UrgentPlayer.m in Sources */, 90C8CA2E1B6AE3E80073E026 /* HomeFeedService.swift in Sources */, F52A8B7917CD366E00C3379C /* ActivityMapController.m in Sources */, + 90AC8E881D8745A600A16082 /* TimelineOnboardViewController.swift in Sources */, + 9086FD931C84E9900053493D /* SchamperStore.swift in Sources */, + 90AA4BC11D840F65003303FE /* SKOLineUpCollectionViewCell.swift in Sources */, + 90AA4BB21D835BB0003303FE /* SKOMapViewController.swift in Sources */, 90B010A51C308CB2000DE080 /* NSCalendar+HydraCalendar.swift in Sources */, + 9063FC621D5E277400BD01F6 /* InfoStore.swift in Sources */, F5EBF0E1164BE583002C14BF /* SORelativeDateTransformer.m in Sources */, 9035BF181B6CB52A008E7875 /* HomeActivityCollectionViewCell.swift in Sources */, F53A5E7716700242009EA4CC /* UrgentViewController.m in Sources */, + 90CDDA2F1D85CDFE00CCF8FB /* SKOStudentVillageTableViewCell.swift in Sources */, F53A5E8416700B29009EA4CC /* ApplicationWithRemoteSupport.m in Sources */, 90BE1F891B6AB3390061888C /* HomeViewController.swift in Sources */, F52B9B77168507BD000B71D6 /* NSDateFormatter+AppLocale.m in Sources */, 90EF4A591B6D8AE40034AD8F /* HomeUrgentCollectionViewCell.swift in Sources */, - 90EB7BEA1688ECE300A9582D /* RestoLegendItem.m in Sources */, + 90C288A91D47AD1500231462 /* MinervaStore.swift in Sources */, + 90AC8E8A1D874F1500A16082 /* InitialOnboardingViewController.swift in Sources */, + 90FFEA061D610D8900C18FDE /* CalendarViewController.swift in Sources */, + ED20E64B1D2D809000255C57 /* UIViewExtension.swift in Sources */, + 900295F61D2EBA98005F86E6 /* UGentOAuth2Service.swift in Sources */, + 90E3F5601C86585600343DF2 /* RestoMenu.swift in Sources */, + 90AA4BAB1D8335BB003303FE /* SKOTimelineCollectionViewController.swift in Sources */, 901603DC1B716DED002E0D60 /* HomeNewsItemCollectionViewCell.swift in Sources */, + 90530DC81D2E51B800A0CC8A /* PreferencesService.swift in Sources */, + 9086FD8E1C84E8BF0053493D /* SchamperArticle.swift in Sources */, + 9092D6AA1D5A358F00B75CEE /* MinervaAnnounceDetailViewController.swift in Sources */, + 902B7CAB1D5FA83000B53B98 /* HomeMinervaAnnouncementCell.swift in Sources */, + 900295F01D2EB8F8005F86E6 /* Course.swift in Sources */, 90F0525E168C663F004CAFFC /* RestoMapController.m in Sources */, + 90AA4BBC1D840097003303FE /* SKOStageHeaderCollectionReusableView.swift in Sources */, 9035BF141B6C1F5D008E7875 /* HomeSchamperCollectionViewCell.swift in Sources */, + 904A58231CB505F00079ED25 /* HomeSpecialEventBasicCollectionViewCell.swift in Sources */, + 9086FD9B1C84F0750053493D /* String.swift in Sources */, 90D55B9B1B7A4CE800813179 /* HydraTabbarController.swift in Sources */, + 90C288A51D47A69000231462 /* MinervaCourseAnnouncementViewController.swift in Sources */, 90DDFB791B6BB3D300DDAFA0 /* HomeRestoCollectionViewCell.swift in Sources */, - 90F05266168C7D19004CAFFC /* RestoLocation.m in Sources */, - F59CEE8A16A188050038FD24 /* FacebookSession.m in Sources */, - 90C532AB16A325A200857EB0 /* FacebookEvent.m in Sources */, - F55E89EC16A4AF8E008595CB /* ShareKitConfigurator.m in Sources */, + 9086FD961C84EEC50053493D /* Config.swift in Sources */, + 9047505C1D64FCD00091E2EF /* MinervaCalendarDetailViewController.swift in Sources */, + 90CA18371D8BF33200309815 /* TimelinePost.swift in Sources */, + 90AC8E8E1D87696C00A16082 /* TimeLineTableViewCell.swift in Sources */, + 904AD4EE1C88B845008F88E8 /* FacebookEvent.swift in Sources */, F569582716AA095800C45D00 /* UINavigationController+ReplaceController.m in Sources */, - 90969AD61BFF6014006EDD9D /* SchamperStore.swift in Sources */, 9041C36D1B7E801B00E4A50C /* RestoMenuHeader.swift in Sources */, 9035BF1A1B6CF663008E7875 /* LocationService.swift in Sources */, - 445C81F516C3DB0B0080819C /* AssociationPreferenceController.m in Sources */, F5DF2FB916C41DDE003B05EC /* MarqueeLabel.m in Sources */, - F521D11616C53A1200B686B2 /* PreferencesController.m in Sources */, + 9093B0241D50F5EC00EB00E2 /* UIColor.swift in Sources */, + 90B3D7C51CB1D22400A7E25E /* SpecialEventStore.swift in Sources */, + 9086FD9A1C84F0750053493D /* p2_OAuth2.swift in Sources */, + 90AA4BA61D831F1B003303FE /* SKOBackViewController.swift in Sources */, + 90692EE71D6399C600F607D8 /* CalendarSingleTableViewCell.swift in Sources */, + 90E4BBBB1D8446C500582586 /* Stage.swift in Sources */, + 9086FD8B1C84E8BF0053493D /* Association.swift in Sources */, 90B010A31C305F73000DE080 /* RestoSandwich.swift in Sources */, F56B177116C9069300AB59D6 /* NSMutableArray+Shuffling.m in Sources */, + 904AD4F01C88B858008F88E8 /* FacebookSession.swift in Sources */, F5ACC3B116D2694000626EAE /* CustomTableViewCell.m in Sources */, - F5C6093816D2DFCB0043BD44 /* PreferencesService.m in Sources */, 906C1D8917C8A4A600145CD3 /* MapViewController.m in Sources */, - 903A8AF61C04F369002DAF5B /* ActivityOverviewCell.swift in Sources */, + 90AA4BB01D835B67003303FE /* SKOLineupViewController.swift in Sources */, + 9086FD921C84E9900053493D /* AssociationStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1598,6 +1975,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "be.ugent.zeus.Hydra${CUSTOM_IDENTIFIER_SUFFIX}.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = YES; @@ -1623,8 +2001,8 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -1636,6 +2014,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "be.ugent.zeus.Hydra${CUSTOM_IDENTIFIER_SUFFIX}.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = YES; @@ -1646,6 +2025,7 @@ F5BAECBD1518868200F6A1B1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; APPLY_RULES_IN_COPY_FILES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; @@ -1653,6 +2033,7 @@ COPY_PHASE_STRIP = NO; CUSTOM_IDENTIFIER_SUFFIX = "-dev"; ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -1674,6 +2055,7 @@ F5BAECBE1518868200F6A1B1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; APPLY_RULES_IN_COPY_FILES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; @@ -1698,7 +2080,7 @@ }; F5BAECC01518868200F6A1B1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F2E1004E081A013343FCA87A /* Pods.debug.xcconfig */; + baseConfigurationReference = 3A8CEDFFE59A2DEF914797B2 /* Pods-Hydra.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; @@ -1710,9 +2092,15 @@ EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Hydra/Hydra-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(SDKROOT)/usr/include/libz", + "$(inherited)", + ); INFOPLIST_FILE = "Hydra-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "be.ugent.zeus.${PRODUCT_NAME:rfc1034identifier}${CUSTOM_IDENTIFIER_SUFFIX}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SWIFT_OBJC_BRIDGING_HEADER = "Hydra/Hydra-Bridging-Header.h"; @@ -1724,21 +2112,27 @@ }; F5BAECC11518868200F6A1B1 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 39B9A4055F5F374AA56058ED /* Pods.release.xcconfig */; + baseConfigurationReference = 9CC44FFCB3066F968CB87B10 /* Pods-Hydra.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CUSTOM_BUNDLE_DISPLAY_NAME = "${PRODUCT_NAME}"; CUSTOM_IDENTIFIER_SUFFIX = ""; EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Hydra/Hydra-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(SDKROOT)/usr/include/libz", + "$(inherited)", + ); INFOPLIST_FILE = "Hydra-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DRELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "be.ugent.zeus.${PRODUCT_NAME:rfc1034identifier}${CUSTOM_IDENTIFIER_SUFFIX}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SWIFT_OBJC_BRIDGING_HEADER = "Hydra/Hydra-Bridging-Header.h"; @@ -1749,7 +2143,6 @@ }; F5BAECC31518868200F6A1B1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F2E1004E081A013343FCA87A /* Pods.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Hydra.app/Hydra"; FRAMEWORK_SEARCH_PATHS = ( @@ -1822,6 +2215,7 @@ "-framework", XCTest, ); + PRODUCT_BUNDLE_IDENTIFIER = "be.ugent.zeus.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUNDLE_LOADER)"; }; @@ -1829,7 +2223,6 @@ }; F5BAECC41518868200F6A1B1 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 39B9A4055F5F374AA56058ED /* Pods.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Hydra.app/Hydra"; FRAMEWORK_SEARCH_PATHS = ( @@ -1902,6 +2295,7 @@ "-framework", XCTest, ); + PRODUCT_BUNDLE_IDENTIFIER = "be.ugent.zeus.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUNDLE_LOADER)"; }; diff --git a/Hydra/ActivitiesController.h b/Hydra/ActivitiesController.h deleted file mode 100644 index 9cddcb2..0000000 --- a/Hydra/ActivitiesController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// ActivityViewController.h -// Hydra -// -// Created by Pieter De Baets on 11/10/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@interface ActivitiesController : UITableViewController - -@end diff --git a/Hydra/ActivitiesController.m b/Hydra/ActivitiesController.m deleted file mode 100644 index b7cb306..0000000 --- a/Hydra/ActivitiesController.m +++ /dev/null @@ -1,438 +0,0 @@ -// -// ActivityViewController.m -// Hydra -// -// Created by Pieter De Baets on 11/10/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "ActivitiesController.h" -#import "AssociationStore.h" -#import "AssociationActivity.h" -#import "Association.h" -#import "NSDate+Utilities.h" -#import "ActivityDetailController.h" -#import "NSDateFormatter+AppLocale.h" -#import "PreferencesService.h" -#import "RMPickerViewController.h" -#import "Hydra-Swift.h" -#import - -@interface ActivitiesController () - -@property (nonatomic, assign) BOOL activitiesUpdated; - -@property (nonatomic, strong) NSArray *days; -@property (nonatomic, strong) NSDictionary *data; -@property (nonatomic, strong) NSArray *oldDays; -@property (nonatomic, strong) NSDictionary *oldData; -@property (nonatomic, assign) NSUInteger count; -@property (nonatomic, assign) NSUInteger previousSearchLength; - -@property (nonatomic, strong) UISearchDisplayController *searchController; -@property (nonatomic, strong) UIPickerView *datePicker; - -@end - -@implementation ActivitiesController - -- (instancetype)init -{ - if (self = [super initWithStyle:UITableViewStylePlain]) { - self.count = 0; - [self loadActivities]; - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center addObserver:self selector:@selector(activitiesUpdated:) - name:AssociationStoreDidUpdateActivitiesNotification object:nil]; - } - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - self.title = @"Activiteiten"; - - // Switch dates using the calendar icon - UIBarButtonItem *btn = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icon-calendar.png"] - style:UIBarButtonItemStylePlain - target:self action:@selector(dateButtonTapped:)]; - btn.enabled = self.days.count > 0; - self.navigationItem.rightBarButtonItem = btn; - - UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0,0,320,44)]; - self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self]; - self.searchController.delegate = self; - self.searchController.searchResultsDataSource = self; - self.searchController.searchResultsDelegate = self; - - self.tableView.tableHeaderView = searchBar; - - if ([UIRefreshControl class]) { - UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; - refreshControl.tintColor = [UIColor hydraTintColor]; - [refreshControl addTarget:self action:@selector(didPullRefreshControl:) - forControlEvents:UIControlEventValueChanged]; - - self.refreshControl = refreshControl; - } - - [RMPickerViewController setLocalizedTitleForCancelButton:@"Sluit"]; - [RMPickerViewController setLocalizedTitleForSelectButton:@"Gereed"]; - - UINib *nib = [UINib nibWithNibName:@"ActivityOverviewCell" bundle:nil]; - [self.tableView registerNib:nib forCellReuseIdentifier:@"ActivityOverviewCell"]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - // Make sure we scroll with any selection that may have been set - [self.tableView scrollToNearestSelectedRowAtScrollPosition:UITableViewScrollPositionNone animated:NO]; - - // Call super last, as it will clear the selection - [super viewWillAppear:animated]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - GAI_Track(@"Activities"); - - // Show loading indicator when no content is found yet - if (self.days.count == 0 && !self.activitiesUpdated) { - [SVProgressHUD show]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - [SVProgressHUD dismiss]; -} - -- (void)didPullRefreshControl:(id)sender -{ - [[AssociationStore sharedStore] reloadActivities]; -} - -- (void)loadActivities -{ - NSArray *activities = [AssociationStore sharedStore].activities; - - // Filter activities - PreferencesService *prefs = [PreferencesService sharedService]; - if (prefs.filterAssociations) { - NSArray *associations = prefs.preferredAssociations; - NSPredicate *pred = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { - return [associations containsObject:[obj association].internalName] || - [obj highlighted]; - }]; - activities = [activities filteredArrayUsingPredicate:pred]; - } - - // Group activities by day - NSDate *now = [NSDate date]; - NSMutableDictionary *groups = [[NSMutableDictionary alloc] init]; - - for (AssociationActivity *activity in activities) { - NSDate *day = [activity.start dateAtStartOfDay]; - - // Check that activity is not over yet - if (!activity.end || [activity.end isEarlierThanDate:now]) continue; - - NSMutableArray *group = groups[day]; - if (!group) { - groups[day] = group = [[NSMutableArray alloc] init]; - } - [group addObject:activity]; - } - - self.days = [[groups allKeys] sortedArrayUsingSelector:@selector(compare:)]; - - // Sort activities per day - for (NSDate *date in self.days) { - groups[date] = [groups[date] sortedArrayUsingComparator: - ^(AssociationActivity *obj1, AssociationActivity *obj2) { - return [obj1.start compare:obj2.start]; - }]; - } - - self.data = groups; - self.navigationItem.rightBarButtonItem.enabled = self.days.count > 0; - [self.tableView reloadData]; -} - -- (void)activitiesUpdated:(NSNotification *)notification -{ - self.activitiesUpdated = YES; - - [self loadActivities]; - [self.tableView reloadData]; - - // Hide or update HUD - if ([SVProgressHUD isVisible]) { - [SVProgressHUD dismiss]; - } - - if ([UIRefreshControl class]) { - [self.refreshControl endRefreshing]; - } -} - -#pragma mark - Table view delegate - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return self.days.count; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - NSDate *date = self.days[section]; - return [self.data[date] count]; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - static NSDateFormatter *dateFormatter = nil; - if (!dateFormatter) { - dateFormatter = [NSDateFormatter H_dateFormatterWithAppLocale]; - dateFormatter.dateFormat = @"E d MMMM"; - } - return [dateFormatter stringFromDate:self.days[section]]; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return 44; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDate *date = self.days[indexPath.section]; - AssociationActivity *activity = self.data[date][indexPath.row]; - static NSString *CellIdentifier = @"ActivityOverviewCell"; - ActivityOverviewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - - cell.activity = activity; - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDate *date = self.days[indexPath.section]; - AssociationActivity *activity = self.data[date][indexPath.row]; - ActivityDetailController *detailViewController = [[ActivityDetailController alloc] - initWithActivity:activity delegate:self]; - [self.navigationController pushViewController:detailViewController animated:YES]; -} - -#pragma mark - Searchbar delegate - -- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller -{ - self.oldDays = [[NSArray alloc] initWithArray:self.days copyItems:YES]; - self.oldData = [[NSDictionary alloc] initWithDictionary:self.data copyItems:YES]; - - [self filterActivities]; -} - -- (void)searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView -{ - self.data = self.oldData; - self.days = self.oldDays; - self.previousSearchLength = 0; -} - -- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString -{ - [self filterActivities]; - return YES; -} - -- (void) filterActivities -{ - NSString *searchString = self.searchController.searchBar.text; - if (searchString.length == 0) { - self.days = self.oldDays; - self.data = self.oldData; - self.previousSearchLength = 0; - } - else { - if (self.previousSearchLength > searchString.length){ - self.days = self.oldDays; - self.data = self.oldData; - } - - self.previousSearchLength = searchString.length; - - NSMutableArray *filteredDays = [[NSMutableArray alloc] init]; - NSMutableDictionary *filteredData = [[NSMutableDictionary alloc] init]; - - for (NSDate *day in self.days) { - NSMutableArray *activities = self.data[day]; - NSMutableArray *filteredActivities = [[NSMutableArray alloc] init]; - for (AssociationActivity *activity in activities) { - if ([self filterActivity:activity fromString:searchString]) { - [filteredActivities addObject:activity]; - } - } - if (filteredActivities.count > 0){ - [filteredDays addObject:day]; - filteredData[day] = filteredActivities; - } - } - self.days = filteredDays; - self.data = filteredData; - } -} - -- (BOOL) filterActivity:(AssociationActivity*)activity fromString:(NSString*)searchString -{ - NSStringCompareOptions option = NSCaseInsensitiveSearch; - if ([activity.title rangeOfString:searchString options:option].location != NSNotFound || - [activity.association.fullName rangeOfString:searchString options:option].location != NSNotFound || - [activity.association.internalName rangeOfString:searchString options:option].location != NSNotFound) { - return YES; - } - if (![activity.categories isEqual: @[[NSNull null]]]) { - for(NSString* categorie in activity.categories){ - if([categorie rangeOfString:searchString options:option].location != NSNotFound){ - return YES; - } - } - } - return NO; -} - -#pragma mark - Activy list delegate - -- (AssociationActivity *)activityBefore:(AssociationActivity *)current -{ - NSDate *day = [current.start dateAtStartOfDay]; - NSUInteger index = [self.data[day] indexOfObject:current]; - if (index == NSNotFound) return nil; - - // Is there another activity in the same day? - if (index > 0) { - return self.data[day][index - 1]; - } - - // Is there another day we can find activities in - NSUInteger dayIndex = [self.days indexOfObject:day]; - if (dayIndex == 0 || dayIndex == NSNotFound) return nil; - else { - // Assuming each category has at least one date - NSDate *prevDay = self.days[dayIndex - 1]; - return [self.data[prevDay] lastObject]; - } -} - -- (AssociationActivity *)activityAfter:(AssociationActivity *)current -{ - NSDate *day = [current.start dateAtStartOfDay]; - NSUInteger index = [self.data[day] indexOfObject:current]; - if (index == NSNotFound) return nil; - - // Is there another activity in the same day? - if (index < [self.data[day] count] - 1) { - return self.data[day][index + 1]; - } - - // Is there another day we can find activities in - NSUInteger dayIndex = [self.days indexOfObject:day]; - if (dayIndex == self.days.count - 1 || dayIndex == NSNotFound) return nil; - else { - // Assuming each category has at least one date - NSDate *nextDay = self.days[dayIndex + 1]; - return self.data[nextDay][0]; - } -} - -- (void)didSelectActivity:(AssociationActivity *)activity -{ - NSDate *day = [activity.start dateAtStartOfDay]; - NSUInteger section = [self.days indexOfObject:day]; - NSUInteger row = [self.data[day] indexOfObject:activity]; - - if (row != NSNotFound) { - NSIndexPath *selection = [NSIndexPath indexPathForRow:row inSection:section]; - [self.tableView selectRowAtIndexPath:selection animated:NO - scrollPosition:UITableViewScrollPositionNone]; - } -} - -#pragma mark - Date button and UIPickerView - -- (void)dateButtonTapped:(id)sender -{ - RMPickerViewController *pickerVC = [RMPickerViewController pickerController]; - pickerVC.delegate = self; - UIPickerView *picker = pickerVC.picker; - NSInteger row = ((NSIndexPath *)[self.tableView indexPathsForVisibleRows][0]).section; - [picker selectRow:row inComponent:0 animated:NO]; - - [pickerVC show]; -} - -- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView -{ - return 1; -} - -- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component -{ - return self.days.count; -} - -- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view -{ - UILabel *label; - if (!view) { - label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 37)]; - label.font = [UIFont boldSystemFontOfSize:18]; - label.textAlignment = NSTextAlignmentCenter; - label.backgroundColor = [UIColor clearColor]; - } - else { - label = (UILabel *)view; - } - - static NSDateFormatter *formatter; - if (!formatter) { - formatter = [NSDateFormatter H_dateFormatterWithAppLocale]; - formatter.dateFormat = @"EEEE d MMMM"; - } - label.text = [formatter stringFromDate:self.days[row]]; - - return label; -} - -- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component -{ - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:row]; - [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; -} - -- (void)dismissActionSheet:(id)sender -{ - UIActionSheet *sheet = (UIActionSheet *)[self.datePicker superview]; - [sheet dismissWithClickedButtonIndex:0 animated:YES]; - self.datePicker = nil; -} - -#pragma mark - RMPickerViewController Delegates -- (void)pickerViewController:(RMPickerViewController *)vc didSelectRows:(NSArray *)selectedRows { - //Do something -} - -- (void)pickerViewControllerDidCancel:(RMPickerViewController *)vc { - //Do something else -} -@end diff --git a/Hydra/Activity.swift b/Hydra/Activity.swift new file mode 100644 index 0000000..b5b0a7d --- /dev/null +++ b/Hydra/Activity.swift @@ -0,0 +1,150 @@ +// +// Activity.swift +// +// Created by Feliciaan De Palmenaer on 27/02/2016 +// Copyright (c) . All rights reserved. +// + +import Foundation +import ObjectMapper + +class Activity: NSObject, NSCoding, Mappable { + + // MARK: Properties + var title: String + var association: Association + var start: NSDate + var end: NSDate? + var location: String + var latitude: Double + var longitude: Double + var descriptionText: String + var url: String + var facebookId: String? + var highlighted: Bool + private var _facebookEvent: FacebookEvent? + var facebookEvent: FacebookEvent? { + get { + if let facebookEvent = _facebookEvent { + return facebookEvent + } else { + if let facebookId = self.facebookId where facebookId.characters.count > 0 { + print("Created facebookEvent") + _facebookEvent = FacebookEvent(eventId: facebookId) + return _facebookEvent + } + + return nil + } + } + + set (newValue) { + _facebookEvent = newValue + } + } + + override var description: String { + get { + return "Activity: \(self.title)" + } + } + + init(title: String, association: Association, start: NSDate, end: NSDate?, location: String, latitude: Double, longitude: Double, descriptionText: String, url: String, highlighted: Bool) { + self.title = title + self.association = association + self.start = start + self.end = end + self.location = location + self.latitude = latitude + self.longitude = longitude + self.descriptionText = descriptionText + self.url = url + self.highlighted = highlighted + } + + // MARK: ObjectMapper Initalizers + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + required convenience init?(_ map: Map){ + // Give empty values, because they will get filled + self.init(title: "", association: Association(internalName: "", displayName: ""), start: NSDate(), end: nil, location: "", latitude: 0.0, longitude: 0.0, descriptionText: "", url: "", highlighted: false) + } + + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + func mapping(map: Map) { + title <- map[PropertyKey.activityTitleKey] + association <- map[PropertyKey.activityAssociationKey] + start <- (map[PropertyKey.activityStartKey], ISO8601DateTransform()) + end <- (map[PropertyKey.activityEndKey], ISO8601DateTransform()) + location <- map[PropertyKey.activityLocationKey] + latitude <- map[PropertyKey.activityLatitudeKey] + longitude <- map[PropertyKey.activityLongitudeKey] + facebookId <- map[PropertyKey.activityFacebookIdKey] + descriptionText <- map[PropertyKey.activitydescriptionTextKey] + url <- map[PropertyKey.activityUrlKey] + highlighted <- map[PropertyKey.activityHighlightedKey] + } + + // MARK: NSCoding Protocol + required init(coder aDecoder: NSCoder) { + self.title = aDecoder.decodeObjectForKey(PropertyKey.activityTitleKey) as! String + self.facebookId = aDecoder.decodeObjectForKey(PropertyKey.activityFacebookIdKey) as? String + self.longitude = aDecoder.decodeObjectForKey(PropertyKey.activityLongitudeKey) as! Double + self.descriptionText = aDecoder.decodeObjectForKey(PropertyKey.activitydescriptionTextKey) as! String + self.start = aDecoder.decodeObjectForKey(PropertyKey.activityStartKey) as! NSDate + self.latitude = aDecoder.decodeObjectForKey(PropertyKey.activityLatitudeKey) as! Double + self.location = aDecoder.decodeObjectForKey(PropertyKey.activityLocationKey) as! String + self.association = aDecoder.decodeObjectForKey(PropertyKey.activityAssociationKey) as! Association + self.end = aDecoder.decodeObjectForKey(PropertyKey.activityEndKey) as? NSDate + self.url = aDecoder.decodeObjectForKey(PropertyKey.activityUrlKey) as! String + self.highlighted = aDecoder.decodeObjectForKey(PropertyKey.activityHighlightedKey) as! Bool + self._facebookEvent = aDecoder.decodeObjectForKey(PropertyKey.activityFacebookEventKey) as? FacebookEvent + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(title, forKey: PropertyKey.activityTitleKey) + aCoder.encodeObject(facebookId, forKey: PropertyKey.activityFacebookIdKey) + aCoder.encodeObject(longitude, forKey: PropertyKey.activityLongitudeKey) + aCoder.encodeObject(descriptionText, forKey: PropertyKey.activitydescriptionTextKey) + aCoder.encodeObject(start, forKey: PropertyKey.activityStartKey) + aCoder.encodeObject(latitude, forKey: PropertyKey.activityLatitudeKey) + aCoder.encodeObject(location, forKey: PropertyKey.activityLocationKey) + aCoder.encodeObject(association, forKey: PropertyKey.activityAssociationKey) + aCoder.encodeObject(end, forKey: PropertyKey.activityEndKey) + aCoder.encodeObject(url, forKey: PropertyKey.activityUrlKey) + aCoder.encodeObject(highlighted, forKey: PropertyKey.activityHighlightedKey) + aCoder.encodeObject(_facebookEvent, forKey: PropertyKey.activityFacebookEventKey) + } + + func hasCoordinates() -> Bool { + return longitude != 0.0 && latitude != 0.0 + } + + func hasFacebookEvent() -> Bool { + if let facebookEvent = _facebookEvent { + return facebookEvent.valid + } + return false + } + + struct PropertyKey { + // MARK: Declaration for string constants to be used to decode and also serialize. + static let activityTitleKey: String = "title" + static let activityFacebookIdKey: String = "facebook_id" + static let activityLongitudeKey: String = "longitude" + static let activitydescriptionTextKey: String = "description" + static let activityStartKey: String = "start" + static let activityLatitudeKey: String = "latitude" + static let activityLocationKey: String = "location" + static let activityAssociationKey: String = "association" + static let activityEndKey: String = "end" + static let activityUrlKey: String = "url" + static let activityHighlightedKey: String = "highlighted" + static let activityFacebookEventKey: String = "facebookEvent" + } +} diff --git a/Hydra/ActivityDetailController.h b/Hydra/ActivityDetailController.h index 98e98c1..1123418 100644 --- a/Hydra/ActivityDetailController.h +++ b/Hydra/ActivityDetailController.h @@ -8,18 +8,18 @@ #import -@class AssociationActivity; +@class Activity; @protocol ActivityListDelegate -- (AssociationActivity *)activityBefore:(AssociationActivity *)current; -- (AssociationActivity *)activityAfter:(AssociationActivity *)current; -- (void)didSelectActivity:(AssociationActivity *)activity; +- (Activity *)activityBefore:(Activity *)current; +- (Activity *)activityAfter:(Activity *)current; +- (void)didSelectActivity:(Activity *)activity; @end @interface ActivityDetailController : UITableViewController -- (id)initWithActivity:(AssociationActivity *)activity delegate:(id)delegate; +- (id)initWithActivity:(Activity *)activity delegate:(id)delegate; @end diff --git a/Hydra/ActivityDetailController.m b/Hydra/ActivityDetailController.m index 1575f0b..41821ed 100644 --- a/Hydra/ActivityDetailController.m +++ b/Hydra/ActivityDetailController.m @@ -7,15 +7,12 @@ // #import "ActivityDetailController.h" -#import "AssociationActivity.h" -#import "Association.h" +#import "Hydra-Swift.h" #import "NSDateFormatter+AppLocale.h" -#import "FacebookEvent.h" #import "NSDate+Utilities.h" #import "CustomTableViewCell.h" -#import "FacebookSession.h" -#import "PreferencesService.h" #import "ActivityMapController.h" +#import "Hydra-Swift.h" #import #import @@ -39,7 +36,7 @@ @interface ActivityDetailController () -@property (nonatomic, strong) AssociationActivity *activity; +@property (nonatomic, strong) Activity *activity; @property (nonatomic, strong) NSArray *fields; @property (nonatomic, strong) id listDelegate; @@ -51,7 +48,7 @@ @interface ActivityDetailController () )delegate +- (id)initWithActivity:(Activity *)activity delegate:(id)delegate { if (self = [super initWithStyle:UITableViewStyleGrouped]) { self.activity = activity; @@ -59,7 +56,7 @@ - (id)initWithActivity:(AssociationActivity *)activity delegate:(id " stringByAppendingString:self.activity.title]); - if (self.activity.facebookEvent) { + if (self.activity.facebookEvent != nil) { FacebookSession *session = [FacebookSession sharedSession]; PreferencesService *prefs = [PreferencesService sharedService]; if (!session.open && !prefs.shownFacebookPrompt){ @@ -113,7 +110,7 @@ - (void)viewDidAppear:(BOOL)animated preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"Annuleer" style:UIAlertActionStyleCancel handler:nil]; UIAlertAction *login = [UIAlertAction actionWithTitle:@"Koppel" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - [session openWithAllowLoginUI:YES]; + [session openWithAllowLoginUI:YES completion:nil]; }]; [alert addAction:cancel]; @@ -121,7 +118,7 @@ - (void)viewDidAppear:(BOOL)animated [self presentViewController:alert animated:YES completion:nil]; } else { - [session openWithAllowLoginUI:YES]; + [session openWithAllowLoginUI:YES completion:nil]; } } } @@ -242,10 +239,10 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa width = tableView.frame.size.width - 40; spacing = 0; - if (self.activity.facebookEvent.smallImageUrl) { - minHeight = 70; - width -= 70; - } + // height for picture + minHeight = 70; + width -= 70; + break; case kInfoSection: @@ -430,13 +427,18 @@ - (UITableViewCell *)tableView:(UITableView *)tableView headerCellForRowAtIndex: cell.textLabel.text = self.activity.title; // Show image - NSURL *imageUrl = self.activity.facebookEvent.smallImageUrl; + NSURL *imageUrl = self.activity.facebookEvent.imageUrl; + if (!imageUrl) { + imageUrl = [[NSURL alloc] initWithString:[[NSString alloc] + initWithFormat:@"https://zeus.ugent.be/hydra/api/2.0/association/logo/%@.png", + [self.activity.association.internalName lowercaseString]]]; + } if (imageUrl) { if (!self.imageView) { CGRect imageRect = CGRectMake(0, 0, 70, 70); self.imageView = [[UIImageView alloc] initWithFrame:imageRect]; self.imageView.backgroundColor = [UIColor whiteColor]; - self.imageView.contentMode = UIViewContentModeScaleAspectFill; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; self.imageView.layer.masksToBounds = YES; self.imageView.layer.borderColor = [UIColor colorWithWhite:0.65 alpha:1].CGColor; } @@ -563,8 +565,9 @@ - (UITableViewCell *)tableView:(UITableView *)tableView actionCellForRowAtIndex: } else { cell.textLabel.text = @"Aanwezigheid wijzigen"; + NSString *localizedString = [self facebookEventRsvpAsLocalizedString:fbEvent.userRsvp]; cell.detailTextLabel.text = [NSString stringWithFormat:@"Momenteel sta je op '%@'", - FacebookEventRsvpAsLocalizedString(fbEvent.userRsvp)]; + (localizedString)]; if (fbEvent.userRsvpUpdating) { UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] @@ -726,15 +729,18 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn - (void)enableSegments:(UISegmentedControl *)control { - AssociationActivity *prev = [self.listDelegate activityBefore:self.activity]; + Activity *prev = [self.listDelegate activityBefore:self.activity]; [control setEnabled:(prev != nil) forSegmentAtIndex:0]; - AssociationActivity *next = [self.listDelegate activityAfter:self.activity]; + Activity *next = [self.listDelegate activityAfter:self.activity]; [control setEnabled:(next != nil) forSegmentAtIndex:1]; + if (prev == nil && next == nil) { // when started from home view + control.hidden = YES; + } } - (void)segmentTapped:(UISegmentedControl *)control { - AssociationActivity *activity; + Activity *activity; if (control.selectedSegmentIndex == 0) { activity = [self.listDelegate activityBefore:self.activity]; } @@ -762,4 +768,22 @@ - (void)facebookEventUpdated:(NSNotification *)notification [self reloadData]; } +#pragma mark - Extra functions (that cannot be imported from Swift) +- (NSString *)facebookEventRsvpAsLocalizedString:(FacebookEventRsvp) rsvp +{ + switch (rsvp) { + case FacebookEventRsvpNone: + return nil; + case FacebookEventRsvpAttending: + return @"aanwezig"; + case FacebookEventRsvpUnsure: + return @"misschien"; + case FacebookEventRsvpDeclined: + return @"niet aanwezig"; + } +} + @end + + + diff --git a/Hydra/ActivityMapController.h b/Hydra/ActivityMapController.h index 16573d2..fc55d91 100644 --- a/Hydra/ActivityMapController.h +++ b/Hydra/ActivityMapController.h @@ -7,10 +7,11 @@ // #import "MapViewController.h" -#import "AssociationActivity.h" + +@class Activity; @interface ActivityMapController : MapViewController -- (id)initWithActivity:(AssociationActivity *)activity; +- (id)initWithActivity:(Activity *)activity; @end diff --git a/Hydra/ActivityMapController.m b/Hydra/ActivityMapController.m index e8c7db1..bb46f58 100644 --- a/Hydra/ActivityMapController.m +++ b/Hydra/ActivityMapController.m @@ -7,6 +7,7 @@ // #import "ActivityMapController.h" +#import "Hydra-Swift.h" @interface SimpleMapAnnotation : NSObject @@ -19,13 +20,13 @@ - (id)initWithCoordinate:(CLLocationCoordinate2D)coordinate title:(NSString *)ti @interface ActivityMapController () -@property (nonatomic, strong) AssociationActivity *activity; +@property (nonatomic, strong) Activity *activity; @end @implementation ActivityMapController -- (id)initWithActivity:(AssociationActivity *)activity +- (id)initWithActivity:(Activity *)activity { if (self = [super init]) { self.activity = activity; diff --git a/Hydra/ActivityOverviewCell.swift b/Hydra/ActivityOverviewCell.swift deleted file mode 100644 index bb62cf3..0000000 --- a/Hydra/ActivityOverviewCell.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ActivityOverviewCell.swift -// Hydra -// -// Created by Feliciaan De Palmenaer on 24/11/2015. -// Copyright © 2015 Zeus WPI. All rights reserved. -// - -import UIKit - -@objc class ActivityOverviewCell: UITableViewCell { - - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var associationLabel: UILabel! - @IBOutlet weak var dateLabel: UILabel! - - var activity: AssociationActivity? { - didSet { - associationLabel.text = activity?.association.displayName - titleLabel.text = activity?.title - - let dateStartFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() - dateStartFormatter.dateFormat = "H:mm"; - dateLabel.text = "\(dateStartFormatter.stringFromDate((self.activity?.start)!))" - - //TODO: do something if highlighted - } - } - -} \ No newline at end of file diff --git a/Hydra/ActivityOverviewCell.xib b/Hydra/ActivityOverviewCell.xib deleted file mode 100644 index 8585e85..0000000 --- a/Hydra/ActivityOverviewCell.xib +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Hydra/Announcement.swift b/Hydra/Announcement.swift new file mode 100644 index 0000000..2dc3cda --- /dev/null +++ b/Hydra/Announcement.swift @@ -0,0 +1,86 @@ +// +// Announcement.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 21/06/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class Announcement: NSObject, Mappable, NSCoding { + var title: String + var content: String + var emailSent: Bool + var itemId: Int + var editUser: String + var date: NSDate + private var _read: Bool + + var course: Course? + + var read: Bool { + set { + MinervaStore.sharedStore.saveLater() + self._read = newValue + } + get { + return self._read + } + } + + + convenience required init?(_ map: Map) { + self.init(title: "", content: "", emailSent: false, itemId: 0, editUser: "", date: NSDate(timeIntervalSince1970: 0)) + } + + init(title: String, content: String, emailSent: Bool, itemId: Int, editUser: String, date: NSDate, read: Bool = false) { + self.title = title + self.content = content + self.emailSent = emailSent + self.itemId = itemId + self.editUser = editUser + self.date = date + self._read = read + } + + func mapping(map: Map) { + self.title <- map[PropertyKey.titleKey] + self.content <- map[PropertyKey.contentKey] + self.emailSent <- map[PropertyKey.emailSentKey] + self.itemId <- map[PropertyKey.itemIdKey] + self.editUser <- map[PropertyKey.editUserKey] + self.date <- (map[PropertyKey.dateKey], ISO8601DateTransform()) + } + + required init?(coder aDecoder: NSCoder) { + self.title = aDecoder.decodeObjectForKey(PropertyKey.titleKey) as! String + self.content = aDecoder.decodeObjectForKey(PropertyKey.contentKey) as! String + self.emailSent = aDecoder.decodeObjectForKey(PropertyKey.emailSentKey) as! Bool + self.itemId = aDecoder.decodeObjectForKey(PropertyKey.itemIdKey) as! Int + self.editUser = aDecoder.decodeObjectForKey(PropertyKey.editUserKey) as! String + self.date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as! NSDate + self._read = aDecoder.decodeBoolForKey(PropertyKey.readKey) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(self.title, forKey: PropertyKey.titleKey) + aCoder.encodeObject(self.content, forKey: PropertyKey.contentKey) + aCoder.encodeObject(self.emailSent, forKey: PropertyKey.emailSentKey) + aCoder.encodeObject(self.itemId, forKey: PropertyKey.itemIdKey) + aCoder.encodeObject(self.editUser, forKey: PropertyKey.editUserKey) + aCoder.encodeObject(self.date, forKey: PropertyKey.dateKey) + aCoder.encodeBool(self._read, forKey: PropertyKey.readKey) + } + + struct PropertyKey { + static let titleKey = "title" + static let contentKey = "content" + static let emailSentKey = "email_sent" + static let itemIdKey = "item_id" + static let editUserKey = "last_edit_user" + static let dateKey = "last_edit_time" + static let readKey = "read" + } +} diff --git a/Hydra/AppDelegate.m b/Hydra/AppDelegate.m index b398a55..10619ae 100644 --- a/Hydra/AppDelegate.m +++ b/Hydra/AppDelegate.m @@ -8,39 +8,18 @@ #import "AppDelegate.h" #import "UIColor+AppColors.h" -#import "ShareKitConfigurator.h" -#import "FacebookSession.h" -#import "SchamperStore.h" -#import "AssociationStore.h" - -#import -#import -#import -#import -#import +#import "Hydra-Swift.h" + #import -#define kGoogleAnalyticsToken @"UA-25444917-3" +//@import FBSDKCoreKit; +//@import FBSDKLoginKit; +@import Firebase; @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - -#if GoogleAnalyticsEnabled - GAI *gai = [GAI sharedInstance]; - gai.trackUncaughtExceptions = YES; - gai.dispatchInterval = 30; - gai.defaultTracker = [gai trackerWithTrackingId:kGoogleAnalyticsToken]; - gai.defaultTracker.allowIDFACollection = NO; -#endif - -#if DEBUG - // Change RKLogLevelInfo to RKLoglevelTrace for debugging - RKLogConfigureByName("RestKit/Network", RKLogLevelInfo); - RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelInfo); -#endif - // Configure some parts of the application asynchronously dispatch_queue_t async = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(async, ^{ @@ -51,22 +30,36 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( name:kReachabilityChangedNotification object:nil]; [reachability startNotifier]; - - // Enable network activity indicator - [AFNetworkActivityIndicatorManager sharedManager].enabled = YES; - - // Configure ShareKit - ShareKitConfigurator *config = [[ShareKitConfigurator alloc] init]; - [SHKConfiguration sharedInstanceWithConfigurator:config]; - [SHK flushOfflineQueue]; }); - // Restore Facebook-session - [[FacebookSession sharedSession] openWithAllowLoginUI:NO]; +/* // Restore Facebook-session + [FacebookSession.sharedSession openWithAllowLoginUI:NO completion:nil]; - // Start storyboard - UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:[NSBundle mainBundle]]; - UIViewController *rootvc = [storyboard instantiateInitialViewController]; + [[FBSDKApplicationDelegate sharedInstance] application:application + didFinishLaunchingWithOptions:launchOptions];*/ + + // Configure Firebase + [FIRApp configure]; + + // Root view controller + UIViewController *rootvc; + + // Configure user defaults + [PreferencesService registerAppDefaults]; + + bool firstLaunch = [PreferencesService sharedService].firstLaunch; + if (firstLaunch) { + // Start onboarding + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"onboarding" bundle:[NSBundle mainBundle]]; + rootvc = [storyboard instantiateInitialViewController]; + } else { + // Start storyboard + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:[NSBundle mainBundle]]; + rootvc = [storyboard instantiateInitialViewController]; + } + + // Test if user is logged in on minerva + [[UGentOAuth2Service sharedService] isLoggedIn]; // Set root view controller and make windows visible self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; @@ -77,10 +70,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication annotation:(id)annotation -{ - return [[FBSession activeSession] handleOpenURL:url]; +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + return false; //[[FBSDKApplicationDelegate sharedInstance] application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; } - (void)applicationWillResignActive:(UIApplication *)application @@ -89,6 +80,22 @@ - (void)applicationWillResignActive:(UIApplication *)application // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. } +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + // If you are receiving a notification message while your app is in the background, + // this callback will not be fired till the user taps on the notification launching the application. + // TODO: Handle data of notification + + // Print message ID. + NSLog(@"Message ID: %@", userInfo[@"gcm.message_id"]); + + // Pring full message. + NSLog(@"%@", userInfo); + UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Notification" message:userInfo.description delegate:self + cancelButtonTitle:@"OK" otherButtonTitles:nil]; + [av show]; +} + - (void)applicationDidEnterBackground:(UIApplication *)application { // 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. @@ -109,7 +116,7 @@ - (void)applicationDidBecomeActive:(UIApplication *)application // We need to properly handle activation of the application with regards to Facebook Login // (e.g., returning from iOS 6.0 Login Dialog or from fast app switching). - [[FBSession activeSession] handleDidBecomeActive]; + //[[FBSession activeSession] handleDidBecomeActive]; } - (void)applicationWillTerminate:(UIApplication *)application @@ -117,7 +124,33 @@ - (void)applicationWillTerminate:(UIApplication *)application // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // You should also take care of closing the session if the app is about to terminate. - [[FBSession activeSession] close]; + //[[FBSession activeSession] close]; +} + +- (BOOL) application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options +{ + if (url != nil && [url.scheme isEqual: @"hydra-ugent"] && ([url.path containsString:@"zeus/callback"])) { + // FIXME: work arround until the UGent allows app url-schemes + NSString *absuluteURL = [url absoluteString]; + absuluteURL = [absuluteURL stringByReplacingOccurrencesOfString:@"hydra-ugent://oauth/zeus/callback" withString:@"https://zeus.UGent.be/hydra/oauth/callback"]; + + [[UGentOAuth2Service sharedService] handleRedirectURL:[[NSURL alloc] initWithString:absuluteURL]]; + return true; + } + return false; +} + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + if ([PreferencesService sharedService].skoNotificationsEnabled) { + [[FIRMessaging messaging] subscribeToTopic:[NotificationService SKOTopic]]; + } +} + + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + } - (void)reachabilityStatusChanged:(NSNotification *)notification @@ -143,9 +176,6 @@ - (void)reachabilityStatusChanged:(NSNotification *)notification - (void)handleError:(NSError *)error { NSLog(@"An error occured: %@, %@", error, error.domain); - id tracker = [GAI sharedInstance].defaultTracker; - [tracker send:[[GAIDictionaryBuilder createExceptionWithDescription:[error description] - withFatal:NO] build]]; if (errorDialogShown) return; @@ -157,12 +187,15 @@ - (void)handleError:(NSError *)error if (!message) message = @"Er trad een onbekende fout op."; // Try to improve the error message - if ([error.domain isEqual:RKErrorDomain]) { + if ([error.domain isEqual:NSURLErrorDomain]) { title = @"Netwerkfout"; message = @"Er trad een fout op het bij het ophalen van externe informatie. " "Gelieve later opnieuw te proberen."; } - else if ([error.domain isEqual:FacebookSDKDomain]) { + else if ([error.domain containsString:@"com.facebook"]) { + return; // hide facebook errors + } + /*else if ([error.domain isEqual:FacebookSDKDomain]) { title = @"Facebook"; switch (error.code) { case FBErrorLoginFailedOrCancelled: @@ -181,7 +214,7 @@ - (void)handleError:(NSError *)error message = @"Er trad een onbekende fout op."; break; } - } + }*/ // Show an alert errorDialogShown = true; @@ -195,4 +228,32 @@ - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger) errorDialogShown = false; } +- (void) resetApp { + [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + NSFileManager *fileMgr = [[NSFileManager alloc] init]; + NSError *error = nil; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSArray *files = [fileMgr contentsOfDirectoryAtPath:documentsDirectory error:nil]; + + while (files.count > 0) { + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSArray *directoryContents = [fileMgr contentsOfDirectoryAtPath:documentsDirectory error:&error]; + if (error == nil) { + for (NSString *path in directoryContents) { + NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:path]; + BOOL removeSuccess = [fileMgr removeItemAtPath:fullPath error:&error]; + files = [fileMgr contentsOfDirectoryAtPath:documentsDirectory error:nil]; + if (!removeSuccess) { + // Error + } + } + } else { + // Error + } + } +} + @end diff --git a/Hydra/Artist.swift b/Hydra/Artist.swift new file mode 100644 index 0000000..c89d395 --- /dev/null +++ b/Hydra/Artist.swift @@ -0,0 +1,62 @@ +// +// Artist.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 10/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class Artist: NSObject, NSCoding, Mappable { + var name: String = "" + var start: NSDate = NSDate(timeIntervalSince1970: 0) + var end: NSDate = NSDate(timeIntervalSince1970: 0) + var picture: String? + + required init?(coder aDecoder: NSCoder) { + guard let name = aDecoder.decodeObjectForKey(name) as? String, + let start = aDecoder.decodeObjectForKey(PropertyKey.startKey) as? NSDate, + let end = aDecoder.decodeObjectForKey(PropertyKey.endKey) as? NSDate else { + return nil + } + + self.name = name + self.start = start + self.end = end + + picture = aDecoder.decodeObjectForKey(PropertyKey.pictureKey) as? String + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(name, forKey: PropertyKey.nameKey) + aCoder.encodeObject(start, forKey: PropertyKey.startKey) + aCoder.encodeObject(end, forKey: PropertyKey.endKey) + aCoder.encodeObject(picture, forKey: PropertyKey.pictureKey) + } + + required init?(_ map: Map) { + + } + + func mapping(map: Map) { + let formatter = NSDateFormatter() + formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) + formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)! + let dateTransform = DateFormatterTransform(dateFormatter: formatter) + name <- map["artist"] + start <- (map[PropertyKey.startKey], dateTransform) + end <- (map[PropertyKey.endKey], dateTransform) + picture <- map[PropertyKey.pictureKey] + } + + struct PropertyKey { + static let nameKey = "name" + static let startKey = "start" + static let endKey = "end" + static let pictureKey = "picture" + } +} \ No newline at end of file diff --git a/Hydra/Association.h b/Hydra/Association.h deleted file mode 100644 index 169906a..0000000 --- a/Hydra/Association.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// Association.h -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class RKObjectMapping; - -@interface Association : NSObject - -@property (nonatomic, strong) NSString *displayName; -@property (nonatomic, strong) NSString *fullName; -@property (nonatomic, strong) NSString *internalName; -@property (nonatomic, strong) NSString *parentAssociation; -@property (nonatomic, strong, readonly) NSString *displayedFullName; - -// Check that the current association list is up-to-date with the one provided -// in the application bundle -+ (RKObjectMapping *)objectMapping; -+ (RKObjectMapping *)objectMappingActivities; - -- (BOOL)matches:(NSString *)query; - -@end \ No newline at end of file diff --git a/Hydra/Association.m b/Hydra/Association.m deleted file mode 100644 index 13ab061..0000000 --- a/Hydra/Association.m +++ /dev/null @@ -1,106 +0,0 @@ -// -// Association.m -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "Association.h" -#import "NSDate+Utilities.h" -#import - -@implementation Association - -- (NSString *)displayedFullName -{ - if (_fullName) { - return [NSString stringWithFormat:@"%@ (%@)", _displayName, _fullName]; - } - else { - return _displayName; - } -} - -- (NSString *)fullName -{ - if (_fullName) { - return _fullName; - } - else { - return _displayName; - } -} - -- (BOOL)matches:(NSString *)query -{ - NSStringCompareOptions opts = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch; - return (_internalName && [_internalName rangeOfString:query options:opts].location != NSNotFound) || - (_displayName && [_displayName rangeOfString:query options:opts].location != NSNotFound) || - (_fullName && [_fullName rangeOfString:query options:opts].location != NSNotFound); -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", self.displayName]; -} - -+ (RKObjectMapping *)objectMappingActivities -{ - RKObjectMapping *mapping = [RKObjectMapping mappingForClass:self]; - [mapping addAttributeMappingsFromDictionary:@{ - @"internal_name": @"internalName", - @"full_name": @"fullName", - @"display_name": @"displayName" - }]; - return mapping; -} - -+ (RKObjectMapping *)objectMapping -{ - RKObjectMapping *mapping = [RKObjectMapping mappingForClass:self]; - [mapping addAttributeMappingsFromArray:@[ - @"displayName", @"fullName", @"internalName", @"parentAssociation" - ]]; - return mapping; -} - -#pragma mark - NSCoding - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - _displayName = [coder decodeObjectForKey:@"displayName"]; - _fullName = [coder decodeObjectForKey:@"fullName"]; - _internalName = [coder decodeObjectForKey:@"internalName"]; - _parentAssociation = [coder decodeObjectForKey:@"parentAssociation"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:_displayName forKey:@"displayName"]; - [coder encodeObject:_fullName forKey:@"fullName"]; - [coder encodeObject:_internalName forKey:@"internalName"]; - [coder encodeObject:_parentAssociation forKey:@"parentAssociation"]; -} - -#pragma mark - NSCopying - -- (NSUInteger)hash -{ - return [self.internalName hash]; -} - -- (BOOL)isEqual:(id)object -{ - return [object isKindOfClass:[self class]] && [self.internalName isEqual:[object internalName]]; -} - -- (id)copyWithZone:(NSZone *)zone -{ - return self; -} - -@end \ No newline at end of file diff --git a/Hydra/Association.swift b/Hydra/Association.swift new file mode 100644 index 0000000..f101e99 --- /dev/null +++ b/Hydra/Association.swift @@ -0,0 +1,95 @@ +// +// Association.swift +// +// Created by Feliciaan De Palmenaer on 28/02/2016 +// Copyright (c) . All rights reserved. +// + +import Foundation +import ObjectMapper + +class Association: NSObject, NSCoding, Mappable { + + // MARK: Properties + var internalName: String + var displayName: String + var parentAssociation: String? + var fullName: String? + + var displayedFullName: String { + get { + if let fullName = fullName { + return fullName + } + return displayName + } + } + + override var description: String { + get { + return "Association: \(self.internalName)" + } + } + + init(internalName: String, displayName: String) { + self.internalName = internalName + self.displayName = displayName + } + + + // MARK: ObjectMapper Initalizers + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + required convenience init?(_ map: Map){ + // Give empty values, because they will get filled + self.init(internalName: "", displayName: "") + } + + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + func mapping(map: Map) { + internalName <- map[PropertyKey.associationInternalNameKey] + displayName <- map[PropertyKey.associationDisplayNameKey] + parentAssociation <- map[PropertyKey.associationParentAssociationKey] + fullName <- map[PropertyKey.associationFullNameKey] + + } + + // MARK: NSCoding Protocol + required init(coder aDecoder: NSCoder) { + self.internalName = aDecoder.decodeObjectForKey(PropertyKey.associationInternalNameKey) as! String + self.displayName = aDecoder.decodeObjectForKey(PropertyKey.associationDisplayNameKey) as! String + self.parentAssociation = aDecoder.decodeObjectForKey(PropertyKey.associationParentAssociationKey) as? String + self.fullName = aDecoder.decodeObjectForKey(PropertyKey.associationFullNameKey) as? String + + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(internalName, forKey: PropertyKey.associationInternalNameKey) + aCoder.encodeObject(displayName, forKey: PropertyKey.associationDisplayNameKey) + aCoder.encodeObject(parentAssociation, forKey: PropertyKey.associationParentAssociationKey) + aCoder.encodeObject(fullName, forKey: PropertyKey.associationFullNameKey) + } + + func matches(query: String) -> Bool { + if internalName.contains(query) || displayName.contains(query) { + return true + } + if let fullName = fullName where fullName.contains(query) { + return true + } + return false + } + + struct PropertyKey { + // MARK: Declaration for string constants to be used to decode and also serialize. + static let associationInternalNameKey: String = "internal_name" + static let associationDisplayNameKey: String = "display_name" + static let associationParentAssociationKey: String = "parent_association" + static let associationFullNameKey: String = "full_name" + } +} diff --git a/Hydra/AssociationActivity.h b/Hydra/AssociationActivity.h deleted file mode 100644 index 9959de0..0000000 --- a/Hydra/AssociationActivity.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// AssociationActivity.h -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class RKObjectMapping, Association, FacebookEvent; - -@interface AssociationActivity : NSObject - -@property (nonatomic, strong) NSString *title; -@property (nonatomic, strong) Association *association; -@property (nonatomic, strong) NSDate *start; -@property (nonatomic, strong) NSDate *end; -@property (nonatomic, strong) NSString *location; -@property (nonatomic, assign) double latitude; -@property (nonatomic, assign) double longitude; -@property (nonatomic, strong) NSString *descriptionText; -@property (nonatomic, strong) NSString *url; -@property (nonatomic, strong) NSArray *categories; -@property (nonatomic, strong) NSString *facebookId; -@property (nonatomic, assign) BOOL highlighted; - -@property (nonatomic, strong) FacebookEvent *facebookEvent; - -+ (RKObjectMapping *)objectMapping; -- (BOOL)hasCoordinates; -- (BOOL)hasFacebookEvent; - -@end diff --git a/Hydra/AssociationActivity.m b/Hydra/AssociationActivity.m deleted file mode 100644 index 0afbb53..0000000 --- a/Hydra/AssociationActivity.m +++ /dev/null @@ -1,101 +0,0 @@ -// -// AssociationActivity.m -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "AssociationActivity.h" -#import "Association.h" -#import "NSDate+Utilities.h" -#import "FacebookEvent.h" -#import - -@interface AssociationActivity () - -@end - -@implementation AssociationActivity - -+ (RKObjectMapping *)objectMapping -{ - RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:self]; - [objectMapping addAttributeMappingsFromArray:@[ - @"title", @"location", @"start", @"end", @"url", - @"latitude", @"longitude", @"categories", @"highlighted" - ]]; - [objectMapping addAttributeMappingsFromDictionary:@{ - @"facebook_id": @"facebookId", - @"description": @"descriptionText" - }]; - [objectMapping addRelationshipMappingWithSourceKeyPath:@"association" - mapping:[Association objectMappingActivities]]; - - return objectMapping; -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", - self.title, self.association.displayName, self.start]; -} - -- (FacebookEvent *)facebookEvent -{ - if(!_facebookEvent && [self.facebookId length] > 0) { - _facebookEvent = [[FacebookEvent alloc] initWithEventId:self.facebookId]; - } - return _facebookEvent; -} - -- (BOOL)hasCoordinates -{ - return self.latitude != 0 && self.longitude != 0; -} - -- (BOOL)hasFacebookEvent -{ - return _facebookEvent && _facebookEvent.valid; -} - -#pragma mark - NSCoding - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - self.title = [coder decodeObjectForKey:@"title"]; - self.association = [coder decodeObjectForKey:@"association"]; - self.start = [coder decodeObjectForKey:@"start"]; - self.end = [coder decodeObjectForKey:@"end"]; - self.location = [coder decodeObjectForKey:@"location"]; - self.longitude = [coder decodeDoubleForKey:@"longitude"]; - self.latitude = [coder decodeDoubleForKey:@"latitude"]; - self.facebookId = [coder decodeObjectForKey:@"facebookId"]; - self.descriptionText = [coder decodeObjectForKey:@"descriptionText"]; - self.url = [coder decodeObjectForKey:@"url"]; - self.categories = [coder decodeObjectForKey:@"categories"]; - self.highlighted = [coder decodeBoolForKey:@"highlighted"]; - self.facebookEvent = [coder decodeObjectForKey:@"facebookEvent"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.title forKey:@"title"]; - [coder encodeObject:self.association forKey:@"association"]; - [coder encodeObject:self.start forKey:@"start"]; - [coder encodeObject:self.end forKey:@"end"]; - [coder encodeObject:self.location forKey:@"location"]; - [coder encodeDouble:self.longitude forKey:@"longitude"]; - [coder encodeDouble:self.latitude forKey:@"latitude"]; - [coder encodeObject:self.facebookId forKey:@"facebookId"]; - [coder encodeObject:self.descriptionText forKey:@"descriptionText"]; - [coder encodeObject:self.url forKey:@"url"]; - [coder encodeObject:self.categories forKey:@"categories"]; - [coder encodeBool:self.highlighted forKey:@"highlighted"]; - [coder encodeObject:_facebookEvent forKey:@"facebookEvent"]; -} - -@end diff --git a/Hydra/AssociationNewsItem.h b/Hydra/AssociationNewsItem.h deleted file mode 100644 index 0b85844..0000000 --- a/Hydra/AssociationNewsItem.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// AssociationNewsItem.h -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class RKObjectMapping, Association; - -@interface AssociationNewsItem : NSObject - -@property (nonatomic, assign) NSInteger itemId; -@property (nonatomic, strong) NSString *title; -@property (nonatomic, strong) Association *association; -@property (nonatomic, strong) NSDate *date; -@property (nonatomic, strong) NSString *content; -@property (nonatomic, assign) BOOL highlighted; -@property (nonatomic, assign) BOOL read; - -+ (RKObjectMapping *)objectMapping; - -@end diff --git a/Hydra/AssociationNewsItem.m b/Hydra/AssociationNewsItem.m deleted file mode 100644 index d584ccf..0000000 --- a/Hydra/AssociationNewsItem.m +++ /dev/null @@ -1,68 +0,0 @@ -// -// AssociationNewsItem.m -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "AssociationNewsItem.h" -#import "Association.h" -#import "AssociationStore.h" -#import - -@implementation AssociationNewsItem - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", self.title]; -} - -+ (RKObjectMapping *)objectMapping -{ - RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:self]; - [objectMapping addAttributeMappingsFromArray:@[@"title", @"date", @"content", @"highlighted"]]; - [objectMapping addAttributeMappingsFromDictionary:@{@"id": @"itemId"}]; - [objectMapping addRelationshipMappingWithSourceKeyPath:@"association" mapping:[Association objectMappingActivities]]; - - return objectMapping; -} - -#pragma mark - NSCoding - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - _itemId = [coder decodeIntegerForKey:@"itemId"]; - _title = [coder decodeObjectForKey:@"title"]; - _association = [coder decodeObjectForKey:@"association"]; - _date = [coder decodeObjectForKey:@"date"]; - _content = [coder decodeObjectForKey:@"content"]; - _highlighted = [coder decodeBoolForKey:@"highlighted"]; - _read = [coder decodeBoolForKey:@"read"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeInteger:self.itemId forKey:@"itemId"]; - [coder encodeObject:self.title forKey:@"title"]; - [coder encodeObject:self.association forKey:@"association"]; - [coder encodeObject:self.date forKey:@"date"]; - [coder encodeObject:self.content forKey:@"content"]; - [coder encodeBool:self.highlighted forKey:@"highlighted"]; - [coder encodeBool:self.read forKey:@"read"]; -} - -#pragma mark - Properties - -- (void)setRead:(BOOL)read -{ - if (read != _read) { - _read = read; - [[AssociationStore sharedStore] markStorageOutdated]; - } -} - -@end \ No newline at end of file diff --git a/Hydra/AssociationPreferenceController.m b/Hydra/AssociationPreferenceController.m index 027322d..38575f2 100644 --- a/Hydra/AssociationPreferenceController.m +++ b/Hydra/AssociationPreferenceController.m @@ -7,17 +7,15 @@ // #import "AssociationPreferenceController.h" -#import "Association.h" -#import "AssociationStore.h" -#import "PreferencesService.h" +#import "Hydra-Swift.h" -@interface AssociationPreferenceController () +@interface AssociationPreferenceController () @property (nonatomic, strong) NSArray *convents; @property (nonatomic, strong) NSDictionary *associations; @property (nonatomic, strong) NSMutableArray *filteredConvents; @property (nonatomic, strong) NSMutableDictionary *filteredAssociations; -@property (nonatomic, strong) UISearchDisplayController *searchController; +@property (nonatomic, strong) UISearchController *searchController; @end @@ -35,16 +33,14 @@ - (void)loadView { [super loadView]; - UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)]; - searchBar.placeholder = @"Zoek een vereniging"; - self.tableView.tableHeaderView = searchBar; - - // TODO: replace cancel button in search-mode by 'OK' - self.searchController = [[UISearchDisplayController alloc] - initWithSearchBar:searchBar contentsController:self]; - self.searchController.delegate = self; - self.searchController.searchResultsDataSource = self; - self.searchController.searchResultsDelegate = self; + self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil]; + self.searchController.searchResultsUpdater = self; + self.searchController.dimsBackgroundDuringPresentation = NO; + self.searchController.searchBar.placeholder = @"Zoek een vereniging"; + self.tableView.tableHeaderView = self.searchController.searchBar; + + // Set UISearchBar button text, normally "Cancel" + [[UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil] setTitle:@"OK"]; } - (void)viewDidLoad @@ -59,6 +55,11 @@ - (void)viewDidAppear:(BOOL)animated GAI_Track(@"Voorkeuren > Verenigingen"); } +- (void)viewDidDisappear:(BOOL)animated +{ + [[NSNotificationCenter defaultCenter] postNotificationName:@"PreferencesControllerDidUpdatePreference" object:nil]; +} + - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationPortrait); @@ -76,7 +77,7 @@ - (void)loadAssociations // Group by parentAssociation NSMutableDictionary *grouped = [[NSMutableDictionary alloc] init]; - sort = [NSSortDescriptor sortDescriptorWithKey:@"fullName" ascending:YES]; + sort = [NSSortDescriptor sortDescriptorWithKey:@"displayedFullName" ascending:YES]; for (NSString *name in self.convents) { NSPredicate *pred = [NSPredicate predicateWithFormat:@"%K == %@", @"parentAssociation", name]; @@ -88,16 +89,16 @@ - (void)loadAssociations } #pragma mark - Search Control Delegate - -- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)query +- (void)updateSearchResultsForSearchController:(UISearchController *)searchController { - if (query.length > 0) { + NSString *searchString = searchController.searchBar.text; + if (searchString.length > 0) { for(NSString *convent in [self.associations allKeys]) { NSPredicate *filter = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { - return [evaluatedObject matches:query]; + return [evaluatedObject matches:searchString]; }]; self.filteredAssociations[convent] = [self.associations[convent] filteredArrayUsingPredicate:filter]; - + // Remove convent from list if it does not have any items if ([self.filteredAssociations[convent] count] == 0) { [self.filteredConvents removeObject:convent]; @@ -115,12 +116,6 @@ - (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldRe self.filteredAssociations = [self.associations mutableCopy]; self.filteredConvents = [self.convents mutableCopy]; } - - return YES; -} - -- (void)searchDisplayController:(UISearchDisplayController *)controller willHideSearchResultsTableView:(UITableView *)tableView -{ [self.tableView reloadData]; } @@ -129,7 +124,7 @@ - (void)searchDisplayController:(UISearchDisplayController *)controller willHide - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *internalName; - if (tableView == self.tableView) { + if (!self.searchController.isActive) { internalName = self.convents[section]; } else { @@ -142,7 +137,7 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInte - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - if (tableView == self.tableView) { + if (!self.searchController.isActive) { return self.convents.count; } else { @@ -152,7 +147,7 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - if (tableView == self.tableView) { + if (!self.searchController.isActive) { return [self.associations[self.convents[section]] count]; } else { @@ -170,7 +165,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N } Association *association; - if (tableView == self.tableView) { + if (!self.searchController.isActive) { NSString *convent = self.convents[indexPath.section]; association = self.associations[convent][indexPath.row]; } @@ -178,8 +173,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N NSString *convent = self.filteredConvents[indexPath.section]; association = self.filteredAssociations[convent][indexPath.row]; } - cell.textLabel.text = association.fullName; - + cell.textLabel.text = association.displayedFullName; + NSArray *preferred = [PreferencesService sharedService].preferredAssociations; if ([preferred containsObject:association.internalName]){ cell.accessoryType = UITableViewCellAccessoryCheckmark; @@ -194,7 +189,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSString *name; - if (tableView == self.tableView) { + if (!self.searchController.isActive) { NSString *convent = self.convents[indexPath.section]; name = [self.associations[convent][indexPath.row] internalName]; } diff --git a/Hydra/AssociationStore.h b/Hydra/AssociationStore.h deleted file mode 100644 index d352668..0000000 --- a/Hydra/AssociationStore.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// AssociationStore.h -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class Association; - -extern NSString *const AssociationStoreDidUpdateNewsNotification; -extern NSString *const AssociationStoreDidUpdateActivitiesNotification; - -@interface AssociationStore : NSObject - -@property (nonatomic, strong, readonly) NSArray *associations; -@property (nonatomic, strong, readonly) NSArray *activities; -@property (nonatomic, strong, readonly) NSArray *newsItems; - -+ (AssociationStore *)sharedStore; - -- (Association *)associationWithName:(NSString *)internalName; -- (void)reloadNewsItems; -- (void)reloadActivities; - -- (void)syncStorage; -- (void)markStorageOutdated; - -@end \ No newline at end of file diff --git a/Hydra/AssociationStore.m b/Hydra/AssociationStore.m deleted file mode 100644 index f899ed3..0000000 --- a/Hydra/AssociationStore.m +++ /dev/null @@ -1,358 +0,0 @@ -// -// AssociationStore.m -// Hydra -// -// Created by Pieter De Baets on 21/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "AssociationStore.h" -#import "Association.h" -#import "AssociationActivity.h" -#import "AssociationNewsItem.h" -#import "AppDelegate.h" -#import "FacebookEvent.h" -#import - -#define kBaseUrl @"http://student.ugent.be/hydra/api/1.0/" -#define kActivitiesResource @"all_activities.json" -#define kNewsResource @"all_news.json" -#define kAssociationResource @"associations.json" -#define kUpdateInterval (15 * 60) - -NSString *const AssociationStoreDidUpdateNewsNotification = -@"AssociationStoreDidUpdateNewsNotification"; -NSString *const AssociationStoreDidUpdateActivitiesNotification = -@"AssociationStoreDidUpdateActivitiesNotification"; - -@interface AssociationStore () - -@property (nonatomic, strong) NSDictionary *associationLookup; -@property (nonatomic, strong) NSArray *associations; -@property (nonatomic, strong) NSArray *newsItems; -@property (nonatomic, strong) NSArray *activities; - -@property (nonatomic, strong) NSDate *newsLastUpdated; -@property (nonatomic, strong) NSDate *activitiesLastUpdated; -@property (nonatomic, strong) NSDate *associationsLastUpdated; - -@property (nonatomic, strong) RKObjectManager *objectManager; -@property (nonatomic, strong) NSMutableArray *activeRequests; - -@property (nonatomic, assign) BOOL storageOutdated; - -@end - -@implementation AssociationStore - -+ (AssociationStore *)sharedStore -{ - static AssociationStore *sharedInstance = nil; - if (!sharedInstance) { - // Try restoring the store from archive - @try { - sharedInstance = [NSKeyedUnarchiver unarchiveObjectWithFile:self.storeCachePath]; - } - @catch (NSException *exception) { - NSLog(@"Got exception while reading Associations archive: %@", exception); - } - @finally { - if (!sharedInstance) sharedInstance = [[AssociationStore alloc] init]; - } - } - return sharedInstance; -} - -- (id)init -{ - self = [super init]; - if (self) { - [self sharedInit]; - } - return self; -} - -- (void)sharedInit -{ - self.activeRequests = [[NSMutableArray alloc] init]; - self.objectManager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:kBaseUrl]]; - - // Listen for facebook-updates to activities - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center addObserver:self selector:@selector(facebookEventUpdated:) - name:FacebookEventDidUpdateNotification object:nil]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Caching - -- (id)initWithCoder:(NSCoder *)decoder -{ - if (self = [super init]) { - _associations = [decoder decodeObjectForKey:@"associations"]; - AssertClassOrNil(_associations, NSArray); - _associationsLastUpdated = [decoder decodeObjectForKey:@"associationsLastUpdated"]; - AssertClassOrNil(_associationsLastUpdated, NSDate); - - _newsItems = [decoder decodeObjectForKey:@"newsItems"]; - AssertClassOrNil(_newsItems, NSArray); - _newsLastUpdated = [decoder decodeObjectForKey:@"newsLastUpdated"]; - AssertClassOrNil(_newsLastUpdated, NSDate); - - _activities = [decoder decodeObjectForKey:@"activities"]; - AssertClassOrNil(_activities, NSArray); - _activitiesLastUpdated = [decoder decodeObjectForKey:@"activitiesLastUpdated"]; - AssertClassOrNil(_activitiesLastUpdated, NSDate); - - [self sharedInit]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:_associationsLastUpdated forKey:@"associationsLastUpdated"]; - [coder encodeObject:_associations forKey:@"associations"]; - [coder encodeObject:_newsLastUpdated forKey:@"newsLastUpdated"]; - [coder encodeObject:_newsItems forKey:@"newsItems"]; - [coder encodeObject:_activitiesLastUpdated forKey:@"activitiesLastUpdated"]; - [coder encodeObject:_activities forKey:@"activities"]; -} - -+ (NSString *)storeCachePath -{ - // Get cache directory - NSArray *cacheDirectories = - NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString *cacheDirectory = cacheDirectories[0]; - - return [cacheDirectory stringByAppendingPathComponent:@"association.archive"]; -} - -- (void)syncStorage -{ - if (!self.storageOutdated) { - return; - } - - // Immediately mark the cache as being updated, as this is an async operation - self.storageOutdated = NO; - - dispatch_queue_t async = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(async, ^{ - [NSKeyedArchiver archiveRootObject:self toFile:self.class.storeCachePath]; - }); -} - -- (void)markStorageOutdated -{ - self.storageOutdated = YES; -} - -#pragma mark - Accessors - -- (Association *)associationWithName:(NSString *)internalName -{ - if (self.associationLookup == nil) { - [self createAssociationsLookup]; - } - Association *association = self.associationLookup[internalName]; - - // If the association is unknown, just give a fake record - if (!association) { - association = [[Association alloc] init]; - association.internalName = internalName; - association.displayName = internalName; - } - - return association; -} - -- (NSArray *)associations -{ - [self _updateResource:kAssociationResource lastUpdated:self.associationsLastUpdated - objectMapping:[Association objectMapping]]; - return _associations; -} - -- (NSArray *)activities -{ - [self _updateResource:kActivitiesResource lastUpdated:self.activitiesLastUpdated - objectMapping:[AssociationActivity objectMapping]]; - return _activities; -} - -- (NSArray *)newsItems -{ - [self _updateResource:kNewsResource lastUpdated:self.newsLastUpdated - objectMapping:[AssociationNewsItem objectMapping]]; - return _newsItems; -} - -- (void)reloadActivities -{ - // Force reload, remove cache-entry - [[NSURLCache sharedURLCache] removeAllCachedResponses]; - [self _updateResource:kActivitiesResource lastUpdated:nil - objectMapping:[AssociationActivity objectMapping]]; - [self reloadAssociations]; -} - -- (void)reloadNewsItems -{ - [[NSURLCache sharedURLCache] removeAllCachedResponses]; - [self _updateResource:kNewsResource lastUpdated:nil - objectMapping:[AssociationNewsItem objectMapping]]; - [self reloadAssociations]; -} - -- (void)reloadAssociations -{ - [self _updateResource:kAssociationResource lastUpdated:nil - objectMapping:[Association objectMapping]]; -} - -#pragma mark - RestKit Object loading - -- (void)_updateResource:(NSString *)resource lastUpdated:(NSDate *)lastUpdated objectMapping:(RKObjectMapping *)mapping -{ - DLog(@"updateResource %@ (last: %@ => %f)", resource, lastUpdated, [lastUpdated timeIntervalSinceNow]); - - // Check if an update is required - if (lastUpdated && [lastUpdated timeIntervalSinceNow] > -kUpdateInterval) { - return; - } - - // Already working on request - if ([self.activeRequests containsObject:resource]) { - return; - } - - DLog(@"Updating %@", resource); - [self.activeRequests addObject:resource]; - [self.objectManager addResponseDescriptor: - [RKResponseDescriptor responseDescriptorWithMapping:mapping - method:RKRequestMethodGET - pathPattern:resource - keyPath:nil - statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]; - [self.objectManager getObjectsAtPath:resource - parameters:nil - success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { - [self _processResult:mappingResult forResource:resource]; - } - failure:^(RKObjectRequestOperation *operation, NSError *error) { - [self _processError:error forResource:resource]; - }]; -} - -- (void)_processResult:(RKMappingResult *)mappingResult forResource:(NSString *)resource -{ - NSString *notification = nil; - NSArray *objects = [mappingResult array]; - - // Received some NewsItems - if ([resource isEqualToString:kNewsResource]) { - NSMutableSet *readItems = [NSMutableSet set]; - // Using direct access because accessors reload the data - for (AssociationNewsItem *item in _newsItems) { - if (item.read) { - [readItems addObject:@(item.itemId)]; - } - } - for (AssociationNewsItem *item in objects) { - if ([readItems containsObject:@(item.itemId)]) { - item.read = YES; - } - } - self.newsItems = objects; - self.newsLastUpdated = [NSDate date]; - notification = AssociationStoreDidUpdateNewsNotification; - } - // Received Activities - else if ([resource isEqualToString:kActivitiesResource]) { - NSMutableDictionary *availableEvents = [NSMutableDictionary dictionary]; - // Using direct access because accessors reload the data - for (AssociationActivity *activity in _activities) { - if ([activity hasFacebookEvent]) { - availableEvents[activity.facebookId] = activity; - } - } - for (AssociationActivity *activity in objects) { - if ([availableEvents objectForKey:activity.facebookId]) { - AssociationActivity *oldActivity = availableEvents[activity.facebookId]; - activity.facebookEvent = oldActivity.facebookEvent; - } - } - self.activities = objects; - self.activitiesLastUpdated = [NSDate date]; - notification = AssociationStoreDidUpdateActivitiesNotification; - } - else if ([resource isEqualToString:kAssociationResource]) { - self.associations = objects; - [self createAssociationsLookup]; - } - - [self markStorageOutdated]; - [self syncStorage]; - - // Send notification - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:notification object:self userInfo:nil]; - - [self.activeRequests removeObject:resource]; -} - -- (void)_processError:(NSError *)error forResource:(NSString *)resource -{ - NSLog(@"Updating resource %@ failed: %@", resource, error); - AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [app handleError:error]; - - NSString *notification = nil; - if ([resource isEqualToString:kNewsResource]) { - notification = AssociationStoreDidUpdateNewsNotification; - } - else if ([resource isEqualToString:kActivitiesResource]) { - notification = AssociationStoreDidUpdateActivitiesNotification; - } - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:notification object:self userInfo:nil]; - - // Only clear the request after 10 seconds, to prevent failed requests - // restarting due to related succesful requests - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { - [self.activeRequests removeObject:resource]; - }); -} - -#pragma mark - Utility functions -- (void)createAssociationsLookup -{ - NSMutableDictionary *associationsLookup = [NSMutableDictionary dictionary]; - for (Association *association in self.associations) { - associationsLookup[association.internalName] = association; - } - - self.associationLookup = associationsLookup; -} - -#pragma mark - Notifications - -- (void)facebookEventUpdated:(NSNotification *)notification -{ - [self markStorageOutdated]; - - // Call method in 10 seconds so multiple changes are written at once - [[self class] cancelPreviousPerformRequestsWithTarget:self - selector:@selector(syncStorage) - object:nil]; - [self performSelector:@selector(syncStorage) withObject:nil afterDelay:10]; -} - -@end \ No newline at end of file diff --git a/Hydra/AssociationStore.swift b/Hydra/AssociationStore.swift index f058d23..6422174 100644 --- a/Hydra/AssociationStore.swift +++ b/Hydra/AssociationStore.swift @@ -1,69 +1,265 @@ // // AssociationStore.swift -// Hydra +// OAuthTest // -// Created by Feliciaan De Palmenaer on 20/11/2015. -// Copyright © 2015 Zeus WPI. All rights reserved. +// Created by Feliciaan De Palmenaer on 27/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. // import Foundation +import Alamofire +import ObjectMapper +import AlamofireObjectMapper +let AssociationStoreDidUpdateNewsNotification = "AssociationStoreDidUpdateNewsNotification" +let AssociationStoreDidUpdateActivitiesNotification = "AssociationStoreDidUpdateActivitiesNotification" +let AssociationStoreDidUpdateAssociationsNotification = "AssociationStoreDidUpdateAssociationsNotification" + +class AssociationStore: SavableStore, NSCoding { + + private static var _SharedStore: AssociationStore? + static var sharedStore: AssociationStore { + get { + //TODO: make lazy, and catch NSKeyedUnarchiver errors + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let associationStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.AssociationStoreArchive.path!) as? AssociationStore + if let associationStore = associationStore { + _SharedStore = associationStore + return _SharedStore! + } + } + // initialize new one + _SharedStore = AssociationStore() + return _SharedStore! + } + } + + var associationLookup: [String: Association] + + private var _associations: [Association] + var associations: [Association] { + get { + self.reloadAssociations() + return self._associations + } + } + private var _activities: [Activity] + var activities: [Activity] { + get { + self.reloadActivities() + return self._activities + } + } + private var _newsItems: [NewsItem] + var newsItems: [NewsItem] { + get { + self.reloadNewsItems() + return self._newsItems + } + } + + var associationsLastUpdated: NSDate + var activitiesLastUpdated: NSDate + var newsLastUpdated: NSDate + + init() { + associationsLastUpdated = NSDate(timeIntervalSince1970: 0) + activitiesLastUpdated = NSDate(timeIntervalSince1970: 0) + newsLastUpdated = NSDate(timeIntervalSince1970: 0) + + associationLookup = [:] + _associations = [] + _activities = [] + _newsItems = [] + + super.init(storagePath: Config.AssociationStoreArchive.path!) + self.sharedInit() + } + + func sharedInit() { + let center = NSNotificationCenter.defaultCenter() + center.addObserver(self, selector: #selector(AssociationStore.facebookEventUpdated(_:)), name: FacebookEventDidUpdateNotification, object: nil) + } + + // MARK: NSCoding + required init?(coder aDecoder: NSCoder) { + guard let associations = aDecoder.decodeObjectForKey(PropertyKey.associationsKey) as? [Association], + let activities = aDecoder.decodeObjectForKey(PropertyKey.activitiesKey) as? [Activity], + let newsItems = aDecoder.decodeObjectForKey(PropertyKey.newsItemsKey) as? [NewsItem], + let associationsLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.associationsLastUpdatedKey) as? NSDate, + let activitiesLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.activitiesLastUpdatedKey) as? NSDate, + let newsLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.newsItemsLastUpdatedKey) as? NSDate else { + return nil + } + + self._associations = associations + self._activities = activities + self._newsItems = newsItems + self.associationsLastUpdated = associationsLastUpdated + self.activitiesLastUpdated = activitiesLastUpdated + self.newsLastUpdated = newsLastUpdated + + associationLookup = AssociationStore.createAssociationLookup(_associations) + + super.init(storagePath: Config.AssociationStoreArchive.path!) + self.sharedInit() + } + + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(_associations, forKey: PropertyKey.associationsKey) + aCoder.encodeObject(_activities, forKey: PropertyKey.activitiesKey) + aCoder.encodeObject(_newsItems, forKey: PropertyKey.newsItemsKey) + + aCoder.encodeObject(associationsLastUpdated, forKey: PropertyKey.associationsLastUpdatedKey) + aCoder.encodeObject(activitiesLastUpdated, forKey: PropertyKey.activitiesLastUpdatedKey) + aCoder.encodeObject(newsLastUpdated, forKey: PropertyKey.newsItemsLastUpdatedKey) + } + + + private static func createAssociationLookup(associations: [Association]) -> [String: Association] { + var associationsLookup = [String: Association]() + for association in associations { + associationsLookup[association.internalName] = association + } + return associationsLookup + } + + func associationWithName(internalName: String) -> Association? { + let association = associationLookup[internalName] + return association + } + + func reloadAssociations(forceUpdate: Bool = false) { + updateResource(APIConfig.DSA + "2.0/associations.json", + notificationName: AssociationStoreDidUpdateAssociationsNotification, + lastUpdated: self.associationsLastUpdated, + forceUpdate: forceUpdate) { (associations:[Association]) -> () in + print("Updating associations") + self._associations = associations + self.associationsLastUpdated = NSDate() + + self.associationLookup = AssociationStore.createAssociationLookup(associations) + } + } + + func reloadActivities(forceUpdate: Bool = false) { + updateResource(APIConfig.DSA + "2.0/all_activities.json", notificationName: AssociationStoreDidUpdateActivitiesNotification,lastUpdated: self.activitiesLastUpdated, forceUpdate: forceUpdate) { (activities: [Activity]) -> () in + print("Updating activities") + var facebookEvents: Dictionary = [:] + // cache all facebookEvents to dict + for activity in self._activities where activity.hasFacebookEvent() { + facebookEvents[activity.facebookId!] = activity.facebookEvent + } + + // add them to the new objects + for activity in activities where activity.facebookId != nil{ + if let facebookEvent = facebookEvents[activity.facebookId!] { + activity.facebookEvent = facebookEvent + } + } + self._activities = activities + self.activitiesLastUpdated = NSDate() + } + } + + func reloadNewsItems(forceUpdate: Bool = false) { + updateResource(APIConfig.DSA + "2.0/all_news.json", notificationName: AssociationStoreDidUpdateNewsNotification,lastUpdated: self.newsLastUpdated, forceUpdate: forceUpdate) { (newsItems:[NewsItem]) -> () in + print("Updating News Items") + let readItems = Set(self._newsItems.filter({ $0.read }).map({ $0.internalIdentifier})) + for item in newsItems { + if readItems.contains(item.internalIdentifier) { + item.read = true + } + } + + self._newsItems = newsItems + self.newsLastUpdated = NSDate() + } + } + + // MARK: notifications + func facebookEventUpdated(notification: NSNotification) { + self.markStorageOutdated() + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(10*Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + self.syncStorage() + } + } + + // MARK: field information struct + struct PropertyKey { + static let associationsKey = "associations" + static let activitiesKey = "activities" + static let newsItemsKey = "newsItems" + + static let associationsLastUpdatedKey = "associationsLastUpdated" + static let activitiesLastUpdatedKey = "activitiesLastUpdated" + static let newsItemsLastUpdatedKey = "newsItemsLastUpdated" + } +} + +// MARK: Implement FeedItemProtocol extension AssociationStore: FeedItemProtocol { func feedItems() -> [FeedItem] { return getActivities() + getNewsItems() } - + private func getActivities() -> [FeedItem] { var feedItems = [FeedItem]() - let preferencesService = PreferencesService.sharedService() - if let activities = activities as? [AssociationActivity] { - var filter: ((AssociationActivity) -> (Bool)) + let preferencesService = PreferencesService.sharedService + var filter: ((Activity) -> (Bool)) + if preferencesService.showActivitiesInFeed { if preferencesService.filterAssociations { let associations = preferencesService.preferredAssociations - filter = { activity in activity.highlighted || associations.contains { activity.association.internalName == ($0 as! String) } } + filter = { activity in activity.highlighted || associations.contains { activity.association.internalName == ($0) } } } else { - if preferencesService.showActivitiesInFeed { - filter = { _ in true } - } else { - filter = { $0.highlighted } - feedItems.append(FeedItem(itemType: .SettingsItem, object: nil, priority: 850)) - } + filter = { _ in true } } - - for activity in activities.filter(filter) { - // Force load facebookEvent - if let facebookEvent = activity.facebookEvent { - facebookEvent.update() - } - var priority = 999 //TODO: calculate priorities, with more options - priority -= activity.start.daysAfterDate(NSDate()) * 100 - if priority > 0 { - feedItems.append(FeedItem(itemType: .ActivityItem, object: activity, priority: priority)) - } + } else { + filter = { $0.highlighted } + feedItems.append(FeedItem(itemType: .AssociationsSettingsItem, object: nil, priority: 850)) + } + + for activity in activities.filter(filter) { + // Force load facebookEvent + if let facebookEvent = activity.facebookEvent { + facebookEvent.update() + } + var priority = 950 //TODO: calculate priorities, with more options + priority -= max(activity.start.hoursAfterDate(NSDate()), 0) + if priority > 0 { + feedItems.append(FeedItem(itemType: .ActivityItem, object: activity, priority: priority)) } } return feedItems } - + private func getNewsItems() -> [FeedItem] { var feedItems = [FeedItem]() - - if let newsItems = newsItems as? [AssociationNewsItem] { - for newsItem in newsItems { - var priority = 999 - let daysOld = newsItem.date.daysBeforeDate(NSDate()) - if newsItem.highlighted { - priority -= 25*daysOld - } else { - priority -= 90*daysOld - } - - if priority > 0 { - feedItems.append(FeedItem(itemType: .NewsItem, object: newsItem, priority: priority)) - } + var filter: ((NewsItem) -> (Bool)) + + if PreferencesService.sharedService.showNewsInFeed { + filter = { _ in true } + } else { + filter = { $0.highlighted } + } + + for newsItem in newsItems.filter(filter) { + var priority = 999 + let daysOld = newsItem.date.daysBeforeDate(NSDate()) + if newsItem.highlighted { + priority -= 25*daysOld + } else { + priority -= 90*daysOld + } + + if priority > 0 { + feedItems.append(FeedItem(itemType: .NewsItem, object: newsItem, priority: priority)) } } - + return feedItems } } \ No newline at end of file diff --git a/Hydra/CalendarItem.swift b/Hydra/CalendarItem.swift new file mode 100644 index 0000000..06bac0a --- /dev/null +++ b/Hydra/CalendarItem.swift @@ -0,0 +1,81 @@ +// +// Calendar.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 30/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +import ObjectMapper + +class CalendarItem: NSObject, Mappable, NSCoding { + var title: String = "" + var content: String? + var startDate: NSDate = NSDate(timeIntervalSince1970: 0) + var endDate: NSDate = NSDate(timeIntervalSince1970: 0) + var location: String? + var itemId: Int64 = 0 + var courseId: String = "" + var creator: String? + var created: NSDate = NSDate(timeIntervalSince1970: 0) + + var course: Course? { + get { + return MinervaStore.sharedStore.course(courseId) + } + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(self.title, forKey: PropertyKey.titleKey) + aCoder.encodeObject(self.content, forKey: PropertyKey.contentKey) + aCoder.encodeObject(self.startDate, forKey: PropertyKey.startDateKey) + aCoder.encodeObject(self.endDate, forKey: PropertyKey.endDateKey) + aCoder.encodeObject(self.location, forKey: PropertyKey.locationKey) + aCoder.encodeInt64(self.itemId, forKey: PropertyKey.itemIdKey) + aCoder.encodeObject(self.courseId, forKey: PropertyKey.courseIdKey) + aCoder.encodeObject(self.creator, forKey: PropertyKey.creatorKey) + aCoder.encodeObject(self.created, forKey: PropertyKey.createdKey) + } + + required init?(_ map: Map) { + + } + + required init?(coder aDecoder: NSCoder) { + self.title = aDecoder.decodeObjectForKey(PropertyKey.titleKey) as! String + self.content = aDecoder.decodeObjectForKey(PropertyKey.contentKey) as? String + self.startDate = aDecoder.decodeObjectForKey(PropertyKey.startDateKey) as! NSDate + self.endDate = aDecoder.decodeObjectForKey(PropertyKey.endDateKey) as! NSDate + self.location = aDecoder.decodeObjectForKey(PropertyKey.locationKey) as? String + self.itemId = aDecoder.decodeInt64ForKey(PropertyKey.itemIdKey) + self.courseId = aDecoder.decodeObjectForKey(PropertyKey.courseIdKey) as! String + self.creator = aDecoder.decodeObjectForKey(PropertyKey.creatorKey) as? String + self.created = aDecoder.decodeObjectForKey(PropertyKey.createdKey) as! NSDate + } + + func mapping(map: Map) { + self.title <- map[PropertyKey.titleKey] + self.content <- map[PropertyKey.contentKey] + self.startDate <- (map[PropertyKey.startDateKey], ISO8601DateTransform()) + self.endDate <- (map[PropertyKey.endDateKey], ISO8601DateTransform()) + self.location <- map[PropertyKey.locationKey] + self.itemId <- map[PropertyKey.itemIdKey] + self.courseId <- map[PropertyKey.courseIdKey] + self.creator <- map[PropertyKey.creatorKey] + self.created <- (map[PropertyKey.createdKey], ISO8601DateTransform()) + } + + struct PropertyKey { + static let titleKey = "title" + static let contentKey = "content" + static let startDateKey = "start_date" + static let endDateKey = "end_date" + static let locationKey = "location" + static let itemIdKey = "item_id" + static let courseIdKey = "course_id" + static let creatorKey = "last_edit_user" + static let createdKey = "last_edit_time" + } +} \ No newline at end of file diff --git a/Hydra/CalendarSingleTableViewCell.swift b/Hydra/CalendarSingleTableViewCell.swift new file mode 100644 index 0000000..a0799a6 --- /dev/null +++ b/Hydra/CalendarSingleTableViewCell.swift @@ -0,0 +1,60 @@ +// +// CalendarViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 16/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +class CalendarSingleTableViewCell: UITableViewCell { + + @IBOutlet weak var startTimeLabel: UILabel? + @IBOutlet weak var endTimeLabel: UILabel? + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var courseLabel: UILabel! + @IBOutlet weak var locationLabel: UILabel! + + let shortDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + + override func awakeFromNib() { + super.awakeFromNib() + shortDateFormatter.timeStyle = .ShortStyle + shortDateFormatter.dateStyle = .NoStyle + } + var calendarItem: CalendarItem? { + didSet { + if let calendarItem = calendarItem { + startTimeLabel?.text = shortDateFormatter.stringFromDate(calendarItem.startDate) + endTimeLabel?.text = shortDateFormatter.stringFromDate(calendarItem.endDate) + + titleLabel.text = calendarItem.title + courseLabel.text = calendarItem.course?.title + locationLabel.text = calendarItem.location + if calendarItem.content != nil { + self.accessoryType = .DisclosureIndicator + } else { + self.accessoryType = .None + } + } + } + } + + var activity: Activity? { + didSet { + if let activity = activity { + startTimeLabel?.text = shortDateFormatter.stringFromDate(activity.start) + if let end = activity.end { + endTimeLabel?.text = shortDateFormatter.stringFromDate(end) + } else { + endTimeLabel?.text = "" + } + + titleLabel.text = activity.title + courseLabel.text = activity.association.displayName + locationLabel.text = activity.location + + self.accessoryType = .DisclosureIndicator + } + } + } +} diff --git a/Hydra/CalendarViewController.swift b/Hydra/CalendarViewController.swift new file mode 100644 index 0000000..f83e3e3 --- /dev/null +++ b/Hydra/CalendarViewController.swift @@ -0,0 +1,348 @@ +// +// CalendarViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 14/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit +import CVCalendar + +class CalendarViewController: UIViewController { + @IBOutlet weak var menuView: CVCalendarMenuView! + @IBOutlet weak var calendarView: CVCalendarView! + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var selectedDayLabel: UILabel! + var selectedDay:CVDate = CVDate(date: NSDate()) { + didSet { + UIView.transitionWithView(tableView, duration: 0.8, options: .TransitionCrossDissolve, animations: {self.tableView.reloadData()}, completion: nil) + } + } + + var minervaCalendarItems: [NSDate: [CalendarItem]]? + var associationCalendarItems: [NSDate: [Activity]]? + + // MARK: - Life cycle + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + selectedDay = CVDate(date: NSDate()) + calendarView.presentedDate = selectedDay + calendarView.toggleViewWithDate(selectedDay.convertedDate()!) + setNavBarTitleDate(selectedDay.convertedDate()) + + // only show rows that are filled + self.tableView.tableFooterView = UIView() + // only scroll when content doesn't fit the whole screen + self.tableView.alwaysBounceVertical = false + self.tableView.estimatedRowHeight = 75 + + calendarUpdated() + loadAssociatonActivities() + + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(CalendarViewController.calendarUpdated), name: MinervaStoreDidUpdateCourseInfoNotification, object: nil) + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(CalendarViewController.loadAssociatonActivities), name: AssociationStoreDidUpdateActivitiesNotification, object: nil) + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(CalendarViewController.reloadCalendarData), name: PreferencesControllerDidUpdatePreferenceNotification, object: nil) + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + self.calendarUpdated() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + menuView.commitMenuViewUpdate() + calendarView.commitCalendarViewUpdate() + } + + func setNavBarTitleDate(date: NSDate?) { + guard let date = date else { + setNavBarTitle("") + return + } + + let dateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + dateFormatter.dateFormat = "MMMM YYYY" + + setNavBarTitle(dateFormatter.stringFromDate(date)) + } + + func setNavBarTitle(title: String) { + self.title = title + self.navigationController?.tabBarItem.title = "Agenda" + } + + func reloadCalendarData() { + calendarUpdated() + loadAssociatonActivities() + } + + func calendarUpdated() { + dispatch_async(dispatch_get_main_queue()) { + self.minervaCalendarItems = MinervaStore.sharedStore.sortedByDate() + self.calendarView.contentController.refreshPresentedMonth() + self.tableView.reloadData() + } + } + + func loadAssociatonActivities() { + var activities = AssociationStore.sharedStore.activities + + let prefs = PreferencesService.sharedService + if prefs.filterAssociations { + let associations = Set(prefs.preferredAssociations) + activities = activities.filter { $0.highlighted || associations.contains($0.association.internalName)} + } + + var grouped = [NSDate: [Activity]]() + for activity in activities { + let date = activity.start.dateAtStartOfDay() + if case nil = grouped[date]?.append(activity) { + grouped[date] = [activity] + } + + if let endDay = activity.end?.dateAtStartOfDay() where endDay > date { + var nextDate = date.dateByAddingDays(1) + while nextDate.dateByAddingHours(8) <= endDay { + if case nil = grouped[nextDate]?.append(activity) { + grouped[nextDate] = [activity] + } + + nextDate = nextDate.dateByAddingDays(1) + } + } + } + + for (k, _) in grouped { + var activitiesDay = grouped[k]! + activitiesDay.sortInPlace({ $0.start <= $1.start }) + } + + dispatch_async(dispatch_get_main_queue()) { + self.associationCalendarItems = grouped + self.calendarView.contentController.refreshPresentedMonth() + self.tableView.reloadData() + } + } +} + +extension CalendarViewController: CVCalendarViewDelegate, CVCalendarMenuViewDelegate { + + func presentationMode() -> CalendarMode { + return .WeekView + } + + func firstWeekday() -> Weekday { + return .Monday + } + + func shouldShowWeekdaysOut() -> Bool { + return true + } + + func didSelectDayView(dayView: DayView, animationDidFinish: Bool) { + debugPrint("\(dayView.date.commonDescription) is selected!") + guard let date = dayView.date.convertedDate() else { return } + + selectedDay = dayView.date + + let dateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + dateFormatter.dateFormat = "EEEE d MMMM" + selectedDayLabel.text = dateFormatter.stringFromDate(date) + } + + func dotMarker(shouldShowOnDayView dayView: DayView) -> Bool { + var count = 0 + if let date = dayView.date.convertedDate() { + if let calendarItems = self.minervaCalendarItems, let items = calendarItems[date] { + count = count + items.count + } + + if let associationCalendarItems = self.associationCalendarItems, let items = associationCalendarItems[date] { + count = count + items.count + } + } + + return count > 0 + } + + func dotMarker(colorOnDayView dayView: DayView) -> [UIColor] { + var colors = [UIColor]() + + if let date = dayView.date.convertedDate() { + if let calendarItems = self.minervaCalendarItems, let items = calendarItems[date] { + if items.count > 0 { + colors.append(UIColor.hydraTintcolor()) + } + } + + if let associationCalendarItems = self.associationCalendarItems, let items = associationCalendarItems[date] { + if items.count > 0 { + colors.append(UIColor.hydraBackgroundColor()) + } + } + } + + return colors + } + + func dotMarker(sizeOnDayView dayView: DayView) -> CGFloat { + return CGFloat(14) + } + + func presentedDateUpdated(date: CVDate) { + setNavBarTitleDate(date.convertedDate()) + } +} + +extension CalendarViewController: UITableViewDelegate, UITableViewDataSource { + + func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 2 + } + + func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + guard let calendarSection = CalendarSection(rawValue: section) else { + return nil + } + + switch calendarSection { + case .Minerva: + return "Minerva Lessenrooster" + case .Associations: + return "Studentenactiviteiten" + } + } + + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let calendarSection = CalendarSection(rawValue: section) else { + return 0 + } + if let date = selectedDay.convertedDate() { + switch calendarSection { + case .Minerva: + if let calendarItems = self.minervaCalendarItems, let items = calendarItems[date] { + return items.count + } + case .Associations: + if let associationCalendarItems = self.associationCalendarItems, let items = associationCalendarItems[date] { + return items.count + } + } + } + return 0 + } + + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + guard let calendarSection = CalendarSection(rawValue: indexPath.section), let date = selectedDay.convertedDate() else { + return UITableViewCell() + } + + func cellIdentifier(startDate: NSDate, endDate: NSDate?) -> String { + let start = startDate.dateAtStartOfDay() + let end = endDate?.dateAtStartOfDay() + if (end != nil && (start == end! || start == end!.dateBySubtractingDays(1))) { + return "hourCalendarCell" + } else if end == nil || start == date { + return "startDayCell" + } else if end! == date { + return "endDayCell" + } else { + return "allDayCell" + } + } + + var calendarItem: CalendarItem? + var activity: Activity? + let identifier: String + + switch calendarSection { + case .Minerva: + guard let calendarItems = self.minervaCalendarItems, let items = calendarItems[date] else { + return UITableViewCell() + } + + calendarItem = items[indexPath.row] + identifier = cellIdentifier(calendarItem!.startDate, endDate: calendarItem?.endDate) + case .Associations: + guard let associationCalendarItems = self.associationCalendarItems, let items = associationCalendarItems[date] else { + return UITableViewCell() + } + + activity = items[indexPath.row] + identifier = cellIdentifier(activity!.start, endDate: activity!.end) + } + + let cell = tableView.dequeueReusableCellWithIdentifier(identifier) as! CalendarSingleTableViewCell + cell.calendarItem = calendarItem + cell.activity = activity + + return cell + } + + func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + guard let calendarSection = CalendarSection(rawValue: indexPath.section), let date = selectedDay.convertedDate() else { + return + } + + self.tableView.deselectRowAtIndexPath(indexPath, animated: true) + + switch calendarSection { + case .Minerva: + if let calendarItems = self.minervaCalendarItems, let items = calendarItems[date] { + let item = items[indexPath.row] + if item.content != nil { + self.performSegueWithIdentifier("calendarDetailSegue", sender: item) + } + } + case .Associations: + guard let associationCalendarItems = self.associationCalendarItems, let items = associationCalendarItems[date] else { + return + } + let activity = items[indexPath.row] + let detailViewController = ActivityDetailController(activity: activity, delegate: nil) + + self.navigationController?.pushViewController(detailViewController, animated: true) + } + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "calendarDetailSegue" { + guard let vc = segue.destinationViewController as? MinervaCalendarDetailViewController else { return } + + vc.calendarItem = sender as? CalendarItem + } + } +} + +extension CalendarViewController { + @IBAction func swipeLeft() { + var date = selectedDay.convertedDate()! + + date = date.dateByAddingDays(1) + calendarView.toggleViewWithDate(date) + } + + @IBAction func swipeRight() { + var date = selectedDay.convertedDate()! + + date = date.dateBySubtractingDays(1) + calendarView.toggleViewWithDate(date) + } + + @IBAction func todayButton() { + calendarView.toggleViewWithDate(NSDate()) + } +} + +enum CalendarSection: Int { + case Minerva = 0, Associations = 1 +} \ No newline at end of file diff --git a/Hydra/Config.swift b/Hydra/Config.swift new file mode 100644 index 0000000..a1107b1 --- /dev/null +++ b/Hydra/Config.swift @@ -0,0 +1,30 @@ +// +// ApiEndpoints.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 27/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +struct APIConfig { + static let Minerva = "https://minerva.ugent.be/api/rest/v2/" + static let OAuth = "https://oauth.ugent.be/" + static let Zeus = "https://zeus.UGent.be/hydra/api/" + static let Zeus1_0 = "https://zeus.UGent.be/hydra/api/1.0/" + static let Zeus2_0 = "https://zeus.UGent.be/hydra/api/2.0/" + static let DSA = "http://student.UGent.be/hydra/api/" + static let SKO = "http://live.studentkickoff.be/" +} + +struct Config { + static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first! + static let AssociationStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("association.archive") + static let InfoStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("info.archive") + static let SchamperStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("schamper.archive") + static let RestoStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("resto.archive") + static let SpecialEventStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("specialEvent.archive") + static let MinervaStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("minerva.archive") + static let SKOStoreArchive = DocumentsDirectory.URLByAppendingPathComponent("sko.archive") +} \ No newline at end of file diff --git a/Hydra/Course.swift b/Hydra/Course.swift new file mode 100644 index 0000000..703c0dd --- /dev/null +++ b/Hydra/Course.swift @@ -0,0 +1,71 @@ +// +// Courses.swift +// +// Created by Feliciaan De Palmenaer on 28/02/2016 +// Copyright (c) Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class Course: NSObject, Mappable, NSCoding { + + // MARK: Properties + var title: String? + var code: String? + var tutorName: String? + var internalIdentifier: String? + var descriptionValue: String? + var academicYear: String? + + // MARK: ObjectMapper Initalizers + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + required init?(_ map: Map){ + + } + + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + func mapping(map: Map) { + title <- map[PropertyKey.courseTitleKey] + code <- map[PropertyKey.courseCodeKey] + tutorName <- map[PropertyKey.courseTutorNameKey] + internalIdentifier <- map[PropertyKey.courseInternalIdentifierKey] + descriptionValue <- map[PropertyKey.courseDescriptionValueKey] + academicYear <- map[PropertyKey.academicYearKey] + } + + // MARK: NSCoding Protocol + required init(coder aDecoder: NSCoder) { + self.title = aDecoder.decodeObjectForKey(PropertyKey.courseTitleKey) as? String + self.code = aDecoder.decodeObjectForKey(PropertyKey.courseCodeKey) as? String + self.tutorName = aDecoder.decodeObjectForKey(PropertyKey.courseTutorNameKey) as? String + self.internalIdentifier = aDecoder.decodeObjectForKey(PropertyKey.courseInternalIdentifierKey) as? String + self.descriptionValue = aDecoder.decodeObjectForKey(PropertyKey.courseDescriptionValueKey) as? String + self.academicYear = aDecoder.decodeObjectForKey(PropertyKey.academicYearKey) as? String + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(title, forKey: PropertyKey.courseTitleKey) + aCoder.encodeObject(code, forKey: PropertyKey.courseCodeKey) + aCoder.encodeObject(tutorName, forKey: PropertyKey.courseTutorNameKey) + aCoder.encodeObject(internalIdentifier, forKey: PropertyKey.courseInternalIdentifierKey) + aCoder.encodeObject(descriptionValue, forKey: PropertyKey.courseDescriptionValueKey) + aCoder.encodeObject(academicYear, forKey: PropertyKey.academicYearKey) + } + + // MARK: Declaration for string constants to be used to decode and also serialize. + struct PropertyKey { + static let courseTitleKey: String = "title" + static let courseCodeKey: String = "code" + static let courseTutorNameKey: String = "tutor_name" + static let courseInternalIdentifierKey: String = "id" + static let courseDescriptionValueKey: String = "description" + static let academicYearKey: String = "academic_year" + } +} diff --git a/Hydra/Exihibitor.swift b/Hydra/Exihibitor.swift new file mode 100644 index 0000000..7d68f24 --- /dev/null +++ b/Hydra/Exihibitor.swift @@ -0,0 +1,48 @@ +// +// Exihibitor.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 11/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import ObjectMapper + +class Exihibitor: NSObject, NSCoding, Mappable { + + var name: String = "" + var content: String = "" + var logo: String = "" + + required init?(_ map: Map) { + } + + required init?(coder aDecoder: NSCoder) { + guard let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as? String, + let content = aDecoder.decodeObjectForKey(PropertyKey.contentKey) as? String, + let logo = aDecoder.decodeObjectForKey(PropertyKey.logoKey) as? String + else { return nil } + + self.name = name + self.content = content + self.logo = logo + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(name, forKey: PropertyKey.nameKey) + aCoder.encodeObject(content, forKey: PropertyKey.contentKey) + aCoder.encodeObject(logo, forKey: PropertyKey.logoKey) + } + + func mapping(map: Map) { + name <- map["naam"] + content <- map[PropertyKey.contentKey] + logo <- map[PropertyKey.logoKey] + } + + struct PropertyKey { + static let nameKey = "name" + static let contentKey = "content" + static let logoKey = "logo" + } +} \ No newline at end of file diff --git a/Hydra/FacebookEvent.h b/Hydra/FacebookEvent.h deleted file mode 100644 index b792852..0000000 --- a/Hydra/FacebookEvent.h +++ /dev/null @@ -1,48 +0,0 @@ -// -// FacebookEvent.h -// Hydra -// -// Created by Feliciaan De Palmenaer on 13/01/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import - -extern NSString *const FacebookEventDidUpdateNotification; - -typedef NS_ENUM(NSUInteger, FacebookEventRsvp) { - FacebookEventRsvpNone = 0, - FacebookEventRsvpAttending, - FacebookEventRsvpUnsure, - FacebookEventRsvpDeclined, -}; - -NSString *FacebookEventRsvpAsLocalizedString(FacebookEventRsvp rsvp); -FacebookEventRsvp FacebookEventRsvpFromString(NSString *rsvp); - -@interface FacebookEvent : NSObject - -@property (nonatomic, assign) BOOL valid; - -@property (nonatomic, strong) NSURL *smallImageUrl; -@property (nonatomic, strong) NSURL *largeImageUrl; - -@property (nonatomic, assign) NSUInteger attendees; -@property (nonatomic, strong) NSArray *friendsAttending; -@property (nonatomic, assign) FacebookEventRsvp userRsvp; -@property (nonatomic, assign) BOOL userRsvpUpdating; - -- (id)initWithEventId:(NSString *)eventId; -- (void)update; -- (void)showExternally; - -@end - -@interface FacebookEventFriend : NSObject - -@property (nonatomic, strong) NSString *name; -@property (nonatomic, strong) NSURL *photoUrl; - -- (id)initWithName:(NSString *)name photoUrl:(NSString *)url; - -@end diff --git a/Hydra/FacebookEvent.m b/Hydra/FacebookEvent.m deleted file mode 100644 index 5ac4eaf..0000000 --- a/Hydra/FacebookEvent.m +++ /dev/null @@ -1,376 +0,0 @@ -// -// FacebookEvent.m -// Hydra -// -// Created by Feliciaan De Palmenaer on 13/01/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import "FacebookEvent.h" -#import -#import "FacebookSession.h" -#import "NSMutableArray+Shuffling.h" -#import "AppDelegate.h" - -#define kUpdateInterval (15 * 60) /* Update every 15 minutes */ - -NSString *const FacebookEventDidUpdateNotification = @"FacebookEventDidUpdateNotification"; - -@interface FacebookEvent () - -@property (nonatomic, strong) NSString *eventId; -@property (nonatomic, strong) NSDate *lastUpdated; - -@end - -@implementation FacebookEvent - -- (id)initWithEventId:(NSString *)eventId -{ - if (self = [super init]) { - self.eventId = eventId; - - [self sharedInit]; - [self update]; - } - return self; -} - -- (void)sharedInit -{ - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center addObserver:self selector:@selector(facebookSessionStateChanged:) - name:FacebookSessionStateChangedNotification object:nil]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)showExternally -{ - UIApplication *app = [UIApplication sharedApplication]; - NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://m.facebook.com/events/%@", self.eventId]]; - [app openURL:url]; -} - -#pragma mark - NSCoding - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - self.valid = [coder decodeBoolForKey:@"valid"]; - self.eventId = [coder decodeObjectForKey:@"eventId"]; - self.lastUpdated = [coder decodeObjectForKey:@"lastUpdated"]; - self.smallImageUrl = [coder decodeObjectForKey:@"smallImageUrl"]; - self.largeImageUrl = [coder decodeObjectForKey:@"largeImageUrl"]; - self.attendees = [coder decodeIntegerForKey:@"attendees"]; - - NSString *accessToken = [coder decodeObjectForKey:@"fbAccessToken"]; - if ([accessToken isEqualToString:[FBSession activeSession].accessTokenData.accessToken]) { - _friendsAttending = [coder decodeObjectForKey:@"friendsAttending"]; - _userRsvp = [coder decodeIntegerForKey:@"userRsvp"]; - } - - [self sharedInit]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeBool:self.valid forKey:@"valid"]; - [coder encodeObject:self.eventId forKey:@"eventId"]; - [coder encodeObject:self.lastUpdated forKey:@"lastUpdated"]; - [coder encodeObject:self.smallImageUrl forKey:@"smallImageUrl"]; - [coder encodeObject:self.largeImageUrl forKey:@"largeImageUrl"]; - [coder encodeInteger:self.attendees forKey:@"attendees"]; - - // Store user-specific details with the access-token used - [coder encodeObject:[FBSession activeSession].accessTokenData.accessToken forKey:@"fbAccessToken"]; - [coder encodeObject:_friendsAttending forKey:@"friendsAttending"]; - [coder encodeInteger:_userRsvp forKey:@"userRsvp"]; -} - -#pragma mark - Fetching info - -- (void)update -{ - if (self.lastUpdated && [self.lastUpdated timeIntervalSinceNow] > -kUpdateInterval) { - return; - } - - FBRequestConnection *connection = [[FBRequestConnection alloc] init]; - [self fetchEventInfo:connection]; - [self fetchUserInfo:connection]; - [self fetchFriendsInfo:connection]; - [connection start]; - - self.lastUpdated = [NSDate date]; -} - -- (void)fetchEventInfo:(FBRequestConnection *)conn -{ - NSLog(@"Fetching information on event '%@'", self.eventId); - NSString *q = [NSString stringWithFormat: - @"SELECT attending_count, pic, pic_big " - "FROM event WHERE eid = '%@'", self.eventId]; - FBRequest *request = [[FacebookSession sharedSession] requestWithQuery:q]; - - [conn addRequest:request completionHandler:^(FBRequestConnection *c, id result, NSError *error) { - if (error) { - NSLog(@"Error while fetching information on event '%@': %@", - self.eventId, [error localizedDescription]); - return; - } - - if ([result[@"data"] count] > 0) { - self.valid = YES; - - NSDictionary *data = result[@"data"][0]; - self.attendees = [data[@"attending_count"] intValue]; - - NSString *smallImageUrl = data[@"pic"]; - if (smallImageUrl) { - self.smallImageUrl = [NSURL URLWithString:smallImageUrl]; - } - else { - self.smallImageUrl = nil; - } - - NSString *largeImageUrl = data[@"pic_big"]; - if (largeImageUrl) { - self.largeImageUrl = [NSURL URLWithString:largeImageUrl]; - } - else { - self.largeImageUrl = nil; - } - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:FacebookEventDidUpdateNotification object:self]; - } - else { - NSLog(@"Could not find information on event '%@'", self.eventId); - } - }]; -} - -- (void)fetchUserInfo:(FBRequestConnection *)conn -{ - if (![FacebookSession sharedSession].open) { - return; - } - - NSLog(@"Fetching user information on event '%@'", self.eventId); - NSString *q = [NSString stringWithFormat: - @"SELECT rsvp_status FROM event_member " - "WHERE eid = '%@' AND uid = me()", self.eventId]; - FBRequest *request = [[FacebookSession sharedSession] requestWithQuery:q]; - - [conn addRequest:request completionHandler:^(FBRequestConnection *c, id result, NSError *error) { - if (error) { - NSLog(@"Error while fetching user information on event '%@': %@", - self.eventId, [error localizedDescription]); - return; - } - - if ([result[@"data"] count] > 0) { - NSDictionary *data = result[@"data"][0]; - _userRsvp = FacebookEventRsvpFromString(data[@"rsvp_status"]); - } - else { - _userRsvp = FacebookEventRsvpNone; - } - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:FacebookEventDidUpdateNotification object:self]; - }]; -} - -- (void)fetchFriendsInfo:(FBRequestConnection *)conn -{ - if (![FacebookSession sharedSession].open) { - return; - } - - NSLog(@"Fetching friends information on event '%@'", self.eventId); - NSString *q = [NSString stringWithFormat: - @"SELECT name, pic_square FROM user WHERE uid IN " - "(SELECT uid2 FROM friend WHERE uid1 = me() AND uid2 IN " - "(SELECT uid FROM event_member WHERE eid = '%@' AND " - "rsvp_status = 'attending'))", self.eventId]; - FBRequest *request = [[FacebookSession sharedSession] requestWithQuery:q]; - - [conn addRequest:request completionHandler:^(FBRequestConnection *c, id result, NSError *error) { - if (error) { - NSLog(@"Error while fetching friends information on event '%@': %@", - self.eventId, [error localizedDescription]); - return; - } - - NSMutableArray *friendsAttending = [NSMutableArray array]; - for (NSDictionary *item in result[@"data"]) { - FacebookEventFriend *friend = [[FacebookEventFriend alloc] - initWithName:item[@"name"] - photoUrl:item[@"pic_square"]]; - [friendsAttending addObject:friend]; - } - [friendsAttending H_shuffle]; - self.friendsAttending = friendsAttending; - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:FacebookEventDidUpdateNotification object:self]; - }]; -} - -#pragma mark - Submitting info - -- (void)setUserRsvp:(FacebookEventRsvp)userRsvp -{ - if (userRsvp == self.userRsvp) { - return; - } - - // Open facebook-session - FBSession *fb = [FBSession activeSession]; - if (![fb isOpen]) { - [[FacebookSession sharedSession] openWithAllowLoginUI:YES completion:^{ - self.userRsvp = userRsvp; - }]; - } - // Check for permissions - else if (![fb.permissions containsObject:@"rsvp_event"]) { - self.userRsvpUpdating = YES; - - NSLog(@"Requesting publishPermission 'rsvp_event'"); - [fb requestNewPublishPermissions:@[ @"rsvp_event" ] - defaultAudience:FBSessionDefaultAudienceFriends - completionHandler:^(FBSession *session, NSError *error) { - if (error) { - self.userRsvpUpdating = NO; - AppDelegate *delegate = (AppDelegate *)([UIApplication sharedApplication].delegate); - [delegate handleError:error]; - } - else { - self.userRsvp = userRsvp; - } - }]; - } - else { - self.userRsvpUpdating = YES; - - FBRequest *req = [self graphRequestForRsvp:userRsvp]; - NSLog(@"POSTing presence to %@", req.graphPath); - [req startWithCompletionHandler:^(FBRequestConnection *c, id result, NSError *error) { - self.userRsvpUpdating = NO; - if (error) { - AppDelegate *delegate = (AppDelegate *)([UIApplication sharedApplication].delegate); - [delegate handleError:error]; - } - else { - _userRsvp = userRsvp; - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:FacebookEventDidUpdateNotification object:self]; - } - }]; - } -} - -- (FBRequest *)graphRequestForRsvp:(FacebookEventRsvp)rsvp -{ - NSString *state; - switch (rsvp) { - case FacebookEventRsvpAttending: - state = @"attending"; - break; - case FacebookEventRsvpUnsure: - state = @"maybe"; - break; - case FacebookEventRsvpDeclined: - state = @"declined"; - break; - case FacebookEventRsvpNone: - state = nil; - break; - } - - NSString *path = [NSString stringWithFormat:@"%@/%@", self.eventId, state]; - return [FBRequest requestForPostWithGraphPath:path graphObject:nil]; -} - -#pragma mark - Facebook session state - -- (void)facebookSessionStateChanged:(NSNotification *)notification -{ - FBSession *session = [notification object]; - if (![session isOpen]) { - _userRsvp = FacebookEventRsvpNone; - _friendsAttending = nil; - } - // Force update on next access - self.lastUpdated = nil; -} - -@end - -@implementation FacebookEventFriend - -- (id)initWithName:(NSString *)name photoUrl:(NSString *)url; -{ - if (self = [super init]) { - self.name = name; - if (url) { - self.photoUrl = [NSURL URLWithString:url]; - } - } - return self; -} - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - self.name = [coder decodeObjectForKey:@"name"]; - self.photoUrl = [coder decodeObjectForKey:@"photoUrl"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.name forKey:@"name"]; - [coder encodeObject:self.photoUrl forKey:@"photoUrl"]; -} - -@end - -NSString *FacebookEventRsvpAsLocalizedString(FacebookEventRsvp rsvp) -{ - switch (rsvp) { - case FacebookEventRsvpNone: - return nil; - case FacebookEventRsvpAttending: - return @"aanwezig"; - case FacebookEventRsvpUnsure: - return @"misschien"; - case FacebookEventRsvpDeclined: - return @"niet aanwezig"; - } -} - -FacebookEventRsvp FacebookEventRsvpFromString(NSString *rsvp) -{ - if ([rsvp isEqualToString:@"attending"]) { - return FacebookEventRsvpAttending; - } - else if ([rsvp isEqualToString:@"unsure"]) { - return FacebookEventRsvpUnsure; - } - else if ([rsvp isEqualToString:@"declined"]) { - return FacebookEventRsvpDeclined; - } - else { - return FacebookEventRsvpNone; - } -} diff --git a/Hydra/FacebookEvent.swift b/Hydra/FacebookEvent.swift new file mode 100644 index 0000000..8fa62ca --- /dev/null +++ b/Hydra/FacebookEvent.swift @@ -0,0 +1,284 @@ +// +// FacebookEvent.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 03/03/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import FBSDKLoginKit + +let FacebookEventDidUpdateNotification = "FacebookEventDidUpdateNotification" + +@objc enum FacebookEventRsvp: Int { + case None, Attending, Unsure, Declined + func localizedString() -> String { + switch(self) { + case .None: + return "" + case .Attending: + return "aanwezig" + case .Unsure: + return "misschien" + case .Declined: + return "niet aanwezig" + } + } + + func graphRequestString() -> String? { + switch(self) { + case .Attending: + return "attending" + case .Unsure: + return "unsure" + case .Declined: + return "declined" + case .None: + return nil + } + } +} + +class FacebookEvent: NSObject, NSCoding { + var valid: Bool = false + var imageUrl: NSURL? + + var attendees: UInt = 0 + var friendsAttending: [FacebookEventFriend]? + var userRsvp: FacebookEventRsvp = .None + var userRsvpUpdating = false + + private var eventId: String + private var lastUpdated: NSDate? + + init(eventId: String) { + self.eventId = eventId + + super.init() + + let center = NSNotificationCenter.defaultCenter() + center.addObserver(self, selector: #selector(FacebookEvent.facebookSessionStateChanged(_:)), name: FacebookSessionStateChangedNotification, object: nil) + + self.update() + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + func showExternally() { + let app = UIApplication.sharedApplication() + let url = NSURL(string: "https://m.facebook.com/events/\(self.eventId)") + app.openURL(url!) + } + +// MARK: NSCoding + required convenience init?(coder aDecoder: NSCoder) { + let eventId = aDecoder.decodeObjectForKey(PropertyKey.eventIdKey) as! String + self.init(eventId: eventId) + + self.valid = aDecoder.decodeObjectForKey(PropertyKey.validKey) as! Bool + self.imageUrl = aDecoder.decodeObjectForKey(PropertyKey.smallImageUrlKey) as? NSURL + + self.attendees = aDecoder.decodeObjectForKey(PropertyKey.attendeesKey) as! UInt + self.friendsAttending = aDecoder.decodeObjectForKey(PropertyKey.friendsAttendingKey) as? [FacebookEventFriend] + self.userRsvp = FacebookEventRsvp(rawValue: aDecoder.decodeObjectForKey(PropertyKey.userRsvpKey) as! Int)! + + self.lastUpdated = aDecoder.decodeObjectForKey(PropertyKey.lastUpdatedKey) as? NSDate + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(valid, forKey: PropertyKey.validKey) + aCoder.encodeObject(imageUrl, forKey: PropertyKey.smallImageUrlKey) + + aCoder.encodeObject(attendees, forKey: PropertyKey.attendeesKey) + aCoder.encodeObject(friendsAttending, forKey: PropertyKey.friendsAttendingKey) + aCoder.encodeObject(userRsvp.hashValue, forKey: PropertyKey.userRsvpKey) + + aCoder.encodeObject(lastUpdated, forKey: PropertyKey.lastUpdatedKey) + aCoder.encodeObject(eventId, forKey: PropertyKey.eventIdKey) + } + + // MARK: Fill-in event + func facebookSessionStateChanged(notification: NSNotification) { + let session = FacebookSession.sharedSession + if !session.open { + userRsvp = .None + friendsAttending = nil + } + + // Force update on next access + self.lastUpdated = nil + } + + func update() { + if let lastUpdated = self.lastUpdated where lastUpdated.minutesBeforeDate(NSDate()) < 60 { + return + } + + self.fetchEventInfo() + self.fetchUserInfo() + //self.fetchFriendsInfo() //TODO: do fetch friends info + + self.lastUpdated = NSDate() + } + + func fetchEventInfo() { + print("Fetching information on event '\(self.eventId)'") + + let query = "/'\(self.eventId)'" + + FacebookSession.sharedSession.requestWithGraphPath(query, parameters: ["fields": "attending_count,cover"]) { (result) -> Void in + if let data = result.valueForKey("data") as? NSArray, let dict: NSDictionary? = data[0] as? NSDictionary where data.count > 0 { + if let attending_count = dict?.valueForKey("attending_count") as? UInt { + self.attendees = attending_count + } + if let cover = dict?.valueForKey("cover") as? NSDictionary, let pic = cover.valueForKey("source") as? String { + self.imageUrl = NSURL(string: pic) + } + NSNotificationCenter.defaultCenter().postNotificationName(FacebookEventDidUpdateNotification, object: nil) + + self.valid = true + } + } + } + + func fetchUserInfo() { + /*print("Fetching user information on event \(self.eventId)") + + let query = "SELECT rsvp_status FROM event_member WHERE eid = '\(self.eventId)' AND uid = me()" + + FacebookSession.sharedSession.requestWithQuery(query) { (result) -> Void in + if let data = result.valueForKey("data") as? NSArray where data.count > 0 { + if let dict = data[0] as? NSDictionary { + if let rsvp_status = dict.valueForKey("rsvp_status") as? String { + switch (rsvp_status) { + case "attending": + self.userRsvp = .Attending + break + case "unsure": + self.userRsvp = .Unsure + break + case "declined": + self.userRsvp = .Declined + break + default: + self.userRsvp = .None + } + NSNotificationCenter.defaultCenter().postNotificationName(FacebookEventDidUpdateNotification, object: nil) + } + else { + self.userRsvp = .None + print("No dictionary") + } + } + } + }*/ + } + + func fetchFriendsInfo() { + // TODO: find some other way to get this info + /* print("Fetching users friends information on event \(self.eventId)") + + let query = "SELECT name, pic_square FROM user WHERE uid IN " + + "(SELECT uid2 FROM friend WHERE uid1 = me() AND uid2 IN " + + "(SELECT uid FROM event_member WHERE eid = '\(self.eventId)' AND " + + "rsvp_status = 'attending'))" + + print(query) + + FacebookSession.sharedSession.requestWithQuery(query) { (result) -> Void in + if let data = result.valueForKey("data") as? NSArray { + print(data) + } + }*/ + } + + func updateUserRsvp(userRsvp: FacebookEventRsvp) { + if self.userRsvp == userRsvp { + return + } + + // Check if logged in + let session = FacebookSession.sharedSession + self.userRsvpUpdating = true + if !session.open { + session.openWithAllowLoginUI(true, completion: { () -> Void in + self.updateUserRsvp(userRsvp) + }) + // Check if permission are granted + } else if FBSDKAccessToken.currentAccessToken().hasGranted("rsvp_event") { + let state = userRsvp.graphRequestString() + let request = FBSDKGraphRequest(graphPath: "\(self.eventId)/\(state)", parameters: nil) + + request.startWithCompletionHandler({ (connection, response, error) -> Void in + if let error = error { + self.userRsvpUpdating = false + // Handle error + let delegate = UIApplication.sharedApplication().delegate as! AppDelegate + delegate.handleError(error) + } else { + self.userRsvp = userRsvp + + let center = NSNotificationCenter.defaultCenter() + center.postNotificationName(FacebookSessionStateChangedNotification, object: nil) + } + }) + + } else { + // Request permissions + let loginManager = FBSDKLoginManager() + print("Requesting publish permission 'rsvp_event' for \(self.eventId)") + loginManager.logInWithPublishPermissions(["rsvp_event"], handler: { (result, error) -> Void in + if let error = error { + self.userRsvpUpdating = false + // Handle error + let delegate = UIApplication.sharedApplication().delegate as! AppDelegate + delegate.handleError(error) + } else { + self.updateUserRsvp(userRsvp) + } + }) + } + + } + + struct PropertyKey { + static let validKey = "valid" + static let smallImageUrlKey = "smallImageUrl" + static let largeImageUrlKey = "largeImageUrl" + static let attendeesKey = "attendees" + static let friendsAttendingKey = "friendsAttending" + static let userRsvpKey = "userRsvp" + static let eventIdKey = "eventIdKey" + static let lastUpdatedKey = "lastUpdated" + } +} + + +class FacebookEventFriend: NSObject, NSCoding { + var name: String + var photoUrl: NSURL? + + init(name: String, photoUrl: NSURL?) { + self.name = name + self.photoUrl = photoUrl + } + + // MARK: NSCoding + required init?(coder aDecoder: NSCoder) { + self.name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String + self.photoUrl = aDecoder.decodeObjectForKey(PropertyKey.photoUrlKey) as? NSURL + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(name, forKey: PropertyKey.nameKey) + aCoder.encodeObject(photoUrl, forKey: PropertyKey.photoUrlKey) + } + + struct PropertyKey { + static let nameKey = "name" + static let photoUrlKey = "photoUrl" + } +} \ No newline at end of file diff --git a/Hydra/FacebookSession.h b/Hydra/FacebookSession.h deleted file mode 100644 index 673a81e..0000000 --- a/Hydra/FacebookSession.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// FacebookSession.h -// Hydra -// -// Created by Feliciaan De Palmenaer on 7/01/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import -#import - -extern NSString *const FacebookSessionStateChangedNotification; -extern NSString *const FacebookUserInfoUpdatedNotifcation; - -@interface FacebookSession : NSObject - -+ (FacebookSession *)sharedSession; - -@property (nonatomic, readonly) BOOL open; -@property (nonatomic, readonly) id userInfo; - -- (BOOL)openWithAllowLoginUI:(BOOL)allowLoginUI; -- (BOOL)openWithAllowLoginUI:(BOOL)allowLoginUI completion:(void (^)())completion; -- (void)close; - -- (FBRequest *)requestWithQuery:(NSString *)query; -- (FBRequest *)requestWithGraphPath:(NSString *)path parameters:(NSDictionary *)parameters; -- (FBRequest *)requestWithGraphPath:(NSString *)path parameters:(NSDictionary *)parameters HTTPMethod:(NSString *)method; - -@end diff --git a/Hydra/FacebookSession.m b/Hydra/FacebookSession.m deleted file mode 100644 index 3a4d3e3..0000000 --- a/Hydra/FacebookSession.m +++ /dev/null @@ -1,153 +0,0 @@ -// -// FacebookLogin.m -// Hydra -// -// Created by Feliciaan De Palmenaer on 7/01/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import "FacebookSession.h" -#import "AppDelegate.h" -#import - -// Generated by visiting https://graph.facebook.com/oauth/access_token?client_id=_&client_secret=_&grant_type=client_credentials -#define kAppAccessToken @"146947948791011|QqOR99OREkC_vAvOkfJm2tp-02k" - -NSString *const FacebookSessionStateChangedNotification = - @"FacebookSessionStateChangedNotification"; -NSString *const FacebookUserInfoUpdatedNotifcation = - @"FacebookUserInfoUpdatedNotifcation"; - -@interface FacebookSession () - -@property (nonatomic, strong) NSString *appAccessToken; -@property (nonatomic, strong) id userInfo; - -@end - -@implementation FacebookSession - -+ (FacebookSession *)sharedSession -{ - static FacebookSession *sharedInstance = nil; - if (!sharedInstance) { - sharedInstance = [[FacebookSession alloc] init]; - } - return sharedInstance; -} - -#pragma mark - Session management - -- (BOOL)openWithAllowLoginUI:(BOOL)allowLoginUI -{ - return [self openWithAllowLoginUI:allowLoginUI completion:NULL]; -} - -- (BOOL)openWithAllowLoginUI:(BOOL)allowLoginUI completion:(void (^)())c -{ - // Prevent calling the block twice - __block void(^completion)() = c; - FBSessionStateHandler handler = ^(FBSession *session, FBSessionState state, NSError *error) { - [self sessionStateChanged:session state:state error:error]; - if (!error && completion) { - completion(); - completion = NULL; - } - }; - - return [FBSession openActiveSessionWithReadPermissions:nil allowLoginUI:allowLoginUI - completionHandler:handler]; -} - -- (void)close -{ - [[FBSession activeSession] closeAndClearTokenInformation]; -} - -- (BOOL)open -{ - return [[FBSession activeSession] isOpen]; -} - -- (void)sessionStateChanged:(FBSession *)session state:(FBSessionState)state error:(NSError *)error -{ - DLog(@"%@ %@", session, error); - switch (state) { - case FBSessionStateClosed: - case FBSessionStateClosedLoginFailed: - self.userInfo = nil; - [[FBSession activeSession] closeAndClearTokenInformation]; - break; - default: - break; - } - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:FacebookSessionStateChangedNotification object:session]; - - if (error) { - AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [delegate handleError:error]; - } -} - -#pragma mark - User info - -- (id)userInfo -{ - if (self.open && !_userInfo) { - [self updateUserInfo]; - } - return _userInfo; -} - -- (void)updateUserInfo -{ - FBRequest *request = [FBRequest requestForMe]; - [request startWithCompletionHandler:^(FBRequestConnection *c, id result, NSError *error) { - if (error) { - NSLog(@"Error while fetching user information: %@", [error localizedDescription]); - } - else { - self.userInfo = result; - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:FacebookUserInfoUpdatedNotifcation object:result]; - } - }]; -} - -#pragma mark - Request helpers - -- (FBRequest *)requestWithGraphPath:(NSString *)path parameters:(NSDictionary *)parameters -{ - return [self requestWithGraphPath:path parameters:parameters HTTPMethod:@"GET"]; -} - -- (FBRequest *)requestWithQuery:(NSString *)query -{ - // TODO: change all fql requests to graph requests (fix before 7 August 2016) - return [self requestWithGraphPath:@"/v2.0/fql" parameters:@{@"q": query} HTTPMethod:@"GET"]; -} - -- (FBRequest *)requestWithGraphPath:(NSString *)path parameters:(NSDictionary *)parameters HTTPMethod:(NSString *)method -{ - FBSession *session = [FBSession activeSession]; - - // If there's no session, try accessing the resource with the app token - if (![session isOpen] && ![FBSession openActiveSessionWithAllowLoginUI:NO]) { - NSMutableDictionary *mutableParams = [parameters mutableCopy]; - if (!mutableParams) { - mutableParams = [NSMutableDictionary dictionary]; - } - mutableParams[@"access_token"] = kAppAccessToken; - - session = nil; - parameters = mutableParams; - } - - return [[FBRequest alloc] initWithSession:session graphPath:path - parameters:parameters HTTPMethod:method]; -} - -@end diff --git a/Hydra/FacebookSession.swift b/Hydra/FacebookSession.swift new file mode 100644 index 0000000..852b88e --- /dev/null +++ b/Hydra/FacebookSession.swift @@ -0,0 +1,130 @@ +// +// FacebookSession.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 03/03/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import FBSDKCoreKit +import FBSDKLoginKit +import FBSDKShareKit + +// Generated by visiting https://graph.facebook.com/oauth/access_token?client_id=_&client_secret=_&grant_type=client_credentials +let kAppAccessToken = "146947948791011|QqOR99OREkC_vAvOkfJm2tp-02k" +let FacebookSessionStateChangedNotification = "FacebookSessionStateChangedNotification" +let FacebookUserInfoUpdatedNotifcation = "FacebookUserInfoUpdatedNotifcation" + +class FacebookSession: NSObject { + + static var sharedSession = FacebookSession() + + override init() { + super.init() + + self.updateUserInfo() + } + + var open: Bool { + if (FBSDKAccessToken.currentAccessToken() != nil) && (userInfo == nil) && !updatingUserInfo { + self.updateUserInfo(true) + } + return FBSDKAccessToken.currentAccessToken() != nil + } + + var userInfo: FacebookUser? + var updatingUserInfo = false + + func openWithAllowLoginUI(allowLoginUI: Bool, completion: (()->Void)? = nil) { + let userLoggedIn = PreferencesService.sharedService.userLoggedInToFacebook + if !allowLoginUI && !userLoggedIn { + return + } + if self.open { + self.updateUserInfo() + return + } + let login = FBSDKLoginManager() + login.logInWithReadPermissions(["public_profile", "user_friends"]) { (result, error) -> Void in + if let error = error{ + // Handle error + let delegate = UIApplication.sharedApplication().delegate as! AppDelegate + delegate.handleError(error) + } else { + if result.isCancelled || result.declinedPermissions.contains("public_profile") { + // HANDLE DECLINED SHIT + PreferencesService.sharedService.userLoggedInToFacebook = false + } else { + // HANDLE WINNING + self.updateUserInfo() + PreferencesService.sharedService.userLoggedInToFacebook = true + } + } + + let center = NSNotificationCenter.defaultCenter() + center.postNotificationName(FacebookSessionStateChangedNotification, object: nil) + } + } + + func close() { + let manager = FBSDKLoginManager() + manager.logOut() + userInfo = nil + updatingUserInfo = false + let center = NSNotificationCenter.defaultCenter() + center.postNotificationName(FacebookSessionStateChangedNotification, object: nil) + } + + private func updateUserInfo(force: Bool = false) { + if force || self.open { // Use force to not get in a infinite loop + self.updatingUserInfo = true + requestWithGraphPath("me", parameters: [:], completionHandler: { (result) -> Void in + let userName = result.valueForKey("name") as! String + let userId = result.valueForKey("id") as! String + + self.userInfo = FacebookUser(name: userName, id: userId) + let center = NSNotificationCenter.defaultCenter() + center.postNotificationName(FacebookSessionStateChangedNotification, object: nil) + self.updatingUserInfo = false + }) + } + } + + func requestWithQuery(query: String, completionHandler: ((AnyObject)->Void)?) { + // TODO: change all fql request to open graph request (fix before 7 august 2016) + self.requestWithGraphPath("/v2.0/fql", parameters: ["q": query], completionHandler: completionHandler) + } + + func requestWithGraphPath(path: String, parameters: [NSObject: AnyObject], HTTPMethod: String = "GET", completionHandler: ((AnyObject)-> Void)? ) { + var parameters = parameters + + if !self.open { + parameters["access_token"] = kAppAccessToken + } + + FBSDKGraphRequest(graphPath: path, parameters: parameters, HTTPMethod: HTTPMethod).startWithCompletionHandler { (connection, obj, err) -> Void in + if let error = err { + let app = UIApplication.sharedApplication().delegate as! AppDelegate + app.handleError(error) + print("An error occured", error) //TODO: add error completion handler + } + if let obj = obj { + if let completionHandler = completionHandler { + completionHandler(obj) + } + } + } + + } +} + +@objc class FacebookUser: NSObject { // Use class because it's used in Obj-C + let name: String + let id: String + + init(name: String, id: String) { + self.name = name + self.id = id + } +} \ No newline at end of file diff --git a/Hydra/HomeActivityCollectionViewCell.swift b/Hydra/HomeActivityCollectionViewCell.swift index 422574f..bb75a09 100644 --- a/Hydra/HomeActivityCollectionViewCell.swift +++ b/Hydra/HomeActivityCollectionViewCell.swift @@ -7,6 +7,7 @@ // import UIKit +import SDWebImage class HomeActivityCollectionViewCell: UICollectionViewCell { @IBOutlet weak var titleLabel: UILabel! @@ -15,51 +16,58 @@ class HomeActivityCollectionViewCell: UICollectionViewCell { @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var locationLabel: UILabel! @IBOutlet weak var imageView: UIImageView! + + override func awakeFromNib() { + self.contentView.setShadow() + } - var activity: AssociationActivity? { + var activity: Activity? { didSet { - let longDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() - longDateFormatter.timeStyle = .ShortStyle - longDateFormatter.dateStyle = .LongStyle - longDateFormatter.doesRelativeDateFormatting = true - - let shortDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() - shortDateFormatter.timeStyle = .ShortStyle - shortDateFormatter.dateStyle = .NoStyle - - associationLabel.text = activity?.association.displayName - titleLabel.text = activity?.title - - if (self.activity!.end != nil) { - if self.activity!.start.dateByAddingDays(1).isLaterThanDate(self.activity!.end) { - dateLabel.text = "\(longDateFormatter.stringFromDate((self.activity?.start)!)) - \(shortDateFormatter.stringFromDate((self.activity?.end)!))" + if let activity = self.activity { + let longDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + longDateFormatter.timeStyle = .ShortStyle + longDateFormatter.dateStyle = .LongStyle + longDateFormatter.doesRelativeDateFormatting = true + + let shortDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + shortDateFormatter.timeStyle = .ShortStyle + shortDateFormatter.dateStyle = .NoStyle + + associationLabel.text = activity.association.displayName + titleLabel.text = activity.title + + if let end = activity.end { + if activity.start.dateByAddingDays(1).isLaterThanDate(activity.end) { + dateLabel.text = "\(longDateFormatter.stringFromDate(activity.start)) - \(shortDateFormatter.stringFromDate(end))" + } else { + dateLabel.text = "\(longDateFormatter.stringFromDate(activity.start))\n\(longDateFormatter.stringFromDate(end))" + } } else { - dateLabel.text = "\(longDateFormatter.stringFromDate((self.activity?.start)!)) - \(longDateFormatter.stringFromDate((self.activity?.end)!))" + dateLabel.text = longDateFormatter.stringFromDate((self.activity?.start)!) } - } else { - dateLabel.text = longDateFormatter.stringFromDate((self.activity?.start)!) - } - - descriptionLabel.text = activity?.descriptionText - var distance: Double? = nil - if (activity?.latitude != nil && activity?.longitude != nil) { - distance = LocationService.sharedService.calculateDistance(activity!.latitude, longitude: activity!.longitude) - } - - if let d = distance where d < 100*1000{ - if d < 1000 { - locationLabel.text = activity!.location + " (\(Int(d))m)" + + descriptionLabel.text = activity.descriptionText + var distance: Double? = nil + if (activity.latitude != 0.0 && activity.longitude != 0.0) { + distance = LocationService.sharedService.calculateDistance(activity.latitude, longitude: activity.longitude) + } + + if let d = distance where d < 100*1000{ + if d < 1000 { + locationLabel.text = activity.location + " (\(Int(d))m)" + } else { + locationLabel.text = activity.location + " (\(Int(d/1000))km)" + } } else { - locationLabel.text = activity!.location + " (\(Int(d/1000))km)" + locationLabel.text = activity.location + } + + if let url = activity.facebookEvent?.imageUrl { + imageView.sd_setImageWithURL(url, placeholderImage: imageView.image) + } else { + let association = activity.association.internalName.lowercaseString + imageView.sd_setImageWithURL(NSURL(string: "https://zeus.ugent.be/hydra/api/2.0/association/logo/\(association).png")!) } - } else { - locationLabel.text = activity?.location - } - - if let url = activity?.facebookEvent?.smallImageUrl { - imageView.sd_setImageWithURL(url, placeholderImage: imageView.image) - } else { - imageView.image = nil } } } diff --git a/Hydra/HomeFeedService.swift b/Hydra/HomeFeedService.swift index 3fb0955..8955fcf 100644 --- a/Hydra/HomeFeedService.swift +++ b/Hydra/HomeFeedService.swift @@ -15,27 +15,35 @@ class HomeFeedService { static let sharedService = HomeFeedService() - let associationStore = AssociationStore.sharedStore() - let restoStore = RestoStore.sharedStore() - let schamperStore = SchamperStore.sharedStore() - let preferencesService = PreferencesService.sharedService() + let associationStore = AssociationStore.sharedStore + let restoStore = RestoStore.sharedStore + let schamperStore = SchamperStore.sharedStore + let preferencesService = PreferencesService.sharedService + let specialEventStore = SpecialEventStore.sharedStore + let minervaStore = MinervaStore.sharedStore let locationService = LocationService.sharedService - + var previousRefresh = NSDate() + var previousNotificationDate = NSDate(timeIntervalSince1970: 0) private init() { refreshStores() - locationService.startUpdating() + locationService.updateLocation() - let notifications = [RestoStoreDidReceiveMenuNotification, AssociationStoreDidUpdateActivitiesNotification, AssociationStoreDidUpdateNewsNotification, SchamperStoreDidUpdateArticlesNotification] + let notifications = [RestoStoreDidReceiveMenuNotification, AssociationStoreDidUpdateActivitiesNotification, AssociationStoreDidUpdateNewsNotification, SchamperStoreDidUpdateArticlesNotification, SpecialEventStoreDidUpdateNotification, MinervaStoreDidUpdateCourseInfoNotification, PreferencesControllerDidUpdatePreferenceNotification] for notification in notifications { - NSNotificationCenter.defaultCenter().addObserver(self, selector: "storeUpdatedNotification:", name: notification, object: nil) + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(HomeFeedService.storeUpdatedNotification(_:)), name: notification, object: nil) } } @objc func storeUpdatedNotification(notification: NSNotification) { - NSNotificationCenter.defaultCenter().postNotificationName(HomeFeedDidUpdateFeedNotification, object: nil) + if previousNotificationDate.dateByAddingTimeInterval(5).isEarlierThanDate(NSDate()) { + previousNotificationDate = NSDate() + doLater(4) { + NSNotificationCenter.defaultCenter().postNotificationName(HomeFeedDidUpdateFeedNotification, object: nil) + } + } } deinit { @@ -57,27 +65,41 @@ class HomeFeedService { associationStore.reloadNewsItems() restoStore.menuForDay(NSDate()) - restoStore.locations + _ = restoStore.locations schamperStore.reloadArticles() + + specialEventStore.updateSpecialEvents() + + minervaStore.update() + + locationService.updateLocation() } func createFeed() -> [FeedItem] { var list = [FeedItem]() - let feedItemProviders: [FeedItemProtocol] = [associationStore, restoStore, schamperStore] + let feedItemProviders: [FeedItemProtocol] = [associationStore, schamperStore, restoStore, specialEventStore, minervaStore] for provider in feedItemProviders { list.appendContentsOf(provider.feedItems()) } // Urgent.fm - list.append(FeedItem(itemType: .UrgentItem, object: nil, priority: 825)) - + if preferencesService.showUrgentfmInFeed { + list.append(FeedItem(itemType: .UrgentItem, object: nil, priority: 825)) + } + list.sortInPlace{ $0.priority > $1.priority } return list } + + func doLater(timeSec: Int = 1, function: (()->Void)) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(timeSec)*Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + function() + } + } } protocol FeedItemProtocol { @@ -103,5 +125,9 @@ enum FeedItemType { case RestoItem case UrgentItem case SchamperNewsItem - case SettingsItem + case AssociationsSettingsItem + case SpecialEventItem + case MinervaSettingsItem + case MinervaAnnouncementItem + case MinervaCalendarItem } \ No newline at end of file diff --git a/Hydra/HomeMinervaAnnouncementCell.swift b/Hydra/HomeMinervaAnnouncementCell.swift new file mode 100644 index 0000000..f798536 --- /dev/null +++ b/Hydra/HomeMinervaAnnouncementCell.swift @@ -0,0 +1,44 @@ +// +// HomeMinervaAnnouncementCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 13/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +class HomeMinervaAnnouncementCell: UICollectionViewCell { + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var authorLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var courseLabel: UILabel! + + + override func awakeFromNib() { + self.contentView.setShadow() + } + + var announcement: Announcement? { + didSet { + if let announcement = announcement { + titleLabel.text = announcement.title + authorLabel.text = announcement.editUser + descriptionLabel.text = announcement.content.stripHtmlTags + courseLabel.text = announcement.course?.title?.stripHtmlTags + + let longDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + longDateFormatter.timeStyle = .ShortStyle + longDateFormatter.dateStyle = .LongStyle + longDateFormatter.doesRelativeDateFormatting = true + dateLabel.text = longDateFormatter.stringFromDate(announcement.date) + } else { + titleLabel.text = "" + authorLabel.text = "" + descriptionLabel.text = "" + courseLabel.text = "" + dateLabel.text = "" + } + } + } + +} diff --git a/Hydra/HomeMinervaCalendarItemCell.swift b/Hydra/HomeMinervaCalendarItemCell.swift new file mode 100644 index 0000000..df4bb1c --- /dev/null +++ b/Hydra/HomeMinervaCalendarItemCell.swift @@ -0,0 +1,59 @@ +// +// HomeMinervaCalendarItemCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 13/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import FontAwesome_swift + +class HomeMinervaCalendarItemCell: UICollectionViewCell { + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var locationLabel: UILabel! + @IBOutlet weak var locationIcon: UIView! + @IBOutlet weak var dateLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var courseLabel: UILabel! + + var calendarItem: CalendarItem? { + didSet { + if let calendarItem = calendarItem { + titleLabel.text = calendarItem.title + + if let location = calendarItem.location { + locationLabel.text = location + locationIcon.hidden = false + } else { + locationLabel.text = nil + locationIcon.hidden = true + } + + descriptionLabel.text = calendarItem.content?.stripHtmlTags + courseLabel.text = calendarItem.course?.title?.stripHtmlTags + + let longDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + longDateFormatter.timeStyle = .ShortStyle + longDateFormatter.dateStyle = .LongStyle + longDateFormatter.doesRelativeDateFormatting = true + + let shortDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + shortDateFormatter.timeStyle = .ShortStyle + shortDateFormatter.dateStyle = .NoStyle + + if calendarItem.startDate.dateByAddingDays(1).isLaterThanDate(calendarItem.endDate) { + dateLabel.text = "\(longDateFormatter.stringFromDate(calendarItem.startDate)) - \(shortDateFormatter.stringFromDate(calendarItem.endDate))" + } else { + dateLabel.text = "\(longDateFormatter.stringFromDate(calendarItem.startDate)) - \(longDateFormatter.stringFromDate(calendarItem.endDate))" + } + } else { + titleLabel.text = nil + locationLabel.text = nil + locationIcon.hidden = true + descriptionLabel.text = nil + courseLabel.text = nil + dateLabel.text = nil + } + } + } +} diff --git a/Hydra/HomeNewsItemCollectionViewCell.swift b/Hydra/HomeNewsItemCollectionViewCell.swift index afd84c8..a8197b5 100644 --- a/Hydra/HomeNewsItemCollectionViewCell.swift +++ b/Hydra/HomeNewsItemCollectionViewCell.swift @@ -14,7 +14,11 @@ class HomeNewsItemCollectionViewCell: UICollectionViewCell { @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var highlightImage: UIImageView! - var article: AssociationNewsItem? { + override func awakeFromNib() { + self.contentView.setShadow() + } + + var article: NewsItem? { didSet { titleLabel.text = article?.title let dateTransformer = SORelativeDateTransformer() diff --git a/Hydra/HomeRestoCollectionViewCell.swift b/Hydra/HomeRestoCollectionViewCell.swift index ddde6f6..f93d29b 100644 --- a/Hydra/HomeRestoCollectionViewCell.swift +++ b/Hydra/HomeRestoCollectionViewCell.swift @@ -13,18 +13,19 @@ class HomeRestoCollectionViewCell: UICollectionViewCell, UITableViewDataSource, @IBOutlet weak var dayLabel: UILabel! @IBOutlet weak var closedLabel: UILabel! + var restoMenu: RestoMenu? { didSet { if restoMenu != nil { closedLabel.hidden = restoMenu!.open - if restoMenu!.day.isToday() { + if restoMenu!.date.isToday() { dayLabel.text = "vandaag" - } else if restoMenu!.day.isTomorrow() { + } else if restoMenu!.date.isTomorrow() { dayLabel.text = "morgen" } else { let formatter = NSDateFormatter.H_dateFormatterWithAppLocale() formatter.dateFormat = "EEEE d MMMM" - dayLabel.text = formatter.stringFromDate(restoMenu!.day) + dayLabel.text = formatter.stringFromDate(restoMenu!.date) } } else { dayLabel.text = "" @@ -37,11 +38,12 @@ class HomeRestoCollectionViewCell: UICollectionViewCell, UITableViewDataSource, override func awakeFromNib() { tableView.separatorColor = UIColor.clearColor() + self.contentView.setShadow() } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if restoMenu!.open { - if let count = restoMenu?.meat.count where restoMenu!.open{ + if let count = restoMenu?.mainDishes?.count where restoMenu!.open{ return count } } @@ -51,7 +53,7 @@ class HomeRestoCollectionViewCell: UICollectionViewCell, UITableViewDataSource, func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("restoMenuTableViewCell") as? HomeRestoMenuItemTableViewCell - cell!.menuItem = restoMenu?.meat[indexPath.row] as? RestoMenuItem + cell!.menuItem = restoMenu?.mainDishes![indexPath.row] return cell! } diff --git a/Hydra/HomeSchamperCollectionViewCell.swift b/Hydra/HomeSchamperCollectionViewCell.swift index 9fa1989..4d8885e 100644 --- a/Hydra/HomeSchamperCollectionViewCell.swift +++ b/Hydra/HomeSchamperCollectionViewCell.swift @@ -13,6 +13,10 @@ class HomeSchamperCollectionViewCell: UICollectionViewCell { @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var authorLabel: UILabel! + override func awakeFromNib() { + self.contentView.setShadow() + } + var article: SchamperArticle? { didSet { titleLabel.text = article?.title diff --git a/Hydra/HomeSpecialEventBasicCollectionViewCell.swift b/Hydra/HomeSpecialEventBasicCollectionViewCell.swift new file mode 100644 index 0000000..f7518d8 --- /dev/null +++ b/Hydra/HomeSpecialEventBasicCollectionViewCell.swift @@ -0,0 +1,29 @@ +// +// HomeSpecialEventBasicCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 06/04/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class HomeSpecialEventBasicCollectionViewCell: UICollectionViewCell { + @IBOutlet weak var image: UIImageView! + @IBOutlet weak var name: UILabel! + @IBOutlet weak var text: UILabel! + + override func awakeFromNib() { + self.contentView.setShadow() + } + + var specialEvent: SpecialEvent? { + didSet { + if let specialEvent = specialEvent { + name.text = specialEvent.name + text.text = specialEvent.simpleText + image.sd_setImageWithURL(NSURL(string: specialEvent.image)) + } + } + } +} \ No newline at end of file diff --git a/Hydra/HomeUrgentCollectionViewCell.swift b/Hydra/HomeUrgentCollectionViewCell.swift index 0f6b495..2c86410 100644 --- a/Hydra/HomeUrgentCollectionViewCell.swift +++ b/Hydra/HomeUrgentCollectionViewCell.swift @@ -14,8 +14,9 @@ class HomeUrgentCollectionViewCell: UICollectionViewCell { let notificationCenter = NSNotificationCenter.defaultCenter() override func awakeFromNib() { - notificationCenter.addObserver(self, selector: "playerStatusChanged:", name: UrgentPlayerDidChangeStateNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(HomeUrgentCollectionViewCell.playerStatusChanged(_:)), name: UrgentPlayerDidChangeStateNotification, object: nil) button.selected = UrgentPlayer.sharedPlayer().isPlaying() + self.contentView.setShadow() } deinit { diff --git a/Hydra/HomeViewController.swift b/Hydra/HomeViewController.swift index 957513c..50b69c2 100644 --- a/Hydra/HomeViewController.swift +++ b/Hydra/HomeViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SafariServices class HomeViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { @IBOutlet weak var feedCollectionView: UICollectionView! @@ -28,7 +29,7 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec } private func sharedInit() { - NSNotificationCenter.defaultCenter().addObserver(self, selector: "homeFeedUpdatedNotification:", name: HomeFeedDidUpdateFeedNotification, object: nil) + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(HomeViewController.homeFeedUpdatedNotification(_:)), name: HomeFeedDidUpdateFeedNotification, object: nil) } deinit { @@ -37,9 +38,10 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec func homeFeedUpdatedNotification(notification: NSNotification) { self.feedItems = HomeFeedService.sharedService.createFeed() - self.feedCollectionView?.reloadData() - - self.refreshControl.endRefreshing() + dispatch_async(dispatch_get_main_queue()) { + self.feedCollectionView?.reloadData() + self.refreshControl.endRefreshing() + } } // MARK - View initialization @@ -47,17 +49,19 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec super.viewDidLoad() refreshControl.tintColor = .whiteColor() - refreshControl.addTarget(self, action: "startRefresh", forControlEvents: .ValueChanged) + refreshControl.addTarget(self, action: #selector(HomeViewController.startRefresh), forControlEvents: .ValueChanged) feedCollectionView.addSubview(refreshControl) - + // REMOVE ME IF THE BUG IS FIXED, THIS IS FUCKING UGLY - NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("refreshDataTimer"), userInfo: nil, repeats: false) + NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: #selector(HomeViewController.refreshDataTimer), userInfo: nil, repeats: false) } - + func refreshDataTimer(){ // REMOVE ME WHEN THE BUG IS FIXED - self.feedCollectionView?.reloadData() + dispatch_async(dispatch_get_main_queue()) { + self.feedCollectionView?.reloadData() + } } - + override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) @@ -76,8 +80,11 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec override func viewDidAppear(animated: Bool) { UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: animated) + + GAI_track("Home") + NotificationService.askSKONotification(self) } - + override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) { // this is called when changing layout :) self.feedCollectionView.collectionViewLayout.invalidateLayout() @@ -108,17 +115,32 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec return cell! case .ActivityItem: let cell = collectionView.dequeueReusableCellWithReuseIdentifier("activityCell", forIndexPath: indexPath) as? HomeActivityCollectionViewCell - cell?.activity = feedItem.object as? AssociationActivity + cell?.activity = feedItem.object as? Activity cell?.layoutIfNeeded() // iOS 9 bug return cell! case .NewsItem: let cell = collectionView.dequeueReusableCellWithReuseIdentifier("newsItemCell", forIndexPath: indexPath) as? HomeNewsItemCollectionViewCell - cell?.article = feedItem.object as? AssociationNewsItem + cell?.article = feedItem.object as? NewsItem + return cell! + case .MinervaAnnouncementItem: + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("minervaAnnouncementCell", forIndexPath: indexPath) as? HomeMinervaAnnouncementCell + cell?.announcement = feedItem.object as? Announcement + return cell! + case .MinervaCalendarItem: + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("minervaCalendarItemCell", forIndexPath: indexPath) as? HomeMinervaCalendarItemCell + cell?.calendarItem = feedItem.object as? CalendarItem + return cell! + case .SpecialEventItem: + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("specialEventBasicCell", forIndexPath: indexPath) as? HomeSpecialEventBasicCollectionViewCell + cell?.specialEvent = feedItem.object as? SpecialEvent + cell?.layoutIfNeeded() return cell! case .UrgentItem: return collectionView.dequeueReusableCellWithReuseIdentifier("urgentfmCell", forIndexPath: indexPath) - case .SettingsItem: + case .AssociationsSettingsItem: return collectionView.dequeueReusableCellWithReuseIdentifier("settingsCell", forIndexPath: indexPath) + case .MinervaSettingsItem: + return collectionView.dequeueReusableCellWithReuseIdentifier("minervaSettingsCell", forIndexPath: indexPath) default: return collectionView.dequeueReusableCellWithReuseIdentifier("testCell", forIndexPath: indexPath) } @@ -130,28 +152,63 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { let feedItem = feedItems[indexPath.row] - + let width: CGFloat + if self.view.frame.size.width < 640 { + width = self.view.frame.size.width + } else { + width = self.view.frame.size.width / 2 + // make all cards same size for consistency in splitview + return CGSizeMake(width, 180) + } switch feedItem.itemType { case .RestoItem: let restoMenu = feedItem.object as? RestoMenu var count = 1 if (restoMenu != nil && restoMenu!.open) { - count = restoMenu!.meat.count + count = restoMenu!.mainDishes!.count } - return CGSizeMake(self.view.frame.size.width, CGFloat(90+count*15)) + return CGSizeMake(width, CGFloat(90+count*15)) case .ActivityItem: - let activity = feedItem.object as? AssociationActivity - //TODO: guess height of cell - let activity_height = activity!.descriptionText.isEmpty ? 60 : 0 - - return CGSizeMake(self.view.frame.size.width, CGFloat(180 - activity_height)) - case .SettingsItem: - return CGSizeMake(self.view.frame.size.width, 80) + guard let activity = feedItem.object as? Activity else { + return CGSizeMake(width, 120) + } + + let descriptionHeight = activity.descriptionText.boundingHeight(CGSizeMake(width, 150)) + + return CGSizeMake(width, descriptionHeight + 120) + case .MinervaAnnouncementItem: + guard let announcement = feedItem.object as? Announcement else { + return CGSizeMake(width, 120) + } + + let contentHeight: CGFloat + if announcement.content.isEmpty { + contentHeight = 0 + } else { + contentHeight = 80 + } + return CGSizeMake(width, 100 + contentHeight) + case .MinervaCalendarItem: + guard let calendarItem = feedItem.object as? CalendarItem else { + return CGSizeMake(width, 120) + } + + let contentHeight: CGFloat + if let content = calendarItem.content where !content.isEmpty { + contentHeight = 80 + } else { + contentHeight = 0 + } + return CGSizeMake(width, 100 + contentHeight) + case .AssociationsSettingsItem, .MinervaSettingsItem: + return CGSizeMake(width, 80) case .NewsItem: - return CGSizeMake(self.view.frame.size.width, 100) + return CGSizeMake(width, 100) + case .SpecialEventItem: + return CGSizeMake(width, 130) default: - return CGSizeMake(self.view.frame.size.width, 135) //TODO: per type + return CGSizeMake(width, 135) //TODO: per type } } @@ -164,28 +221,72 @@ class HomeViewController: UIViewController, UICollectionViewDataSource, UICollec switch feedItem.itemType { case .RestoItem: - let index = self.tabBarController?.viewControllers?.indexOf({$0.tabBarItem.tag == 221}) // using hardcoded tag of Resto Menu viewcontroller - self.tabBarController?.selectedIndex = index! - let navigationController = self.tabBarController?.viewControllers![index!] as? UINavigationController - if let menuController = navigationController?.visibleViewController as? RestoMenuViewController { - let menu = feedItem.object as! RestoMenu - menuController.scrollToDate(menu.day) + //FIXME: using hardcoded tag of Resto Menu viewcontroller + guard let index = self.tabBarController?.viewControllers?.indexOf({$0.tabBarItem.tag == TabViewControllerTags.Resto.rawValue}) else { + break + } + + self.tabBarController?.selectedIndex = index + if let restoMenu = feedItem.object as? RestoMenu { + NSNotificationCenter.defaultCenter().postNotificationName(RestoMenuViewControllerShouldScrollToNotification, object: restoMenu.date) } case .ActivityItem: - self.navigationController?.pushViewController(ActivityDetailController(activity: feedItem.object as! AssociationActivity, delegate: nil), animated: true) + self.navigationController?.pushViewController(ActivityDetailController(activity: feedItem.object as! Activity, delegate: nil), animated: true) case .SchamperNewsItem: let article = feedItem.object as! SchamperArticle if !article.read { article.read = true - SchamperStore.sharedStore().syncStorage() + SchamperStore.sharedStore.syncStorage() } self.navigationController?.pushViewController(SchamperDetailViewController(article: article), animated: true) + case .MinervaAnnouncementItem: + self.performSegueWithIdentifier("homeMinervaDetailSegue", sender: feedItem.object) + case .MinervaCalendarItem: + self.performSegueWithIdentifier("homeCalendarDetailSegue", sender: feedItem.object) case .NewsItem: - self.navigationController?.pushViewController(NewsDetailViewController(newsItem: feedItem.object as! AssociationNewsItem), animated: true) - case .SettingsItem: + self.navigationController?.pushViewController(NewsDetailViewController(newsItem: feedItem.object as! NewsItem), animated: true) + case .AssociationsSettingsItem: self.navigationController?.pushViewController(PreferencesController(), animated: true) + case .MinervaSettingsItem: + let oauthService = UGentOAuth2Service.sharedService + let oauth2 = oauthService.oauth2 + if oauth2.accessToken == nil { + oauth2.authConfig.authorizeEmbedded = true + oauth2.authConfig.authorizeContext = self + oauth2.authorize() + } + case .SpecialEventItem: + let specialEvent = feedItem.object as! SpecialEvent + let url = NSURL(string: specialEvent.link)! + if #available(iOS 9.0, *) { + let svc = SFSafariViewController(URL: url) + self.presentViewController(svc, animated: true, completion: nil) + } else { + // Fallback on earlier versions + let wvc = WebViewController() + wvc.loadUrl(url) + self.navigationController?.pushViewController(wvc, animated: true) + } default: break } } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + guard let identifier = segue.identifier else { return } + switch identifier { + case "homeMinervaDetailSegue": + guard let announcement = sender as? Announcement, let vc = segue.destinationViewController as? MinervaAnnounceDetailViewController else { + return + } + vc.title = "" + vc.announcement = announcement + case "homeCalendarDetailSegue": + guard let item = sender as? CalendarItem, let vc = segue.destinationViewController as? MinervaCalendarDetailViewController else { return } + vc.title = "" + vc.calendarItem = item + default: + break + } + } } \ No newline at end of file diff --git a/Hydra/Hydra-Bridging-Header.h b/Hydra/Hydra-Bridging-Header.h index 9bc0d26..bab315d 100644 --- a/Hydra/Hydra-Bridging-Header.h +++ b/Hydra/Hydra-Bridging-Header.h @@ -2,42 +2,30 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +// App Delegate +#import "AppDelegate.h" + // Services -#import "AssociationStore.h" -#import "PreferencesService.h" -#import "RestoStore.h" -#import "SchamperStore.h" #import "UrgentPlayer.h" // Models -#import "Association.h" -#import "AssociationActivity.h" -#import "AssociationNewsItem.h" #import "NewsDetailViewController.h" -#import "RestoLegendItem.h" -#import "RestoMenu.h" -#import "SchamperArticle.h" -#import "FacebookEvent.h" // Controllers #import "NewsViewController.h" -#import "ActivitiesController.h" +#import "AssociationPreferenceController.h" #import "ActivityDetailController.h" -#import "InfoViewController.h" -#import "PreferencesController.h" #import "RestoMapController.h" #import "SchamperViewController.h" #import "SchamperDetailViewController.h" #import "UrgentViewController.h" +#import "WebViewController.h" + // Categories and extenions #import "NSDateFormatter+AppLocale.h" // Third party classes #import "NSDate+Utilities.h" #import "SORelativeDateTransformer.h" - - -// Remove from bridiging header when removing iOS 7 support, so we can use the iOS >= 8 frameworks in Cocoapods -#import "UIImageView+WebCache.h" -#import +//#import diff --git a/Hydra/Hydra-Prefix.pch b/Hydra/Hydra-Prefix.pch index 794c648..06d3e44 100644 --- a/Hydra/Hydra-Prefix.pch +++ b/Hydra/Hydra-Prefix.pch @@ -13,10 +13,8 @@ #import #import #import -#import -#import -#import #import "UIColor+AppColors.h" +@import Firebase; #endif #define PUBLIC_RELEASE 1 @@ -31,10 +29,11 @@ #endif #if GoogleAnalyticsEnabled - #define GAI_Track(screen) { id tracker = [[GAI sharedInstance] defaultTracker]; \ - [tracker set:kGAIScreenName value:screen]; \ - [tracker send:[[GAIDictionaryBuilder createAppView] build]];\ - } +#define GAI_Track(screen) { [FIRAnalytics logEventWithName:kFIREventSelectContent parameters:@{ \ + kFIRParameterContentType:@"cont", \ + kFIRParameterItemID:@"1" \ + }];\ + } #else #define GAI_Track(screen) do { } while (0) #endif diff --git a/Hydra/HydraTabbarController.swift b/Hydra/HydraTabbarController.swift index abbc8a5..91118f8 100644 --- a/Hydra/HydraTabbarController.swift +++ b/Hydra/HydraTabbarController.swift @@ -15,30 +15,39 @@ class HydraTabBarController: UITabBarController, UITabBarControllerDelegate { self.delegate = self let newsViewController = UINavigationController(rootViewController: NewsViewController()) - let activityController = UINavigationController(rootViewController: ActivitiesController()) let infoController = UINavigationController(rootViewController: InfoViewController()) let schamperController = UINavigationController(rootViewController: SchamperViewController()) let prefsController = UINavigationController(rootViewController: PreferencesController()) let urgentController = UrgentViewController() - infoController.tabBarItem.configure(nil, image: "info", tag: 231) - activityController.tabBarItem.configure(nil, image: "activities", tag: 232) - schamperController.tabBarItem.configure(nil, image: "schamper", tag: 233) - newsViewController.tabBarItem.configure(nil, image: "news", tag: 234) - urgentController.tabBarItem.configure("Urgent.fm", image: "urgent", tag: 235) - prefsController.tabBarItem.configure("Voorkeuren", image: "settings", tag: 236) + infoController.tabBarItem.configure(nil, image: "info", tag: .Info) + schamperController.tabBarItem.configure("Schamper Daily", image: "schamper", tag: .Schamper) + newsViewController.tabBarItem.configure("Nieuws", image: "news", tag: .News) + urgentController.tabBarItem.configure("Urgent.fm", image: "urgent", tag: .Urgentfm) + prefsController.tabBarItem.configure("Voorkeuren", image: "settings", tag: .Preferences) var viewControllers = self.viewControllers! - viewControllers.appendContentsOf([infoController, activityController, newsViewController, schamperController, urgentController, prefsController]) + viewControllers.appendContentsOf([infoController, newsViewController, schamperController, urgentController, prefsController]) self.viewControllers = orderViewControllers(viewControllers) // Fix gray tabbars self.tabBar.translucent = false + + let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian) + let skoDate = calendar?.dateWithEra(1, year: 2016, month: 9, day: 28, hour: 14, minute: 0, second: 0, nanosecond: 0)! + let currentDate = NSDate() + if currentDate.isEarlierThanDate(skoDate?.dateByAddingDays(2)) { + var viewControllers = self.viewControllers + let skoController = SKOHydraTabBarController() + skoController.tabBarItem.configure("Student Kick-Off", image: "sko", tag: .SKO) + viewControllers?.insert(skoController, atIndex: 1) + self.viewControllers = viewControllers + } } func orderViewControllers(viewControllers: [UIViewController]) -> [UIViewController]{ - let tagsOrder = PreferencesService.sharedService().hydraTabBarOrder as! [Int] + let tagsOrder = PreferencesService.sharedService.hydraTabBarOrder if tagsOrder.count == 0 { return viewControllers } @@ -72,19 +81,32 @@ class HydraTabBarController: UITabBarController, UITabBarControllerDelegate { tagsOrder.append(controller.tabBarItem.tag) } - PreferencesService.sharedService().hydraTabBarOrder = tagsOrder + PreferencesService.sharedService.hydraTabBarOrder = tagsOrder } } +enum TabViewControllerTags: Int { + case Home = 220 + case Resto = 221 + case Minerva = 222 + case Info = 231 + case Activities = 232 + case Schamper = 233 + case News = 234 + case Urgentfm = 235 + case Preferences = 236 + case SKO = 999 +} + // MARK: UITabBarItem functions extension UITabBarItem { // Configure UITabBarItem with string, image and tag - func configure(title: String?, image: String, tag: Int) { + func configure(title: String?, image: String, tag: TabViewControllerTags) { if let title = title { self.title = title } self.image = UIImage(named: "tabbar-" + image + ".png") - self.tag = tag + self.tag = tag.rawValue } } \ No newline at end of file diff --git a/Hydra/Images.xcassets/AppIcon.appiconset/Contents.json b/Hydra/Images.xcassets/AppIcon.appiconset/Contents.json index 5998393..eb21cd5 100644 --- a/Hydra/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Hydra/Images.xcassets/AppIcon.appiconset/Contents.json @@ -115,8 +115,9 @@ "scale" : "2x" }, { - "idiom" : "ipad", "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "ipad.png", "scale" : "2x" }, { diff --git a/Hydra/Images.xcassets/AppIcon.appiconset/ipad.png b/Hydra/Images.xcassets/AppIcon.appiconset/ipad.png new file mode 100644 index 0000000..40ab039 Binary files /dev/null and b/Hydra/Images.xcassets/AppIcon.appiconset/ipad.png differ diff --git a/Hydra/InfoItem.swift b/Hydra/InfoItem.swift new file mode 100644 index 0000000..fef24e7 --- /dev/null +++ b/Hydra/InfoItem.swift @@ -0,0 +1,91 @@ +// +// InfoItem.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 11/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import ObjectMapper + +class InfoItem: NSObject, NSCoding, Mappable { + + var title: String = "" + var image: String? + var url: String? + var appStore: String? + var html: String? + var subcontent: [InfoItem]? + + var imageLocation: UIImage? { + get { + if let image = self.image { + // TODO: fix this some time in the future + return UIImage(named: image.stringByReplacingOccurrencesOfString("_", withString: "-")) + } + return nil + } + } + + var htmlURL: NSURL? { + get { + if let html = html { + return NSURL(string: "\(APIConfig.Zeus2_0)info/\(html)") + } + return nil + } + } + var type: InfoItemType { + get { + if (url != nil) || (appStore != nil) { + return .ExternalLink + } + return .InternalLink + } + } + + required init?(_ map: Map) { + + } + + func mapping(map: Map) { + title <- map[PropertyKey.titleKey] + url <- map[PropertyKey.urlKey] + appStore <- map[PropertyKey.appStoreKey] + html <- map[PropertyKey.htmlKey] + image <- map[PropertyKey.imageKey] + subcontent <- map[PropertyKey.subcontentKey] + } + + required init?(coder aDecoder: NSCoder) { + title = aDecoder.decodeObjectForKey(PropertyKey.titleKey) as! String + image = aDecoder.decodeObjectForKey(PropertyKey.imageKey) as? String + url = aDecoder.decodeObjectForKey(PropertyKey.urlKey) as? String + appStore = aDecoder.decodeObjectForKey(PropertyKey.appStoreKey) as? String + html = aDecoder.decodeObjectForKey(PropertyKey.htmlKey) as? String + subcontent = aDecoder.decodeObjectForKey(PropertyKey.subcontentKey) as? [InfoItem] + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(title, forKey: PropertyKey.titleKey) + aCoder.encodeObject(image, forKey: PropertyKey.imageKey) + aCoder.encodeObject(url, forKey: PropertyKey.urlKey) + aCoder.encodeObject(appStore, forKey: PropertyKey.appStoreKey) + aCoder.encodeObject(html, forKey: PropertyKey.htmlKey) + aCoder.encodeObject(subcontent, forKey: PropertyKey.subcontentKey) + } + + struct PropertyKey { + static let titleKey = "title" + static let urlKey = "url" + static let appStoreKey = "url-ios" + static let imageKey = "image" + static let htmlKey = "html" + static let subcontentKey = "subcontent" + } +} + +enum InfoItemType { + case InternalLink + case ExternalLink +} diff --git a/Hydra/InfoStore.swift b/Hydra/InfoStore.swift new file mode 100644 index 0000000..ffc73a1 --- /dev/null +++ b/Hydra/InfoStore.swift @@ -0,0 +1,68 @@ +// +// InfoStore.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 12/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +let InfoStoreDidUpdateInfoNotification = "InfoStoreDidUpdateInfoNotification" + +class InfoStore: SavableStore, NSCoding { + private static var _SharedStore: InfoStore? + static var sharedStore: InfoStore { + get { + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let infoStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.InfoStoreArchive.path!) as? InfoStore + if let infoStore = infoStore { + _SharedStore = infoStore + return _SharedStore! + } + } + // initialize new one + _SharedStore = InfoStore() + return _SharedStore! + } + } + + private var _infoItems: [InfoItem] = [] + var infoItems: [InfoItem] { + get { + self.updateInfoItems() + return self._infoItems + } + } + + private var infoItemsLastUpdated = NSDate(timeIntervalSince1970: 0) + + init() { + super.init(storagePath: Config.InfoStoreArchive.path!) + } + + required convenience init?(coder aDecoder: NSCoder) { + self.init() + self._infoItems = aDecoder.decodeObjectForKey(PropertyKey.infoItemsKey) as! [InfoItem] + self.infoItemsLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.infoItemsLastUpdatedKey) as! NSDate + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(self._infoItems, forKey: PropertyKey.infoItemsKey) + aCoder.encodeObject(self.infoItemsLastUpdated, forKey: PropertyKey.infoItemsLastUpdatedKey) + } + + func updateInfoItems(forcedUpdate: Bool = false) { + let url = APIConfig.Zeus2_0 + "info/info-content.json" + + self.updateResource(url, notificationName: InfoStoreDidUpdateInfoNotification, lastUpdated: infoItemsLastUpdated, forceUpdate: forcedUpdate) { (items: [InfoItem]) in + self._infoItems = items + self.infoItemsLastUpdated = NSDate() + } + } + + struct PropertyKey { + static let infoItemsKey = "infoItems" + static let infoItemsLastUpdatedKey = "infoItemsLastUpdated" + } +} diff --git a/Hydra/InfoViewController.h b/Hydra/InfoViewController.h deleted file mode 100644 index 9d062b7..0000000 --- a/Hydra/InfoViewController.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// InfoViewController.h -// Hydra -// -// Created by Yasser Deceukelier on 19/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@interface InfoViewController : UITableViewController - -- (id)init; -- (id)initWithContent:(NSArray *)content; - -@end diff --git a/Hydra/InfoViewController.m b/Hydra/InfoViewController.m deleted file mode 100644 index 9384ff4..0000000 --- a/Hydra/InfoViewController.m +++ /dev/null @@ -1,160 +0,0 @@ -// -// InfoViewController.m -// Hydra -// -// Created by Yasser Deceukelier on 19/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -@import SafariServices; - -#import "InfoViewController.h" -#import "WebViewController.h" - -@interface InfoViewController () - -@property (nonatomic, strong) NSArray *content; -@property (nonatomic, strong) NSString *trackedViewName; - -@end - -@implementation InfoViewController - -#pragma mark - Initializing + loading - -- (id)init -{ - NSString *path = [[NSBundle mainBundle] pathForResource:@"info-content" ofType:@"plist"]; - self = [self initWithContent:[[NSArray alloc] initWithContentsOfFile:path]]; - - self.title = @"Info"; - self.trackedViewName = self.title; - - return self; -} - -- (id)initWithContent:(NSArray *)content -{ - if (self = [super initWithStyle:UITableViewStylePlain]) { - self.content = content; - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - [[self tableView] setTableFooterView:[[UIView alloc] initWithFrame:CGRectZero]]; - [[self tableView] setBounces:NO]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - GAI_Track(self.trackedViewName); -} - -- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - return (interfaceOrientation == UIInterfaceOrientationPortrait); -} - -#pragma mark - Table view data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return 1; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - return [self.content count]; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - static NSString *CellIdentifier = @"InfoCell"; - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if(!cell) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault - reuseIdentifier:CellIdentifier]; - cell.separatorInset = UIEdgeInsetsZero; - } - - cell.contentView.backgroundColor = [UIColor whiteColor]; - cell.textLabel.backgroundColor = cell.contentView.backgroundColor; - - NSDictionary *item = (self.content)[indexPath.row]; - cell.textLabel.text = item[@"title"]; - - UIImage *icon = [UIImage imageNamed:item[@"image"]]; - if(icon) { - cell.imageView.contentMode = UIViewContentModeScaleAspectFit; - cell.imageView.image = icon; - } - else { - cell.imageView.image = nil; - } - - // Show an icon, depending on the subview - if (item[@"url-ios"] || item[@"url"]) { - UIImage *linkImage = [UIImage imageNamed:@"external-link.png"]; - UIImage *highlightedLinkImage = [UIImage imageNamed:@"external-link-active.png"]; - UIImageView *linkAccessory = [[UIImageView alloc] initWithImage:linkImage - highlightedImage:highlightedLinkImage]; - linkAccessory.contentMode = UIViewContentModeScaleAspectFit; - cell.accessoryView = linkAccessory; - } - else { - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - } - - return cell; -} - -#pragma mark - Table view delegate - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDictionary *item = self.content[indexPath.row]; - - // Choose a different action depending on what data is available - if(item[@"subcontent"]){ - NSArray *subContent = item[@"subcontent"]; - - InfoViewController *c = [[InfoViewController alloc] initWithContent:subContent]; - c.title = item[@"title"]; - c.trackedViewName = [NSString stringWithFormat:@"%@ > %@", self.trackedViewName, c.title]; - - [self.navigationController pushViewController:c animated:YES]; - } - else if(item[@"html"]) { - WebViewController *c = [[WebViewController alloc] init]; - c.title = item[@"title"]; - c.trackedViewName = [NSString stringWithFormat:@"%@ > %@", self.trackedViewName, c.title]; - [c loadHtml:item[@"html"]]; - - [self.navigationController pushViewController:c animated:YES]; - } - else if(item[@"url"]) { - NSURL *url = [NSURL URLWithString:item[@"url"]]; - if (IOS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9")) { - SFSafariViewController *svc = [[SFSafariViewController alloc] initWithURL:url]; - [self.navigationController presentViewController:svc animated:YES completion:nil]; - } else { - [[UIApplication sharedApplication] openURL:url]; - } - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - } - else if(item[@"url-ios"]) { - NSURL *url = [NSURL URLWithString:item[@"url-ios"]]; - [[UIApplication sharedApplication] openURL:url]; - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - } - else { - NSLog(@"Unknown action in %@", item); - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - } -} - -@end diff --git a/Hydra/InfoViewController.swift b/Hydra/InfoViewController.swift new file mode 100644 index 0000000..49b2644 --- /dev/null +++ b/Hydra/InfoViewController.swift @@ -0,0 +1,111 @@ +// +// InfoViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 12/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import SafariServices + +class InfoViewController: UITableViewController { + + var infoItems = InfoStore.sharedStore.infoItems + + init() { + super.init(style: .Plain) + + NSNotificationCenter.defaultCenter().addObserverForName(InfoStoreDidUpdateInfoNotification, object: nil, queue: nil) { (_) in + dispatch_async(dispatch_get_main_queue(), { + self.infoItems = InfoStore.sharedStore.infoItems + self.tableView.reloadData() + }) + } + + self.title = "Info" + } + + init(content infoItems: [InfoItem]) { + super.init(style: .Plain) + self.infoItems = infoItems + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.tableView.bounces = false + self.tableView.tableFooterView = UIView() // No extra divider + } + + // MARK: - Tableview delegate and dataSource + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.infoItems.count + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cellIdentifier = "InfoCell" + var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) + if cell == nil { + cell = UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier) + cell?.contentView.backgroundColor = UIColor.whiteColor() + cell?.textLabel?.backgroundColor = cell?.contentView.backgroundColor + } + + let item = self.infoItems[indexPath.row] + + cell?.textLabel?.text = item.title + + let image = item.imageLocation + cell?.imageView?.image = image + + if item.type == .ExternalLink { + let linkImage = UIImage(named: "external-link.png") + let highlightedLinkImage = UIImage(named: "external-link-active.png") + + let linkAccessory = UIImageView(image: linkImage, highlightedImage: highlightedLinkImage) + linkAccessory.contentMode = .ScaleAspectFit + cell?.accessoryView = linkAccessory + } else { + cell?.accessoryType = .DisclosureIndicator + } + + return cell! + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let item = self.infoItems[indexPath.row] + + // TODO: click tracking + if let subItems = item.subcontent { + let c = InfoViewController(content: subItems) + c.title = item.title + self.navigationController?.pushViewController(c, animated: true) + } else if let htmlLink = item.htmlURL { + let c = WebViewController() + + c.title = item.title + c.loadUrl(htmlLink) + self.navigationController?.pushViewController(c, animated: true) + } else if let urlString = item.url, let url = NSURL(string: urlString) { + if #available(iOS 9.0, *) { + let c = SFSafariViewController(URL: url) + self.navigationController?.presentViewController(c, animated: true, completion: nil) + } else { + // Fallback on earlier versions + UIApplication.sharedApplication().openURL(url) + } + tableView.deselectRowAtIndexPath(indexPath, animated: true) + } else if let urlString = item.appStore, let url = NSURL(string: urlString) { + UIApplication.sharedApplication().openURL(url) + tableView.deselectRowAtIndexPath(indexPath, animated: true) + } + } +} diff --git a/Hydra/InitialOnboardingViewController.swift b/Hydra/InitialOnboardingViewController.swift new file mode 100644 index 0000000..d62e738 --- /dev/null +++ b/Hydra/InitialOnboardingViewController.swift @@ -0,0 +1,35 @@ +// +// InitialOnboardingViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 12/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class InitialOnboardingViewController: UIViewController { + + @IBOutlet weak var skoView: UIView? + + @IBAction func openSKOClicked() { + let vc = UIStoryboard(name: "sko", bundle: nil).instantiateInitialViewController()! + UIApplication.sharedApplication().windows[0].rootViewController = vc + } + + @IBAction func skip() { + #if RELEASE + PreferencesService.sharedService.firstLaunch = false + #endif + let vc = UIStoryboard(name: "MainStoryboard", bundle: nil).instantiateInitialViewController()! + UIApplication.sharedApplication().windows[0].rootViewController = vc + } + + override func viewDidLoad() { + let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian) + let skoDate = calendar?.dateWithEra(1, year: 2016, month: 9, day: 28, hour: 14, minute: 0, second: 0, nanosecond: 0)! + if NSDate().isLaterThanDate(skoDate?.dateByAddingDays(1)) { + skoView?.hidden = true + } + } +} \ No newline at end of file diff --git a/Hydra/LocationService.swift b/Hydra/LocationService.swift index a2b7e1e..dea128d 100644 --- a/Hydra/LocationService.swift +++ b/Hydra/LocationService.swift @@ -9,11 +9,13 @@ import Foundation import CoreLocation -public class LocationService: NSObject, CLLocationManagerDelegate { +let LocationServiceDidUpdateLocationNotification = "LocationServiceDidUpdateLocation" + +class LocationService: NSObject, CLLocationManagerDelegate { static let sharedService = LocationService() - public var allowedLocation: Bool = false + var allowedLocation: Bool = false private var locationManager: CLLocationManager = CLLocationManager() private var location: CLLocation? @@ -26,26 +28,28 @@ public class LocationService: NSObject, CLLocationManagerDelegate { self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters } - public func startUpdating() { + func updateLocation() { let status = CLLocationManager.authorizationStatus() if status == CLAuthorizationStatus.Restricted || status == CLAuthorizationStatus.Denied { allowedLocation = false return } else if status == .NotDetermined { - if #available(iOS 8.0, *) { - locationManager.requestWhenInUseAuthorization() - } + locationManager.requestWhenInUseAuthorization() } allowedLocation = true - - self.locationManager.startUpdatingLocation() + if #available(iOS 9.0, *) { + self.locationManager.requestLocation() + } else { + // Fallback on earlier versions + self.locationManager.startUpdatingLocation() + } } - public func pauseUpdating() { //TODO: call this + private func pauseUpdating() { self.locationManager.stopUpdatingLocation() } - public func calculateDistance(latitude: Double, longitude: Double) -> CLLocationDistance? { + func calculateDistance(latitude: Double, longitude: Double) -> CLLocationDistance? { if !allowedLocation || location == nil{ return nil } @@ -53,13 +57,28 @@ public class LocationService: NSObject, CLLocationManagerDelegate { } //MARK: - Implement core location delegate methods - @objc public func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - debugPrint("Locations updated") + func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + debugPrint("LocationService: location updated") - location = locations[0] + location = locations.first + NSNotificationCenter.defaultCenter().postNotificationName(LocationServiceDidUpdateLocationNotification, object: nil) + + if #available(iOS 9.0, *) { + // else is used + } else { + self.pauseUpdating() + } } - @objc public func locationManagerDidResumeLocationUpdates(manager: CLLocationManager) { - debugPrint("Resumed location updates") + func locationManagerDidResumeLocationUpdates(manager: CLLocationManager) { + debugPrint("LocationService: Resumed location updates") + } + + func locationManagerDidPauseLocationUpdates(manager: CLLocationManager) { + debugPrint("LocationService: Paused location updated") + } + + func locationManager(manager: CLLocationManager, didFailWithError error: NSError) { + debugPrint("LocationService: failed with error: \(error.localizedDescription)") } } \ No newline at end of file diff --git a/Hydra/MainStoryboard.storyboard b/Hydra/MainStoryboard.storyboard index 9462707..4c9a281 100644 --- a/Hydra/MainStoryboard.storyboard +++ b/Hydra/MainStoryboard.storyboard @@ -1,8 +1,8 @@ - + - + @@ -23,9 +23,11 @@ + + - + @@ -63,8 +65,8 @@ - - + + @@ -79,10 +81,10 @@ - + - - - - - - - - + @@ -128,20 +123,17 @@ - - + - + - - - + @@ -149,10 +141,10 @@ - + - + @@ -161,18 +153,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + - + - + - + - - - - - - - @@ -292,9 +727,9 @@ - + - + @@ -302,25 +737,74 @@ - - + + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -346,111 +827,177 @@ - + - + - + - + - - + + - - + + + + - - + + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -458,43 +1005,11 @@ - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + - - - - - - @@ -504,104 +1019,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + - + + + + + + + + + + + + + + - - - + + + - + + + + + - + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + @@ -611,7 +1258,7 @@ - + @@ -634,53 +1281,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -699,6 +1299,7 @@ + @@ -710,7 +1311,7 @@ - + @@ -727,6 +1328,7 @@ + @@ -736,7 +1338,7 @@ - + @@ -1066,69 +1668,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - + @@ -1169,7 +1726,7 @@ - + @@ -1188,7 +1745,7 @@ - + @@ -1332,7 +1889,7 @@ - + @@ -1405,26 +1962,884 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + - + + - diff --git a/Hydra/MapViewController.m b/Hydra/MapViewController.m index 204b4e2..85cc57a 100644 --- a/Hydra/MapViewController.m +++ b/Hydra/MapViewController.m @@ -50,7 +50,7 @@ - (void)loadView UIButton *trackButton = [UIButton buttonWithType:UIButtonTypeCustom]; trackButton.frame = CGRectMake(bounds.size.width - 50, bounds.size.height - 40, 42, 34); trackButton.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin; - trackButton.hidden = ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized); + trackButton.hidden = ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedAlways); [trackButton setBackgroundImage:[UIImage imageNamed:@"button-track"] forState:UIControlStateNormal]; [trackButton setBackgroundImage:[UIImage imageNamed:@"button-track-highlighted"] @@ -204,8 +204,6 @@ - (void)routeButtonTapped:(UIButton *)sender id annotation = [(MKAnnotationView *)view annotation]; CLLocationCoordinate2D coordinates = [annotation coordinate]; - // Check for iOS 6 - Class mapItemClass = [MKMapItem class]; // Create an MKMapItem to pass to the Maps app MKPlacemark *placemark = [[MKPlacemark alloc] initWithCoordinate:coordinates addressDictionary:nil]; diff --git a/Hydra/MinervaAnnounceDetailViewController.swift b/Hydra/MinervaAnnounceDetailViewController.swift new file mode 100644 index 0000000..297eb8b --- /dev/null +++ b/Hydra/MinervaAnnounceDetailViewController.swift @@ -0,0 +1,55 @@ +// +// MinervaAnnounceDetailViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class MinervaAnnounceDetailViewController: UIViewController { + @IBOutlet weak var titleLabel: UILabel? + @IBOutlet weak var authorLabel: UILabel? + @IBOutlet weak var courseLabel: UILabel? + @IBOutlet weak var dateLabel: UILabel? + @IBOutlet weak var contentView: UITextView? + + let dateTransformer = SORelativeDateTransformer() + + var announcement: Announcement? { + didSet { + loadAnnouncement() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + loadAnnouncement() + } + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + + // make sure contentview is scrolled to the top + contentView?.scrollRectToVisible(CGRectMake(0, 0, 10, 10), animated: false) + } + + func loadAnnouncement() { + if let announcement = announcement { + titleLabel?.text = announcement.title + authorLabel?.text = announcement.editUser + dateLabel?.text = dateTransformer.transformedValue(announcement.date) as? String + courseLabel?.text = announcement.course?.title + + if let contentView = contentView { + let contentAttributedText = announcement.content.html2AttributedString + if let contentAttributedText = contentAttributedText, let font = contentView.font { + contentAttributedText.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, contentAttributedText.length)) + contentView.attributedText = contentAttributedText + } + } + } + } + +} diff --git a/Hydra/MinervaCalendarDetailViewController.swift b/Hydra/MinervaCalendarDetailViewController.swift new file mode 100644 index 0000000..d751d24 --- /dev/null +++ b/Hydra/MinervaCalendarDetailViewController.swift @@ -0,0 +1,71 @@ +// +// MinervaCalendarDetailViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 17/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class MinervaCalendarDetailViewController: UIViewController { + @IBOutlet weak var titleLabel: UILabel? + @IBOutlet weak var authorLabel: UILabel? + @IBOutlet weak var locationLabel: UILabel? + @IBOutlet weak var courseLabel: UILabel? + @IBOutlet weak var dateLabel: UILabel? + @IBOutlet weak var contentView: UITextView? + + let dateTransformer = SORelativeDateTransformer() + + var calendarItem: CalendarItem? { + didSet { + loadItem() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + loadItem() + } + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + + // make sure contentview is scrolled to the top + contentView?.scrollRectToVisible(CGRectMake(0, 0, 10, 10), animated: false) + } + + func loadItem() { + if let item = calendarItem { + titleLabel?.text = item.title + authorLabel?.text = item.creator + courseLabel?.text = item.course?.title + locationLabel?.text = item.location + + let longDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + longDateFormatter.timeStyle = .ShortStyle + longDateFormatter.dateStyle = .LongStyle + longDateFormatter.doesRelativeDateFormatting = true + + let shortDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + shortDateFormatter.timeStyle = .ShortStyle + shortDateFormatter.dateStyle = .NoStyle + + if item.startDate.dateByAddingDays(1).isLaterThanDate(item.endDate) { + dateLabel?.text = "\(longDateFormatter.stringFromDate(item.startDate)) - \(shortDateFormatter.stringFromDate(item.endDate))" + } else { + dateLabel?.text = "\(longDateFormatter.stringFromDate(item.startDate))\n\(longDateFormatter.stringFromDate(item.endDate))" + } + + if let contentView = contentView { + let contentAttributedText = item.content?.html2AttributedString + if let contentAttributedText = contentAttributedText, let font = contentView.font { + contentAttributedText.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, contentAttributedText.length)) + contentView.attributedText = contentAttributedText + } + } + } + } + +} diff --git a/Hydra/MinervaCourseAnnouncementViewController.swift b/Hydra/MinervaCourseAnnouncementViewController.swift new file mode 100644 index 0000000..0b25988 --- /dev/null +++ b/Hydra/MinervaCourseAnnouncementViewController.swift @@ -0,0 +1,157 @@ +// +// MinervaCourseAnnouncementViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 26/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import RMPickerViewController + +class MinervaAnnouncementController: UITableViewController, UIPickerViewDelegate, UIPickerViewDataSource { + + private var courses = MinervaStore.sharedStore.filteredCourses + + private let dateTransformer = SORelativeDateTransformer() + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + sharedInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + sharedInit() + } + + private func sharedInit() { + let notificationCenter = NSNotificationCenter.defaultCenter() + notificationCenter.addObserver(self, selector: #selector(MinervaAnnouncementController.minervaNotification), name: MinervaStoreDidUpdateCourseInfoNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(MinervaAnnouncementController.minervaNotification), name: MinervaStoreDidUpdateCoursesNotification, object: nil) + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + func minervaNotification() { + dispatch_async(dispatch_get_main_queue()) { + self.courses = MinervaStore.sharedStore.filteredCourses + self.tableView.reloadData() + self.refreshControl?.endRefreshing() + + self.navigationItem.rightBarButtonItem?.enabled = self.courses.count > 0 + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + for course in courses { + MinervaStore.sharedStore.updateAnnouncements(course) + } + + let refreshControl = UIRefreshControl() + refreshControl.tintColor = UIColor.hydraTintcolor() + refreshControl.addTarget(self, action: #selector(MinervaAnnouncementController.didPullRefreshControl), forControlEvents: .ValueChanged) + + self.refreshControl = refreshControl + + let button = UIBarButtonItem(title: "Cursus", style: .Plain, target: self, action: #selector(MinervaAnnouncementController.pickerBarButtonPressed)) + self.navigationItem.rightBarButtonItem = button + self.navigationItem.rightBarButtonItem?.enabled = self.courses.count > 0 + } + + func didPullRefreshControl() { + self.courses = MinervaStore.sharedStore.filteredCourses + for course in courses { + MinervaStore.sharedStore.updateAnnouncements(course, forcedUpdate: true) + } + + self.tableView.reloadData() + } + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return courses.count + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let announcements = MinervaStore.sharedStore.announcement(courses[section]) { + return min(announcements.count, 10) + } + return 0 + } + + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return courses[section].title + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let course = courses[indexPath.section] + + if let announcements = MinervaStore.sharedStore.announcement(course) { + guard indexPath.row < announcements.count else { + return UITableViewCell() + } + let announcement = announcements[indexPath.row] + let cell = UITableViewCell(style: .Subtitle, reuseIdentifier: "Cell") + cell.textLabel?.text = announcement.title + + cell.detailTextLabel?.text = dateTransformer.transformedValue(announcement.date) as! String? + return cell + } + + return UITableViewCell() + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let course = courses[indexPath.section] + if let announcements = MinervaStore.sharedStore.announcement(course) { + let announcement = announcements[indexPath.row] + let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil) + let vc = storyboard.instantiateViewControllerWithIdentifier("minerva-detail-controller") as! MinervaAnnounceDetailViewController + + vc.announcement = announcement + self.navigationController?.pushViewController(vc, animated: true) + } + + } + + // MARK: Implement UIPickerView + func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return courses.count + } + + func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return courses[row].title + } + + func pickerBarButtonPressed() { + let selectAction = RMAction(title: "Selecteer", style: .Done) { (rma) in + if let rmpvc = rma as? RMPickerViewController { + let selectedSection = rmpvc.picker.selectedRowInComponent(0) + let course = self.courses[selectedSection] + let announcements = MinervaStore.sharedStore.announcement(course) + if let announcements = announcements where announcements.count > 0 { + self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: selectedSection), atScrollPosition: .Top, animated: true) + } + } + } + + let pickerController = RMPickerViewController(style: RMActionControllerStyle.Default, selectAction: selectAction, andCancelAction: nil) + pickerController?.picker.delegate = self + pickerController?.picker.dataSource = self + + if let tabBarController = self.tabBarController { + tabBarController.presentViewController(pickerController!, animated: true, completion: nil) + + } else { + self.presentViewController(pickerController!, animated: true, completion: nil) + } + } +} \ No newline at end of file diff --git a/Hydra/MinervaCoursePreferenceViewController.swift b/Hydra/MinervaCoursePreferenceViewController.swift new file mode 100644 index 0000000..ae9be79 --- /dev/null +++ b/Hydra/MinervaCoursePreferenceViewController.swift @@ -0,0 +1,147 @@ +// +// MinervaCoursePreferenceViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 26/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import SVProgressHUD + +class MinervaCoursePreferenceViewController: UITableViewController { + + private var courses: [Course] = [] + private var unselectedCourses = PreferencesService.sharedService.unselectedMinervaCourses + + private var selectAllBarButtonItem: UIBarButtonItem? + + init() { + super.init(style: .Plain) + let center = NSNotificationCenter.defaultCenter() + center.addObserver(self, selector: #selector(MinervaCoursePreferenceViewController.loadMinervaCourses), name: MinervaStoreDidUpdateCoursesNotification, object: nil) + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + func loadMinervaCourses() { + dispatch_async(dispatch_get_main_queue()) { + self.courses = MinervaStore.sharedStore.courses + self.tableView.reloadData() + + if self.courses.count > 0 { + SVProgressHUD.dismiss() + } + + self.refreshControl?.endRefreshing() + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "Cursussen" + + let refreshControl = UIRefreshControl() + refreshControl.tintColor = UIColor.hydraTintcolor() + refreshControl.addTarget(self, action: #selector(MinervaCoursePreferenceViewController.didPullRefreshControl), forControlEvents: .ValueChanged) + + self.refreshControl = refreshControl + + selectAllBarButtonItem = UIBarButtonItem(title: "Selecteer alles", style: .Plain, target: self, action: #selector(MinervaCoursePreferenceViewController.selectAllCourses)) + if courses.count > 0 && unselectedCourses.contains(courses[0].internalIdentifier!) { + selectAllBarButtonItem?.title = "Deselecteer alles" + } + self.navigationController?.navigationBarHidden = false + self.navigationItem.rightBarButtonItem = selectAllBarButtonItem + + loadMinervaCourses() + } + + override func viewDidDisappear(animated: Bool) { + super.viewDidDisappear(animated) + + NSNotificationCenter.defaultCenter().postNotificationName(PreferencesControllerDidUpdatePreferenceNotification, object: nil) + } + + func selectAllCourses() { + if courses.count > 0 && unselectedCourses.contains(courses[0].internalIdentifier!) { + self.unselectedCourses = Set() + selectAllBarButtonItem?.title = "Deselecteer alles" + } else { + for course in courses { + unselectedCourses.insert(course.internalIdentifier!) + } + selectAllBarButtonItem?.title = "Selecteer alles" + } + self.tableView.reloadData() + PreferencesService.sharedService.unselectedMinervaCourses = unselectedCourses + } + + + func didPullRefreshControl() { + MinervaStore.sharedStore.updateCourses(true) + } + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + GAI_track("Voorkeuren > Vakken") + + if courses.count == 0 { + SVProgressHUD.show() + } + } + + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + SVProgressHUD.dismiss() + } + + // MARK: Tableview methods + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return courses.count + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cellIdentifier = "MinervaCoursePreferenceCell" + let course = courses[indexPath.row] + + var cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) + if cell == nil { + cell = UITableViewCell(style: .Subtitle, reuseIdentifier: cellIdentifier) + } + + cell?.textLabel?.text = course.title + let tutorName = NSMutableAttributedString(attributedString: course.tutorName!.html2AttributedString!) + tutorName.addAttribute(NSFontAttributeName, value: cell!.detailTextLabel!.font, range: NSMakeRange(0, tutorName.length)) + cell?.detailTextLabel?.attributedText = tutorName + + if let identifier = course.internalIdentifier where unselectedCourses.contains(identifier) { + cell?.accessoryType = .None + } else { + cell?.accessoryType = .Checkmark + } + + return cell! + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let course = self.courses[indexPath.row] + if let id = course.internalIdentifier { + if unselectedCourses.contains(id) { + self.unselectedCourses.remove(id) + } else { + self.unselectedCourses.insert(id) + } + self.tableView.reloadRowsAtIndexPaths( + [indexPath], withRowAnimation: .Automatic) + PreferencesService.sharedService.unselectedMinervaCourses = unselectedCourses + } + } + +} \ No newline at end of file diff --git a/Hydra/MinervaOnboardingViewController.swift b/Hydra/MinervaOnboardingViewController.swift new file mode 100644 index 0000000..d0814fe --- /dev/null +++ b/Hydra/MinervaOnboardingViewController.swift @@ -0,0 +1,63 @@ +// +// MinervaOnboardingViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 12/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class MinervaOnboardingViewController: UIViewController { + + @IBOutlet weak var loginButton: UIButton? + @IBOutlet weak var loadCoursesButton: UIButton? + @IBOutlet weak var nextButton: UIButton? + + override func viewDidLoad() { + super.viewDidLoad() + let center = NSNotificationCenter.defaultCenter() + center.addObserver(self, selector: #selector(MinervaOnboardingViewController.updateState), name: UGentOAuth2ServiceDidUpdateUserNotification, object: nil) + center.addObserver(self, selector: #selector(MinervaOnboardingViewController.updateState), name: MinervaStoreDidUpdateUserNotification, object: nil) + updateState() + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.navigationBarHidden = true + } + + @IBAction func login() { + let oauthService = UGentOAuth2Service.sharedService + let oauth2 = oauthService.oauth2 + if oauth2.accessToken == nil { + oauth2.authConfig.authorizeEmbedded = true + oauth2.authConfig.authorizeContext = self + oauth2.authorize() + } + } + + @IBAction func showCourses() { + if UGentOAuth2Service.sharedService.isAuthenticated() { + self.navigationController?.pushViewController(MinervaCoursePreferenceViewController(), animated: true) + } + } + + func updateState() { + loadCoursesButton?.enabled = UGentOAuth2Service.sharedService.isAuthenticated() + if UGentOAuth2Service.sharedService.isAuthenticated() { + if let user = MinervaStore.sharedStore.user { + loginButton?.setTitle("Welkom \(user.name)", forState: .Normal) + } else { + loginButton?.setTitle("Ingelogd op Minerva", forState: .Normal) + } + nextButton?.setTitle("Volgende", forState: .Normal) + } + + } +} \ No newline at end of file diff --git a/Hydra/MinervaRootViewController.swift b/Hydra/MinervaRootViewController.swift new file mode 100644 index 0000000..e2124a9 --- /dev/null +++ b/Hydra/MinervaRootViewController.swift @@ -0,0 +1,9 @@ +// +// MinervaRootViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 30/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation diff --git a/Hydra/MinervaStore.swift b/Hydra/MinervaStore.swift new file mode 100644 index 0000000..1cccac6 --- /dev/null +++ b/Hydra/MinervaStore.swift @@ -0,0 +1,349 @@ +// +// MinervaStore.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 26/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import Alamofire +import ObjectMapper +import AlamofireObjectMapper + +let MinervaStoreDidUpdateCoursesNotification = "MinervaStoreDidUpdateCourses" +let MinervaStoreDidUpdateCalendarNotification = "MinervaStoreDidUpdateCalendar" +let MinervaStoreDidUpdateCourseInfoNotification = "MinervaStoreDidUpdateCourseInfo" +let MinervaStoreDidUpdateUserNotification = "MinervaStoreDidUpdateUser" + +class MinervaStore: SavableStore, NSCoding { + + private static var _SharedStore: MinervaStore? + static var sharedStore: MinervaStore { + get { + //TODO: make lazy, and catch NSKeyedUnarchiver errors + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let minervaStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.MinervaStoreArchive.path!) as? MinervaStore + if let minervaStore = minervaStore { + _SharedStore = minervaStore + return _SharedStore! + } + } + // initialize new one + _SharedStore = MinervaStore() + return _SharedStore! + } + } + + init() { + super.init(storagePath: Config.MinervaStoreArchive.path!) + } + + private var coursesLastUpdated = NSDate(timeIntervalSince1970: 0) + private var coursesDict = [String: Course]() + private var _courses: [Course] = [] + var courses: [Course] { + get { + self.updateCourses() + return _courses + } + } + + var filteredCourses: [Course] { + get { + let hiddenCourses = PreferencesService.sharedService.unselectedMinervaCourses + + self.updateCourses() + return _courses.filter({ !hiddenCourses.contains($0.internalIdentifier!) }) + } + } + + private var userLastUpdated = NSDate(timeIntervalSince1970: 0) + private var _user: User? = nil + var user: User? { + get { + if let user = self._user { + return user + } + + self.updateUser() + return nil + } + } + + private var _calendarItems = [CalendarItem]() + private var calendarItemsLastUpdated = NSDate(timeIntervalSince1970: 0) + var calendarItems: [CalendarItem] { + get { + self.updateCalendarItems() + return _calendarItems + } + } + + private var courseLastUpdated: [String: NSDate] = [:] + private var _announcements: [String: [Announcement]] = [:] + + func announcement(course: Course, forcedUpdate: Bool = false) -> [Announcement]? { + updateAnnouncements(course, forcedUpdate: forcedUpdate) + if let announcements = _announcements[course.internalIdentifier!] { + return announcements + } + + return nil + } + + func update() { + //TODO: fill in + guard UGentOAuth2Service.sharedService.isLoggedIn() else { + return + } + + updateCourses() + updateUser() + updateCalendarItems() + for course in _courses { + updateAnnouncements(course) + } + } + + // MARK: - Communication functions + func updateCourses(forcedUpdate: Bool = false) { + let url = APIConfig.Minerva + "courses" + + self.updateResource(url, notificationName: MinervaStoreDidUpdateCoursesNotification, lastUpdated: coursesLastUpdated, forceUpdate: forcedUpdate, keyPath: "courses", oauth: true) { (courses: [Course]) in + self._courses = courses + self.createCourseDict() + if self._courses.count > 0 { + self.coursesLastUpdated = NSDate() + } + } + } + + func updateUser(forcedUpdate: Bool = false) { + let url = APIConfig.OAuth + "tokeninfo" + var forcedUpdate = forcedUpdate + if _user == nil { + forcedUpdate = true + } + self.updateResource(url, notificationName: MinervaStoreDidUpdateUserNotification, lastUpdated: self.userLastUpdated, forceUpdate: forcedUpdate, oauth: true) { (tokenInfo: OAuthTokenInfo) in + self._user = tokenInfo.user + if self._user != nil { + self.userLastUpdated = NSDate() + } + } + } + + func updateCalendarItems(forcedUpdate: Bool = false, start: NSDate? = nil, end: NSDate? = nil) { + let url: String + if let start = start, let end = end { + url = APIConfig.Minerva + "agenda?start=\(start.timeIntervalSince1970)&end=\(end.timeIntervalSince1970)" + } else { + url = APIConfig.Minerva + "agenda" + } + + self.updateResource(url, notificationName: MinervaStoreDidUpdateCalendarNotification, lastUpdated: self.calendarItemsLastUpdated, forceUpdate: forcedUpdate, keyPath: "items", oauth: true) { (items: [CalendarItem]) in + if items.count > 0 { + self._calendarItems = items + self.calendarItemsLastUpdated = NSDate() + } + } + } + + func updateAnnouncements(course: Course, forcedUpdate: Bool = false) { + let url = APIConfig.Minerva + "course/\(course.internalIdentifier!)/announcement" + + var lastUpdated = self.courseLastUpdated[course.internalIdentifier!] + + if lastUpdated == nil { + lastUpdated = NSDate(timeIntervalSince1970: 0) + } + + self.updateResource(url, notificationName: MinervaStoreDidUpdateCourseInfoNotification, lastUpdated: lastUpdated!, forceUpdate: forcedUpdate, keyPath: "items", oauth: true) { (items: [Announcement]) in + print("\(course.title): \(items.count) announcements") + var items = items + let readAnnouncements: Set + if let oldAnnouncements = self._announcements[course.internalIdentifier!] { + readAnnouncements = Set(oldAnnouncements.filter{ $0.read }.map({ $0.itemId })) + } else { + readAnnouncements = Set() + } + + for announcement in items { + if readAnnouncements.contains(announcement.itemId) { + announcement.read = true + } + } + + items.sortInPlace { $0.date > $1.date } + + self._announcements[course.internalIdentifier!] = items + if self._announcements[course.internalIdentifier!] != nil && + self._announcements[course.internalIdentifier!]?.count > 0 { + self.courseLastUpdated[course.internalIdentifier!] = NSDate() + } + } + } + + func logoff() { + self._courses = [] + self.coursesLastUpdated = NSDate(timeIntervalSince1970: 0) + self._announcements = [String : [Announcement]]() + self.courseLastUpdated = [String: NSDate]() + self._calendarItems = [] + self._user = nil + self.userLastUpdated = NSDate(timeIntervalSince1970: 0) + self.coursesDict = [:] + NSNotificationCenter.defaultCenter().postNotificationName(MinervaStoreDidUpdateCoursesNotification, object: nil) + + PreferencesService.sharedService.unselectedMinervaCourses = Set() + self.syncStorage() + } + + func course(identifier: String) -> Course? { + return coursesDict[identifier] + } + + func createCourseDict() { + var courseDict = [String: Course]() + for course in _courses { + courseDict[course.internalIdentifier!] = course + } + self.coursesDict = courseDict + } + + // MARK: Conform to NSCoding + required init?(coder aDecoder: NSCoder) { + super.init(storagePath: Config.MinervaStoreArchive.path!) + guard let courses = aDecoder.decodeObjectForKey(PropertyKey.coursesKey) as? [Course], + let coursesLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.coursesLastUpdatedKey) as? NSDate, + let announcements = aDecoder.decodeObjectForKey(PropertyKey.announcementsKey) as? [String: [Announcement]], + let courseLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.courseLastUpdatedKey) as? [String: NSDate], + let calendarItems = aDecoder.decodeObjectForKey(PropertyKey.calendarItemsKey) as? [CalendarItem] else { + return nil + } + self._courses = courses + self.coursesLastUpdated = coursesLastUpdated + self._announcements = announcements + self.courseLastUpdated = courseLastUpdated + self._calendarItems = calendarItems + + self._user = aDecoder.decodeObjectForKey(PropertyKey.userKey) as? User + self.userLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.userLastUpdatedKey) as! NSDate + + createCourseDict() + + if !PreferencesService.sharedService.userLoggedInToMinerva { + self.logoff() + } + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(self._courses, forKey: PropertyKey.coursesKey) + aCoder.encodeObject(self.coursesLastUpdated, forKey: PropertyKey.coursesLastUpdatedKey) + aCoder.encodeObject(self._announcements, forKey: PropertyKey.announcementsKey) + aCoder.encodeObject(self.courseLastUpdated, forKey: PropertyKey.courseLastUpdatedKey) + aCoder.encodeObject(self._calendarItems, forKey: PropertyKey.calendarItemsKey) + aCoder.encodeObject(self.user, forKey: PropertyKey.userKey) + aCoder.encodeObject(self.userLastUpdated, forKey: PropertyKey.userLastUpdatedKey) + } + + func sortedByDate() -> [NSDate: [CalendarItem]] { + // TODO: write somewhat better algorithm + var sorted = [NSDate: [CalendarItem]]() + + let hiddenCourses = PreferencesService.sharedService.unselectedMinervaCourses + + for calendarItem in _calendarItems { + if hiddenCourses.contains(calendarItem.courseId) { + break + } + let date = calendarItem.startDate.dateAtStartOfDay() + let endDate = calendarItem.endDate.dateAtStartOfDay() + + var dateItems = sorted[date] + if dateItems == nil { + dateItems = [] + } + dateItems?.append(calendarItem) + sorted[date] = dateItems + + //TODO: maybe in while loop + if date != endDate { + // ends in different day + var dateItems = sorted[endDate] + if dateItems == nil { + dateItems = [] + } + dateItems?.append(calendarItem) + sorted[endDate] = dateItems + } + } + return sorted + } + + struct PropertyKey { + static let coursesKey = "courses" + static let coursesLastUpdatedKey = "coursesLastUpdated" + static let announcementsKey = "announcements" + static let courseLastUpdatedKey = "courseLastUpdated" + static let calendarItemsKey = "calendarItems" + static let userKey = "user" + static let userLastUpdatedKey = "userLastUpdatedKey" + } +} + +extension MinervaStore: FeedItemProtocol { + + func feedItems() -> [FeedItem] { + guard UGentOAuth2Service.sharedService.isLoggedIn() else { + return [FeedItem(itemType: .MinervaSettingsItem, object: nil, priority: 900)] + } + + var feedItems = [FeedItem]() + let hiddenCourses = PreferencesService.sharedService.unselectedMinervaCourses + + let oneWeekLater = NSDate(daysFromNow: 7) + let now = NSDate() + for course in _courses.filter({ !hiddenCourses.contains($0.internalIdentifier!) }) { + let announcements = _announcements[course.internalIdentifier!] + if let announcements = announcements { + for announcement in announcements { + let date = announcement.date + let hoursBetween = date.hoursBeforeDate(now) + let priority = 950 - hoursBetween * 10 + + if priority < 0 { + continue + } + announcement.course = course + feedItems.append(FeedItem(itemType: .MinervaAnnouncementItem, object: announcement, priority: priority)) + } + } + } + + for calendarItem in _calendarItems.filter({ + if let course = $0.course, let internalIdentifier = course.internalIdentifier { + return !hiddenCourses.contains(internalIdentifier) + } + return false + }) { + let endDate = calendarItem.endDate + let startDate = calendarItem.startDate + if endDate.isEarlierThanDate(now) || startDate.isLaterThanDate(oneWeekLater) { + continue + } + let hoursBetween = startDate.hoursAfterDate(now) + let priority: Int + + if hoursBetween < 2 { + priority = 1000 + } else { + priority = 950 - hoursBetween * 10 + } + feedItems.append(FeedItem(itemType: .MinervaCalendarItem, object: calendarItem, priority: priority)) + } + return feedItems + } +} \ No newline at end of file diff --git a/Hydra/NSDate+Utilities.m b/Hydra/NSDate+Utilities.m index b1ce126..10ec37d 100644 --- a/Hydra/NSDate+Utilities.m +++ b/Hydra/NSDate+Utilities.m @@ -13,7 +13,7 @@ #import "NSDate+Utilities.h" #import "Hydra-Swift.h" -#define DATE_COMPONENTS (NSYearCalendarUnit| NSMonthCalendarUnit | NSDayCalendarUnit | NSWeekCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit | NSWeekdayCalendarUnit | NSWeekdayOrdinalCalendarUnit) +#define DATE_COMPONENTS (NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekOfYear | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitWeekday | NSCalendarUnitWeekdayOrdinal) #define CURRENT_CALENDAR [NSCalendar hydraCalendar] @implementation NSDate (Utilities) @@ -101,14 +101,14 @@ - (BOOL) isYesterday // This hard codes the assumption that a week is 7 days - (BOOL) isSameWeekAsDate: (NSDate *) aDate { - NSDateComponents *components1 = [CURRENT_CALENDAR components:NSWeekOfYearCalendarUnit fromDate:self]; - NSDateComponents *components2 = [CURRENT_CALENDAR components:NSWeekOfYearCalendarUnit fromDate:aDate]; + NSDateComponents *components1 = [CURRENT_CALENDAR components:NSCalendarUnitWeekOfYear fromDate:self]; + NSDateComponents *components2 = [CURRENT_CALENDAR components:NSCalendarUnitWeekOfYear fromDate:aDate]; // Must be same week. 12/31 and 1/1 will both be week "1" if they are in the same week if (components1.weekOfYear != components2.weekOfYear) return NO; // Must have a time interval under 1 week. Thanks @aclark - return (abs([self timeIntervalSinceDate:aDate]) < D_WEEK); + return (fabs([self timeIntervalSinceDate:aDate]) < D_WEEK); } - (BOOL) isThisWeek @@ -132,8 +132,8 @@ - (BOOL) isLastWeek - (BOOL) isSameMonthAsDate: (NSDate *) aDate { - NSDateComponents *components1 = [CURRENT_CALENDAR components:NSYearCalendarUnit|NSMonthCalendarUnit fromDate:self]; - NSDateComponents *components2 = [CURRENT_CALENDAR components:NSYearCalendarUnit|NSMonthCalendarUnit fromDate:aDate]; + NSDateComponents *components1 = [CURRENT_CALENDAR components:NSCalendarUnitYear|NSCalendarUnitMonth fromDate:self]; + NSDateComponents *components2 = [CURRENT_CALENDAR components:NSCalendarUnitYear|NSCalendarUnitMonth fromDate:aDate]; return ((components1.month == components2.month) && (components1.year == components2.year)); } @@ -145,8 +145,8 @@ - (BOOL) isThisMonth - (BOOL) isSameYearAsDate: (NSDate *) aDate { - NSDateComponents *components1 = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:self]; - NSDateComponents *components2 = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:aDate]; + NSDateComponents *components1 = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:self]; + NSDateComponents *components2 = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:aDate]; return (components1.year == components2.year); } @@ -157,16 +157,16 @@ - (BOOL) isThisYear - (BOOL) isNextYear { - NSDateComponents *components1 = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:self]; - NSDateComponents *components2 = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:[NSDate date]]; + NSDateComponents *components1 = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:self]; + NSDateComponents *components2 = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:[NSDate date]]; return (components1.year == (components2.year + 1)); } - (BOOL) isLastYear { - NSDateComponents *components1 = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:self]; - NSDateComponents *components2 = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:[NSDate date]]; + NSDateComponents *components1 = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:self]; + NSDateComponents *components2 = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:[NSDate date]]; return (components1.year == (components2.year - 1)); } @@ -184,7 +184,7 @@ - (BOOL) isLaterThanDate: (NSDate *) aDate #pragma mark Roles - (BOOL) isTypicallyWeekend { - NSDateComponents *components = [CURRENT_CALENDAR components:NSWeekdayCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitWeekday fromDate:self]; if ((components.weekday == 1) || (components.weekday == 7)) return YES; @@ -293,73 +293,73 @@ - (NSInteger) nearestHour { NSTimeInterval aTimeInterval = [[NSDate date] timeIntervalSinceReferenceDate] + D_MINUTE * 30; NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:aTimeInterval]; - NSDateComponents *components = [CURRENT_CALENDAR components:NSHourCalendarUnit fromDate:newDate]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitHour fromDate:newDate]; return components.hour; } - (NSInteger) hour { - NSDateComponents *components = [CURRENT_CALENDAR components:NSHourCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitHour fromDate:self]; return components.hour; } - (NSInteger) minute { - NSDateComponents *components = [CURRENT_CALENDAR components:NSMinuteCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitMinute fromDate:self]; return components.minute; } - (NSInteger) seconds { - NSDateComponents *components = [CURRENT_CALENDAR components:NSSecondCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitSecond fromDate:self]; return components.second; } - (NSInteger) day { - NSDateComponents *components = [CURRENT_CALENDAR components:NSDayCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitDay fromDate:self]; return components.day; } - (NSInteger) month { - NSDateComponents *components = [CURRENT_CALENDAR components:NSMonthCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitMonth fromDate:self]; return components.month; } - (NSInteger) week { - NSDateComponents *components = [CURRENT_CALENDAR components:NSWeekOfYearCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitWeekOfYear fromDate:self]; return components.weekOfYear; } - (NSInteger) yearOfCalendarWeek { - NSDateComponents *components = [CURRENT_CALENDAR components:NSYearForWeekOfYearCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitYearForWeekOfYear fromDate:self]; return components.yearForWeekOfYear; } - (NSInteger) weekday { - NSDateComponents *components = [CURRENT_CALENDAR components:NSWeekdayCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitWeekday fromDate:self]; return components.weekday; } - (NSInteger) nthWeekday // e.g. 2nd Tuesday of the month is 2 { - NSDateComponents *components = [CURRENT_CALENDAR components:NSWeekdayOrdinalCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitWeekdayOrdinal fromDate:self]; return components.weekdayOrdinal; } - (NSInteger) year { - NSDateComponents *components = [CURRENT_CALENDAR components:NSYearCalendarUnit fromDate:self]; + NSDateComponents *components = [CURRENT_CALENDAR components:NSCalendarUnitYear fromDate:self]; return components.year; } - (NSDateComponents *)timeComponents { - return [CURRENT_CALENDAR components:NSHourCalendarUnit|NSMinuteCalendarUnit|NSSecondCalendarUnit fromDate:self]; + return [CURRENT_CALENDAR components:NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond fromDate:self]; } @end diff --git a/Hydra/NSDate.swift b/Hydra/NSDate.swift new file mode 100644 index 0000000..874eb65 --- /dev/null +++ b/Hydra/NSDate.swift @@ -0,0 +1,19 @@ +// +// NSDate.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 05/04/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +public func ==(lhs: NSDate, rhs: NSDate) -> Bool { + return lhs === rhs || lhs.compare(rhs) == .OrderedSame +} + +public func <(lhs: NSDate, rhs: NSDate) -> Bool { + return lhs.compare(rhs) == .OrderedAscending +} + +extension NSDate: Comparable { } \ No newline at end of file diff --git a/Hydra/NewsDetailViewController.h b/Hydra/NewsDetailViewController.h index 5aa4931..ed5556d 100644 --- a/Hydra/NewsDetailViewController.h +++ b/Hydra/NewsDetailViewController.h @@ -7,10 +7,11 @@ // #import -#import "AssociationNewsItem.h" + +@class NewsItem; @interface NewsDetailViewController : UIViewController -- (id)initWithNewsItem:(AssociationNewsItem *)newsItem; +- (id)initWithNewsItem:(NewsItem *)newsItem; @end diff --git a/Hydra/NewsDetailViewController.m b/Hydra/NewsDetailViewController.m index 009f0c5..4554fad 100644 --- a/Hydra/NewsDetailViewController.m +++ b/Hydra/NewsDetailViewController.m @@ -7,19 +7,19 @@ // #import "NewsDetailViewController.h" -#import "Association.h" #import "NSDateFormatter+AppLocale.h" +#import "Hydra-Swift.h" #import @interface NewsDetailViewController () -@property (nonatomic, strong) AssociationNewsItem *newsItem; +@property (nonatomic, strong) NewsItem *newsItem; @end @implementation NewsDetailViewController -- (id)initWithNewsItem:(AssociationNewsItem *)newsItem +- (id)initWithNewsItem:(NewsItem *)newsItem { if (self = [super init]) { self.newsItem = newsItem; @@ -29,6 +29,8 @@ - (id)initWithNewsItem:(AssociationNewsItem *)newsItem - (void)viewDidLoad { + [super viewDidLoad]; + self.edgesForExtendedLayout = UIRectEdgeNone; CGSize viewSize = self.view.bounds.size; diff --git a/Hydra/NewsItem.swift b/Hydra/NewsItem.swift new file mode 100644 index 0000000..a74ef78 --- /dev/null +++ b/Hydra/NewsItem.swift @@ -0,0 +1,83 @@ +// +// NewsItem.swift +// +// Created by Feliciaan De Palmenaer on 28/02/2016 +// Copyright (c) . All rights reserved. +// + +import Foundation +import ObjectMapper + +@objc class NewsItem: NSObject, NSCoding, Mappable { + + // MARK: Properties + var title: String + var content: String + var association: Association + var internalIdentifier: Int + var highlighted: Bool + var date: NSDate + var read: Bool + + override var description: String { + get { + return "NewsItem: \(self.title)" + } + } + + init(title: String, content: String, association: Association, internalIdentifier: Int, highlighted: Bool, date: NSDate, read: Bool = false) { + self.title = title + self.content = content + self.association = association + self.internalIdentifier = internalIdentifier + self.highlighted = highlighted + self.date = date + self.read = read + } + + // MARK: Mappable Protocol + convenience required init?(_ map: Map) { + self.init(title: "", content: "", association: Association(internalName: "", displayName: ""), internalIdentifier: 0, highlighted: false, date: NSDate()) + } + + func mapping(map: Map) { + title <- map[PropertyKey.newsItemTitleKey] + content <- map[PropertyKey.newsItemContentKey] + association <- map[PropertyKey.newsItemAssociationKey] + internalIdentifier <- map[PropertyKey.newsIteminternalIdentifierKey] + highlighted <- map[PropertyKey.newsItemHighlightedKey] + date <- (map[PropertyKey.newsItemDateKey], ISO8601DateTransform()) + } + + // MARK: NSCoding Protocol + required init(coder aDecoder: NSCoder) { + self.title = aDecoder.decodeObjectForKey(PropertyKey.newsItemTitleKey) as! String + self.content = aDecoder.decodeObjectForKey(PropertyKey.newsItemContentKey) as! String + self.association = aDecoder.decodeObjectForKey(PropertyKey.newsItemAssociationKey) as! Association + self.internalIdentifier = aDecoder.decodeObjectForKey(PropertyKey.newsIteminternalIdentifierKey) as! Int + self.highlighted = aDecoder.decodeObjectForKey(PropertyKey.newsItemHighlightedKey) as! Bool + self.date = aDecoder.decodeObjectForKey(PropertyKey.newsItemDateKey) as! NSDate + self.read = aDecoder.decodeObjectForKey(PropertyKey.newsItemReadKey) as! Bool + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(title, forKey: PropertyKey.newsItemTitleKey) + aCoder.encodeObject(content, forKey: PropertyKey.newsItemContentKey) + aCoder.encodeObject(association, forKey: PropertyKey.newsItemAssociationKey) + aCoder.encodeObject(internalIdentifier, forKey: PropertyKey.newsIteminternalIdentifierKey) + aCoder.encodeObject(highlighted, forKey: PropertyKey.newsItemHighlightedKey) + aCoder.encodeObject(date, forKey: PropertyKey.newsItemDateKey) + aCoder.encodeObject(read, forKey: PropertyKey.newsItemReadKey) + } + + struct PropertyKey { + // MARK: Declaration for string constants to be used to decode and also serialize. + static let newsItemTitleKey: String = "title" + static let newsItemContentKey: String = "content" + static let newsItemAssociationKey: String = "association" + static let newsIteminternalIdentifierKey: String = "id" + static let newsItemHighlightedKey: String = "highlighted" + static let newsItemDateKey: String = "date" + static let newsItemReadKey: String = "read" + } +} diff --git a/Hydra/NewsViewController.m b/Hydra/NewsViewController.m index 2fbb639..f4ed5f0 100644 --- a/Hydra/NewsViewController.m +++ b/Hydra/NewsViewController.m @@ -7,13 +7,9 @@ // #import "NewsViewController.h" -#import "AssociationStore.h" -#import "AssociationNewsItem.h" #import "NewsDetailViewController.h" -#import "AssociationNewsItem.h" -#import "Association.h" #import "NSDateFormatter+AppLocale.h" -#import "PreferencesService.h" +#import "Hydra-Swift.h" #import @interface NewsViewController () @@ -30,7 +26,7 @@ - (id)init // Check for updates NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(newsUpdated:) - name:AssociationStoreDidUpdateNewsNotification + name:@"AssociationStoreDidUpdateNewsNotification" object:nil]; [self loadNews]; @@ -48,6 +44,11 @@ - (void)viewDidLoad [super viewDidLoad]; self.title = @"Nieuws"; + UIBarButtonItem *bb = [[UIBarButtonItem alloc] initWithTitle:@"Nieuws" + style:UIBarButtonItemStylePlain + target:nil action:nil]; + [self.navigationItem setBackBarButtonItem:bb]; + if ([UIRefreshControl class]) { UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; refreshControl.tintColor = [UIColor hydraTintColor]; @@ -60,7 +61,7 @@ - (void)viewDidLoad - (void)didPullRefreshControl:(id)sender { - [[AssociationStore sharedStore] reloadNewsItems]; + [[AssociationStore sharedStore] reloadNewsItems:YES]; } - (void)viewDidAppear:(BOOL)animated @@ -108,7 +109,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.separatorInset = UIEdgeInsetsZero; } - AssociationNewsItem *newsItem = self.newsItems[indexPath.row]; + NewsItem *newsItem = self.newsItems[indexPath.row]; Association *association = newsItem.association; static NSDateFormatter *dateFormatter = nil; @@ -183,7 +184,7 @@ - (void)newsUpdated:(NSNotification *)notification - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - AssociationNewsItem *item = self.newsItems[indexPath.row]; + NewsItem *item = self.newsItems[indexPath.row]; if (!item.read){ item.read = YES; [tableView reloadRowsAtIndexPaths:@[indexPath] diff --git a/Hydra/NotificationService.swift b/Hydra/NotificationService.swift new file mode 100644 index 0000000..08b2ca1 --- /dev/null +++ b/Hydra/NotificationService.swift @@ -0,0 +1,85 @@ +// +// NotificationService.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 08/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import Firebase + +class NotificationService: NSObject { + + static let SKOTopic = "/topics/studentkickoff" + static func askSKONotification (viewController: UIViewController) { + if PreferencesService.sharedService.notificationsEnabled || (!PreferencesService.sharedService.skoNotificationsEnabled && PreferencesService.sharedService.skoNotificationsAsked){ + return + } + + if PreferencesService.sharedService.skoNotificationsEnabled { + FIRMessaging.messaging().subscribeToTopic(SKOTopic) + } + PreferencesService.sharedService.skoNotificationsAsked = true + + let alertController = UIAlertController(title: "StudentKick-Off Notificaties", message: "Hydra gebruikt notificaties voor belangrijke aankondigingen, voor ieder notificatie type gaan toestemming vragen!", preferredStyle: .Alert) + + alertController.addAction(UIAlertAction(title: "Accepteer", style: .Default, handler: { (action) in + dispatch_async(dispatch_get_main_queue()) { + let application = UIApplication.sharedApplication() + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil) + application.registerUserNotificationSettings(settings) + application.registerForRemoteNotifications() + } + + PreferencesService.sharedService.skoNotificationsEnabled = true + PreferencesService.sharedService.notificationsEnabled = true + })) + + alertController.addAction(UIAlertAction(title: "Annuleer", style: .Destructive, handler: { (action) in + PreferencesService.sharedService.skoNotificationsEnabled = false + })) + + viewController.presentViewController(alertController, animated: true, completion: nil) + } + + static func register(viewController: UIViewController, askNotifications: Bool = true) { + if PreferencesService.sharedService.notificationsEnabled { + return + } + if let lastAsked = PreferencesService.sharedService.lastAskedForNotifications where lastAsked.isEarlierThanDate(NSDate().dateBySubtractingDays(30)) { + return + } + if askNotifications { + let alertController = UIAlertController(title: "Notificaties", message: "Hydra gebruikt notificaties voor belangrijke aankondigingen, voor ieder notificatie type gaan toestemming vragen!", preferredStyle: .ActionSheet) + + alertController.addAction(UIAlertAction(title: "Accepteer", style: .Default, handler: { (action) in + dispatch_async(dispatch_get_main_queue()) { + let application = UIApplication.sharedApplication() + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil) + application.registerUserNotificationSettings(settings) + application.registerForRemoteNotifications() + } + PreferencesService.sharedService.notificationsEnabled = true + })) + + alertController.addAction(UIAlertAction(title: "Annuleer", style: .Destructive, handler: { (action) in + PreferencesService.sharedService.notificationsEnabled = false + PreferencesService.sharedService.lastAskedForNotifications = NSDate() + })) + + viewController.presentViewController(alertController, animated: true, completion: nil) + } else { + dispatch_async(dispatch_get_main_queue()) { + let application = UIApplication.sharedApplication() + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil) + application.registerUserNotificationSettings(settings) + application.registerForRemoteNotifications() + } + PreferencesService.sharedService.notificationsEnabled = true + } + } +} \ No newline at end of file diff --git a/Hydra/OAuthTokenInfo.swift b/Hydra/OAuthTokenInfo.swift new file mode 100644 index 0000000..248ad00 --- /dev/null +++ b/Hydra/OAuthTokenInfo.swift @@ -0,0 +1,68 @@ +// +// OAuthUser.swift +// +// Created by Feliciaan De Palmenaer on 23/02/2016 +// Copyright (c) Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class OAuthTokenInfo: NSObject, NSCoding, Mappable { + + // MARK: Properties + var createdAt: String? + var updatedAt: String? + var user: User? + var expiresIn: String? + var scopes: [String]? + + // MARK: ObjectMapper Initalizers + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + required init?(_ map: Map){ + + } + + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + func mapping(map: Map) { + createdAt <- map[PropertyKey.oAuthUserCreatedAtKey] + updatedAt <- map[PropertyKey.oAuthUserUpdatedAtKey] + user <- map[PropertyKey.oAuthUserUserKey] + expiresIn <- map[PropertyKey.oAuthUserExpiresInKey] + scopes <- map[PropertyKey.oAuthUserScopesKey] + } + + // MARK: NSCoding Protocol + required init(coder aDecoder: NSCoder) { + self.createdAt = aDecoder.decodeObjectForKey(PropertyKey.oAuthUserCreatedAtKey) as? String + self.updatedAt = aDecoder.decodeObjectForKey(PropertyKey.oAuthUserUpdatedAtKey) as? String + self.user = aDecoder.decodeObjectForKey(PropertyKey.oAuthUserUserKey) as? User + self.expiresIn = aDecoder.decodeObjectForKey(PropertyKey.oAuthUserExpiresInKey) as? String + self.scopes = aDecoder.decodeObjectForKey(PropertyKey.oAuthUserScopesKey) as? [String] + + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(createdAt, forKey: PropertyKey.oAuthUserCreatedAtKey) + aCoder.encodeObject(updatedAt, forKey: PropertyKey.oAuthUserUpdatedAtKey) + aCoder.encodeObject(user, forKey: PropertyKey.oAuthUserUserKey) + aCoder.encodeObject(expiresIn, forKey: PropertyKey.oAuthUserExpiresInKey) + aCoder.encodeObject(scopes, forKey: PropertyKey.oAuthUserScopesKey) + } + + // MARK: Declaration for string constants to be used to decode and also serialize. + struct PropertyKey { + static let oAuthUserCreatedAtKey: String = "created_at" + static let oAuthUserUpdatedAtKey: String = "updated_at" + static let oAuthUserUserKey: String = "user" + static let oAuthUserAppKey: String = "app" + static let oAuthUserExpiresInKey: String = "expires_in" + static let oAuthUserScopesKey: String = "scopes" + } +} diff --git a/Hydra/PreferenceTableViewCells.swift b/Hydra/PreferenceTableViewCells.swift new file mode 100644 index 0000000..04f46e3 --- /dev/null +++ b/Hydra/PreferenceTableViewCells.swift @@ -0,0 +1,71 @@ +// +// PreferenceTableViewCells.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 06/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +class PreferenceExtraTableViewCell: UITableViewCell { + + func configure(titleText: String, detailText: String) { + self.textLabel?.text = titleText + self.detailTextLabel?.text = detailText + + // Restore from disabled + self.textLabel?.alpha = 1 + self.detailTextLabel?.alpha = 1 + self.selectionStyle = .Blue + + // Restore from link + self.accessoryView = nil + self.accessoryType = .DisclosureIndicator + } + + func setDisabled() { + self.textLabel?.alpha = 0.5 + self.detailTextLabel?.alpha = 0.5 + self.selectionStyle = .None + self.accessoryType = .None + } + + func setExternalLink() { + let linkImage = UIImage(named: "external-link") + let linkImageActive = UIImage(named: "external-link-active") + let accessory = UIImageView(image: linkImage, highlightedImage: linkImageActive) + accessory.contentMode = .ScaleAspectFit + self.accessoryView = accessory + } +} + +class PreferenceSwitchTableViewCell: UITableViewCell { + @IBOutlet weak var titleLabel: UILabel? + @IBOutlet weak var switchButton: UISwitch? + + var toggleClosure: ((Bool)->())? + + override func awakeFromNib() { + switchButton?.addTarget(self, action: #selector(PreferenceSwitchTableViewCell.toggleAction), forControlEvents: .ValueChanged) + } + + func configure(titleText: String, condition: Bool, toggleClosure: ((newState: Bool)->())?) { + self.titleLabel?.text = titleText + self.switchButton?.on = condition + self.toggleClosure = toggleClosure + } + + func toggleAction() { + if let toggleClosure = toggleClosure, + let switchButton = switchButton { + toggleClosure(switchButton.on) + } + + NSNotificationCenter.defaultCenter().postNotificationName(PreferencesControllerDidUpdatePreferenceNotification, object: nil) + } +} + +class PreferencesTextTableViewCell: UITableViewCell { + @IBOutlet weak var titleLabel: UILabel? +} \ No newline at end of file diff --git a/Hydra/PreferencesController.h b/Hydra/PreferencesController.h deleted file mode 100644 index 6dadc23..0000000 --- a/Hydra/PreferencesController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// PreferencesController.h -// Hydra -// -// Created by Pieter De Baets on 08/02/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import - -@interface PreferencesController : UITableViewController - -@end diff --git a/Hydra/PreferencesController.m b/Hydra/PreferencesController.m deleted file mode 100644 index 0bcd18b..0000000 --- a/Hydra/PreferencesController.m +++ /dev/null @@ -1,336 +0,0 @@ -// -// PreferencesController.m -// Hydra -// -// Created by Pieter De Baets on 08/02/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import "PreferencesController.h" -#import "AssociationPreferenceController.h" -#import "FacebookSession.h" -#import "PreferencesService.h" - -#import - -#define kFacebookSection 0 -#define kFilterSection 1 -#define kInfoSection 2 - -#define kSwitchTag 500 - -@interface PreferencesController () - -@end - -@implementation PreferencesController - -- (id)init -{ - if (self = [super initWithStyle:UITableViewStyleGrouped]) { - // Listen for facebook state changes - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center addObserver:self selector:@selector(facebookUpdated:) - name:FacebookSessionStateChangedNotification object:nil]; - [center addObserver:self selector:@selector(facebookUpdated:) - name:FacebookUserInfoUpdatedNotifcation object:nil]; - } - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - self.title = @"Voorkeuren"; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - GAI_Track(@"Voorkeuren"); - - // Reload changes - [self.tableView reloadData]; -} - -#pragma mark - Table view data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return 3; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - switch (section) { - case kFilterSection: - return 3; - case kFacebookSection: - return 1; - case kInfoSection: - return 3; - default: - return 0; - } -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - static NSString *CellIdentifier = @"PreferencesCell"; - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if (cell == nil) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 - reuseIdentifier:CellIdentifier]; - } - else { - cell.textLabel.alpha = 1; - cell.detailTextLabel.alpha = 1; - cell.accessoryType = UITableViewCellAccessoryNone; - cell.accessoryView = nil; - cell.selectionStyle = UITableViewCellSelectionStyleNone; - [[cell viewWithTag:kSwitchTag] removeFromSuperview]; - } - - PreferencesService *prefs = [PreferencesService sharedService]; - switch (indexPath.section) { - case kFilterSection: - switch (indexPath.row) { - case 0: { - cell.textLabel.text = @"Toon activiteiten in feed"; - cell.detailTextLabel.text = @""; - - CGFloat screenWidth = self.view.frame.size.width; - CGRect toggleRect = CGRectMake(screenWidth - 60, 9, 0, 0); - UISwitch *toggle = [[UISwitch alloc] initWithFrame:toggleRect]; - toggle.tag = kSwitchTag; - toggle.on = prefs.showActivitiesInFeed; - [toggle addTarget:self action:@selector(showActivitiesSwitch:didToggle:) - forControlEvents:UIControlEventValueChanged]; - [cell addSubview:toggle]; - } break; - - case 1: { - cell.textLabel.text = @"Toon alle verenigingen"; - cell.detailTextLabel.text = @""; - - CGFloat screenWidth = self.view.frame.size.width; - CGRect toggleRect = CGRectMake(screenWidth - 60, 9, 0, 0); - UISwitch *toggle = [[UISwitch alloc] initWithFrame:toggleRect]; - toggle.tag = kSwitchTag; - toggle.on = !prefs.filterAssociations; - [toggle addTarget:self action:@selector(filterSwitch:didToggle:) - forControlEvents:UIControlEventValueChanged]; - [cell addSubview:toggle]; - } break; - - case 2: { - cell.textLabel.text = @"Selectie"; - NSUInteger count = prefs.preferredAssociations.count; - cell.detailTextLabel.text = [NSString stringWithFormat:@"%lu %@", (unsigned long)count, - count == 1 ? @"vereniging" : @"verenigingen"]; - - if (prefs.filterAssociations) { - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - } - else { - cell.textLabel.alpha = 0.5; - cell.detailTextLabel.alpha = 0.5; - } - } break; - } - break; - case kFacebookSection: { - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - cell.textLabel.text = @"Facebook"; - - FacebookSession *session = [FacebookSession sharedSession]; - if (session.open) { - id userInfo = [session userInfo]; - cell.detailTextLabel.text = userInfo ? [userInfo name] : @"Aangemeld"; - } - else { - cell.detailTextLabel.text = @"Niet aangemeld"; - } - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - - } break; - - case kInfoSection: - switch (indexPath.row) { - case 0: { - static NSString *TextCellIdentifier = @"PreferencesTextCell"; - cell = [tableView dequeueReusableCellWithIdentifier:TextCellIdentifier]; - if (cell == nil) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault - reuseIdentifier:CellIdentifier]; - cell.textLabel.font = [UIFont systemFontOfSize:14]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.numberOfLines = 0; - cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; - cell.selectionStyle = UITableViewCellSelectionStyleNone; - } - cell.textLabel.text = @"Hydra werd ontwikkeld door Zeus WPI, " - "de studentenwerkgroep informatica van " - "de Universiteit Gent, in opdracht van " - "de Dienst StudentenActiviteiten."; - } break; - - case 1: { - UIImage *linkImage = [UIImage imageNamed:@"external-link"]; - UIImage *highlightedLinkImage = [UIImage imageNamed:@"external-link-active"]; - UIImageView *linkAccessory = [[UIImageView alloc] initWithImage:linkImage - highlightedImage:highlightedLinkImage]; - linkAccessory.contentMode = UIViewContentModeScaleAspectFit; - cell.accessoryView = linkAccessory; - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - cell.textLabel.text = @"Meer informatie"; - } break; - - case 2: { - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - cell.textLabel.text = @"Externe componenten"; - } break; - } - break; - } - - return cell; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == kInfoSection && indexPath.row == 0) { - return 88; - } - else { - return 44; - } -} - -- (CGFloat)tableView:tableView heightForFooterInSection:(NSInteger)section -{ - if (section == kFilterSection) { - return 68; - } - else { - return 0; - } -} - -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section -{ - if (section == kFilterSection) { - CGFloat screenWidth = self.view.frame.size.width; - CGRect infoRect = CGRectMake(10, 3, screenWidth - 20, 58); - UILabel *info = [[UILabel alloc] initWithFrame:infoRect]; - info.backgroundColor = [UIColor clearColor]; - info.font = [UIFont systemFontOfSize:14]; - info.text = @"Selecteer verenigingen om activiteiten en nieuws" - "berichten te filteren. Berichten die in de kijker " - "staan worden steeds getoond."; - info.lineBreakMode = NSLineBreakByWordWrapping; - info.numberOfLines = 0; - info.shadowColor = [UIColor whiteColor]; - info.shadowOffset = CGSizeMake(0, 1); - info.textAlignment = NSTextAlignmentCenter; - info.textColor = [UIColor H_hintColor]; - - UIView *wrapper = [[UIView alloc] initWithFrame:CGRectZero]; - [wrapper addSubview:info]; - return wrapper; - } - else { - return nil; - } -} - -#pragma mark - Table view delegate - -- (void)filterSwitch:(UISwitch *)toggle didToggle:(NSNotification *)notification -{ - PreferencesService *prefs = [PreferencesService sharedService]; - prefs.filterAssociations = !toggle.on; - NSIndexSet *set = [NSIndexSet indexSetWithIndex:kFilterSection]; - [self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationAutomatic]; -} - -- (void)showActivitiesSwitch:(UISwitch *)toggle didToggle:(NSNotification *)notification -{ - PreferencesService *prefs = [PreferencesService sharedService]; - prefs.showActivitiesInFeed = toggle.on; - NSIndexSet *set = [NSIndexSet indexSetWithIndex:kFilterSection]; - [self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationAutomatic]; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - // Navigation logic may go here. Create and push another view controller. - switch (indexPath.section) { - case kFilterSection: - if (indexPath.row == 1) { - PreferencesService *prefs = [PreferencesService sharedService]; - if (prefs.filterAssociations) { - UIViewController *c = [[AssociationPreferenceController alloc] init]; - [self.navigationController pushViewController:c animated:YES]; - } - } - break; - case kFacebookSection: { - FacebookSession *session = [FacebookSession sharedSession]; - if (session.open) { - UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Facebook" - delegate:self - cancelButtonTitle:@"Annuleren" - destructiveButtonTitle:@"Afmelden" - otherButtonTitles:nil]; - [sheet showInView:self.view]; - } - else { - [session openWithAllowLoginUI:YES]; - } - - [tableView reloadRowsAtIndexPaths:@[ indexPath ] - withRowAnimation:UITableViewRowAnimationAutomatic]; - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - } break; - case kInfoSection: { - if (indexPath.row == 1) { - NSURL *url = [NSURL URLWithString:@"http://zeus.ugent.be/hydra"]; - [[UIApplication sharedApplication] openURL:url]; - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - } - else if (indexPath.row == 2) { - UIViewController *c = [VTAcknowledgementsViewController acknowledgementsViewController]; - [self.navigationController pushViewController:c animated:YES]; - } - } - } -} - -#pragma mark - Notifications - -- (void)facebookUpdated:(NSNotification *)notification -{ - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:kFacebookSection]; - [self.tableView reloadRowsAtIndexPaths:@[ indexPath ] - withRowAnimation:UITableViewRowAnimationAutomatic]; -} - -#pragma mark - ActionSheet callback - -- (void)actionSheet:(UIActionSheet *)sheet clickedButtonAtIndex:(NSInteger)index -{ - if (index == 0) { - FacebookSession *session = [FacebookSession sharedSession]; - [session close]; - } -} - -@end diff --git a/Hydra/PreferencesController.swift b/Hydra/PreferencesController.swift new file mode 100644 index 0000000..97fa1a9 --- /dev/null +++ b/Hydra/PreferencesController.swift @@ -0,0 +1,406 @@ +// +// PreferencesController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 06/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit +import AcknowList +import Firebase + +let PreferencesControllerDidUpdatePreferenceNotification = "PreferencesControllerDidUpdatePreference" + +class PreferencesController: UITableViewController { + + init() { + super.init(style: UITableViewStyle.Grouped) + let center = NSNotificationCenter.defaultCenter() + center.addObserver(self, selector: #selector(PreferencesController.updateState), name: FacebookEventDidUpdateNotification, object: nil) + center.addObserver(self, selector: #selector(PreferencesController.updateState), name: FacebookUserInfoUpdatedNotifcation, object: nil) + center.addObserver(self, selector: #selector(PreferencesController.updateState), name: UGentOAuth2ServiceDidUpdateUserNotification, object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + func updateState() { + self.tableView.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "Voorkeuren" + + func registerNib(nibName: String, reuseIdentifier: String) { + let nib = UINib.init(nibName: nibName, bundle: nil) + self.tableView.registerNib(nib, forCellReuseIdentifier: reuseIdentifier) + } + + registerNib("PreferencesExtraTableViewCells", reuseIdentifier: "PreferenceExtraCell") + registerNib("PreferencesSwitchTableViewCells", reuseIdentifier: "PreferenceSwitchCell") + registerNib("PreferencesTextTableViewCell", reuseIdentifier: "PreferencesTextTableViewCell") + } + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + GAI_track("Voorkeuren") + self.tableView.reloadData() + } + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return numberOfSections() + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let section = Sections(rawValue: section) + if let section = section { + switch section { + case .UserAccount: + return 1 + case .Minerva: + return 1 + case .Activity: + return 2 + case .Feed: + return 6 + case .Notification: + return 1 + case .Info: + return 3 + } + } + return 0 + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + if let section = Sections(rawValue: indexPath.section) { + switch section { + case .UserAccount: + if let userAccount = UserAccountSection(rawValue: indexPath.row) { + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceExtraCell") as! PreferenceExtraTableViewCell + + switch userAccount { + /*case .Facebook: + let detailText: String + let fbSession = FacebookSession.sharedSession + if fbSession.open { + if let name = fbSession.userInfo?.name { + detailText = name + } else { + detailText = "Aangemeld" + } + } else { + detailText = "Niet aangemeld" + } + + cell.configure("Facebook", detailText: detailText) + */ + case .UGent: + let detailText: String + if PreferencesService.sharedService.userLoggedInToMinerva { + if let user = MinervaStore.sharedStore.user { + detailText = user.name + } else { + detailText = "Aangemeld" + } + let oauth2 = UGentOAuth2Service.sharedService.oauth2 + if oauth2.accessToken == nil { + oauth2.authorize() + } + } else { + detailText = "Niet aangemeld" + } + cell.configure("UGent", detailText: detailText) + } + + return cell + } + case .Minerva: + if let minervaSection = MinervaSection(rawValue: indexPath.row) { + switch minervaSection { + case .Courses: + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceExtraCell") as! PreferenceExtraTableViewCell + cell.configure("Cursussen", detailText: "") + if !UGentOAuth2Service.sharedService.isAuthenticated() { + cell.setDisabled() + } + return cell + } + } + case .Activity: + let prefs = PreferencesService.sharedService + if let activityS = ActivitySection(rawValue: indexPath.row) { + switch activityS { + case .ShowAll: + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceSwitchCell") as! PreferenceSwitchTableViewCell + cell.configure("Toon alle verenigingen", condition: !prefs.filterAssociations, toggleClosure: { (newState) in + PreferencesService.sharedService.filterAssociations = !newState + self.tableView.reloadData() + }) + return cell + case .Select: + let detailText: String + let count = prefs.preferredAssociations.count + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceExtraCell") as! PreferenceExtraTableViewCell + if count > 1 { + detailText = "\(count) verenigingen" + } else if count == 1 { + detailText = "1 vereniging" + } else { + detailText = "geen geselecteerd" + } + + cell.configure("Selectie", detailText: detailText) + if !prefs.filterAssociations { + cell.setDisabled() + } + + return cell + } + } + case .Feed: + let prefs = PreferencesService.sharedService + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceSwitchCell") as! PreferenceSwitchTableViewCell + if let feedSection = FeedSection(rawValue: indexPath.row) { + switch feedSection { + case .Activities: + cell.configure("Toon activiteiten", condition: prefs.showActivitiesInFeed, toggleClosure: { (newState) in + PreferencesService.sharedService.showActivitiesInFeed = newState + self.tableView.reloadData() + }) + case .News: + cell.configure("Toon verenigingen nieuws", condition: prefs.showNewsInFeed, toggleClosure: { (newState) in + PreferencesService.sharedService.showNewsInFeed = newState + self.tableView.reloadData() + }) + case .Schamper: + cell.configure("Toon Schamper Dailies", condition: prefs.showSchamperInFeed, toggleClosure: { (newState) in + PreferencesService.sharedService.showSchamperInFeed = newState + self.tableView.reloadData() + }) + case .Resto: + cell.configure("Toon de resto menus", condition: prefs.showRestoInFeed, toggleClosure: { (newState) in + PreferencesService.sharedService.showRestoInFeed = newState + self.tableView.reloadData() + }) + case .SpecialEvent: + cell.configure("Toon uitgelichte activiteiten", condition: prefs.showSpecialEventsInFeed, toggleClosure: { (newState) in + PreferencesService.sharedService.showSpecialEventsInFeed = newState + self.tableView.reloadData() + }) + case .Urgentfm: + cell.configure("Toon Urgent.fm-kaartje", condition: prefs.showUrgentfmInFeed, toggleClosure: { (newState) in + PreferencesService.sharedService.showUrgentfmInFeed = newState + self.tableView.reloadData() + }) + } + } + + return cell + case .Notification: + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceSwitchCell") as! PreferenceSwitchTableViewCell + switch NotificationSection(rawValue: indexPath.row)! { + case .SKO: + cell.configure("Student Kick-Off", condition: PreferencesService.sharedService.skoNotificationsEnabled, toggleClosure: { (newState) in + PreferencesService.sharedService.skoNotificationsEnabled = newState + if newState { + FIRMessaging.messaging().subscribeToTopic(NotificationService.SKOTopic) + } else { + FIRMessaging.messaging().unsubscribeFromTopic(NotificationService.SKOTopic) + } + self.tableView.reloadData() + }) + } + return cell + case .Info: + switch InfoSection(rawValue: indexPath.row)! { + case .ZeusText: + return tableView.dequeueReusableCellWithIdentifier("PreferencesTextTableViewCell")! + case .ExternalLink: + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceExtraCell") as! PreferenceExtraTableViewCell + + cell.configure("Meer informatie", detailText: "") + cell.setExternalLink() + return cell + case .Acknowledgements: + let cell = tableView.dequeueReusableCellWithIdentifier("PreferenceExtraCell") as! PreferenceExtraTableViewCell + + cell.configure("Externe componenten", detailText: "") + return cell + } + } + } + fatalError("All cells should be reached before") + } + + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + if indexPath.section == Sections.Info.rawValue && indexPath.row == InfoSection.ZeusText.rawValue { + return 68 + } + + return 44 + } + + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch Sections(rawValue: section)! { + case .Minerva: + return "Selecteer de cursussen waarvoor de agenda en berichten getoond moeten worden." + case .Activity: + return "Selecteer verenigingen om activiteiten en nieuws" + + "berichten te filteren. Berichten die in de kijker " + + "staan worden steeds getoond." + case .Feed: + return "Kies hier welke kaarten er zichtbaar zijn op het home tabblad.\n" + + "Uitgeschakelde kaarten kunnen nog zichtbaar zijn als ze uitgelicht worden." + case .Notification: + return "Kies hierboven van welke bronnen je notificaties kan krijgen." + default: + return nil + } + } + + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch Sections(rawValue: section)! { + case .UserAccount: + return "Gebruikeraccounts" + case .Minerva: + return "Minerva" + case .Activity: + return "Studentenverenigingen" + case .Feed: + return "Home scherm" + case .Notification: + return "Notifications" + case .Info: + return "De ontwikkelaars" + } + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + switch Sections(rawValue: indexPath.section)! { + case .UserAccount: + switch UserAccountSection(rawValue: indexPath.row)! { + /*case .Facebook: + let session = FacebookSession.sharedSession + if session.open { + let action = UIAlertController(title: "Facebook", message: "", preferredStyle: .ActionSheet) + action.addAction(UIAlertAction(title: "Afmelden", style: .Destructive, handler: { _ in + FacebookSession.sharedSession.close() + self.tableView.reloadData() + })) + action.addAction(UIAlertAction(title: "Annuleren", style: .Cancel, handler: nil)) + presentViewController(action, animated: true, completion: nil) + } else { + session.openWithAllowLoginUI(true) + }*/ + case .UGent: + let oauthService = UGentOAuth2Service.sharedService + let oauth2 = oauthService.oauth2 + if oauth2.accessToken == nil { + oauth2.authConfig.authorizeEmbedded = true + oauth2.authConfig.authorizeContext = self + oauth2.authorize() + } else { + let action = UIAlertController(title: "UGent", message: "", preferredStyle: .ActionSheet) + action.addAction(UIAlertAction(title: "Afmelden", style: .Destructive, handler: { _ in + UGentOAuth2Service.sharedService.logoff() + self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + })) + action.addAction(UIAlertAction(title: "Annuleren", style: .Cancel, handler: nil)) + presentViewController(action, animated: true, completion: nil) + + } + } + tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + tableView.deselectRowAtIndexPath(indexPath, animated: true) + case .Minerva: + switch MinervaSection(rawValue: indexPath.row)! { + case .Courses: + if let navigationController = self.navigationController where UGentOAuth2Service.sharedService.isAuthenticated() { + navigationController.pushViewController(MinervaCoursePreferenceViewController(), animated: true) + } + } + case .Info: + switch InfoSection(rawValue: indexPath.row)! { + case .ExternalLink: + let url = NSURL(string: "https://zeus.UGent.be/hydra")! + UIApplication.sharedApplication().openURL(url) + tableView.deselectRowAtIndexPath(indexPath, animated: true) + case .Acknowledgements: + let viewController = AcknowListViewController() + if let navigationController = self.navigationController { + navigationController.pushViewController(viewController, animated: true) + } + default: + break + } + case .Activity: + switch ActivitySection(rawValue: indexPath.row)! { + case .Select: + if PreferencesService.sharedService.filterAssociations { + let c = AssociationPreferenceController() + if let navigationController = self.navigationController { + navigationController.pushViewController(c, animated: true) + } + } + default: + break + } + default: + break + } + } +} + +enum Sections: Int { + case UserAccount + case Minerva + case Activity + case Feed + case Notification + case Info +} + +enum UserAccountSection: Int { + //case Facebook + case UGent +} + +enum MinervaSection: Int { + case Courses +} + +enum ActivitySection: Int { + case ShowAll + case Select +} + +enum FeedSection: Int { + case Resto + case Schamper + case Activities + case Urgentfm + case News + case SpecialEvent +} + +enum NotificationSection: Int { + case SKO +} + +enum InfoSection: Int { + case ZeusText + case ExternalLink + case Acknowledgements +} + +func numberOfSections() -> Int { + return [Sections.UserAccount, Sections.Minerva, Sections.Activity, Sections.Feed, Sections.Notification, Sections.Info].count +} diff --git a/Hydra/PreferencesExtraTableViewCells.xib b/Hydra/PreferencesExtraTableViewCells.xib new file mode 100644 index 0000000..173dbfe --- /dev/null +++ b/Hydra/PreferencesExtraTableViewCells.xib @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hydra/PreferencesService.h b/Hydra/PreferencesService.h deleted file mode 100644 index 5a2bb61..0000000 --- a/Hydra/PreferencesService.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// PreferencesService.h -// Hydra -// -// Created by Pieter De Baets on 18/02/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import - -@interface PreferencesService : NSObject - -+ (PreferencesService *)sharedService; - -@property (nonatomic, assign) BOOL filterAssociations; -@property (nonatomic, assign) BOOL showActivitiesInFeed; -@property (nonatomic, strong) NSArray *preferredAssociations; -@property (nonatomic, strong) NSArray *hydraTabBarOrder; -@property (nonatomic, assign) BOOL shownFacebookPrompt; - -@end diff --git a/Hydra/PreferencesService.m b/Hydra/PreferencesService.m deleted file mode 100644 index e48be75..0000000 --- a/Hydra/PreferencesService.m +++ /dev/null @@ -1,112 +0,0 @@ -// -// PreferencesService.m -// Hydra -// -// Created by Pieter De Baets on 18/02/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import "PreferencesService.h" - -#define kFilterAssociationsKey @"useAssociationFilter" -#define kShowActivitiesInFeedKey @"showActivitiesInFeed" -#define kPreferredAssociationsKey @"preferredAssociations" -#define kHydraTabBarOrder @"hydraTabBarOrder" -#define kShownFacebookPrompt @"shownFacebookPrompt" - -@interface PreferencesService () - -@property (nonatomic, strong) NSUserDefaults *settings; - -@end - -@implementation PreferencesService - -+ (PreferencesService *)sharedService -{ - static PreferencesService *sharedInstance = nil; - if (!sharedInstance) { - sharedInstance = [[PreferencesService alloc] init]; - } - return sharedInstance; -} - -- (id)init -{ - if (self = [super init]) { - self.settings = [NSUserDefaults standardUserDefaults]; - } - return self; -} - -- (BOOL)filterAssociations -{ - return [self.settings boolForKey:kFilterAssociationsKey]; -} - -- (void)setFilterAssociations:(BOOL)filterAssociations -{ - [self willChangeValueForKey:@"filterAssociations"]; - [self.settings setBool:filterAssociations forKey:kFilterAssociationsKey]; - [self didChangeValueForKey:@"filterAssociations"]; -} - -- (BOOL)showActivitiesInFeed -{ - return [self.settings boolForKey:kShowActivitiesInFeedKey]; -} - -- (void)setShowActivitiesInFeed:(BOOL)showActivitiesInFeed -{ - [self willChangeValueForKey:@"showActivitiesInFeed"]; - [self.settings setBool:showActivitiesInFeed forKey:kShowActivitiesInFeedKey]; - [self didChangeValueForKey:@"showActivitiesInFeed"]; -} - -- (BOOL)shownFacebookPrompt -{ - return [self.settings boolForKey:kShownFacebookPrompt]; -} - -- (void)setShownFacebookPrompt:(BOOL)shownFacebookPrompt -{ - [self willChangeValueForKey:@"shownFacebookPrompt"]; - [self.settings setBool:shownFacebookPrompt forKey:kShownFacebookPrompt]; - [self didChangeValueForKey:@"shownFacebookPrompt"]; -} - -- (NSArray *)preferredAssociations -{ - NSArray *list = [self.settings objectForKey:kPreferredAssociationsKey]; - AssertClassOrNil(list, NSArray); - if (list == nil) { - list = [[NSArray alloc] init]; - } - return list; -} - -- (void)setPreferredAssociations:(NSArray *)preferredAssociations -{ - [self willChangeValueForKey:@"preferredAssociations"]; - [self.settings setObject:preferredAssociations forKey:kPreferredAssociationsKey]; - [self didChangeValueForKey:@"preferredAssociations"]; -} - - -- (NSArray *)hydraTabBarOrder -{ - NSArray *list = [self.settings objectForKey:kHydraTabBarOrder]; - AssertClassOrNil(list, NSArray); - if (list == nil) { - list = [[NSArray alloc] init]; - } - return list; -} - -- (void)setHydraTabBarOrder:(NSArray *)hydraTabBarOrder -{ - [self willChangeValueForKey:kHydraTabBarOrder]; - [self.settings setObject:hydraTabBarOrder forKey:kHydraTabBarOrder]; - [self didChangeValueForKey:kHydraTabBarOrder]; -} -@end diff --git a/Hydra/PreferencesService.swift b/Hydra/PreferencesService.swift new file mode 100644 index 0000000..e6db94e --- /dev/null +++ b/Hydra/PreferencesService.swift @@ -0,0 +1,303 @@ +// +// PreferencesService.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 07/07/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class PreferencesService: NSObject { + + static let sharedService = PreferencesService() + + let defaults = NSUserDefaults.standardUserDefaults() + + static func registerAppDefaults() { + let defaultProps = [PropertyKey.showActivitiesInFeedKey: true, + PropertyKey.showNewsInFeedKey: true, + PropertyKey.showUrgentfmInFeedKey: true, + PropertyKey.showSchamperInFeedKey: true, + PropertyKey.showSko: true, + PropertyKey.showRestoInFeedKey: true, + PropertyKey.showSpecialEventsInFeedKey: true, + PropertyKey.filterAssociationsKey: false, + PropertyKey.firstLaunchKey: true + ] + NSUserDefaults.standardUserDefaults().registerDefaults(defaultProps) + } + + var filterAssociations: Bool { + get { + // Default is false because we show the inverse + return getBool(PropertyKey.filterAssociationsKey, defaultValue: false) + } + set { + setBool(PropertyKey.filterAssociationsKey, value: newValue) + } + } + + var showActivitiesInFeed: Bool { + get { + return getBool(PropertyKey.showActivitiesInFeedKey, defaultValue: true) + } + set { + setBool(PropertyKey.showActivitiesInFeedKey, value: newValue) + } + } + + var showSchamperInFeed: Bool { + get { + return getBool(PropertyKey.showSchamperInFeedKey, defaultValue: true) + } + set { + setBool(PropertyKey.showSchamperInFeedKey, value: newValue) + } + } + + + var showRestoInFeed: Bool { + get { + return getBool(PropertyKey.showRestoInFeedKey, defaultValue: true) + } + set { + setBool(PropertyKey.showRestoInFeedKey, value: newValue) + } + } + + + var showUrgentfmInFeed: Bool { + get { + return getBool(PropertyKey.showUrgentfmInFeedKey, defaultValue: true) + } + set { + setBool(PropertyKey.showUrgentfmInFeedKey, value: newValue) + } + } + + + var showNewsInFeed: Bool { + get { + return getBool(PropertyKey.showNewsInFeedKey, defaultValue: true) + } + set { + setBool(PropertyKey.showNewsInFeedKey, value: newValue) + } + } + + + var showSpecialEventsInFeed: Bool { + get { + return getBool(PropertyKey.showSpecialEventsInFeedKey, defaultValue: true) + } + set { + setBool(PropertyKey.showSpecialEventsInFeedKey, value: newValue) + } + } + + + var shownFacebookPrompt: Bool { + get { + return getBool(PropertyKey.shownFacebookPromptKey, defaultValue: true) + } + set { + setBool(PropertyKey.shownFacebookPromptKey, value: newValue) + } + } + + var userLoggedInToFacebook: Bool { + get { + return getBool(PropertyKey.userLoggedInToFacebookKey, defaultValue: false) + } + set { + setBool(PropertyKey.userLoggedInToFacebookKey, value: newValue) + } + } + + var userLoggedInToMinerva: Bool { + get { + return getBool(PropertyKey.userLoggedInToMinervaKey, defaultValue: false) + } + set { + setBool(PropertyKey.userLoggedInToMinervaKey, value: newValue) + } + } + + var developmentMode: Bool { + get { + return getBool(PropertyKey.developmentModeKey, defaultValue: false) + } + set { + setBool(PropertyKey.developmentModeKey, value: newValue) + } + } + + var preferredAssociations: [String] { // Still in Obj-C + get { + return getArray(PropertyKey.preferredAssociationsKey, defaultValue: [String]()) as! [String] + } + set { + setArray(PropertyKey.preferredAssociationsKey, value: newValue) + } + } + + var hydraTabBarOrder: [Int] { + get { + return getArray(PropertyKey.hydraTabBarOrderKey, defaultValue: [Int]()) as! [Int] + } + set { + setArray(PropertyKey.hydraTabBarOrderKey, value: newValue) + } + } + + var unselectedMinervaCourses: Set { + get { + let arr = getArray(PropertyKey.unselectedMinervaCoursesKey, defaultValue: [String]()) as! [String] + return Set(arr) + } + set { + setArray(PropertyKey.unselectedMinervaCoursesKey, value: Array(newValue)) + } + } + + var notificationsEnabled: Bool { + get { + return getBool(PropertyKey.notificationsEnabledKey, defaultValue: false) + } + set { + setBool(PropertyKey.notificationsEnabledKey, value: newValue) + } + + } + + var lastAskedForNotifications: NSDate? { + get { + return getObject(PropertyKey.lastAskedForNoticationsKey) as? NSDate + } + set { + setObject(PropertyKey.lastAskedForNoticationsKey, value: newValue) + } + } + + var skoNotificationsEnabled: Bool { + get { + return getBool(PropertyKey.skoNotificationsEnabledKey, defaultValue: false) + } + set { + setBool(PropertyKey.skoNotificationsEnabledKey, value: newValue) + } + } + + var skoNotificationsAsked: Bool { + get { + return getBool(PropertyKey.skoNotificationsAskedKey, defaultValue: false) + } + set { + setBool(PropertyKey.skoNotificationsAskedKey, value: newValue) + } + } + + var showSKO: Bool { + get { + return getBool(PropertyKey.showSko, defaultValue: false) + } + set { + setBool(PropertyKey.showSko, value: newValue) + } + } + + var firstLaunch: Bool { + get { + return getBool(PropertyKey.firstLaunchKey) + } + set { + setBool(PropertyKey.firstLaunchKey, value: newValue) + } + } + + var resetApp: Bool { + get { + return getBool(PropertyKey.resetAppKey) + } + set { + setBool(PropertyKey.resetAppKey, value: newValue) + } + } + + internal struct PropertyKey { + static let filterAssociationsKey = "useAssociationFilter" + static let preferredAssociationsKey = "preferredAssociations" + static let showActivitiesInFeedKey = "showActivitiesInFeed" + static let showSchamperInFeedKey = "showSchamperInFeed" + static let showRestoInFeedKey = "showRestoInFeed" + static let showUrgentfmInFeedKey = "showUrgentfmInFeed" + static let showNewsInFeedKey = "showNewsInFeed" + static let showSpecialEventsInFeedKey = "showSpecialEventsInFeed" + static let developmentModeKey = "developmentMode" + static let hydraTabBarOrderKey = "hydraTabBarOrder" + static let shownFacebookPromptKey = "shownFacebookPrompt" + static let userLoggedInToFacebookKey = "userLoggedInToFacebook" + static let userLoggedInToMinervaKey = "userLoggedInToMinerva" + static let unselectedMinervaCoursesKey = "unselectedMinervaCourses" + static let notificationsEnabledKey = "notficationsEnabled" + static let lastAskedForNoticationsKey = "lastAskedForNotifications" + static let skoNotificationsEnabledKey = "skoNotificationsEnabled" + static let skoNotificationsAskedKey = "skoNotificationsAsked" + static let showSko = "showSko" + static let firstLaunchKey = "first_launch_preference" + static let resetAppKey = "reset_app_preference" + } + + // MARK: Utility methods + private func getObject(key: String) -> AnyObject? { + return self.defaults.objectForKey(key) + } + + private func getBool(key: String) -> Bool { + return self.defaults.boolForKey(key) + } + + private func getArray(key: String) -> NSArray? { + return self.defaults.arrayForKey(key) + } + + private func getObject(key: String, defaultValue: AnyObject) -> AnyObject? { + if getObject(key) == nil { + return defaultValue + } + return getObject(key) + } + + private func getBool(key: String, defaultValue: Bool) -> Bool { + if getObject(key) == nil { + return defaultValue + } + return getBool(key) + } + + private func getArray(key: String, defaultValue: NSArray) -> NSArray { + if getObject(key) == nil { + return defaultValue + } + return getArray(key)! + } + + private func setObject(key: String, value: AnyObject?) { + if value == nil { + self.defaults.removeObjectForKey(key) + } else { + self.defaults.setObject(value, forKey: key) + } + self.defaults.synchronize() + } + + private func setBool(key: String, value: Bool) { + defaults.setBool(value, forKey: key) + defaults.synchronize() + } + + private func setArray(key: String, value: NSArray?) { + setObject(key, value: value) + } +} \ No newline at end of file diff --git a/Hydra/PreferencesSwitchTableViewCells.xib b/Hydra/PreferencesSwitchTableViewCells.xib new file mode 100644 index 0000000..de6fc92 --- /dev/null +++ b/Hydra/PreferencesSwitchTableViewCells.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hydra/PreferencesTextTableViewCell.xib b/Hydra/PreferencesTextTableViewCell.xib new file mode 100644 index 0000000..259f45a --- /dev/null +++ b/Hydra/PreferencesTextTableViewCell.xib @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hydra/RestoLegendItem.h b/Hydra/RestoLegendItem.h deleted file mode 100644 index 03521f1..0000000 --- a/Hydra/RestoLegendItem.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// RestoLegend.h -// Hydra -// -// Created by Feliciaan De Palmenaer on 24/12/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class RKObjectMapping; - -@interface RestoLegendItem : NSObject - -@property (nonatomic, strong) NSString *key; -@property (nonatomic, strong) NSString *value; -@property (nonatomic, strong) NSString *style; // if bold or underlined - -+ (RKObjectMapping *)objectMapping; - -@end \ No newline at end of file diff --git a/Hydra/RestoLegendItem.m b/Hydra/RestoLegendItem.m deleted file mode 100644 index e777796..0000000 --- a/Hydra/RestoLegendItem.m +++ /dev/null @@ -1,67 +0,0 @@ -// -// RestoLegend.m -// Hydra -// -// Created by Feliciaan De Palmenaer on 24/12/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "RestoLegendItem.h" -#import - -@implementation RestoLegendItem - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", self.key]; -} - -+ (RKObjectMapping *)objectMapping -{ - // Create mapping for locations - RKObjectMapping *legendMapping = [RKObjectMapping mappingForClass:self]; - [legendMapping addAttributeMappingsFromArray:@[@"key", @"value", @"style"]]; - - return legendMapping; -} - -- (id)initWithCoder:(NSCoder *)aCoder -{ - if (self = [super init]) { - self.key = [aCoder decodeObjectForKey:@"key"]; - self.value = [aCoder decodeObjectForKey:@"value"]; - self.style = [aCoder decodeObjectForKey:@"style"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)aCoder -{ - [aCoder encodeObject:self.key forKey:@"key"]; - [aCoder encodeObject:self.value forKey:@"value"]; - [aCoder encodeObject:self.style forKey:@"style"]; -} - -#pragma mark isEqual and hash implementation - -- (BOOL)isEqual:(id)other { - if (other == self) - return YES; - if (!other || ![other isKindOfClass:[self class]]) - return NO; - return [self isEqualToRestoLegendItem:other]; -} - -- (BOOL)isEqualToRestoLegendItem:(RestoLegendItem *)aLegend { - if (self == aLegend) - return YES; - if (![[self key] isEqualToString:[aLegend key]]) - return NO; - if (![[self value] isEqualToString:[aLegend value]]) - return NO; - if (![[self style] isEqualToString:[aLegend style]]) - return NO; - return YES; -} - -@end diff --git a/Hydra/RestoLocation.h b/Hydra/RestoLocation.h deleted file mode 100644 index 20c20d6..0000000 --- a/Hydra/RestoLocation.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// RestoMapPoint.h -// Hydra -// -// Created by Feliciaan De Palmenaer on 27/12/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import -#import -#import - -@class RKObjectMapping; - -@interface RestoLocation : NSObject - -@property (nonatomic, copy) NSString *name; -@property (nonatomic, copy) NSString *address; -@property (nonatomic, copy) NSString *type; -@property (nonatomic, readonly) CLLocationCoordinate2D coordinate; - -+ (RKObjectMapping *)objectMapping; - -- (NSString *)title; - -@end diff --git a/Hydra/RestoLocation.m b/Hydra/RestoLocation.m deleted file mode 100644 index 1bff7e0..0000000 --- a/Hydra/RestoLocation.m +++ /dev/null @@ -1,90 +0,0 @@ -// -// RestoMapPoint.m -// Hydra -// -// Created by Feliciaan De Palmenaer on 27/12/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "RestoLocation.h" -#import - -@interface RestoLocation () - -@property (nonatomic, assign) CLLocationDegrees latitude; -@property (nonatomic, assign) CLLocationDegrees longitude; - -@end - -@implementation RestoLocation - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - self.name = [coder decodeObjectForKey:@"name"]; - self.address = [coder decodeObjectForKey:@"address"]; - self.type = [coder decodeObjectForKey:@"type"]; - self.longitude = [coder decodeDoubleForKey:@"longitude"]; - self.latitude = [coder decodeDoubleForKey:@"latitude"]; - } - return self; -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", self.name]; -} - -+ (RKObjectMapping *)objectMapping -{ - // Create mapping for locations - RKObjectMapping *locationMapping = [RKObjectMapping mappingForClass:self]; - [locationMapping addAttributeMappingsFromArray:@[@"name", @"address", @"type", @"longitude", @"latitude"]]; - - return locationMapping; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.name forKey:@"name"]; - [coder encodeObject:self.address forKey:@"address"]; - [coder encodeObject:self.type forKey:@"type"]; - [coder encodeDouble:self.longitude forKey:@"longitude"]; - [coder encodeDouble:self.latitude forKey:@"latitude"]; -} - -#pragma mark MKAnnotation - -- (CLLocationCoordinate2D)coordinate -{ - return CLLocationCoordinate2DMake(self.latitude, self.longitude); -} - -- (NSString *)title -{ - return self.name; -} - -#pragma mark isEqual and hash implementation - -- (BOOL)isEqual:(id)other { - if (other == self) - return YES; - if (!other || ![other isKindOfClass:[self class]]) - return NO; - return [self isEqualToRestoLocation:other]; -} - -- (BOOL)isEqualToRestoLocation:(RestoLocation *)aPoint { - if (self == aPoint) - return YES; - if (![self.name isEqualToString:aPoint.name]) - return NO; - if (self.longitude != aPoint.longitude) - return NO; - if (self.latitude != aPoint.latitude) - return NO; - return YES; -} - -@end diff --git a/Hydra/RestoLocation.swift b/Hydra/RestoLocation.swift new file mode 100644 index 0000000..2b18530 --- /dev/null +++ b/Hydra/RestoLocation.swift @@ -0,0 +1,102 @@ +// +// RestoLocation.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 05/03/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class RestoLocation: NSObject, NSCoding, MKAnnotation, Mappable { + + var name: String + var address: String + var type: RestoType + var latitude: CLLocationDegrees + var longitude: CLLocationDegrees + var endpoint: String + var coordinate: CLLocationCoordinate2D { + get { + return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) + } + } + + var title: String? { + get { + return self.name + } + } + + override var description: String { + get { + return "" + } + } + + init(name: String, address: String, type: RestoType, latitude: CLLocationDegrees, longitude: CLLocationDegrees, endpoint: String) { + self.name = name + self.address = address + self.type = type + self.latitude = latitude + self.longitude = longitude + self.endpoint = endpoint + } + + // MARK: NSCoding + required init?(coder aDecoder: NSCoder) { + self.name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String + self.address = aDecoder.decodeObjectForKey(PropertyKey.addressKey) as! String + self.type = RestoType(rawValue: aDecoder.decodeObjectForKey(PropertyKey.typeKey) as! String)! + self.latitude = aDecoder.decodeObjectForKey(PropertyKey.latitudeKey) as! CLLocationDegrees + self.longitude = aDecoder.decodeObjectForKey(PropertyKey.longitudeKey) as! CLLocationDegrees + self.endpoint = aDecoder.decodeObjectForKey(PropertyKey.endpointKey) as! String + + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(name, forKey: PropertyKey.nameKey) + aCoder.encodeObject(address, forKey: PropertyKey.addressKey) + aCoder.encodeObject(type.rawValue, forKey: PropertyKey.typeKey) + aCoder.encodeObject(latitude, forKey: PropertyKey.latitudeKey) + aCoder.encodeObject(longitude, forKey: PropertyKey.longitudeKey) + aCoder.encodeObject(endpoint, forKey: PropertyKey.endpointKey) + } + + // MARK: Mappable + required convenience init?(_ map: Map) { + self.init(name: "", address: "", type: .Other, latitude: 0.0, longitude: 0.0, endpoint: "") + } + + func mapping(map: Map) { + let restoTypeTransform = TransformOf(fromJSON: { (jsonString) -> RestoLocation.RestoType? in + return RestoType(rawValue: jsonString!) + }) { (restoType) -> String? in + return restoType?.rawValue + } + + self.name <- map[PropertyKey.nameKey] + self.address <- map[PropertyKey.addressKey] + self.type <- (map[PropertyKey.typeKey], restoTypeTransform) + self.latitude <- map[PropertyKey.latitudeKey] + self.longitude <- map[PropertyKey.longitudeKey] + self.endpoint <- map[PropertyKey.endpointKey] + } + + struct PropertyKey { + static let nameKey = "name" + static let addressKey = "address" + static let typeKey = "type" + static let latitudeKey = "latitude" + static let longitudeKey = "longitude" + static let endpointKey = "endpoint" + } + + enum RestoType: String { + case Resto = "resto" + case Cafetaria = "cafetaria" + case Club = "club" + case Other = "other" // Keep this for the future if an other type is added + } +} \ No newline at end of file diff --git a/Hydra/RestoMapController.m b/Hydra/RestoMapController.m index 0075526..48b27ec 100644 --- a/Hydra/RestoMapController.m +++ b/Hydra/RestoMapController.m @@ -7,14 +7,13 @@ // #import "RestoMapController.h" -#import "RestoLocation.h" -#import "RestoStore.h" +#import "Hydra-Swift.h" #import "UINavigationController+ReplaceController.h" -@interface RestoMapController () -@property (nonatomic, strong) UISearchDisplayController *searchController; +@property (nonatomic, strong) UISearchController *searchController; @property (nonatomic, strong) NSArray *mapItems; @property (nonatomic, strong) NSArray *filteredMapItems; @@ -34,7 +33,7 @@ - (id)init // Register for updates NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(loadMapItems) - name:RestoStoreDidUpdateInfoNotification object:nil]; + name:@"RestoStoreDidUpdateInfoNotification" object:nil]; } return self; } @@ -43,23 +42,22 @@ - (void)loadView { [super loadView]; - // Search field - CGRect bounds = [UIScreen mainScreen].bounds; - CGRect searchBarFrame = CGRectMake(0, 0, bounds.size.width, 44); - UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:searchBarFrame]; - searchBar.placeholder = @"Zoek een resto"; - [self.view addSubview:searchBar]; + // Create table view controller for search + UITableViewController *tableVC = [[UITableViewController alloc] initWithStyle:UITableViewStylePlain]; + tableVC.tableView.delegate = self; + tableVC.tableView.dataSource = self; - self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar - contentsController:self]; - self.searchController.delegate = self; - self.searchController.searchResultsDataSource = self; - self.searchController.searchResultsDelegate = self; + // Create search controller + self.searchController = [[UISearchController alloc] initWithSearchResultsController:tableVC]; + self.searchController.dimsBackgroundDuringPresentation = NO; + self.searchController.searchResultsUpdater = self; + self.searchController.searchBar.placeholder = @"Zoek een resto"; + [self.view addSubview:self.searchController.searchBar]; - // Offset map frame a little bit + /*// Offset map frame a little bit CGRect mapFrame = self.mapView.frame; mapFrame.origin.y = 44; - self.mapView.frame = mapFrame; + self.mapView.frame = mapFrame;*/ } - (void)viewDidLoad @@ -71,7 +69,7 @@ - (void)viewDidLoad - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - GAI_Track(@"Resto Kaart"); + GAI_Track(@"Resto Map"); } - (void)dealloc @@ -83,7 +81,7 @@ - (void)mapLocationUpdated { if (self.searchController.isActive) { [self calculateDistances]; - [[[self searchController] searchResultsTableView] reloadData]; + [[(UITableViewController *)[[self searchController] searchResultsController] tableView] reloadData]; } } @@ -131,6 +129,7 @@ - (void)filterMapItems } [self reorderMapItems]; + [[(UITableViewController *)[[self searchController] searchResultsController] tableView] reloadData]; } - (void)reorderMapItems @@ -152,66 +151,15 @@ - (void)resetMapViewRect } #pragma mark - SearchController delegate - -- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller +- (void)updateSearchResultsForSearchController:(UISearchController *)searchController { - self.endingSearch = NO; - - // Just by accessing the property here, the searchResultsTableView will - // be initialized with the correct frame. Crazy shit. - [controller searchResultsTableView]; - - // After this method the searchcontroller will start its animation, which we - // want in on so we execute our hook in the next run loop. - dispatch_async(dispatch_get_main_queue(), ^{ - [self _showSearchResults:controller.searchResultsTableView animated:YES]; - controller.searchResultsTableView.contentOffset = CGPointZero; - }); + // Always show the searchResultsController when the searchBar is opened + self.searchController.searchResultsController.view.hidden = NO; [self calculateDistances]; [self filterMapItems]; } -- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller -{ - self.endingSearch = YES; -} - -- (void)searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView -{ - if (!self.endingSearch) { - [self _showSearchResults:controller.searchResultsTableView animated:NO]; - } -} - -- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString -{ - [self filterMapItems]; - return YES; -} - -// Hacky method to show the search results tableview immediately -- (void)_showSearchResults:(UITableView *)tableView animated:(BOOL)animated -{ - if (tableView.superview) { - // iOS7 approach: show tableview, hide overlay-view - tableView.hidden = NO; - [[tableView.superview.subviews lastObject] setHidden:YES]; - } - else { - [tableView.layer removeAllAnimations]; - [self.view addSubview:tableView]; - } - - if (animated) { - // Smooth appearance - tableView.alpha = 0; - [UIView animateWithDuration:0.3 animations:^{ - tableView.alpha = 1; - }]; - } -} - #pragma mark - SearchController tableView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section @@ -253,9 +201,10 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Keep the search string - NSString *search = self.searchDisplayController.searchBar.text; - [self.searchDisplayController setActive:NO animated:YES]; - self.searchDisplayController.searchBar.text = search; + NSString *search = self.searchController.searchBar.text; + //[self.searchController setActive:NO animated:YES]; + [self.searchController dismissViewControllerAnimated:NO completion:nil]; + self.searchController.searchBar.text = search; // Highlight the selected item RestoLocation *selected = self.filteredMapItems[indexPath.row]; diff --git a/Hydra/RestoMenu.h b/Hydra/RestoMenu.h deleted file mode 100644 index 47b3f65..0000000 --- a/Hydra/RestoMenu.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// RestoMenu.h -// Hydra -// -// Created by Pieter De Baets on 17/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class RKObjectMapping, RestoMenuItem; - -@interface RestoMenu : NSObject - -@property (nonatomic, strong) NSDate *day; -@property (nonatomic) BOOL open; -@property (nonatomic, strong) NSArray *meat; -@property (nonatomic, strong) NSArray *vegetables; -@property (nonatomic, strong) RestoMenuItem *soup; -@property (nonatomic, strong) NSDate *lastUpdated; - -+ (RKObjectMapping *)objectMapping; - -@end - -@interface RestoMenuItem : NSObject - -@property (nonatomic, strong) NSString *name; -@property (nonatomic, strong) NSString *price; -@property (nonatomic) BOOL recommended; - -@end diff --git a/Hydra/RestoMenu.m b/Hydra/RestoMenu.m deleted file mode 100644 index a7aa74c..0000000 --- a/Hydra/RestoMenu.m +++ /dev/null @@ -1,96 +0,0 @@ -// -// RestoMenu.m -// Hydra -// -// Created by Pieter De Baets on 17/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "RestoMenu.h" -#import - -@implementation RestoMenu - -- (NSString *)description -{ - NSUInteger count = [self.meat count] + [self.vegetables count]; - return [NSString stringWithFormat:@"", - self.day, (unsigned long)count, NSStringFromBOOL(self.open)]; -} - -+ (RKObjectMapping *)objectMapping -{ - // Create mapping for menu-item - RKObjectMapping *itemMapping = [RKObjectMapping mappingForClass:[RestoMenuItem class]]; - [itemMapping addAttributeMappingsFromArray:@[@"name", @"price", @"recommended"]]; - - // Create mapping for menu - RKObjectMapping *menuMapping = [RKObjectMapping mappingForClass:self]; - [menuMapping setForceCollectionMapping:YES]; - [menuMapping addAttributeMappingFromKeyOfRepresentationToAttribute:@"day"]; - [menuMapping addAttributeMappingsFromDictionary:@{ - @"(day).open": @"open", - @"(day).vegetables": @"vegetables" - }]; - - RKRelationshipMapping *meat = [RKRelationshipMapping relationshipMappingFromKeyPath:@"(day).meat" - toKeyPath:@"meat" - withMapping:itemMapping]; - RKRelationshipMapping *soup = [RKRelationshipMapping relationshipMappingFromKeyPath:@"(day).soup" - toKeyPath:@"soup" - withMapping:itemMapping]; - [menuMapping addPropertyMappingsFromArray:@[meat, soup]]; - - return menuMapping; -} - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - self.day = [coder decodeObjectForKey:@"day"]; - self.open = [coder decodeBoolForKey:@"open"]; - self.meat = [coder decodeObjectForKey:@"meat"]; - self.vegetables = [coder decodeObjectForKey:@"vegetables"]; - self.soup = [coder decodeObjectForKey:@"soup"]; - self.lastUpdated = [coder decodeObjectForKey:@"lastUpdated"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.day forKey:@"day"]; - [coder encodeBool:self.open forKey:@"open"]; - [coder encodeObject:self.meat forKey:@"meat"]; - [coder encodeObject:self.vegetables forKey:@"vegetables"]; - [coder encodeObject:self.soup forKey:@"soup"]; - [coder encodeObject:self.lastUpdated forKey:@"lastUpdated"]; -} - -@end - -@implementation RestoMenuItem - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", self.name, self.price]; -} - -- (id)initWithCoder:(NSCoder *)coder -{ - if (self = [super init]) { - self.name = [coder decodeObjectForKey:@"name"]; - self.price = [coder decodeObjectForKey:@"price"]; - self.recommended = [coder decodeBoolForKey:@"recommended"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.name forKey:@"name"]; - [coder encodeObject:self.price forKey:@"price"]; - [coder encodeBool:self.recommended forKey:@"recommended"]; -} - -@end \ No newline at end of file diff --git a/Hydra/RestoMenu.swift b/Hydra/RestoMenu.swift new file mode 100644 index 0000000..6af7554 --- /dev/null +++ b/Hydra/RestoMenu.swift @@ -0,0 +1,182 @@ +// +// RestoMenu.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 02/03/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class RestoMenu: NSObject, NSCoding, Mappable { + + var date: NSDate + var meals: [RestoMenuItem]? + var open: Bool + var vegetables: [String]? + var lastUpdated: NSDate + + var sideDishes: [RestoMenuItem]? { + //TODO: cache... + return meals?.filter({ $0.type == .Side }) + } + + var mainDishes: [RestoMenuItem]? { + //TODO: cache... + return meals?.filter({ $0.type != .Side}) + } + + required convenience init?(_ map: Map) { + self.init(date: NSDate().dateAtStartOfDay()) + } + + init(date: NSDate, meals: [RestoMenuItem]? = nil, open: Bool = false, vegetables: [String]? = nil, lastUpdated: NSDate = NSDate().dateAtStartOfDay()) { + self.date = date + self.meals = meals + self.open = open + self.vegetables = vegetables + self.lastUpdated = lastUpdated + } + + func mapping(map: Map) { + let formatter = NSDateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + self.date <- (map[PropertyKey.dateKey], DateFormatterTransform(dateFormatter: formatter)) + self.meals <- map[PropertyKey.mealsKey] + self.open <- map[PropertyKey.openKey] + self.vegetables <- map[PropertyKey.vegetablesKey] + } + + // MARK: implement NSCoding protocol + required convenience init?(coder aDecoder: NSCoder) { + guard let date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as? NSDate, + let open = aDecoder.decodeObjectForKey(PropertyKey.openKey) as? Bool, + let vegetables = aDecoder.decodeObjectForKey(PropertyKey.vegetablesKey) as? [String], + let lastUpdated = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as? NSDate + else {return nil} + + let meals = aDecoder.decodeObjectForKey(PropertyKey.mealsKey) as? [RestoMenuItem] + + self.init(date: date, meals: meals, open: open, vegetables: vegetables, lastUpdated: lastUpdated) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(date, forKey: PropertyKey.dateKey) + aCoder.encodeObject(meals, forKey: PropertyKey.mealsKey) + aCoder.encodeObject(open, forKey: PropertyKey.openKey) + aCoder.encodeObject(vegetables, forKey: PropertyKey.vegetablesKey) + aCoder.encodeObject(lastUpdated, forKey: PropertyKey.lastUpdatedKey) + } + + struct PropertyKey { + static let dateKey = "date" + static let openKey = "open" + static let mealsKey = "meals" + static let vegetablesKey = "vegetables" + static let lastUpdatedKey = "lastUpdated" + } +} + +class RestoMenuItem: NSObject, NSCoding, Mappable { + var kind: RestoMenuKind + var name: String + var price: String? + var type: RestoMenuType + + // MARK: implement mapping protocol + required convenience init?(_ map: Map) { + self.init(name: "") + } + + init(name: String, price: String? = nil, kind: RestoMenuKind = .Other, type: RestoMenuType = .Other) { + self.name = name + self.price = price + self.kind = kind + self.type = type + } + + func mapping(map: Map) { + let menuKindTransform = TransformOf(fromJSON: { (jsonString) -> RestoMenuKind in + if let jsonString = jsonString { + return self.restoMenuKindFromString(jsonString) + } + return .Other + }) { (menuKind) -> String? in + return menuKind?.rawValue + } + + + let restoTypeTransform = TransformOf(fromJSON: { (jsonString) -> RestoMenuType in + if let jsonString = jsonString { + return self.restoMenuTypeFromString(jsonString) + } + return .Other + }) { (restoType) -> String? in + return restoType?.rawValue + } + + self.kind <- (map[PropertyKey.kindKey], menuKindTransform) + self.name <- map[PropertyKey.nameKey] + self.price <- map[PropertyKey.priceKey] + self.type <- (map[PropertyKey.typeKey], restoTypeTransform) + } + + // MARK: implement NSCoding protocol + required convenience init?(coder aDecoder: NSCoder) { + guard let kindString = aDecoder.decodeObjectForKey(PropertyKey.kindKey) as? String, + let kind = RestoMenuKind.init(rawValue: kindString), + let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as? String, + let typeString = aDecoder.decodeObjectForKey(PropertyKey.typeKey) as? String, + let type = RestoMenuType.init(rawValue: typeString) + else { return nil } + + let price = aDecoder.decodeObjectForKey(PropertyKey.priceKey) as? String + + self.init(name: name, price: price, kind: kind, type: type) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(kind.rawValue, forKey: PropertyKey.kindKey) + aCoder.encodeObject(name, forKey: PropertyKey.nameKey) + aCoder.encodeObject(price, forKey: PropertyKey.priceKey) + aCoder.encodeObject(type.rawValue, forKey: PropertyKey.typeKey) + } + + private func restoMenuTypeFromString(type: String) -> RestoMenuType { + var restoType = RestoMenuType.init(rawValue: type) + if restoType == nil { + restoType = .Some(.Other) + } + return restoType! + } + + private func restoMenuKindFromString(kind: String) -> RestoMenuKind { + var restoKind = RestoMenuKind.init(rawValue: kind) + if restoKind == nil { + restoKind = .Some(.Other) + } + return restoKind! + } + + struct PropertyKey { + static let kindKey = "kind" + static let nameKey = "name" + static let priceKey = "price" + static let typeKey = "type" + } +} + +enum RestoMenuType: String { + case Side = "side" + case Main = "main" + case Other = "other" +} + +enum RestoMenuKind: String { + case Soup = "soup" + case Meat = "meat" + case Vegetarian = "vegetarian" + case Fish = "fish" + case Other = "other" +} \ No newline at end of file diff --git a/Hydra/RestoMenuCollectionCell.swift b/Hydra/RestoMenuCollectionCell.swift index b7da92f..e8e6ec8 100644 --- a/Hydra/RestoMenuCollectionCell.swift +++ b/Hydra/RestoMenuCollectionCell.swift @@ -30,11 +30,11 @@ class RestoMenuCollectionCell: UICollectionViewCell, UITableViewDataSource, UITa let restoMenuSection = RestoMenuSection(rawValue: section) switch restoMenuSection! { case .Soup: - return restoMenu?.soup != nil ? 1 : 0 + return (restoMenu?.sideDishes!.count)! case .Meat: - return (restoMenu?.meat.count)! + return (restoMenu?.mainDishes!.count)! case .Vegetable: - return (restoMenu?.vegetables.count)! + return (restoMenu?.vegetables!.count)! default: return 0 } @@ -51,11 +51,11 @@ class RestoMenuCollectionCell: UICollectionViewCell, UITableViewDataSource, UITa let restoMenuSection = RestoMenuSection(rawValue: indexPath.section) switch restoMenuSection! { case .Soup: - cell!.menuItem = restoMenu?.soup + cell!.menuItem = restoMenu?.sideDishes![indexPath.row] case .Meat: - cell!.menuItem = restoMenu?.meat[indexPath.row] as? RestoMenuItem + cell!.menuItem = restoMenu?.mainDishes![indexPath.row] case .Vegetable: - cell!.vegetable = restoMenu?.vegetables[indexPath.row] as? String + cell!.vegetable = restoMenu?.vegetables![indexPath.row] default: break } diff --git a/Hydra/RestoMenuInfoCollectionViewCell.swift b/Hydra/RestoMenuInfoCollectionViewCell.swift index f5f4a0c..11892fb 100644 --- a/Hydra/RestoMenuInfoCollectionViewCell.swift +++ b/Hydra/RestoMenuInfoCollectionViewCell.swift @@ -10,13 +10,7 @@ import UIKit class RestoMenuInfoCollectionViewCell: UICollectionViewCell, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! - - var legend: [RestoLegendItem]? { - didSet { - tableView.reloadData() - } - } - + var sandwiches: [RestoSandwich]? { didSet { tableView.reloadData() @@ -26,10 +20,6 @@ class RestoMenuInfoCollectionViewCell: UICollectionViewCell, UITableViewDataSour func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch(section) { case 0: - if let legend = self.legend { - return legend.count - } - case 1: if let sandwiches = self.sandwiches { return sandwiches.count } @@ -39,18 +29,12 @@ class RestoMenuInfoCollectionViewCell: UICollectionViewCell, UITableViewDataSour } func numberOfSectionsInTableView(tableView: UITableView) -> Int { - return 2 + return 1 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { switch(indexPath.section) { case 0: - let cell = tableView.dequeueReusableCellWithIdentifier("infoItemCell") as? RestoLegendItemTableViewCell - - cell?.item = legend?[indexPath.item] - - return cell! - case 1: let cell = tableView.dequeueReusableCellWithIdentifier("sandwichCell") as? RestoSandwichTableViewCell cell?.item = sandwiches?[indexPath.item] @@ -62,72 +46,8 @@ class RestoMenuInfoCollectionViewCell: UICollectionViewCell, UITableViewDataSour } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - if indexPath.section == 0 { - let item = legend?[indexPath.item] - - let size = CGSizeMake(tableView.frame.width - 40, CGFloat.max) - let paragraphStyle = NSParagraphStyle() - //paragraphStyle.lineBreakMode = NSLineBreakMode.ByWordWrapping - //paragraphStyle.alignment = NSTextAlignment.Left - - let attributes = [ - NSFontAttributeName: UIFont.systemFontOfSize(15), - NSParagraphStyleAttributeName: paragraphStyle - ] - - let mutableText = NSMutableAttributedString(string: (item?.value)!, attributes: attributes) - - let options = unsafeBitCast(NSStringDrawingOptions.UsesLineFragmentOrigin.rawValue | - NSStringDrawingOptions.UsesFontLeading.rawValue, - NSStringDrawingOptions.self) - - let labelSize: CGSize = mutableText.boundingRectWithSize(size, options: options, context: nil).size - - return labelSize.height + 5 - } - return 25 } - - func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - if section == 0 { - let frame = CGRectMake(0, 0, self.bounds.width, 40) - let header = UIView(frame: frame) - - let label = UILabel(frame: frame) - label.textAlignment = .Center - label.text = "Broodjes" - label.baselineAdjustment = .AlignCenters - label.textColor = UIColor.whiteColor() - - header.addSubview(label) - - return header - } - - return nil - } - - func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if section == 0 { - return 30 - } - return 0 - } -} - -class RestoLegendItemTableViewCell: UITableViewCell { - @IBOutlet weak var keyLabel: UILabel! - @IBOutlet weak var valueLabel: UILabel! - - var item: RestoLegendItem? { - didSet { - if let item = item { - keyLabel.text = item.key - valueLabel.text = item.value - } - } - } } class RestoSandwichTableViewCell: UITableViewCell { diff --git a/Hydra/RestoMenuViewController.swift b/Hydra/RestoMenuViewController.swift index e84a3be..c948112 100644 --- a/Hydra/RestoMenuViewController.swift +++ b/Hydra/RestoMenuViewController.swift @@ -8,13 +8,14 @@ import UIKit +let RestoMenuViewControllerShouldScrollToNotification = "RestoMenuViewControllerShouldScrollTo" + class RestoMenuViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView? @IBOutlet weak var restoMenuHeader: RestoMenuHeaderView? var days: [NSDate] = [] var menus: [RestoMenu?] = [] - var legend: [RestoLegendItem]? var sandwiches: [RestoSandwich]? var currentIndex: Int = 1 @@ -31,10 +32,11 @@ class RestoMenuViewController: UIViewController { func initialize() { let center = NSNotificationCenter.defaultCenter() - center.addObserver(self, selector: "reloadMenu", name: RestoStoreDidReceiveMenuNotification, object: nil) - center.addObserver(self, selector: "reloadInfo", name: RestoStoreDidUpdateInfoNotification, object: nil) - center.addObserver(self, selector: "reloadInfo", name: RestoStoreDidUpdateSandwichesNotification, object: nil) - center.addObserver(self, selector: "applicationDidBecomeActive:", name: UIApplicationDidBecomeActiveNotification, object: nil) + center.addObserver(self, selector: #selector(RestoMenuViewController.reloadMenu), name: RestoStoreDidReceiveMenuNotification, object: nil) + center.addObserver(self, selector: #selector(RestoMenuViewController.reloadInfo), name: RestoStoreDidUpdateInfoNotification, object: nil) + center.addObserver(self, selector: #selector(RestoMenuViewController.reloadInfo), name: RestoStoreDidUpdateSandwichesNotification, object: nil) + center.addObserver(self, selector: #selector(RestoMenuViewController.applicationDidBecomeActive(_:)), name: UIApplicationDidBecomeActiveNotification, object: nil) + center.addObserver(self, selector: #selector(RestoMenuViewController.scrollToIndexNotification(_:)), name: RestoMenuViewControllerShouldScrollToNotification, object: nil) days = calculateDays() } @@ -46,8 +48,8 @@ class RestoMenuViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.loadMenu() - self.legend = RestoStore.sharedStore().legend as? [RestoLegendItem] - self.sandwiches = RestoStore.sharedStore().sandwiches as? [RestoSandwich] + + self.sandwiches = RestoStore.sharedStore.sandwiches // update days and reloadData self.restoMenuHeader?.updateDays() @@ -55,11 +57,13 @@ class RestoMenuViewController: UIViewController { //self.scrollToIndex(self.currentIndex, animated: false) // REMOVE ME IF THE BUG IS FIXED, THIS IS UGLY - NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("refreshDataTimer:"), userInfo: nil, repeats: false) + NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: #selector(RestoMenuViewController.refreshDataTimer(_:)), userInfo: nil, repeats: false) } override func viewDidAppear(animated: Bool) { UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: animated) + + GAI_track("Resto Menu") } func refreshDataTimer(timer: NSTimer){ // REMOVE ME WHEN THE BUG IS FIXED @@ -98,7 +102,7 @@ class RestoMenuViewController: UIViewController { func loadMenu() { // New menus are available - let store = RestoStore.sharedStore() + let store = RestoStore.sharedStore var menus = [RestoMenu?]() for day in days { let menu = store.menuForDay(day) as RestoMenu? @@ -109,15 +113,16 @@ class RestoMenuViewController: UIViewController { func reloadMenu() { debugPrint("Reloading menu") - self.loadMenu() - self.collectionView?.reloadData() + dispatch_async(dispatch_get_main_queue()) { + self.loadMenu() + self.collectionView?.reloadData() + } } func reloadInfo() { // New info is available debugPrint("Reloading info") - self.legend = (RestoStore.sharedStore().legend as? [RestoLegendItem])! - self.sandwiches = (RestoStore.sharedStore().sandwiches as? [RestoSandwich])! + self.sandwiches = RestoStore.sharedStore.sandwiches self.collectionView?.reloadData() } @@ -168,7 +173,6 @@ extension RestoMenuViewController: UICollectionViewDataSource, UICollectionViewD case 0: // info cell let cell = collectionView.dequeueReusableCellWithReuseIdentifier("infoCell", forIndexPath: indexPath) as! RestoMenuInfoCollectionViewCell - cell.legend = self.legend cell.sandwiches = self.sandwiches return cell case 1...self.days.count: @@ -216,10 +220,15 @@ extension RestoMenuViewController: UIScrollViewDelegate { scrollToIndex(index) } } -} -// MARK: - Header view actions -extension RestoMenuViewController { + // MARK: - Header view actions + + func scrollToIndexNotification(notification: NSNotification) { + if let date = notification.object as? NSDate { + self.scrollToDate(date) + } + } + func scrollToIndex(index: Int, animated: Bool = true) { self.collectionView?.scrollToItemAtIndexPath(NSIndexPath(forRow: index, inSection: 0), atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: animated) self.restoMenuHeader?.selectedIndex(index) diff --git a/Hydra/RestoSandwich.swift b/Hydra/RestoSandwich.swift index 2ddcb3d..93cb356 100644 --- a/Hydra/RestoSandwich.swift +++ b/Hydra/RestoSandwich.swift @@ -7,8 +7,9 @@ // import Foundation +import ObjectMapper -@objc class RestoSandwich: NSObject, NSCoding { +@objc class RestoSandwich: NSObject, NSCoding, Mappable { var name: String var ingredients: [String] var priceSmall: String @@ -24,33 +25,40 @@ import Foundation self.priceSmall = priceSmall self.priceMedium = priceMedium } - + + required convenience init?(_ map: Map) { + self.init() + } + + func mapping(map: Map) { + self.name <- map[PropertyKey.nameKey] + self.ingredients <- map[PropertyKey.ingredientsKey] + self.priceSmall <- map["price_small"] + self.priceMedium <- map["price_medium"] + } + // MARK: NSCoding required convenience init?(coder decoder: NSCoder) { - guard let name = decoder.decodeObjectForKey("name") as? String, - let ingredients = decoder.decodeObjectForKey("ingredients") as? [String], - let priceSmall = decoder.decodeObjectForKey("priceSmall") as? String, - let priceMedium = decoder.decodeObjectForKey("priceMedium") as? String + guard let name = decoder.decodeObjectForKey(PropertyKey.nameKey) as? String, + let ingredients = decoder.decodeObjectForKey(PropertyKey.ingredientsKey) as? [String], + let priceSmall = decoder.decodeObjectForKey(PropertyKey.priceSmallKey) as? String, + let priceMedium = decoder.decodeObjectForKey(PropertyKey.priceMediumKey) as? String else {return nil} self.init(name: name, ingredients: ingredients, priceSmall: priceSmall, priceMedium: priceMedium) } func encodeWithCoder(coder: NSCoder) { - coder.encodeObject(name, forKey: "name") - coder.encodeObject(ingredients, forKey: "ingredients") - coder.encodeObject(priceSmall, forKey: "priceSmall") - coder.encodeObject(priceMedium, forKey: "priceMedium") + coder.encodeObject(name, forKey: PropertyKey.nameKey) + coder.encodeObject(ingredients, forKey: PropertyKey.ingredientsKey) + coder.encodeObject(priceSmall, forKey: PropertyKey.priceSmallKey) + coder.encodeObject(priceMedium, forKey: PropertyKey.priceMediumKey) } - - // MARK: RestKit Mapping - class func objectMapping() -> RKObjectMapping { - let mapping = RKObjectMapping(forClass: RestoSandwich.self) - - mapping.addAttributeMappingsFromArray(["name", "ingredients"]) - mapping.addAttributeMappingsFromDictionary(["price_small": "priceSmall", "price_medium": "priceMedium"]) - - return mapping + + struct PropertyKey { + static let nameKey = "name" + static let ingredientsKey = "ingredients" + static let priceSmallKey = "priceSmall" + static let priceMediumKey = "priceMedium" } - } \ No newline at end of file diff --git a/Hydra/RestoStore.h b/Hydra/RestoStore.h deleted file mode 100644 index a5de5ec..0000000 --- a/Hydra/RestoStore.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// SchamperStore.h -// Hydra -// -// Created by Pieter De Baets on 17/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -extern NSString *const RestoStoreDidReceiveMenuNotification; -extern NSString *const RestoStoreDidUpdateInfoNotification; -extern NSString *const RestoStoreDidUpdateSandwichesNotification; - -@class RestoMenu; - -@interface RestoStore : NSObject - -@property (nonatomic, strong, readonly) NSArray *locations; -@property (nonatomic, strong, readonly) NSArray *legend; -@property (nonatomic, strong, readonly) NSArray *sandwiches; - -+ (RestoStore *)sharedStore; -- (RestoMenu *)menuForDay:(NSDate *)day; - -@end \ No newline at end of file diff --git a/Hydra/RestoStore.m b/Hydra/RestoStore.m deleted file mode 100644 index 37f3f3f..0000000 --- a/Hydra/RestoStore.m +++ /dev/null @@ -1,368 +0,0 @@ -// -// SchamperStore.m -// Hydra -// -// Created by Pieter De Baets on 17/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "RestoStore.h" -#import "RestoMenu.h" -#import "RestoLegendItem.h" -#import "RestoLocation.h" -#import "NSDate+Utilities.h" -#import "AppDelegate.h" -#import "Hydra-Swift.h" -#import - -#define kRestoUrl @"https://zeus.ugent.be/hydra/api/1.0/resto/" -#define kRestoInfoPath @"meta.json" -#define kRestoSandwichesPath @"sandwiches.json" -#define kRestoMenuPath @"menu/%lu/%lu.json" - -#define kInfoUpdateIterval (24 * 60 * 60) /* one day */ -#define kSandwichesUpdateInterval kInfoUpdateIterval -#define kMenuUpdateIterval (12 * 60 * 60) - -NSString *const RestoStoreDidReceiveMenuNotification = - @"RestoStoreDidReceiveMenuNotification"; -NSString *const RestoStoreDidUpdateInfoNotification = - @"RestoStoreDidUpdateInfoNotification"; -NSString *const RestoStoreDidUpdateSandwichesNotification = - @"RestoStoreDidUpdateSandwichesNotification"; - -@interface RestoStore () - -@property (nonatomic, strong) RKObjectManager *objectManager; -@property (nonatomic, strong) NSMutableArray *activeRequests; - -@property (atomic, strong) NSMutableDictionary *menus; -@property (nonatomic, strong) NSArray *locations; -@property (nonatomic, strong) NSArray *legend; -@property (nonatomic, strong) NSArray *sandwiches; -@property (nonatomic, strong) NSDate *infoLastUpdated; -@property (nonatomic, strong) NSDate *sandwichesLastUpdated; - -@end - -@interface RestoInfo : NSObject - -+ (RKObjectMapping *)objectMapping; - -@property (nonatomic, strong) NSArray *locations; -@property (nonatomic, strong) NSArray *legend; - -@end - -@implementation RestoStore - -+ (RestoStore *)sharedStore -{ - static RestoStore *sharedInstance = nil; - if (!sharedInstance) { - // Try restoring the store from archive - @try { - sharedInstance = [NSKeyedUnarchiver unarchiveObjectWithFile:[self menuCachePath]]; - } - @catch (NSException *exception) { - NSLog(@"Got exception while reading Resto archive: %@", exception); - } - @finally { - if (!sharedInstance) sharedInstance = [[RestoStore alloc] init]; - } - } - return sharedInstance; -} - -- (instancetype)init -{ - if (self = [super init]) { - self.menus = [[NSMutableDictionary alloc] init]; - self.locations = [[NSArray alloc] init]; - self.legend = [[NSArray alloc] init]; - - [self sharedInit]; - } - return self; -} - -- (void)sharedInit -{ - self.activeRequests = [[NSMutableArray alloc] init]; - - if (!self.infoLastUpdated) { - self.infoLastUpdated = [NSDate dateWithTimeIntervalSince1970:0]; - } - - if (!self.sandwichesLastUpdated) { - self.sandwichesLastUpdated = [NSDate dateWithTimeIntervalSince1970:0]; - } - - // Initialize objectManager - self.objectManager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:kRestoUrl]]; -} - -#pragma mark - Caching - -- (id)initWithCoder:(NSCoder *)decoder -{ - if (self = [super init]) { - self.menus = [decoder decodeObjectForKey:@"menus"]; - AssertClassOrNil(self.menus, NSMutableDictionary); - self.locations = [decoder decodeObjectForKey:@"locations"]; - AssertClassOrNil(self.locations, NSArray); - self.legend = [decoder decodeObjectForKey:@"legend"]; - AssertClassOrNil(self.legend, NSArray); - self.sandwiches = [decoder decodeObjectForKey:@"sandwiches"]; - AssertClassOrNil(self.sandwiches, NSArray); - self.infoLastUpdated = [decoder decodeObjectForKey:@"infoLastUpdated"]; - AssertClassOrNil(self.infoLastUpdated, NSDate); - self.sandwichesLastUpdated = [decoder decodeObjectForKey:@"sandwichesLastUpdated"]; - AssertClassOrNil(self.sandwichesLastUpdated, NSDate); - - [self sharedInit]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.menus forKey:@"menus"]; - [coder encodeObject:self.locations forKey:@"locations"]; - [coder encodeObject:self.legend forKey:@"legend"]; - [coder encodeObject:self.sandwiches forKey:@"sandwiches"]; - [coder encodeObject:self.infoLastUpdated forKey:@"infoLastUpdated"]; - [coder encodeObject:self.sandwichesLastUpdated forKey:@"sandwichesLastUpdated"]; -} - -+ (NSString *)menuCachePath -{ - // Get cache directory - NSArray *cacheDirectories = - NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString *cacheDirectory = cacheDirectories[0]; - - return [cacheDirectory stringByAppendingPathComponent:@"resto.archive"]; -} - -- (void)updateStoreCache -{ - dispatch_queue_t async = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); - dispatch_async(async, ^{ - NSDate *today = [[NSDate date] dateAtStartOfDay]; - NSMutableArray *toRemove = [[NSMutableArray alloc] init]; - - // Remove all old entries - for (NSDate *date in [self.menus keyEnumerator]) { - if ([today compare:date] == NSOrderedDescending) { - [toRemove addObject:date]; - } - } - [self.menus removeObjectsForKeys:toRemove]; - DLog(@"Purged %lu old menus from RestoStore", (unsigned long)[toRemove count]); - - NSString *cachePath = [[self class] menuCachePath]; - [NSKeyedArchiver archiveRootObject:self toFile:cachePath]; - }); -} - -#pragma mark - Data management and requests - -- (RestoMenu *)menuForDay:(NSDate *)day -{ - // TODO: perhaps the menu is outdated - // if the data is more than a day old, start a refresh in the background - - day = [day dateAtStartOfDay]; - RestoMenu *menu = self.menus[day]; - if (!menu || [menu.lastUpdated timeIntervalSinceNow] < -kMenuUpdateIterval) { - [self _fetchMenuForWeek:day.week year:day.yearOfCalendarWeek]; - } - return menu; -} - -- (void)_fetchMenuForWeek:(NSUInteger)week year:(NSUInteger)year -{ - NSString *path = [NSString stringWithFormat:kRestoMenuPath, (unsigned long)year, (unsigned long)week]; - - // Only one request for each resource allowed - if ([self.activeRequests containsObject:path]) { - return; - } - - DLog(@"Fetching resto information for %lu/%lu", (unsigned long)year, (unsigned long)week); - [self.activeRequests addObject:path]; - [self.objectManager addResponseDescriptor: - [RKResponseDescriptor responseDescriptorWithMapping:[RestoMenu objectMapping] - method:RKRequestMethodGET - pathPattern:path - keyPath:nil - statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]; - [self.objectManager getObjectsAtPath:path - parameters:nil - success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { - [self _processMenuResult:mappingResult forPath:path]; - } - failure:^(RKObjectRequestOperation *operation, NSError *error) { - NSLog(@"Updating resource %@ failed: %@", path, error); - AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [app handleError:error]; - - [self _processMenuResult:nil forPath:path]; - }]; -} - -- (void)_processMenuResult:(RKMappingResult *)mappingResult forPath:(NSString *)path -{ - NSArray *objects = [mappingResult array]; - - [self _delayActiveRequestRemoval:path]; - for (RestoMenu *menu in objects) { - NSDate *day = [[menu day] dateAtStartOfDay]; - menu.lastUpdated = [NSDate date]; - self.menus[day] = menu; - } - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:RestoStoreDidReceiveMenuNotification object:self]; - - [self updateStoreCache]; -} - --(NSArray *)locations -{ - [self refreshInfo]; - return _locations; -} - --(NSArray *)legend -{ - [self refreshInfo]; - return _legend; -} - --(NSArray *)sandwiches -{ - [self refreshSandwiches]; - return _sandwiches; -} - -- (void)refreshInfo -{ - // Check if an update is required - if ([self.infoLastUpdated timeIntervalSinceNow] > -kInfoUpdateIterval) { - return; - } - - if ([self.activeRequests containsObject:kRestoInfoPath]) { - return; - } - - DLog(@"Updating resto meta-information"); - [self.activeRequests addObject:kRestoInfoPath]; - [self.objectManager addResponseDescriptor: - [RKResponseDescriptor responseDescriptorWithMapping:[RestoInfo objectMapping] - method:RKRequestMethodGET - pathPattern:kRestoInfoPath - keyPath:nil - statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]; - - [self.objectManager getObjectsAtPath:kRestoInfoPath - parameters:nil - success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { - [self _processInfoResult:mappingResult]; - } - failure:^(RKObjectRequestOperation *operation, NSError *error) { - AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [app handleError:error]; - [self _processInfoResult:nil]; - }]; -} - -- (void)_processInfoResult:(RKMappingResult *)mappingResult -{ - RestoInfo *restoInfo = [mappingResult firstObject]; - DLog(@"Received %lu legends and %lu locations", (unsigned long)[restoInfo.legend count], (unsigned long)[restoInfo.locations count]); - [self _delayActiveRequestRemoval:kRestoInfoPath]; - - if ([restoInfo.legend count] > 0) { - self.legend = restoInfo.legend; - } - if ([restoInfo.locations count] > 0) { - self.locations = restoInfo.locations; - } - self.infoLastUpdated = [NSDate date]; - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:RestoStoreDidUpdateInfoNotification object:self]; - - [self updateStoreCache]; -} - -- (void)refreshSandwiches -{ - if ([self.sandwichesLastUpdated timeIntervalSinceNow] > -kSandwichesUpdateInterval) { - return; - } - - if ([self.activeRequests containsObject:kRestoSandwichesPath]) { - return; - } - - DLog(@"Updating sandwiches"); - [self.activeRequests addObject:kRestoSandwichesPath]; - [self.objectManager addResponseDescriptor: - [RKResponseDescriptor responseDescriptorWithMapping:[RestoSandwich objectMapping] - method:RKRequestMethodGET - pathPattern:kRestoSandwichesPath - keyPath:nil - statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]; - - [self.objectManager getObjectsAtPath:kRestoSandwichesPath - parameters:nil - success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { - NSArray *sandwiches = [mappingResult array]; - if ([sandwiches count] > 0) { - self.sandwiches = sandwiches; - } - self.sandwichesLastUpdated = [NSDate date]; - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:RestoStoreDidUpdateSandwichesNotification object:self]; - - [self updateStoreCache]; - - } - failure:^(RKObjectRequestOperation *operation, NSError *error) { - AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [app handleError:error]; - [self _delayActiveRequestRemoval:kRestoSandwichesPath]; - }]; - -} - -#pragma mark - RestKit Object loading -- (void)_delayActiveRequestRemoval:(NSString *)resourcePath -{ - // Only clear the request after 10 seconds, when all related requests have - // finished with reasonable certainty, to prevent a request loop when not - // all data requested was found. - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { - [self.activeRequests removeObject:resourcePath]; - }); -} - -@end - -@implementation RestoInfo - -+ (RKObjectMapping *)objectMapping -{ - RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RestoInfo class]]; - [mapping addRelationshipMappingWithSourceKeyPath:@"legend" mapping:[RestoLegendItem objectMapping]]; - [mapping addRelationshipMappingWithSourceKeyPath:@"locations" mapping:[RestoLocation objectMapping]]; - return mapping; -} - -@end diff --git a/Hydra/RestoStore.swift b/Hydra/RestoStore.swift index d083754..4a042c1 100644 --- a/Hydra/RestoStore.swift +++ b/Hydra/RestoStore.swift @@ -7,6 +7,157 @@ // import Foundation +import Alamofire +import ObjectMapper +import AlamofireObjectMapper + +let RestoStoreDidReceiveMenuNotification = "RestoStoreDidReceiveMenuNotification" +let RestoStoreDidUpdateInfoNotification = "RestoStoreDidUpdateInfoNotification" +let RestoStoreDidUpdateSandwichesNotification = "RestoStoreDidUpdateSandwichesNotification" + +typealias RestoMenus = [NSDate: RestoMenu] + +class RestoStore: SavableStore, NSCoding { + + private static var _SharedStore: RestoStore? + static var sharedStore: RestoStore { + get { + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let restoStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.RestoStoreArchive.path!) as? RestoStore + if let restoStore = restoStore { + _SharedStore = restoStore + return _SharedStore! + } + } + // initialize new one + _SharedStore = RestoStore() + return _SharedStore! + } + } + + + private var _locations: [RestoLocation] = [] + var locations: [RestoLocation] { + get { + self.updateLocations() + return self._locations + } + } + private var _sandwiches: [RestoSandwich] = [] + var sandwiches: [RestoSandwich] { + get { + self.updateSandwiches() + return self._sandwiches + } + } + var menus: RestoMenus = [:] + var selectedResto: String = "nl" + + var menusLastUpdated: NSDate? + var locationsLastUpdated: NSDate? + var sandwichesLastUpdated: NSDate? + + + init() { + super.init(storagePath: Config.RestoStoreArchive.path!) + } + + required init?(coder aDecoder: NSCoder) { + guard let locations = aDecoder.decodeObjectForKey(PropertyKey.locationsKey) as? [RestoLocation], + let sandwiches = aDecoder.decodeObjectForKey(PropertyKey.sandwichKey) as? [RestoSandwich], + let menus = aDecoder.decodeObjectForKey(PropertyKey.menusKey) as? RestoMenus, + let selectedResto = aDecoder.decodeObjectForKey(PropertyKey.selectedRestoKey) as? String else { + return nil + } + + self._locations = locations + self._sandwiches = sandwiches + self.menus = menus + self.selectedResto = selectedResto + + self.menusLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.menusLastUpdatedKey) as? NSDate + self.locationsLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.locationLastUpdatedKey) as? NSDate + self.sandwichesLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.sandwichLastUpdatedKey) as? NSDate + + super.init(storagePath: Config.RestoStoreArchive.path!) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(_locations, forKey: PropertyKey.locationsKey) + aCoder.encodeObject(_sandwiches, forKey: PropertyKey.sandwichKey) + aCoder.encodeObject(menus, forKey: PropertyKey.menusKey) + aCoder.encodeObject(selectedResto, forKey: PropertyKey.selectedRestoKey) + aCoder.encodeObject(menusLastUpdated, forKey: PropertyKey.menusLastUpdatedKey) + aCoder.encodeObject(locationsLastUpdated, forKey: PropertyKey.locationLastUpdatedKey) + aCoder.encodeObject(sandwichesLastUpdated, forKey: PropertyKey.sandwichLastUpdatedKey) + } + + func menuForDay(day: NSDate) -> RestoMenu? { + let day = day.dateAtStartOfDay() + + let menu = menus[day] + if let menusLastUpdated = self.menusLastUpdated { + self.updateMenus(menusLastUpdated) + } else { + self.updateMenus(NSDate(), forceUpdate: true) + } + return menu + } + + func updateMenus(lastUpdated: NSDate, forceUpdate: Bool = false) { + let url = APIConfig.Zeus2_0 + "resto/menu/\(self.selectedResto)/overview.json" + + self.updateResource(url, notificationName: RestoStoreDidReceiveMenuNotification, lastUpdated: lastUpdated, forceUpdate: forceUpdate) { (menus: [RestoMenu]) -> Void in + self.menus = [:] // Remove old menus + for menu in menus { + self.menus[menu.date] = menu + } + self.menusLastUpdated = NSDate() + } + } + + func updateLocations() { + let url = APIConfig.Zeus2_0 + "resto/meta.json" + var lastUpdated = NSDate() + var forceUpdate = true + if let locationsLastUpdated = self.locationsLastUpdated { + lastUpdated = locationsLastUpdated + forceUpdate = false + } + self.updateResource(url, notificationName: RestoStoreDidUpdateInfoNotification, lastUpdated: lastUpdated, forceUpdate: forceUpdate, keyPath: "locations") { (locations: [RestoLocation]) -> Void in + self._locations = locations + self.locationsLastUpdated = NSDate() + } + } + + func updateSandwiches() { + let url = APIConfig.Zeus2_0 + "resto/sandwiches.json" + + var lastUpdated = NSDate() + var forceUpdate = true + if let locationsLastUpdated = self.sandwichesLastUpdated { + lastUpdated = locationsLastUpdated + forceUpdate = false + } + self.updateResource(url, notificationName: RestoStoreDidUpdateSandwichesNotification, lastUpdated: lastUpdated, forceUpdate: forceUpdate) { (sandwiches: [RestoSandwich]) -> Void in + self._sandwiches = sandwiches + self.sandwichesLastUpdated = NSDate() + } + + } + + struct PropertyKey { + static let locationsKey = "locations" + static let sandwichKey = "sandwich" + static let menusKey = "menus" + static let selectedRestoKey = "selectedResto" + static let locationLastUpdatedKey = "locationsLastUpdated" + static let sandwichLastUpdatedKey = "sandwichLastUpdated" + static let menusLastUpdatedKey = "menusLastUpdated" + } +} extension RestoStore: FeedItemProtocol { func feedItems() -> [FeedItem] { @@ -15,6 +166,10 @@ extension RestoStore: FeedItemProtocol { day = day.dateByAddingDays(1) } var feedItems = [FeedItem]() + + if !PreferencesService.sharedService.showRestoInFeed { + return feedItems + } // Find the next x days to display while (feedItems.count < 5) { //TODO: replace with var @@ -22,9 +177,7 @@ extension RestoStore: FeedItemProtocol { var menu = menuForDay(day) if (menu == nil) { - menu = RestoMenu() - menu.open = false - menu.day = day + menu = RestoMenu(date: day, open: false) } feedItems.append(FeedItem(itemType: .RestoItem, object: menu, priority: 1000 - 100*feedItems.count)) diff --git a/Hydra/SKOBackViewController.swift b/Hydra/SKOBackViewController.swift new file mode 100644 index 0000000..c9b68a8 --- /dev/null +++ b/Hydra/SKOBackViewController.swift @@ -0,0 +1,36 @@ +// +// SKOBackViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +class SKOBackViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let vc = UIStoryboard(name: "MainStoryboard", bundle: nil).instantiateInitialViewController()! + UIApplication.sharedApplication().windows[0].rootViewController = vc + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/Hydra/SKOHydraTabBarController.swift b/Hydra/SKOHydraTabBarController.swift new file mode 100644 index 0000000..ca76f75 --- /dev/null +++ b/Hydra/SKOHydraTabBarController.swift @@ -0,0 +1,18 @@ +// +// SKOHydraTabBarController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class SKOHydraTabBarController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + + let vc = UIStoryboard(name: "sko", bundle: nil).instantiateInitialViewController()! + UIApplication.sharedApplication().windows[0].rootViewController = vc + } +} \ No newline at end of file diff --git a/Hydra/SKOLineUpCollectionViewCell.swift b/Hydra/SKOLineUpCollectionViewCell.swift new file mode 100644 index 0000000..fb7275b --- /dev/null +++ b/Hydra/SKOLineUpCollectionViewCell.swift @@ -0,0 +1,35 @@ +// +// SKOLineUpCollectionViewCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 10/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +class SKOLineUpCollectionViewCell: UICollectionViewCell { + + @IBOutlet var imageView: UIImageView? + @IBOutlet var artistLabel: UILabel? + @IBOutlet var playTimeLabel: UILabel? + + var artist: Artist? { + didSet { + if let artist = artist { + if let picture = artist.picture { + self.imageView?.sd_setImageWithURL(NSURL(string: picture)) + } else { + self.imageView?.image = nil + } + self.artistLabel?.text = artist.name + + let shortDateFormatter = NSDateFormatter.H_dateFormatterWithAppLocale() + shortDateFormatter.timeStyle = .ShortStyle + shortDateFormatter.dateStyle = .NoStyle + + self.playTimeLabel?.text = "\(shortDateFormatter.stringFromDate(artist.start))-\(shortDateFormatter.stringFromDate(artist.end))" + } + } + } +} diff --git a/Hydra/SKOLineupStageCollectionReusableView.xib b/Hydra/SKOLineupStageCollectionReusableView.xib new file mode 100644 index 0000000..54ba7d1 --- /dev/null +++ b/Hydra/SKOLineupStageCollectionReusableView.xib @@ -0,0 +1,53 @@ + + + + + + + + + LeagueGothic-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hydra/SKOLineupViewController.swift b/Hydra/SKOLineupViewController.swift new file mode 100644 index 0000000..67e3533 --- /dev/null +++ b/Hydra/SKOLineupViewController.swift @@ -0,0 +1,153 @@ +// +// SKOLineupViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import SDWebImage + +class SKOLineupViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + @IBOutlet var collectionView: UICollectionView? + + private var lineup = SKOStore.sharedStore.lineup + private var stageNames = ["Main Stage", "Red Bull Elektropedia presents Decadance"] + + override func viewDidLoad() { + super.viewDidLoad() + + // Uncomment the following line to preserve selection between presentations + // self.clearsSelectionOnViewWillAppear = false + + // Do any additional setup after loading the view. + collectionView?.registerNib(UINib(nibName: "SKOLineupStageCollectionReusableView", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "stageHeader") + + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SKOLineupViewController.reloadLineup), name: SKOStoreLineupUpdatedNotification, object: nil) + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false) + + reloadLineup() + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func reloadLineup() { + lineup = SKOStore.sharedStore.lineup + dispatch_async(dispatch_get_main_queue()) { + self.collectionView?.reloadData() + } + } + + override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) { + // this is called when changing layout :) + self.collectionView?.collectionViewLayout.invalidateLayout() + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + } + */ + + // MARK: UICollectionViewDataSource + + func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { + // #warning Incomplete implementation, return the number of sections + return lineup.count + 1 + } + + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + // #warning Incomplete implementation, return the number of items + if section == 0 { + return 0 + } + return lineup[section-1].artists.count + } + + func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { + if indexPath.section == 0 { + return collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "skoHeader", forIndexPath: indexPath) + } + let stageHeader = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "stageHeader", forIndexPath: indexPath) as! SKOStageHeaderCollectionReusableView + + stageHeader.stageName = stageNames[indexPath.section-1] + + return stageHeader + } + + func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + if section == 0 { + return CGSize(width: self.view.frame.width, height: 170) + } + return CGSize(width: self.view.frame.width, height: 70) + } + + func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("LineUpCell", forIndexPath: indexPath) as! SKOLineUpCollectionViewCell + + // Configure the cell + cell.artist = lineup[indexPath.section-1].artists[indexPath.row] + + return cell + } + + func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { + if self.view.frame.size.width >= 640 { + // make all cards same size for consistency in splitview + return CGSizeMake((self.view.frame.size.width / 2) - 10, 205) + } + + return CGSize(width: self.view.frame.width - 10, height: 205) + } + + // MARK: UICollectionViewDelegate + + /* + // Uncomment this method to specify if the specified item should be highlighted during tracking + override func collectionView(collectionView: UICollectionView, shouldHighlightItemAtIndexPath indexPath: NSIndexPath) -> Bool { + return true + } + */ + + /* + // Uncomment this method to specify if the specified item should be selected + override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool { + return true + } + */ + + /* + // Uncomment these methods to specify if an action menu should be displayed for the specified item, and react to actions performed on the item + override func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool { + return false + } + + override func collectionView(collectionView: UICollectionView, canPerformAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool { + return false + } + + override func collectionView(collectionView: UICollectionView, performAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) { + + } + */ + +} diff --git a/Hydra/SKOMapViewController.swift b/Hydra/SKOMapViewController.swift new file mode 100644 index 0000000..b8c99f6 --- /dev/null +++ b/Hydra/SKOMapViewController.swift @@ -0,0 +1,24 @@ +// +// SKOMapViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class SKOMapViewController: UIViewController { + + @IBOutlet var webView: UIWebView? + + override func viewDidLoad() { + super.viewDidLoad() + + webView?.userInteractionEnabled = true + webView?.backgroundColor = UIColor.SKOBackgroundColor() + + let mapUrl = NSURL(string: APIConfig.Zeus1_0 + "grondplan.html")! + webView?.loadRequest(NSURLRequest(URL: mapUrl, cachePolicy: .ReturnCacheDataElseLoad, timeoutInterval: 3600*24)) + } +} diff --git a/Hydra/SKOStageHeaderCollectionReusableView.swift b/Hydra/SKOStageHeaderCollectionReusableView.swift new file mode 100644 index 0000000..ddd893f --- /dev/null +++ b/Hydra/SKOStageHeaderCollectionReusableView.swift @@ -0,0 +1,18 @@ +// +// SKOStageHeaderCollectionReusableView.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 10/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +class SKOStageHeaderCollectionReusableView: UICollectionReusableView { + + @IBOutlet var label: UILabel? + + var stageName: String? { + didSet { + label?.text = stageName + } + } +} diff --git a/Hydra/SKOStore.swift b/Hydra/SKOStore.swift new file mode 100644 index 0000000..061a579 --- /dev/null +++ b/Hydra/SKOStore.swift @@ -0,0 +1,137 @@ +// +// SKOStore.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 10/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +let SKOStoreLineupUpdatedNotification = "SKOStoreLineupUpdated" +let SKOStoreExihibitorsUpdatedNotification = "SKOStoreExihibitorsUpdated" +let SKOStoreTimelineUpdatedNotification = "SKOStoreTimelineUpdated" +class SKOStore: SavableStore { + + private static var _SharedStore: SKOStore? + static var sharedStore: SKOStore { + get { + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let skoStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.SKOStoreArchive.path!) as? SKOStore + if let skoStore = skoStore { + _SharedStore = skoStore + return skoStore + } + } + // initialize new one + _SharedStore = SKOStore() + return _SharedStore! + } + } + + private var _lineup = [Stage]() + private var lineupLastUpdated = NSDate(timeIntervalSince1970: 0) + var lineup: [Stage] { + get { + updateLineUp() + return _lineup + } + } + + private var _exihibitors = [Exihibitor]() + private var exihibitorsLastUpdated = NSDate(timeIntervalSince1970: 0) + var exihibitors: [Exihibitor] { + get { + updateExihibitors() + return _exihibitors + } + } + + private var _timeline = [TimelinePost]() + private var timelineLastUpdated = NSDate(timeIntervalSince1970: 0) + var timeline: [TimelinePost] { + get { + updateTimeline() + return _timeline + } + } + + init() { + super.init(storagePath: Config.SKOStoreArchive.path!) + } + + required init?(coder aDecoder: NSCoder) { + super.init(storagePath: Config.SKOStoreArchive.path!) + + guard let lineup = aDecoder.decodeObjectForKey(PropertyKey.lineupKey) as? [Stage], + let lineupLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.lineupLastUpdateKey) as? NSDate, + let exihibitors = aDecoder.decodeObjectForKey(PropertyKey.exihibitorsKey) as? [Exihibitor], + let exihibitorsLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.exihibitorsLastUpdatedKey) as? NSDate, + let timeline = aDecoder.decodeObjectForKey(PropertyKey.timelineKey) as? [TimelinePost], + let timelineLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.timelineLastUpdatedKey) as? NSDate + else { + return nil + } + + self._lineup = lineup + self.lineupLastUpdated = lineupLastUpdated + self._exihibitors = exihibitors + self.exihibitorsLastUpdated = exihibitorsLastUpdated + self._timeline = timeline + self.timelineLastUpdated = timelineLastUpdated + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(_lineup, forKey: PropertyKey.lineupKey) + aCoder.encodeObject(lineupLastUpdated, forKey: PropertyKey.lineupLastUpdateKey) + aCoder.encodeObject(_exihibitors, forKey: PropertyKey.exihibitorsKey) + aCoder.encodeObject(exihibitorsLastUpdated, forKey: PropertyKey.exihibitorsLastUpdatedKey) + aCoder.encodeObject(_timeline, forKey: PropertyKey.timelineKey) + aCoder.encodeObject(timelineLastUpdated, forKey: PropertyKey.timelineLastUpdatedKey) + } + + // MARK: Rest functions + func updateLineUp(forced: Bool = false) { + let url = APIConfig.SKO + "lineup.json" + + self.updateResource(url, notificationName: SKOStoreLineupUpdatedNotification, lastUpdated: lineupLastUpdated, forceUpdate: forced) { (lineup: [Stage]) in + debugPrint("SKO Lineup updated") + + self._lineup = lineup + self.lineupLastUpdated = NSDate() + } + } + + func updateExihibitors(forced: Bool = false) { + let url = "http://studentkickoff.be/studentvillage.json" + + self.updateResource(url, notificationName: SKOStoreExihibitorsUpdatedNotification, lastUpdated: exihibitorsLastUpdated, forceUpdate: forced) { (exihibitors: [Exihibitor]) in + debugPrint("SKO Exihibitors") + + self._exihibitors = exihibitors + self.exihibitorsLastUpdated = NSDate() + } + } + + func updateTimeline(forced: Bool = false) { + let url = APIConfig.SKO + "timeline.json" + + self.updateResource(url, notificationName: SKOStoreTimelineUpdatedNotification, lastUpdated: timelineLastUpdated, forceUpdate: forced) { (timeline: [TimelinePost]) in + debugPrint("SKO Timeline") + + self._timeline = timeline + self.timelineLastUpdated = NSDate() + } + } + + struct PropertyKey { + static let lineupKey = "lineup" + static let lineupLastUpdateKey = "lineuplastupdated" + static let exihibitorsKey = "exihibitors" + static let exihibitorsLastUpdatedKey = "exihibitorsLastUpdated" + static let timelineKey = "timeline" + static let timelineLastUpdatedKey = "timelineLastUpdatedKey" + } +} diff --git a/Hydra/SKOStudentVillageDetailViewController.swift b/Hydra/SKOStudentVillageDetailViewController.swift new file mode 100644 index 0000000..de5777a --- /dev/null +++ b/Hydra/SKOStudentVillageDetailViewController.swift @@ -0,0 +1,50 @@ +// +// SKOStudentVillageDetailViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 21/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +class SKOStudentVillageDetailViewController: UIViewController { + + @IBOutlet weak var nameLabel: UILabel? + @IBOutlet weak var imageView: UIImageView? + @IBOutlet weak var contentText: UITextView? + + var exihibitor: Exihibitor? { + didSet { + loadExihibitor() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + loadExihibitor() + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.navigationBarHidden = false + } + + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + + self.navigationController?.navigationBarHidden = true + } + + func loadExihibitor() { + if let exihibitor = exihibitor { + self.nameLabel?.text = exihibitor.name + self.contentText?.text = exihibitor.content + if let url = NSURL(string: exihibitor.logo) { + imageView?.sd_setImageWithURL(url) + } + } + } +} diff --git a/Hydra/SKOStudentVillageTableViewCell.swift b/Hydra/SKOStudentVillageTableViewCell.swift new file mode 100644 index 0000000..ff9152f --- /dev/null +++ b/Hydra/SKOStudentVillageTableViewCell.swift @@ -0,0 +1,27 @@ +// +// SKOStudentVillageTableViewCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 11/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +class SKOStudentVillageTableViewCell: UITableViewCell { + @IBOutlet var nameLabel: UILabel? + @IBOutlet var logoView: UIImageView? + @IBOutlet var contentLabel: UILabel? + @IBOutlet var boothLabel: UILabel? + @IBOutlet var categoryLabel: UILabel? + + var exihibitor: Exihibitor? { + didSet { + if let exihibitor = exihibitor { + nameLabel?.text = exihibitor.name + if let url = NSURL(string: exihibitor.logo) { + logoView?.sd_setImageWithURL(url) + } + contentLabel?.text = exihibitor.content + } + } + } +} diff --git a/Hydra/SKOStudentVillageViewController.swift b/Hydra/SKOStudentVillageViewController.swift new file mode 100644 index 0000000..7f5865b --- /dev/null +++ b/Hydra/SKOStudentVillageViewController.swift @@ -0,0 +1,130 @@ +// +// SKOStudentVillageViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + + +class SKOStudentVillageViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchControllerDelegate, UISearchResultsUpdating { + + @IBOutlet var tableView: UITableView? + + var searchController: UISearchController? + + var exihibitors = [Exihibitor]() + var oldExihibitors: [Exihibitor]? + + var previousSearchLength = 0 + + override func viewDidLoad() { + super.viewDidLoad() + + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SKOStudentVillageViewController.reloadExihibitors), name: SKOStoreExihibitorsUpdatedNotification, object: nil) + exihibitors = SKOStore.sharedStore.exihibitors + + searchController = UISearchController(searchResultsController: nil) + searchController?.searchResultsUpdater = self + searchController?.delegate = self + searchController?.dimsBackgroundDuringPresentation = false + + let searchBar = searchController!.searchBar + searchBar.barTintColor = UIColor.SKOBackgroundColor() + self.tableView?.tableHeaderView = searchBar + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + func reloadExihibitors() { + exihibitors = SKOStore.sharedStore.exihibitors + dispatch_async(dispatch_get_main_queue()) { + self.tableView?.reloadData() + } + } + + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + + if let searchController = self.searchController where searchController.active { + self.searchController?.active = false + } + } + + // MARK: Searchbar + func updateSearchResultsForSearchController(searchController: UISearchController) { + let searchString = searchController.searchBar.text?.lowercaseString + if let searchString = searchString { + let searchLength = searchString.characters.count + if searchLength == 0 { + self.exihibitors = oldExihibitors! + self.previousSearchLength = 0 + } + else { + if previousSearchLength >= searchLength { + self.exihibitors = oldExihibitors! + } + self.previousSearchLength = searchLength + + exihibitors = exihibitors.filter({ (exi) -> Bool in + return exi.name.lowercaseString.rangeOfString(searchString) != nil + }) + } + } + + self.tableView?.reloadData() + } + + func willPresentSearchController(searchController: UISearchController) { + oldExihibitors = exihibitors + } + + func willDismissSearchController(searchController: UISearchController) { + exihibitors = oldExihibitors! + previousSearchLength = 0 + } + + // MARK: UITableview DataSource methods + + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return exihibitors.count + } + + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("studentVillageCell") as! SKOStudentVillageTableViewCell + + cell.exihibitor = exihibitors[indexPath.row] + + return cell + } + + func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } + + // MARK: UITableView Delegate methods + func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + return 88 + } + + func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let exihibitor = exihibitors[indexPath.row] + + self.performSegueWithIdentifier("skoStudentVillageDetailSegue", sender: exihibitor) + } + + // MARK: Storyboard segue + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if let identifier = segue.identifier where identifier == "skoStudentVillageDetailSegue" { + guard let exihibitor = sender as? Exihibitor, + let vc = segue.destinationViewController as? SKOStudentVillageDetailViewController + else { return } + + vc.exihibitor = exihibitor + } + } +} \ No newline at end of file diff --git a/Hydra/SKOTimelineCollectionViewCell.swift b/Hydra/SKOTimelineCollectionViewCell.swift new file mode 100644 index 0000000..0c36cc0 --- /dev/null +++ b/Hydra/SKOTimelineCollectionViewCell.swift @@ -0,0 +1,70 @@ +// +// SKOTimelineCollectionViewCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 16/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import FontAwesome_swift + +class SKOTimelineCollectionViewCell: UICollectionViewCell { + @IBOutlet var titleLabel: UILabel? + @IBOutlet var dateLabel: UILabel? + @IBOutlet var bodyText: UITextView? + @IBOutlet var imageView: UIImageView? + @IBOutlet var socialNetwork: UIImageView? + @IBOutlet var imageHeightLarge: NSLayoutConstraint? + @IBOutlet var imageHeightHidden: NSLayoutConstraint? + + var timelinePost: TimelinePost? { + didSet { + if let post = timelinePost { + titleLabel?.text = post.title + dateLabel?.text = SORelativeDateTransformer().transformedValue(post.date) as? String + bodyText?.text = post.body + if let media = post.media, let url = NSURL(string: media) where post.postType == .Photo { + imageView?.sd_setImageWithURL(url) + showImageView() + } else { + hideImageView() + } + + if let poster = post.poster, let url = NSURL(string: poster) { + imageView?.sd_setImageWithURL(url) + showImageView() + } else if let media = post.media, let url = NSURL(string: media) { + imageView?.sd_setImageWithURL(url) + showImageView() + } else { + hideImageView() + } + + if let socialNetwork = socialNetwork { + switch post.origin { + case .Facebook: + socialNetwork.image = UIImage.fontAwesomeIconWithName(.Facebook, textColor: UIColor.blackColor(), size: socialNetwork.frame.size) + case .Instagram: + socialNetwork.image = UIImage.fontAwesomeIconWithName(.Instagram, textColor: UIColor.blackColor(), size: socialNetwork.frame.size) + default: + socialNetwork.image = nil + } + } + } + } + } + + func hideImageView() { + imageView?.hidden = true + imageHeightLarge?.active = false + imageHeightHidden?.active = true + self.layoutIfNeeded() + } + + func showImageView() { + imageView?.hidden = false + imageHeightLarge?.active = true + imageHeightHidden?.active = false + self.layoutIfNeeded() + } +} \ No newline at end of file diff --git a/Hydra/SKOTimelineCollectionViewController.swift b/Hydra/SKOTimelineCollectionViewController.swift new file mode 100644 index 0000000..8cfaee4 --- /dev/null +++ b/Hydra/SKOTimelineCollectionViewController.swift @@ -0,0 +1,119 @@ +// +// SKOTimelineCollectionViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 09/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +class SKOTimelineCollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { + + @IBOutlet var collectionView: UICollectionView? + + var timeline = [TimelinePost]() + + override func viewDidLoad() { + super.viewDidLoad() + + timeline = SKOStore.sharedStore.timeline + + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SKOTimelineCollectionViewController.reloadTimeline), name: SKOStoreTimelineUpdatedNotification, object: nil) + } + + deinit { + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: false) + + reloadTimeline() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func reloadTimeline() { + timeline = SKOStore.sharedStore.timeline + dispatch_async(dispatch_get_main_queue()) { + self.collectionView?.reloadData() + } + } + + override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) { + // this is called when changing layout :) + self.collectionView?.collectionViewLayout.invalidateLayout() + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + } + */ + + // MARK: UICollectionViewDataSource + + func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { + return 1 + } + + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return timeline.count + } + + func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView { + return collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "skoHeader", forIndexPath: indexPath) + } + + func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + let post = timeline[indexPath.row] + let identifier: String = "Cell" + let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath) as! SKOTimelineCollectionViewCell + + // Configure the cell + cell.timelinePost = post + + return cell + } + + func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { + let post = timeline[indexPath.row] + let width: CGFloat + if self.view.frame.size.width >= 640 { + // make all cards same size for consistency in splitview + width = (self.view.frame.size.width / 2) - 10 + } else { + width = self.view.frame.width - 10 + } + var height: CGFloat = 90 + + if post.media != nil || post.poster != nil { + height = height + 180 //TODO: find a way to guess the image size + } + if let body = post.body { + // limit on 1500 + height = height + body.boundingHeight(CGSize(width: width - 30, height: 1500), font: UIFont.systemFontOfSize(14)) + 10 + } + + return CGSize(width: width, height: height) + } + + // MARK: UICollectionViewDelegate + func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { + let post = timeline[indexPath.row] + if let link = post.link, let url = NSURL(string: link) { + UIApplication.sharedApplication().openURL(url) + } + } +} diff --git a/Hydra/SavableStore.swift b/Hydra/SavableStore.swift new file mode 100644 index 0000000..d4c652b --- /dev/null +++ b/Hydra/SavableStore.swift @@ -0,0 +1,158 @@ +// +// SavableStore.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 28/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper +import Alamofire +import AlamofireObjectMapper + +let TIME_BETWEEN_REFRESH: NSTimeInterval = 60 * 15 + +class SavableStore: NSObject { + + let storagePath: String + + var storageOutdated = false + + var currentRequests = Set() + + func markStorageOutdated() { + storageOutdated = true + } + + func syncStorage() { + if !self.storageOutdated { + return + } + + // Immediately mark the cache as being updated, as this is an async operation + self.storageOutdated = false + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + let isSuccesfulSave = NSKeyedArchiver.archiveRootObject(self, toFile: self.storagePath) + + if !isSuccesfulSave { + print("Saving the object failed") + } + } + } + + init(storagePath: String) { + self.storagePath = storagePath + } + + func doLater(timeSec: Int = 1, function: (()->Void)) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(timeSec)*Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + function() + } + + } + + // For array based objects + internal func updateResource(resource: String, notificationName: String, lastUpdated: NSDate, forceUpdate: Bool, keyPath: String? = nil, oauth: Bool = false, completionHandler: ([T]-> Void)) { + if lastUpdated.timeIntervalSinceNow > -TIME_BETWEEN_REFRESH && !forceUpdate { + return + } + + if oauth && !UGentOAuth2Service.sharedService.isLoggedIn() { + print("Request \(resource): cannot be executed because the user is not logged in") + return + } + + objc_sync_enter(currentRequests) + if currentRequests.contains(resource) { + return + } + currentRequests.insert(resource) + objc_sync_exit(currentRequests) + + let request: Alamofire.Request + if !oauth { + request = Alamofire.request(.GET, resource) + } else { + request = UGentOAuth2Service.sharedService.oauth2.request(.GET, resource) + } + + request.responseArray(queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), keyPath: keyPath) { (response: Response<[T], NSError>) -> Void in + if let value = response.result.value where response.result.isSuccess { + completionHandler(value) + self.markStorageOutdated() + self.syncStorage() + } else { + //TODO: Handle error + print("Request array \(resource) errored \(response.data?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.EncodingEndLineWithLineFeed))") + self.handleError(response.result.error!, request: resource) + } + self.postNotification(notificationName) + self.doLater(function: { () -> Void in + objc_sync_enter(self.currentRequests) + if self.currentRequests.contains(resource) { + self.currentRequests.remove(resource) + } + objc_sync_exit(self.currentRequests) + }) + } + + } + + internal func updateResource(resource: String, notificationName: String, lastUpdated: NSDate, forceUpdate: Bool, oauth: Bool = false, completionHandler: (T-> Void)) { + if lastUpdated.timeIntervalSinceNow > -TIME_BETWEEN_REFRESH && !forceUpdate { + return + } + + if currentRequests.contains(resource) { + return + } + currentRequests.insert(resource) + let request: Alamofire.Request + if !oauth { + request = Alamofire.request(.GET, resource) + } else { + request = UGentOAuth2Service.sharedService.oauth2.request(.GET, resource) + } + + request.responseObject { (response: Response) in + if let value = response.result.value where response.result.isSuccess { + completionHandler(value) + self.markStorageOutdated() + self.syncStorage() + } else { + //TODO: Handle error + print("Request object \(resource) errored") + self.handleError(response.result.error!, request: resource) + } + self.postNotification(notificationName) + self.doLater(function: { () -> Void in + if self.currentRequests.contains(resource) { + self.currentRequests.remove(resource) + } + }) + } + } + + + func saveLater(timeSec: Int = 10) { + self.markStorageOutdated() + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(timeSec)*Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + self.syncStorage() + } + } + + func postNotification(notificationName: String) { + let center = NSNotificationCenter.defaultCenter() + center.postNotificationName(notificationName, object: self) + } + + func handleError(error: NSError?, request: String) { + print("Error \(request): \(error?.localizedDescription)") + dispatch_async(dispatch_get_main_queue()) { + let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate + appDelegate.handleError(error) + } + } +} \ No newline at end of file diff --git a/Hydra/SchamperArticle.h b/Hydra/SchamperArticle.h deleted file mode 100644 index 446bf75..0000000 --- a/Hydra/SchamperArticle.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// SchamperArticle.h -// Hydra -// -// Created by Pieter De Baets on 29/06/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -@class RKObjectMapping; - -@interface SchamperArticle : NSObject - -@property (nonatomic, strong) NSString *title; -@property (nonatomic, strong) NSString *link; -@property (nonatomic, strong) NSDate *date; -@property (nonatomic, strong) NSString *author; -@property (nonatomic, strong) NSString *body; -@property (nonatomic, assign) BOOL read; - -+ (RKObjectMapping *)objectMapping; - -@end diff --git a/Hydra/SchamperArticle.m b/Hydra/SchamperArticle.m deleted file mode 100644 index 505fe28..0000000 --- a/Hydra/SchamperArticle.m +++ /dev/null @@ -1,74 +0,0 @@ -// -// SchamperArticle.m -// Hydra -// -// Created by Pieter De Baets on 29/06/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "SchamperArticle.h" -#import "SchamperStore.h" -#import -#import - -@implementation SchamperArticle - -- (NSString *)description -{ - return [NSString stringWithFormat:@"", self.title, self.date]; -} - -- (id)initWithCoder:(NSCoder *)decoder -{ - if (self = [super init]) { - _title = [decoder decodeObjectForKey:@"title"]; - _link = [decoder decodeObjectForKey:@"link"]; - _date = [decoder decodeObjectForKey:@"date"]; - _author = [decoder decodeObjectForKey:@"author"]; - _body = [decoder decodeObjectForKey:@"body"]; - _read = [decoder decodeBoolForKey:@"read"]; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.title forKey:@"title"]; - [coder encodeObject:self.link forKey:@"link"]; - [coder encodeObject:self.date forKey:@"date"]; - [coder encodeObject:self.author forKey:@"author"]; - [coder encodeObject:self.body forKey:@"body"]; - [coder encodeBool:self.read forKey:@"read"]; -} - -+ (RKObjectMapping *)objectMapping -{ - // Register rss+xml MIME-type - [RKMIMETypeSerialization registerClass:[RKXMLReaderSerialization class] forMIMEType:@"application/rss+xml"]; - - // Create mapping - RKObjectMapping *mapping = [RKObjectMapping mappingForClass:self]; - [mapping setForceCollectionMapping:YES]; - - [mapping addAttributeMappingsFromDictionary:@{ - @"title.text": @"title", - @"link.text": @"link", - @"pubDate.text": @"date", - @"dc:creator.text": @"author", - @"description.text": @"body" - }]; - - return mapping; -} - -#pragma mark - Properties - -- (void)setRead:(BOOL)read -{ - if (read != _read) { - _read = read; - [[SchamperStore sharedStore] markStorageOutdated]; - } -} - -@end diff --git a/Hydra/SchamperArticle.swift b/Hydra/SchamperArticle.swift new file mode 100644 index 0000000..ecf5268 --- /dev/null +++ b/Hydra/SchamperArticle.swift @@ -0,0 +1,96 @@ +// +// SchamperArticle.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 28/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper +class SchamperArticle: NSObject, NSCoding, Mappable { + + // MARK: Properties + var title: String + var link: String + var date: NSDate + var author: String? + var body: String + var image: String? + var category: String? + var read: Bool = false + + convenience override init() { + self.init(title: "", link: "", date: NSDate(), author: nil, body: "", image: nil) + } + + init(title: String, link: String, date: NSDate, author: String?, body: String, image: String?, category: String? = nil, read: Bool = false) { + self.title = title + self.link = link + self.date = date + self.author = author + self.body = body + self.image = image + self.read = read + self.category = category + } + + required convenience init?(_ map: Map) { + self.init() + } + + override var description: String { + get { + return "SchamperArticle: \(self.title)" + } + } + + func mapping(map: Map) { + self.title <- map[PropertyKey.titleKey] + self.link <- map[PropertyKey.linkKey] + self.date <- (map["pub_date"], ISO8601DateTransform()) + self.author <- map[PropertyKey.authorKey] + self.body <- map["text"] + self.image <- map[PropertyKey.imageKey] + self.category <- map[PropertyKey.categoryKey] + self.read <- map[PropertyKey.readKey] + } + + // MARK: NSCoding Protocol + required convenience init?(coder aDecoder: NSCoder) { + guard let title = aDecoder.decodeObjectForKey(PropertyKey.titleKey) as? String, + let link = aDecoder.decodeObjectForKey(PropertyKey.linkKey) as? String, + let date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as? NSDate, + let body = aDecoder.decodeObjectForKey(PropertyKey.bodyKey) as? String, + let read = aDecoder.decodeObjectForKey(PropertyKey.readKey) as? Bool + else {return nil} + + let author = aDecoder.decodeObjectForKey(PropertyKey.authorKey) as? String + let image = aDecoder.decodeObjectForKey(PropertyKey.imageKey) as? String + let category = aDecoder.decodeObjectForKey(PropertyKey.categoryKey) as? String + + self.init(title: title, link: link, date: date, author: author, body: body, image: image, category: category, read: read) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(title, forKey: PropertyKey.titleKey) + aCoder.encodeObject(link, forKey: PropertyKey.linkKey) + aCoder.encodeObject(date, forKey: PropertyKey.dateKey) + aCoder.encodeObject(author, forKey: PropertyKey.authorKey) + aCoder.encodeObject(body, forKey: PropertyKey.bodyKey) + aCoder.encodeObject(image, forKey: PropertyKey.imageKey) + aCoder.encodeObject(category, forKey: PropertyKey.categoryKey) + aCoder.encodeObject(read, forKey: PropertyKey.readKey) + } + + struct PropertyKey { + static let titleKey = "title" + static let linkKey = "link" + static let dateKey = "date" + static let authorKey = "author" + static let bodyKey = "body" + static let imageKey = "image" + static let categoryKey = "category" + static let readKey = "read" + } +} \ No newline at end of file diff --git a/Hydra/SchamperDetailViewController.h b/Hydra/SchamperDetailViewController.h index a2a625b..695fb58 100644 --- a/Hydra/SchamperDetailViewController.h +++ b/Hydra/SchamperDetailViewController.h @@ -7,7 +7,8 @@ // #import "WebViewController.h" -#import "SchamperArticle.h" + +@class SchamperArticle; @interface SchamperDetailViewController : WebViewController diff --git a/Hydra/SchamperDetailViewController.m b/Hydra/SchamperDetailViewController.m index 6d764f5..a65cc35 100644 --- a/Hydra/SchamperDetailViewController.m +++ b/Hydra/SchamperDetailViewController.m @@ -8,8 +8,8 @@ #import "SchamperDetailViewController.h" #import "NSDateFormatter+AppLocale.h" -#import -#import +#import "Hydra-Swift.h" +#import "TUSafariActivity.h" @interface SchamperDetailViewController () @@ -78,11 +78,13 @@ - (void)viewDidLoad - (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; self.navigationController.navigationBar.translucent = YES; } - (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; self.navigationController.navigationBar.translucent = NO; } @@ -100,26 +102,12 @@ - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *) - (void)shareButtonTapped:(id)sender { - // Available since iOS6 - if ([UIActivityViewController class]) { - NSArray *items = @[ self.article.title, [NSURL URLWithString:self.article.link] ]; - NSArray *activities = @[ [[TUSafariActivity alloc] init] ]; - - UIActivityViewController *c = [[UIActivityViewController alloc] initWithActivityItems:items - applicationActivities:activities]; - [self presentViewController:c animated:YES completion:NULL]; - } - else { - // Create the item to share - NSURL *link = [NSURL URLWithString:self.article.link]; - SHKItem *item = [SHKItem URL:link title:self.article.title contentType:SHKShareTypeURL]; - - // Get the ShareKit action sheet - SHKActionSheet *actionSheet = [SHKActionSheet actionSheetForItem:item]; + NSArray *items = @[ self.article.title, [NSURL URLWithString:self.article.link] ]; + NSArray *activities = @[ [[TUSafariActivity alloc] init] ]; - // Display the action sheet - [actionSheet showFromToolbar:self.navigationController.toolbar]; - } + UIActivityViewController *c = [[UIActivityViewController alloc] initWithActivityItems:items + applicationActivities:activities]; + [self presentViewController:c animated:YES completion:NULL]; } #pragma mark - Gesture recognizer @@ -184,7 +172,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView return; } // Check if scrolling at high enough speed - else if (scrollView.tracking && abs(differenceFromLast) > 1) { + else if (scrollView.tracking && fabs(differenceFromLast) > 1) { [self setNavigationBarHidden:(differenceFromStart < 0)]; } } diff --git a/Hydra/SchamperStore.h b/Hydra/SchamperStore.h deleted file mode 100644 index f633ed3..0000000 --- a/Hydra/SchamperStore.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// SchamperStore.h -// Hydra -// -// Created by Pieter De Baets on 17/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import - -extern NSString *const SchamperStoreDidUpdateArticlesNotification; - -@interface SchamperStore : NSObject - -@property (nonatomic, strong, readonly) NSArray *articles; - -+ (SchamperStore *)sharedStore; -- (void)updateArticles; -- (void)reloadArticles; - -- (void)syncStorage; -- (void)markStorageOutdated; - -@end \ No newline at end of file diff --git a/Hydra/SchamperStore.m b/Hydra/SchamperStore.m deleted file mode 100644 index ed0ea17..0000000 --- a/Hydra/SchamperStore.m +++ /dev/null @@ -1,179 +0,0 @@ -// -// SchamperStore.m -// Hydra -// -// Created by Pieter De Baets on 17/07/12. -// Copyright (c) 2012 Zeus WPI. All rights reserved. -// - -#import "SchamperStore.h" -#import "SchamperArticle.h" -#import "AppDelegate.h" - -#import -#import - -#define kSchamperBaseUrl @"https://zeus.ugent.be/hydra/api/1.0/schamper/" -#define kSchamperDailyUrl @"daily.xml" -NSString *const SchamperStoreDidUpdateArticlesNotification = - @"SchamperStoreDidUpdateArticlesNotification"; - -@interface SchamperStore () - -@property (nonatomic, strong) RKObjectManager *objectManager; -@property (nonatomic, assign) BOOL active; -@property (nonatomic, strong) NSArray *articles; -@property (nonatomic, strong) NSDate *lastUpdated; -@property (nonatomic, assign) BOOL storageOutdated; - -@end - -@implementation SchamperStore - -+ (SchamperStore *)sharedStore -{ - static SchamperStore *sharedInstance = nil; - if (!sharedInstance) { - // Try restoring the store from archive - @try { - sharedInstance = [NSKeyedUnarchiver unarchiveObjectWithFile:self.articleCachePath]; - } - @catch (NSException *exception) { - NSLog(@"Got exception while reading Schamper archive: %@", exception); - } - @finally { - if (!sharedInstance) sharedInstance = [[SchamperStore alloc] init]; - } - } - return sharedInstance; -} - -- (id)init -{ - if (self = [super init]) { - self.articles = [[NSArray alloc] init]; - self.lastUpdated = [NSDate date]; - self.active = false; - } - return self; -} - -#pragma mark - Caching - -- (id)initWithCoder:(NSCoder *)decoder -{ - if (self = [super init]) { - self.articles = [decoder decodeObjectForKey:@"articles"]; - AssertClassOrNil(self.articles, NSArray); - self.lastUpdated = [decoder decodeObjectForKey:@"lastUpdated"]; - AssertClassOrNil(self.lastUpdated, NSDate); - self.active = false; - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder -{ - [coder encodeObject:self.articles forKey:@"articles"]; - [coder encodeObject:self.lastUpdated forKey:@"lastUpdated"]; -} - -+ (NSString *)articleCachePath -{ - // Get cache directory - NSArray *cacheDirectories = - NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString *cacheDirectory = cacheDirectories[0]; - - return [cacheDirectory stringByAppendingPathComponent:@"schamper.archive"]; -} - -- (void)reloadArticles -{ - [[NSURLCache sharedURLCache] removeAllCachedResponses]; - [self updateArticles]; -} - -- (void)syncStorage -{ - if (!self.storageOutdated) { - return; - } - - // Immediately mark the cache as being updated, as this is an async operation - self.storageOutdated = NO; - - dispatch_queue_t async = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); - dispatch_async(async, ^{ - [NSKeyedArchiver archiveRootObject:self toFile:self.class.articleCachePath]; - }); -} - -- (void)markStorageOutdated -{ - self.storageOutdated = YES; -} - -#pragma mark - Article fetching - -- (void)updateArticles -{ - // Only allow one request at a time - if (self.active) return; - DLog(@"Starting Schamper update"); - - // The RKObjectManager must be retained, otherwise reachability notifications - // will not be received properly and all kinds of weird stuff happen - if (!self.objectManager) { - self.objectManager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:kSchamperBaseUrl]]; - } - - [self.objectManager addResponseDescriptor: - [RKResponseDescriptor responseDescriptorWithMapping:[SchamperArticle objectMapping] - method:RKRequestMethodGET - pathPattern:kSchamperDailyUrl - keyPath:@"rss.channel.item" - statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]; - [self.objectManager getObjectsAtPath:kSchamperDailyUrl - parameters:nil - success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { - [self _processResult:mappingResult]; - } - failure:^(RKObjectRequestOperation *operation, NSError *error) { - NSLog(@"Updating schamper failed: %@", error); - AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate]; - [app handleError:error]; - - [self _processResult:nil]; - }]; -} - -- (void)_processResult:(RKMappingResult *)mappingResult -{ - - NSArray *objects = [mappingResult array]; - if (objects.count > 0) { - NSMutableSet *set = [NSMutableSet set]; - for (SchamperArticle *article in self.articles) { - if (article.read) { - [set addObject:article.link]; - } - } - for (SchamperArticle *article in objects) { - if ([set containsObject:article.link]) { - article.read = YES; - } - } - self.articles = objects; - self.lastUpdated = [NSDate date]; - self.active = NO; - - [self markStorageOutdated]; - [self syncStorage]; - } - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center postNotificationName:SchamperStoreDidUpdateArticlesNotification object:self]; -} - -@end diff --git a/Hydra/SchamperStore.swift b/Hydra/SchamperStore.swift index 1ac703b..80df484 100644 --- a/Hydra/SchamperStore.swift +++ b/Hydra/SchamperStore.swift @@ -1,30 +1,115 @@ // // SchamperStore.swift -// Hydra +// OAuthTest // -// Created by Feliciaan De Palmenaer on 20/11/2015. -// Copyright © 2015 Zeus WPI. All rights reserved. +// Created by Feliciaan De Palmenaer on 28/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. // import Foundation +import Alamofire +import AlamofireObjectMapper +import SWXMLHash +let SchamperStoreDidUpdateArticlesNotification = "SchamperStoreDidUpdateArticlesNotification" + +class SchamperStore: SavableStore { + + private static var _SharedStore: SchamperStore? + static var sharedStore: SchamperStore { + get { + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let schamperStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.SchamperStoreArchive.path!) as? SchamperStore + if let schamperStore = schamperStore { + _SharedStore = schamperStore + return schamperStore + } + } + // initialize new one + _SharedStore = SchamperStore() + return _SharedStore! + } + } + + var articles: [SchamperArticle] = [] + var lastUpdated: NSDate = NSDate(timeIntervalSince1970: 0) + + init() { + super.init(storagePath: Config.SchamperStoreArchive.path!) + } + + //MARK: NSCoding Protocol + required init?(coder aDecoder: NSCoder) { + super.init(storagePath: Config.SchamperStoreArchive.path!) + guard let articles = aDecoder.decodeObjectForKey(PropertyKey.articlesKey) as? [SchamperArticle], + let lastUpdated = aDecoder.decodeObjectForKey(PropertyKey.lastUpdatedKey) as? NSDate else { + return nil + } + + self.articles = articles + self.lastUpdated = lastUpdated + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(articles, forKey: PropertyKey.articlesKey) + aCoder.encodeObject(lastUpdated, forKey: PropertyKey.lastUpdatedKey) + } + + // MARK: Store functions + // Force reload all articles + func reloadArticles() { + //NSURLCache.sharedURLCache().removeAllCachedResponses() + self.updateArticles(true) + } + + func updateArticles(forceUpdate: Bool = false) { + print("Updating Schamper Articles") + + let url = APIConfig.Zeus1_0 + "schamper/daily.json" + + self.updateResource(url, notificationName: SchamperStoreDidUpdateArticlesNotification, lastUpdated: self.lastUpdated, forceUpdate: forceUpdate) { (articles: [SchamperArticle]) in + print("Updating Schamper articles") + let readArticles = Set(self.articles.filter({ $0.read }).map({ $0.title})) + + for article in articles { + article.read = readArticles.contains(article.title) + } + + self.articles = articles + self.lastUpdated = NSDate() + } + + } + + struct PropertyKey { + static let articlesKey = "articles" + static let lastUpdatedKey = "lastUpdated" + } +} + +// MARK: Implement FeedItemProtocol extension SchamperStore: FeedItemProtocol { func feedItems() -> [FeedItem] { var feedItems = [FeedItem]() - if let articles = articles as? [SchamperArticle] { - for article in articles { //TODO: test articles and sort them - let daysOld = article.date.daysBeforeDate(NSDate()) - var priority = 999 - if !article.read { - priority = priority - daysOld*40 - } else { - priority = priority - daysOld*150 - } - if priority > 0 { - feedItems.append(FeedItem(itemType: .SchamperNewsItem, object: article, priority: priority)) - } + + if !PreferencesService.sharedService.showSchamperInFeed { + return feedItems + } + + for article in articles { //TODO: test articles and sort them + let daysOld = article.date.daysBeforeDate(NSDate()) + var priority = 999 + if !article.read { + priority = priority - daysOld*40 + } else { + priority = priority - daysOld*150 + } + if priority > 0 { + feedItems.append(FeedItem(itemType: .SchamperNewsItem, object: article, priority: priority)) } } return feedItems } -} \ No newline at end of file +} diff --git a/Hydra/SchamperViewController.m b/Hydra/SchamperViewController.m index c967a66..52eb7ee 100644 --- a/Hydra/SchamperViewController.m +++ b/Hydra/SchamperViewController.m @@ -7,11 +7,10 @@ // #import "SchamperViewController.h" -#import "SchamperStore.h" -#import "SchamperArticle.h" #import "SchamperDetailViewController.h" #import "SORelativeDateTransformer.h" #import +#import "Hydra-Swift.h" @interface SchamperViewController () @@ -29,9 +28,9 @@ - (id)init // Check for updates NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(articlesUpdated:) - name:SchamperStoreDidUpdateArticlesNotification + name:@"SchamperStoreDidUpdateArticlesNotification" object:nil]; - [[SchamperStore sharedStore] updateArticles]; + [[SchamperStore sharedStore] updateArticles:NO]; } return self; } @@ -47,10 +46,6 @@ - (void)viewDidLoad // Set title in navigation bar, slightly different title on return button self.title = @"Schamper Daily"; - UIBarButtonItem *bb = [[UIBarButtonItem alloc] initWithTitle:@"Schamper" - style:UIBarButtonItemStyleBordered - target:nil action:nil]; - [self.navigationItem setBackBarButtonItem:bb]; if ([UIRefreshControl class]) { UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; diff --git a/Hydra/ShareKitConfigurator.h b/Hydra/ShareKitConfigurator.h deleted file mode 100644 index 457f93f..0000000 --- a/Hydra/ShareKitConfigurator.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// ShareKitConfigurator.h -// Hydra -// -// Created by Pieter De Baets on 14/01/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import -#import - -@interface ShareKitConfigurator : DefaultSHKConfigurator - -@end diff --git a/Hydra/ShareKitConfigurator.m b/Hydra/ShareKitConfigurator.m deleted file mode 100644 index 09883ce..0000000 --- a/Hydra/ShareKitConfigurator.m +++ /dev/null @@ -1,69 +0,0 @@ -// -// ShareKitConfigurator.m -// Hydra -// -// Created by Pieter De Baets on 14/01/13. -// Copyright (c) 2013 Zeus WPI. All rights reserved. -// - -#import "ShareKitConfigurator.h" - -#define TEST_OLD_SHARERS 0 - -@implementation ShareKitConfigurator - -- (NSString *)appName -{ - return @"Hydra"; -} - -- (NSString *)appURL -{ - return @"http://student.ugent.be/hydra"; -} - -- (NSString *)facebookAppId -{ - return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"FacebookAppID"]; -} - -- (NSString *)twitterConsumerKey -{ - return @"WHwijVLkJD5PDIqATzEmhQ"; -} - -- (NSString *)twitterSecret -{ - return @"9oPVJG4VN58mymrHuqCyjctEW30k96bJJo5OgGqPQjw"; -} - -- (NSString *)twitterCallbackUrl -{ - return @"http://student.ugent.be/hydra/twitter_callback"; -} - -- (UIColor *)barTintForView:(UIViewController*)vc -{ - return [UIColor hydraTintColor]; -} - -- (NSArray*)defaultFavoriteURLSharers -{ - return [NSArray arrayWithObjects:@"SHKTwitter",@"SHKFacebook", @"SHKSafari", nil]; -} - -#if TEST_OLD_SHARERS - -- (NSNumber*)forcePreIOS6FacebookPosting -{ - return [NSNumber numberWithBool:true]; -} - -- (NSNumber*)forcePreIOS5TwitterAccess -{ - return [NSNumber numberWithBool:true]; -} - -#endif - -@end diff --git a/Hydra/SpecialEvent.swift b/Hydra/SpecialEvent.swift new file mode 100644 index 0000000..bf31074 --- /dev/null +++ b/Hydra/SpecialEvent.swift @@ -0,0 +1,97 @@ +// +// SpecialEvent.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 04/04/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class SpecialEvent: NSObject, NSCoding, Mappable { + + var name: String + var link: String + var simpleText: String + var image: String + var priority: Int + var start: NSDate + var end: NSDate + var html: String? + var development: Bool + + required init(name: String, link: String, simpleText: String, image: String, priority: Int, start: NSDate, end: NSDate, development: Bool, html: String? = nil) { + self.name = name + self.link = link + self.simpleText = simpleText + self.image = image + self.priority = priority + self.start = start + self.end = end + self.development = development + self.html = html + } + + //MARK: NSCoding + required convenience init?(coder aDecoder: NSCoder) { + guard let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as? String, + let link = aDecoder.decodeObjectForKey(PropertyKey.linkKey) as? String, + let simpleText = aDecoder.decodeObjectForKey(PropertyKey.simpleTextKey) as? String, + let image = aDecoder.decodeObjectForKey(PropertyKey.imageKey) as? String, + let priority = aDecoder.decodeObjectForKey(PropertyKey.priorityKey) as? Int, + let start = aDecoder.decodeObjectForKey(PropertyKey.startKey) as? NSDate, + let end = aDecoder.decodeObjectForKey(PropertyKey.endKey) as? NSDate, + let develoment = aDecoder.decodeObjectForKey(PropertyKey.developmentKey) as? Bool + else { + return nil + } + + let html = aDecoder.decodeObjectForKey(PropertyKey.htmlKey) as? String + + self.init(name: name, link: link, simpleText: simpleText, image: image, priority: priority, start: start, end: end, development: develoment, html: html) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(name, forKey: PropertyKey.nameKey) + aCoder.encodeObject(link, forKey: PropertyKey.linkKey) + aCoder.encodeObject(simpleText, forKey: PropertyKey.simpleTextKey) + aCoder.encodeObject(image, forKey: PropertyKey.imageKey) + aCoder.encodeObject(priority, forKey: PropertyKey.priorityKey) + aCoder.encodeObject(start, forKey: PropertyKey.startKey) + aCoder.encodeObject(end, forKey: PropertyKey.endKey) + aCoder.encodeObject(development, forKey: PropertyKey.developmentKey) + aCoder.encodeObject(html, forKey: PropertyKey.htmlKey) + } + + //MARK: Mapping + required convenience init?(_ map: Map) { + self.init(name: "", link: "", simpleText: "", image: "", priority: 0, start: NSDate(), end: NSDate(), development: false) + } + + func mapping(map: Map) { + name <- map[PropertyKey.nameKey] + link <- map[PropertyKey.linkKey] + simpleText <- map[PropertyKey.simpleTextKey] + image <- map[PropertyKey.imageKey] + priority <- map[PropertyKey.priorityKey] + start <- (map[PropertyKey.startKey], ISO8601DateTransform()) + end <- (map[PropertyKey.endKey], ISO8601DateTransform()) + development <- map[PropertyKey.developmentKey] + html <- map[PropertyKey.htmlKey] + } + + + struct PropertyKey { + static let nameKey = "name" + static let linkKey = "link" + static let simpleTextKey = "simple-text" + static let imageKey = "image" + static let priorityKey = "priority" + static let startKey = "start" + static let endKey = "end" + static let htmlKey = "html" + static let developmentKey = "development" + } + +} \ No newline at end of file diff --git a/Hydra/SpecialEventStore.swift b/Hydra/SpecialEventStore.swift new file mode 100644 index 0000000..32f2db3 --- /dev/null +++ b/Hydra/SpecialEventStore.swift @@ -0,0 +1,100 @@ +// +// SpecialEventStore.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 04/04/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +let SpecialEventStoreDidUpdateNotification = "SpecialEventStoreDidUpdateNotification" + +class SpecialEventStore: SavableStore, NSCoding { + + private static var _SharedStore: SpecialEventStore? + static var sharedStore: SpecialEventStore { + get { + //TODO: make lazy, and catch NSKeyedUnarchiver errors + if let _SharedStore = _SharedStore { + return _SharedStore + } else { + let specialEventStore = NSKeyedUnarchiver.unarchiveObjectWithFile(Config.SpecialEventStoreArchive.path!) as? SpecialEventStore + if let specialEventStore = specialEventStore { + _SharedStore = specialEventStore + return _SharedStore! + } + } + // initialize new one + _SharedStore = SpecialEventStore() + return _SharedStore! + } + } + + private var _specialEvents: [SpecialEvent] = [] + var specialEvents: [SpecialEvent] { + get { + self.updateSpecialEvents() + return _specialEvents + } + } + + var specialEventsLastUpdated = NSDate(timeIntervalSince1970: 0) + + init() { + super.init(storagePath: Config.SpecialEventStoreArchive.path!) + } + + func updateSpecialEvents(forced: Bool = false) { + updateResource(APIConfig.Zeus2_0 + "association/special_events.json", notificationName: SpecialEventStoreDidUpdateNotification, lastUpdated: specialEventsLastUpdated, forceUpdate: forced, keyPath: "special-events") { (specialEvents: [SpecialEvent]) in + self._specialEvents = specialEvents + self.specialEventsLastUpdated = NSDate() + } + } + + // MARK: Conform to NSCoding + required init?(coder aDecoder: NSCoder) { + guard let specialEvents = aDecoder.decodeObjectForKey(PropertyKey.specialEventsKey) as? [SpecialEvent], + let specialEventsLastUpdated = aDecoder.decodeObjectForKey(PropertyKey.specialEventsLastUpdatedKey) as? NSDate else { + return nil + } + + self._specialEvents = specialEvents + self.specialEventsLastUpdated = specialEventsLastUpdated + + super.init(storagePath: Config.SpecialEventStoreArchive.path!) + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(self._specialEvents, forKey: PropertyKey.specialEventsKey) + aCoder.encodeObject(self.specialEventsLastUpdated, forKey: PropertyKey.specialEventsLastUpdatedKey) + } + + struct PropertyKey { + static let specialEventsKey = "specialEvents" + static let specialEventsLastUpdatedKey = "specialEventsLastUpdated" + } +} + +extension SpecialEventStore: FeedItemProtocol { + func feedItems() -> [FeedItem] { + let date = NSDate() + var feedItems = [FeedItem]() + + if !PreferencesService.sharedService.showSpecialEventsInFeed { + return feedItems + } + + let developmentEnabled = PreferencesService.sharedService.developmentMode + for specialEvent in self._specialEvents { + if ((specialEvent.start <= date) && (specialEvent.end >= date)) || (specialEvent.development && developmentEnabled) { + let feedItem = FeedItem(itemType: .SpecialEventItem, + object: specialEvent, + priority: specialEvent.priority) + feedItems.append(feedItem) + } + } + + return feedItems + } +} \ No newline at end of file diff --git a/Hydra/Stage.swift b/Hydra/Stage.swift new file mode 100644 index 0000000..c7740c6 --- /dev/null +++ b/Hydra/Stage.swift @@ -0,0 +1,43 @@ +// +// Stage.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 10/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import ObjectMapper + +class Stage: NSObject, NSCoding, Mappable { + var stageName: String = "" + var artists: [Artist] = [] + + required init?(_ map: Map) { + + } + + required init?(coder aDecoder: NSCoder) { + guard let stageName = aDecoder.decodeObjectForKey(PropertyKey.stageNameKey) as? String, + let artists = aDecoder.decodeObjectForKey(PropertyKey.artistsKey) as? [Artist] else { + return nil + } + + self.stageName = stageName + self.artists = artists + } + + func mapping(map: Map) { + stageName <- map[PropertyKey.stageNameKey] + artists <- map[PropertyKey.artistsKey] + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(stageName, forKey: PropertyKey.stageNameKey) + aCoder.encodeObject(artists, forKey: PropertyKey.artistsKey) + } + + struct PropertyKey { + static let stageNameKey = "stage" + static let artistsKey = "artists" + } +} diff --git a/Hydra/String.swift b/Hydra/String.swift new file mode 100644 index 0000000..40308d4 --- /dev/null +++ b/Hydra/String.swift @@ -0,0 +1,49 @@ +// +// String.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 28/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +extension String { + func contains(query: String) -> Bool { + let opts: NSStringCompareOptions = [.CaseInsensitiveSearch, .DiacriticInsensitiveSearch] + return self.rangeOfString(query, options: opts) != nil + } + + var html2AttributedString: NSMutableAttributedString? { + guard + let data = dataUsingEncoding(NSUTF8StringEncoding) + else { return nil } + do { + return try NSMutableAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType,NSCharacterEncodingDocumentAttribute:NSUTF8StringEncoding], documentAttributes: nil) + } catch let error as NSError { + print(error.localizedDescription) + return nil + } + } + + var html2String: String { + return html2AttributedString?.string ?? "" + } + + var stripHtmlTags: String { + return self.stringByReplacingOccurrencesOfString("<[^>]+>", withString: "", options: .RegularExpressionSearch, range: nil) + } + + func html2AttributedString(font: UIFont) -> NSMutableAttributedString? { + if let attributedString = html2AttributedString { + attributedString.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, attributedString.length)) + return attributedString + } + return nil + } + + func boundingHeight(size: CGSize, font: UIFont = UIFont.systemFontOfSize(12)) -> CGFloat { + let attributedText = NSAttributedString(string: self, attributes: [NSFontAttributeName: font]) + return attributedText.boundingRectWithSize(size, options: .UsesLineFragmentOrigin, context: nil).height + } +} \ No newline at end of file diff --git a/Hydra/TimeLineTableViewCell.swift b/Hydra/TimeLineTableViewCell.swift new file mode 100644 index 0000000..2e6deb8 --- /dev/null +++ b/Hydra/TimeLineTableViewCell.swift @@ -0,0 +1,62 @@ +// +// TimeLineTableViewCell.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 13/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +class TimeLineTableViewCell: UITableViewCell { + + @IBOutlet weak var label: UILabel? + @IBOutlet weak var switchButton: UISwitch? + + override func awakeFromNib() { + switchButton?.addTarget(self, action: #selector(TimeLineTableViewCell.toggleAction), forControlEvents: .ValueChanged) + self.selectionStyle = .None + } + + var timeLineSetting: TimelineSetting? { + didSet { + if let timeLineSetting = timeLineSetting { + label?.text = timeLineSetting.name + switchButton?.on = timeLineSetting.currentValue + } + } + } + + func toggleAction() { + if let timeLineSetting = timeLineSetting, let switchButton = switchButton { + timeLineSetting.currentValue = switchButton.on + } + } +} + +class TimelineSetting { + let name: String + let defaultPreference: String + let action: ((Bool)->())? + let switched: Bool + + init(name: String, defaultPref: String, switched: Bool = false, action:((state: Bool) -> ())? = nil) { + self.name = name + self.defaultPreference = defaultPref + self.action = action + // boolean value to say when the value should be switched + self.switched = !switched + } + + var currentValue: Bool { + get { + return switched == NSUserDefaults.standardUserDefaults().boolForKey(defaultPreference) + } + set { + if let action = action { + action(newValue) + } + // newValue == switched => flip boolean if switch == false, so set as true + NSUserDefaults.standardUserDefaults().setBool(newValue == switched, forKey: defaultPreference) + NSUserDefaults.standardUserDefaults().synchronize() + } + } +} diff --git a/Hydra/TimelineOnboardViewController.swift b/Hydra/TimelineOnboardViewController.swift new file mode 100644 index 0000000..4ffadba --- /dev/null +++ b/Hydra/TimelineOnboardViewController.swift @@ -0,0 +1,118 @@ +// +// TimelineOnboardViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 12/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +class TimelineOnboardViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + + @IBOutlet weak var tableView: UITableView? + + var settings = [TimelineSetting(name: "Resto menu", defaultPref: PreferencesService.PropertyKey.showRestoInFeedKey), + TimelineSetting(name: "Schamper Daily", defaultPref: PreferencesService.PropertyKey.showSchamperInFeedKey), + TimelineSetting(name: "Urgent.fm", defaultPref: PreferencesService.PropertyKey.showUrgentfmInFeedKey), + TimelineSetting(name: "Verenigingsnieuws", defaultPref: PreferencesService.PropertyKey.showNewsInFeedKey), + TimelineSetting(name: "Activiteiten", defaultPref: PreferencesService.PropertyKey.showActivitiesInFeedKey), + TimelineSetting(name: "Uitgelichte activiteiten", defaultPref: PreferencesService.PropertyKey.showSpecialEventsInFeedKey) + ] + + @IBAction func startHydra() { + #if RELEASE + PreferencesService.sharedService.firstLaunch = false + #endif + let vc = UIStoryboard(name: "MainStoryboard", bundle: nil).instantiateInitialViewController()! + UIApplication.sharedApplication().windows[0].rootViewController = vc + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.navigationBarHidden = true + } + + func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 2 + } + + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + return settings.count + case 1: + return 2 + default: + fatalError("Tableview has only 2 sections") + } + } + + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch indexPath.section { + case 0: + let cell = tableView.dequeueReusableCellWithIdentifier("timelineSettingsCell") as! TimeLineTableViewCell + + cell.timeLineSetting = settings[indexPath.row] + + return cell + case 1: + if indexPath.row == 0 { + let cell = tableView.dequeueReusableCellWithIdentifier("timelineSettingsCell") as! TimeLineTableViewCell + + cell.timeLineSetting = TimelineSetting(name: "Toon alle verenigingen", defaultPref: PreferencesService.PropertyKey.filterAssociationsKey, switched: true) { (state: Bool) -> () in + self.tableView?.reloadData() + } + + return cell + } else { + let cell = UITableViewCell(style: .Default, reuseIdentifier: nil) + + cell.textLabel?.text = "Selecteer verenigingen" + cell.textLabel?.textColor = UIColor.whiteColor() + cell.backgroundColor = UIColor.clearColor() + + cell.accessoryType = .DisclosureIndicator + + if !PreferencesService.sharedService.filterAssociations { + cell.textLabel?.alpha = 0.5 + cell.detailTextLabel?.alpha = 0.5 + cell.selectionStyle = .None + cell.accessoryType = .None + } + return cell + } + default: + fatalError() + } + } + + func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + if indexPath.section == 1 && indexPath.row == 1 { + if PreferencesService.sharedService.filterAssociations { + let c = AssociationPreferenceController() + if let navigationController = self.navigationController { + navigationController.navigationBarHidden = false + navigationController.pushViewController(c, animated: true) + } + } + tableView.deselectRowAtIndexPath(indexPath, animated: false) + } + } + + func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } + + func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: + return "Feed instellingen" + case 1: + return "Verenigingen selecteren" + default: + return nil + } + } +} \ No newline at end of file diff --git a/Hydra/TimelinePost.swift b/Hydra/TimelinePost.swift new file mode 100644 index 0000000..cd198f8 --- /dev/null +++ b/Hydra/TimelinePost.swift @@ -0,0 +1,113 @@ +// +// TimelinePost.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 16/09/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import ObjectMapper + +class TimelinePost: NSObject, NSCoding, Mappable { + var title: String? + var body: String? + var link: String? + var media: String? + var date: NSDate? + var origin: Origin = .None + var postType: PostType = .None + var poster: String? + + required init?(_ map: Map) { + + } + + func mapping(map: Map) { + let formatter = NSDateFormatter() + formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) + formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)! + let dateTransform = DateFormatterTransform(dateFormatter: formatter) + + let originTransform = TransformOf(fromJSON: { (s) -> TimelinePost.Origin? in + if let s = s { + if let type = Origin(rawValue: s) { + return type + } + } + return .None + }) { $0?.rawValue } + + let postTypeTransform = TransformOf(fromJSON: { (s) -> TimelinePost.PostType? in + if let s = s, let type = PostType(rawValue: s) { + return type + } + return .None + }) { $0?.rawValue } + + title <- map[PropertyKey.titleKey] + body <- map[PropertyKey.bodyKey] + link <- map[PropertyKey.linkKey] + media <- map[PropertyKey.mediaKey] + date <- (map["created_at"], dateTransform) + origin <- (map[PropertyKey.originKey], originTransform) + postType <- (map[PropertyKey.postTypeKey], postTypeTransform) + poster <- map[PropertyKey.posterKey] + } + + required init?(coder aDecoder: NSCoder) { + title = aDecoder.decodeObjectForKey(PropertyKey.titleKey) as? String + body = aDecoder.decodeObjectForKey(PropertyKey.bodyKey) as? String + link = aDecoder.decodeObjectForKey(PropertyKey.linkKey) as? String + media = aDecoder.decodeObjectForKey(PropertyKey.mediaKey) as? String + date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as? NSDate + poster = aDecoder.decodeObjectForKey(PropertyKey.posterKey) as? String + + guard let originValue = aDecoder.decodeObjectForKey(PropertyKey.originKey) as? String, + let postTypeValue = aDecoder.decodeObjectForKey(PropertyKey.postTypeKey) as? String, + let origin = Origin(rawValue: originValue), + let postType = PostType(rawValue: postTypeValue) else { return nil } + + self.origin = origin + self.postType = postType + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(title, forKey: PropertyKey.titleKey) + aCoder.encodeObject(body, forKey: PropertyKey.bodyKey) + aCoder.encodeObject(link, forKey: PropertyKey.linkKey) + aCoder.encodeObject(media, forKey: PropertyKey.mediaKey) + aCoder.encodeObject(date, forKey: PropertyKey.dateKey) + aCoder.encodeObject(poster, forKey: PropertyKey.posterKey) + aCoder.encodeObject(origin.rawValue, forKey: PropertyKey.originKey) + aCoder.encodeObject(postType.rawValue, forKey: PropertyKey.postTypeKey) + } + + struct PropertyKey { + static let titleKey = "title" + static let bodyKey = "body" + static let linkKey = "link" + static let mediaKey = "media" + static let dateKey = "date" + static let originKey = "origin" + static let postTypeKey = "post_type" + static let posterKey = "poster" + } + + enum Origin: String { + case Facebook = "facebook" + case Instagram = "instagram" + case Blog = "dafault" + case None = "none" + } + + enum PostType: String { + case Photo = "photo" + case Video = "video" + case Text = "text" + case Link = "link" + case None = "none" + } +} + diff --git a/Hydra/UGentOAuth2Service.swift b/Hydra/UGentOAuth2Service.swift new file mode 100644 index 0000000..08d8850 --- /dev/null +++ b/Hydra/UGentOAuth2Service.swift @@ -0,0 +1,78 @@ +// +// UGentOAuth2Service.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 09/01/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import p2_OAuth2 +import Alamofire +import ObjectMapper +import AlamofireObjectMapper + +let UGentOAuth2ServiceDidUpdateUserNotification = "UGentOAuth2ServiceDidUpdateUserNotification" + +class UGentOAuth2Service: NSObject { + + static let sharedService = UGentOAuth2Service() + + let oauth2: OAuth2CodeGrant + + private override init() { + let path = NSBundle.mainBundle().pathForResource("UGentOAuthConfig", ofType: "plist") + let UGentOAuth2 = NSDictionary(contentsOfFile: path!)?.valueForKey("UGentOAuth2") + let settings: OAuth2JSON = [ + "client_id": (UGentOAuth2!.valueForKey("ClientID") as? String)!, + "client_secret": (UGentOAuth2!.valueForKey("ClientSecret") as? String)!, + "authorize_uri": APIConfig.OAuth + "authorize", + "token_uri": APIConfig.OAuth + "access_token", + "redirect_uris": ["https://zeus.UGent.be/hydra/oauth/callback", "hydra-ugent://oauth/zeus/callback"], + "title": "UGent Authentication" + ] + + oauth2 = OAuth2CodeGrant(settings: settings) + super.init() + + oauth2.verbose = true + oauth2.onAuthorize = { parameters in + MinervaStore.sharedStore.updateUser(true) + PreferencesService.sharedService.userLoggedInToMinerva = true + NSNotificationCenter.defaultCenter().postNotificationName(UGentOAuth2ServiceDidUpdateUserNotification, object: self) + } + oauth2.onFailure = { error in + if let error = error { + //TODO: do something + PreferencesService.sharedService.userLoggedInToMinerva = false + print("Authorization went wrong: \(error)") + } + } + } + + func handleRedirectURL(redirect: NSURL) { + self.oauth2.handleRedirectURL(redirect) + } + + func isAuthenticated() -> Bool{ + return oauth2.accessToken != nil + } + + + func isLoggedIn() -> Bool { + if PreferencesService.sharedService.userLoggedInToMinerva { + if oauth2.accessToken == nil { + oauth2.authorize() + } + return true + } + return false + } + + func logoff() { + oauth2.forgetTokens() + PreferencesService.sharedService.userLoggedInToMinerva = false + MinervaStore.sharedStore.logoff() + + } +} diff --git a/Hydra/UIColor.swift b/Hydra/UIColor.swift new file mode 100644 index 0000000..f9cd402 --- /dev/null +++ b/Hydra/UIColor.swift @@ -0,0 +1,27 @@ +// +// UIColor.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 02/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation + +extension UIColor { + + class func hydraTintcolor() -> UIColor { + // #204E80 + return UIColor(red: 0.126, green:0.304, blue:0.500, alpha:1.000) + } + + class func hydraBackgroundColor() -> UIColor { + // #CED6E0 + return UIColor(red: 0.807, green: 0.840, blue: 0.878, alpha: 1.000) + } + + class func SKOBackgroundColor() -> UIColor { + // #009ade + return UIColor(red: 0, green: 0.604, blue: 0.871, alpha: 1.000) + } +} \ No newline at end of file diff --git a/Hydra/UIViewController.swift b/Hydra/UIViewController.swift new file mode 100644 index 0000000..34280ce --- /dev/null +++ b/Hydra/UIViewController.swift @@ -0,0 +1,19 @@ +// +// UIViewController.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 31/03/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import FirebaseAnalytics +import Firebase + +extension UIViewController { + + func GAI_track(title: String) { + FIRAnalytics.logEventWithName("screen", parameters: ["screenName": title]) + + } +} \ No newline at end of file diff --git a/Hydra/UIViewExtension.swift b/Hydra/UIViewExtension.swift new file mode 100644 index 0000000..b33540d --- /dev/null +++ b/Hydra/UIViewExtension.swift @@ -0,0 +1,64 @@ +// +// UIViewExtension.swift +// Hydra +// +// Created by Timo De Waele on 06/07/16. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import UIKit + +extension UIView { + + @IBInspectable var border: Bool { + get { + return layer.cornerRadius > 0 && layer.borderWidth > 0 + } + set { + if newValue { + layer.cornerRadius = 5 + layer.borderWidth = 1 + layer.borderColor = UIColor.whiteColor().CGColor + } else { + layer.cornerRadius = 0 + layer.borderWidth = 0 + } + layer.masksToBounds = layer.cornerRadius > 0 + } + } + + @IBInspectable var cornerRadius: CGFloat { + get { + return layer.cornerRadius + } + set { + layer.cornerRadius = newValue + layer.masksToBounds = newValue > 0 + } + } + + @IBInspectable var borderWidth: CGFloat { + get { + return layer.borderWidth + } + set { + layer.borderWidth = newValue + } + } + + @IBInspectable var borderColor: UIColor? { + get { + return UIColor(CGColor: layer.borderColor!) + } + set { + layer.borderColor = newValue?.CGColor + } + } + + func setShadow() { + self.layer.shadowColor = UIColor.blackColor().CGColor + self.layer.shadowRadius = 7 + self.layer.shadowOpacity = 0.25 + self.layer.shadowOffset = CGSizeMake(7, 7) + } +} diff --git a/Hydra/UrgentPlayer.m b/Hydra/UrgentPlayer.m index 209e86a..afb0d32 100644 --- a/Hydra/UrgentPlayer.m +++ b/Hydra/UrgentPlayer.m @@ -10,7 +10,6 @@ #import "NSDate+Utilities.h" #import -#import #define kSongUpdateInterval 30 #define kShowUpdateInterval (30*60) diff --git a/Hydra/UrgentViewController.m b/Hydra/UrgentViewController.m index 4b1f22e..fe09b4d 100644 --- a/Hydra/UrgentViewController.m +++ b/Hydra/UrgentViewController.m @@ -9,7 +9,6 @@ #import "UrgentViewController.h" #import "UrgentPlayer.h" #import "MarqueeLabel.h" -#import #import #import @@ -97,6 +96,7 @@ - (void)viewWillAppear:(BOOL)animated - (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault]; } @@ -166,23 +166,11 @@ - (void)shareButtonTapped:(id)sender NSURL *url = [NSURL URLWithString:@"http://www.urgent.fm"]; NSString *message =[self createShareMessage]; // Available since iOS6 - if ([UIActivityViewController class]) { - NSArray *items = @[ message, url ]; + NSArray *items = @[ message, url ]; - UIActivityViewController *c = [[UIActivityViewController alloc] initWithActivityItems:items + UIActivityViewController *c = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:@[]]; - [self presentViewController:c animated:YES completion:NULL]; - } - else { - // Create the item to share - SHKItem *item = [SHKItem URL:url title:message contentType:SHKURLContentTypeUndefined]; - - // Get the ShareKit action sheet - SHKActionSheet *actionSheet = [SHKActionSheet actionSheetForItem:item]; - - // Display the action sheet - [actionSheet showFromToolbar:self.navigationController.toolbar]; - } + [self presentViewController:c animated:YES completion:NULL]; } - (NSString*)createShareMessage diff --git a/Hydra/User.swift b/Hydra/User.swift new file mode 100644 index 0000000..63d7649 --- /dev/null +++ b/Hydra/User.swift @@ -0,0 +1,90 @@ +// +// User.swift +// +// Created by Feliciaan De Palmenaer on 28/02/2016 +// Copyright (c) . All rights reserved. +// + +import Foundation +import ObjectMapper + +class User: NSObject, Mappable, NSCoding { + + // MARK: Properties + var ugentStudentID: [String]? + var mail: [String]? + var lastenrolled: [String]? + var givenname: [String]? + var surname: [String]? + var uid: [String]? + + var name: String { + get { + var _name = "" + if let givenname = givenname { + for gname in givenname { + _name.appendContentsOf(gname) + _name = _name.stringByAppendingString(" ") + } + } + if let surname = surname { + for sname in surname { + _name.appendContentsOf(sname) + } + } + + return _name + } + } + + // MARK: ObjectMapper Initalizers + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + required init?(_ map: Map){ + + } + + /** + Map a JSON object to this class using ObjectMapper + - parameter map: A mapping from ObjectMapper + */ + func mapping(map: Map) { + ugentStudentID <- map[PropertyKey.userUgentStudentIDKey] + mail <- map[PropertyKey.userMailKey] + lastenrolled <- map[PropertyKey.userLastenrolledKey] + givenname <- map[PropertyKey.userGivennameKey] + surname <- map[PropertyKey.userSurnameKey] + uid <- map[PropertyKey.userUidKey] + } + + // MARK: NSCoding Protocol + required init(coder aDecoder: NSCoder) { + self.ugentStudentID = aDecoder.decodeObjectForKey(PropertyKey.userUgentStudentIDKey) as? [String] + self.mail = aDecoder.decodeObjectForKey(PropertyKey.userMailKey) as? [String] + self.lastenrolled = aDecoder.decodeObjectForKey(PropertyKey.userLastenrolledKey) as? [String] + self.givenname = aDecoder.decodeObjectForKey(PropertyKey.userGivennameKey) as? [String] + self.surname = aDecoder.decodeObjectForKey(PropertyKey.userSurnameKey) as? [String] + self.uid = aDecoder.decodeObjectForKey(PropertyKey.userUidKey) as? [String] + } + + func encodeWithCoder(aCoder: NSCoder) { + aCoder.encodeObject(ugentStudentID, forKey: PropertyKey.userUgentStudentIDKey) + aCoder.encodeObject(mail, forKey: PropertyKey.userMailKey) + aCoder.encodeObject(lastenrolled, forKey: PropertyKey.userLastenrolledKey) + aCoder.encodeObject(givenname, forKey: PropertyKey.userGivennameKey) + aCoder.encodeObject(surname, forKey: PropertyKey.userSurnameKey) + aCoder.encodeObject(uid, forKey: PropertyKey.userUidKey) + } + + // MARK: Declaration for string constants to be used to decode and also serialize. + struct PropertyKey { + static let userUgentStudentIDKey: String = "ugentStudentID" + static let userMailKey: String = "mail" + static let userLastenrolledKey: String = "lastenrolled" + static let userGivennameKey: String = "givenname" + static let userSurnameKey: String = "surname" + static let userUidKey: String = "uid" + } +} diff --git a/Hydra/WebViewController.h b/Hydra/WebViewController.h index c084634..0e3f47f 100644 --- a/Hydra/WebViewController.h +++ b/Hydra/WebViewController.h @@ -7,7 +7,6 @@ // #import -#import "SchamperArticle.h" @interface WebViewController : UIViewController @@ -15,5 +14,6 @@ @property (nonatomic, strong) NSString *trackedViewName; - (void)loadHtml:(NSString *)path; +- (void)loadUrl: (NSURL*)url; @end diff --git a/Hydra/WebViewController.m b/Hydra/WebViewController.m index 4e2485e..1a1f7ed 100644 --- a/Hydra/WebViewController.m +++ b/Hydra/WebViewController.m @@ -41,6 +41,11 @@ - (void)loadHtml:(NSString *)path [self view]; NSURL *url = [[NSBundle mainBundle] URLForResource:path withExtension:nil]; + [self loadUrl:url]; +} + +- (void)loadUrl:(NSURL*)url +{ [self.webView loadRequest:[NSURLRequest requestWithURL:url]]; } diff --git a/Hydra/Whatsnew.swift b/Hydra/Whatsnew.swift new file mode 100644 index 0000000..939bbc2 --- /dev/null +++ b/Hydra/Whatsnew.swift @@ -0,0 +1,30 @@ +// +// Whatsnew.swift +// Hydra +// +// Created by Feliciaan De Palmenaer on 02/08/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Foundation +import ObjectMapper + +class WhatsNew: Mappable { + + var agenda = [CalendarItem]() + var announcement = [Announcement]() + + required init?(_ map: Map) { + + } + + func mapping(map: Map) { + self.agenda <- map[PropertyKey.agendaKey] + self.announcement <- map[PropertyKey.announcementsKey] + } + + struct PropertyKey { + static let agendaKey = "agenda" + static let announcementsKey = "announcement" + } +} \ No newline at end of file diff --git a/Hydra/nl.lproj/UrgentViewController.xib b/Hydra/nl.lproj/UrgentViewController.xib index f392622..40898d1 100644 --- a/Hydra/nl.lproj/UrgentViewController.xib +++ b/Hydra/nl.lproj/UrgentViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -23,14 +23,12 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hydra/p2_OAuth2.swift b/Hydra/p2_OAuth2.swift new file mode 100644 index 0000000..c8a8be5 --- /dev/null +++ b/Hydra/p2_OAuth2.swift @@ -0,0 +1,36 @@ +// +// p2_OAut2.swift +// OAuthTest +// +// Created by Feliciaan De Palmenaer on 23/02/2016. +// Copyright © 2016 Zeus WPI. All rights reserved. +// + +import Alamofire +import p2_OAuth2 + +extension OAuth2 { + public func request( + method: Alamofire.Method, + _ URLString: URLStringConvertible, + parameters: [String: AnyObject]? = nil, + encoding: Alamofire.ParameterEncoding = .URL, + headers: [String: String]? = [String: String]()) + -> Alamofire.Request + { + var hdrs = headers + if let accessToken = accessToken { + hdrs!["Authorization"] = "Bearer \(accessToken)" + #if (arch(i386) || arch(x86_64)) && os(iOS) // print when debugging from simulator + print("Token \(accessToken)\tURL: \(URLString)") + #endif + } + return Alamofire.request( + method, + URLString, + parameters: parameters, + encoding: encoding, + headers: hdrs) + } + +} \ No newline at end of file diff --git a/Hydra/sko.storyboard b/Hydra/sko.storyboard new file mode 100644 index 0000000..a490aaf --- /dev/null +++ b/Hydra/sko.storyboard @@ -0,0 +1,859 @@ + + + + + + + + + + LeagueGothic-Regular + LeagueGothic-Regular + LeagueGothic-Regular + LeagueGothic-Regular + LeagueGothic-Regular + LeagueGothic-Regular + LeagueGothic-Regular + LeagueGothic-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HydraTests/HydraTests-Info.plist b/HydraTests/HydraTests-Info.plist index 5e16d57..169b6f7 100644 --- a/HydraTests/HydraTests-Info.plist +++ b/HydraTests/HydraTests-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - be.ugent.zeus.${PRODUCT_NAME:rfc1034identifier} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType diff --git a/Images/logo-sko.png b/Images/logo-sko.png new file mode 100644 index 0000000..47ee53f Binary files /dev/null and b/Images/logo-sko.png differ diff --git a/Images/minerva-icon.png b/Images/minerva-icon.png new file mode 100644 index 0000000..0188a76 Binary files /dev/null and b/Images/minerva-icon.png differ diff --git a/Images/minerva-icon@2x.png b/Images/minerva-icon@2x.png new file mode 100644 index 0000000..1f8406c Binary files /dev/null and b/Images/minerva-icon@2x.png differ diff --git a/Images/minerva-icon@3x.png b/Images/minerva-icon@3x.png new file mode 100644 index 0000000..e86e355 Binary files /dev/null and b/Images/minerva-icon@3x.png differ diff --git a/Images/tabbar-hydra.png b/Images/tabbar-hydra.png new file mode 100644 index 0000000..157b1f4 Binary files /dev/null and b/Images/tabbar-hydra.png differ diff --git a/Images/tabbar-hydra@2x.png b/Images/tabbar-hydra@2x.png new file mode 100644 index 0000000..8a9635b Binary files /dev/null and b/Images/tabbar-hydra@2x.png differ diff --git a/Images/tabbar-minerva.png b/Images/tabbar-minerva.png new file mode 100644 index 0000000..d007a36 Binary files /dev/null and b/Images/tabbar-minerva.png differ diff --git a/Images/tabbar-minerva@2x.png b/Images/tabbar-minerva@2x.png new file mode 100644 index 0000000..4127185 Binary files /dev/null and b/Images/tabbar-minerva@2x.png differ diff --git a/Images/tabbar-minerva@3x.png b/Images/tabbar-minerva@3x.png new file mode 100644 index 0000000..6851561 Binary files /dev/null and b/Images/tabbar-minerva@3x.png differ diff --git a/Images/tabbar-sko-lineup.png b/Images/tabbar-sko-lineup.png new file mode 100644 index 0000000..d25e84d Binary files /dev/null and b/Images/tabbar-sko-lineup.png differ diff --git a/Images/tabbar-sko-lineup@2x.png b/Images/tabbar-sko-lineup@2x.png new file mode 100644 index 0000000..acf7052 Binary files /dev/null and b/Images/tabbar-sko-lineup@2x.png differ diff --git a/Images/tabbar-sko-map.png b/Images/tabbar-sko-map.png new file mode 100644 index 0000000..7e7a088 Binary files /dev/null and b/Images/tabbar-sko-map.png differ diff --git a/Images/tabbar-sko-map@2x.png b/Images/tabbar-sko-map@2x.png new file mode 100644 index 0000000..f5a342e Binary files /dev/null and b/Images/tabbar-sko-map@2x.png differ diff --git a/Images/tabbar-sko-student-village.png b/Images/tabbar-sko-student-village.png new file mode 100644 index 0000000..15a115e Binary files /dev/null and b/Images/tabbar-sko-student-village.png differ diff --git a/Images/tabbar-sko-student-village@2x.png b/Images/tabbar-sko-student-village@2x.png new file mode 100644 index 0000000..f471c8c Binary files /dev/null and b/Images/tabbar-sko-student-village@2x.png differ diff --git a/Images/tabbar-sko.png b/Images/tabbar-sko.png new file mode 100644 index 0000000..d5116ed Binary files /dev/null and b/Images/tabbar-sko.png differ diff --git a/Images/tabbar-sko@2x.png b/Images/tabbar-sko@2x.png new file mode 100644 index 0000000..015a35f Binary files /dev/null and b/Images/tabbar-sko@2x.png differ diff --git a/LaunchScreen.storyboard b/LaunchScreen.storyboard new file mode 100644 index 0000000..df0ae1f --- /dev/null +++ b/LaunchScreen.storyboard @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LeagueGothic-Regular.otf b/LeagueGothic-Regular.otf new file mode 100644 index 0000000..6cd753f Binary files /dev/null and b/LeagueGothic-Regular.otf differ diff --git a/Podfile b/Podfile index bab9215..0865172 100644 --- a/Podfile +++ b/Podfile @@ -1,15 +1,26 @@ -platform :ios, '7.0' +platform :ios, '8.0' +use_frameworks! -pod 'GoogleAnalytics-iOS-SDK', '~> 3.10' -pod 'Facebook-iOS-SDK', '~> 3.2' -pod 'SVProgressHUD', '~> 1.1' -pod 'SDWebImage', '~> 3.7' -pod 'RestKit', '~> 0.24.0' -pod 'ShareKit/Facebook', '~> 4.0' -pod 'ShareKit/Twitter', '~> 4.0' -pod 'RKXMLReaderSerialization', :podspec => 'External/RKXMLReaderSerialization.podspec' -pod 'TUSafariActivity', :podspec => 'External/TUSafariActivity.podspec' -pod 'SMPageControl', '~> 1.2' -pod 'Reachability', '~> 3.2' -pod 'VTAcknowledgementsViewController', '~> 0.13' -pod "RMPickerViewController", "~> 1.3.3" +target 'Hydra' do + pod 'SVProgressHUD', '~> 1.1' + pod 'SDWebImage', '~> 3.7' + pod 'TUSafariActivity', :podspec => 'External/TUSafariActivity.podspec' + pod 'SMPageControl', '~> 1.2' + pod 'Reachability', '~> 3.2' + pod 'AcknowList' + pod 'RMPickerViewController', '~> 2.0.3' + pod 'CVCalendar', '~> 1.2.9' + pod 'Firebase/Core' + pod 'Firebase/Messaging' + pod 'Firebase/RemoteConfig' + pod 'p2.OAuth2' + pod 'Alamofire' + pod 'ObjectMapper' + pod 'AlamofireObjectMapper' + pod 'SWXMLHash' + pod 'FBSDKCoreKit' + pod 'FBSDKShareKit' + pod 'FBSDKLoginKit' + pod 'Bolts' + pod 'FontAwesome.swift' +end \ No newline at end of file diff --git a/Podfile.lock b/Podfile.lock index 135ec6e..3aa685d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,111 +1,134 @@ PODS: - - AFNetworking (1.3.4) - - Bolts (1.1.4) - - Facebook-iOS-SDK (3.23.2): - - Bolts (~> 1.0) - - GoogleAnalytics-iOS-SDK (3.10): - - GoogleAnalytics-iOS-SDK/Core (= 3.10) - - GoogleAnalytics-iOS-SDK/Core (3.10) - - ISO8601DateFormatterValueTransformer (0.6.0): - - RKValueTransformers (~> 1.1.0) - - PKMultipartInputStream (1.1.0) + - AcknowList (1.0) + - Alamofire (3.4.1) + - AlamofireObjectMapper (3.0.0): + - Alamofire (~> 3.2) + - ObjectMapper (~> 1.0) + - Bolts (1.8.0): + - Bolts/AppLinks (= 1.8.0) + - Bolts/Tasks (= 1.8.0) + - Bolts/AppLinks (1.8.0): + - Bolts/Tasks + - Bolts/Tasks (1.8.0) + - CVCalendar (1.2.9) + - FBSDKCoreKit (4.13.1): + - Bolts (~> 1.7) + - FBSDKLoginKit (4.13.1): + - FBSDKCoreKit + - FBSDKShareKit (4.13.1): + - FBSDKCoreKit + - Firebase/Analytics (3.5.2): + - FirebaseAnalytics (~> 3.3) + - Firebase/Core (3.5.2): + - Firebase/Analytics + - Firebase/Messaging (3.5.2): + - Firebase/Analytics + - FirebaseMessaging (= 1.1.1) + - Firebase/RemoteConfig (3.5.2): + - Firebase/Analytics + - FirebaseRemoteConfig (= 1.2.0) + - FirebaseAnalytics (3.3.1): + - FirebaseInstanceID (~> 1.0) + - GoogleInterchangeUtilities (~> 1.2) + - GoogleSymbolUtilities (~> 1.1) + - GoogleUtilities (~> 1.2) + - FirebaseInstanceID (1.0.8) + - FirebaseMessaging (1.1.1): + - FirebaseAnalytics (~> 3.3) + - FirebaseInstanceID (~> 1.0) + - GoogleInterchangeUtilities (~> 1.2) + - GoogleIPhoneUtilities (~> 1.2) + - GoogleSymbolUtilities (~> 1.1) + - FirebaseRemoteConfig (1.2.0): + - FirebaseAnalytics (~> 3.3) + - FirebaseInstanceID (~> 1.0) + - GoogleInterchangeUtilities (~> 1.2) + - GoogleIPhoneUtilities (~> 1.2) + - GoogleSymbolUtilities (~> 1.1) + - GoogleUtilities (~> 1.2) + - FontAwesome.swift (0.8.0) + - GoogleInterchangeUtilities (1.2.1): + - GoogleSymbolUtilities (~> 1.0) + - GoogleIPhoneUtilities (1.2.1): + - GoogleSymbolUtilities (~> 1.0) + - GoogleUtilities (~> 1.0) + - GoogleSymbolUtilities (1.1.1) + - GoogleUtilities (1.3.1): + - GoogleSymbolUtilities (~> 1.0) + - ObjectMapper (1.3.0) + - p2.OAuth2 (2.2.7): + - SwiftKeychain (~> 1.0) - Reachability (3.2) - - RestKit (0.24.1): - - RestKit/Core (= 0.24.1) - - RestKit/Core (0.24.1): - - RestKit/CoreData - - RestKit/Network - - RestKit/ObjectMapping - - RestKit/CoreData (0.24.1): - - RestKit/ObjectMapping - - RestKit/Network (0.24.1): - - AFNetworking (~> 1.3.0) - - RestKit/ObjectMapping - - RestKit/Support - - SOCKit - - RestKit/ObjectMapping (0.24.1): - - ISO8601DateFormatterValueTransformer (~> 0.6.0) - - RestKit/Support - - RKValueTransformers (~> 1.1.0) - - RestKit/Support (0.24.1): - - TransitionKit (~> 2.1.0) - - RKValueTransformers (1.1.2) - - RKXMLReaderSerialization (0.1.0): - - RestKit (>= 0.20.0dev) - - XMLReader (>= 0.0.2) - - RMPickerViewController (1.3.3) - - SAMTextView (0.2.2) - - SDWebImage (3.7.1): - - SDWebImage/Core (= 3.7.1) - - SDWebImage/Core (3.7.1) - - ShareKit/Core (4.0.4): - - PKMultipartInputStream - - SAMTextView (~> 0.2.1) - - SDWebImage (~> 3.7) - - ShareKit/Reachability - - SSKeychain (~> 1.2.2) - - UIActivityIndicator-for-SDWebImage (~> 1.2) - - ShareKit/Facebook (4.0.4): - - Facebook-iOS-SDK (~> 3.2) - - ShareKit/Core - - ShareKit/Reachability (4.0.4) - - ShareKit/Twitter (4.0.4): - - ShareKit/Core + - RMActionController (1.0.5) + - RMPickerViewController (2.0.3): + - RMActionController (~> 1.0.5) + - SDWebImage (3.8.1): + - SDWebImage/Core (= 3.8.1) + - SDWebImage/Core (3.8.1) - SMPageControl (1.2) - - SOCKit (1.1) - - SSKeychain (1.2.3) - SVProgressHUD (1.1.3) - - TransitionKit (2.1.1) + - SwiftKeychain (1.0.0) + - SWXMLHash (2.3.2) - TUSafariActivity (0.0.1) - - UIActivityIndicator-for-SDWebImage (1.2): - - SDWebImage (~> 3.7) - - VTAcknowledgementsViewController (0.13) - - XMLReader (0.0.2) DEPENDENCIES: - - Facebook-iOS-SDK (~> 3.2) - - GoogleAnalytics-iOS-SDK (~> 3.10) + - AcknowList + - Alamofire + - AlamofireObjectMapper + - Bolts + - CVCalendar (~> 1.2.9) + - FBSDKCoreKit + - FBSDKLoginKit + - FBSDKShareKit + - Firebase/Core + - Firebase/Messaging + - Firebase/RemoteConfig + - FontAwesome.swift + - ObjectMapper + - p2.OAuth2 - Reachability (~> 3.2) - - RestKit (~> 0.24.0) - - RKXMLReaderSerialization (from `External/RKXMLReaderSerialization.podspec`) - - RMPickerViewController (~> 1.3.3) + - RMPickerViewController (~> 2.0.3) - SDWebImage (~> 3.7) - - ShareKit/Facebook (~> 4.0) - - ShareKit/Twitter (~> 4.0) - SMPageControl (~> 1.2) - SVProgressHUD (~> 1.1) + - SWXMLHash - TUSafariActivity (from `External/TUSafariActivity.podspec`) - - VTAcknowledgementsViewController (~> 0.13) EXTERNAL SOURCES: - RKXMLReaderSerialization: - :podspec: External/RKXMLReaderSerialization.podspec TUSafariActivity: :podspec: External/TUSafariActivity.podspec SPEC CHECKSUMS: - AFNetworking: cf8e418e16f0c9c7e5c3150d019a3c679d015018 - Bolts: f8e2f94edbf9fec7bb8de5ecaaa3aa6b7de6f64e - Facebook-iOS-SDK: cd87f0b2f0c1d9574b44e8fa2baacc275524bac3 - GoogleAnalytics-iOS-SDK: 4f98a376c1d61e6ac4ca47951655820314856eef - ISO8601DateFormatterValueTransformer: 3ac3a721381697cab4e792d8049ce2f2e9ad3036 - PKMultipartInputStream: 1fb466fac9347857e3477e4dfc6fcc62fea7a944 + AcknowList: 06a183d117b7e2bb0c6f5654366d2912dc64a6c4 + Alamofire: 01a82e2f6c0f860ade35534c8dd88be61bdef40c + AlamofireObjectMapper: 2d16585d06177be0116d76de73497e77437798af + Bolts: 127daec4981dab569d07314c198dfcae47f4ce4c + CVCalendar: 0799256e0bc3b43e83b7e86c0729ce9ff0624cb6 + FBSDKCoreKit: 2807fe0da2ddb49a1469da6d6ff597f99912980f + FBSDKLoginKit: 6d350cb6a22d6956e9d078885de6ee8a0ebc51b4 + FBSDKShareKit: c9438df7573b33780ac8c3451201c5ebb260ffca + Firebase: 94e63112efcad75226c0356a8f3032397b8547c9 + FirebaseAnalytics: d968d4d5f6aeb795cd64144337bcd233e530efc6 + FirebaseInstanceID: ba1e640935235e5fac39dfa816fe7660e72e1a8a + FirebaseMessaging: 6bc7cd2ecb875c1a9c0f8b94956398aa3145bbb8 + FirebaseRemoteConfig: c5378e689c1edb54ff62061b8f6a908263dd80ae + FontAwesome.swift: f8c6cc5c42cbe93bdc04371363bc7b371fb3d3a1 + GoogleInterchangeUtilities: def8415a862effc67d549d5b5b0b9c7a2f97d4de + GoogleIPhoneUtilities: 63f25e93a3ddcb66884d182aab3a660d98f1479b + GoogleSymbolUtilities: 33117db1b5f290c6fbf259585e4885b4c84b98d7 + GoogleUtilities: 56c5ac05b7aa5dc417a1bb85221a9516e04d7032 + ObjectMapper: 4eb08a726e9791a93b2632049951b56a7c17e471 + p2.OAuth2: a4391406917fc2a3bb820aec6e1180b8f87321fd Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - RestKit: 1987b5efef289c6b27bd980714d6ca48d3871b78 - RKValueTransformers: 66ac5e4f077fdbe3496e792d89eeff4c3eb67701 - RKXMLReaderSerialization: 92e75808b3e46d8538ddc58c08e6822fc63313ad - RMPickerViewController: 1a64e211e86ac3d961214c9c376de7c0241ac25b - SAMTextView: 979c43bb1fa5859ddfbfeb35e9c296a2b0ec55fd - SDWebImage: ed3095af2ff88b436426037444979b917f6c5575 - ShareKit: 48a9a3aed20d4b59b49ae3c8efdbb46e95beab33 + RMActionController: b83653e7452ed51492094d65dc82151c36443c03 + RMPickerViewController: f774dbdfddfe819bbaf211d5719deebcbf1a55ed + SDWebImage: 35f9627a3e44b4f292a8a8ce6a531fa488239b91 SMPageControl: 922892813001cfaf059e86e6801f46a967e9ee29 - SOCKit: c7376ac262bea9115b8f749358f762522a47d392 - SSKeychain: 3f42991739c6c60a9cf1bbd4dff6c0d3694bcf3d SVProgressHUD: 748080e4f36e603f6c02aec292664239df5279c1 - TransitionKit: 3a14b6acc7cf2d1dd3e454e24dbad1cfab9a1ef1 + SwiftKeychain: fd9ad69f7b3ff7a0c7ba579da508bbaa89a395b2 + SWXMLHash: 2f8c49af765718ad56020c4f875ec3ff22612f06 TUSafariActivity: 83284fca15bb3331b1680ee1fbe39be38fe3a6e3 - UIActivityIndicator-for-SDWebImage: 7bf7ebbf0ed1747dedc0d6e949c4603a6d0f9e0c - VTAcknowledgementsViewController: f2391b007c3abafd3b3afccda7d8914a59b44309 - XMLReader: c7e54b707c368e2fa545d8313276b4de13a36ff5 -COCOAPODS: 0.39.0 +PODFILE CHECKSUM: 09b6aba5ce9709341c3ae4caecbea8e91497962f + +COCOAPODS: 1.0.0.beta.4 diff --git a/Resources/Pods-acknowledgements.plist b/Resources/Pods-acknowledgements.plist index 950aff7..c7f337c 120000 --- a/Resources/Pods-acknowledgements.plist +++ b/Resources/Pods-acknowledgements.plist @@ -1 +1 @@ -../Pods/Target Support Files/Pods/Pods-acknowledgements.plist \ No newline at end of file +../Pods/Target Support Files/Pods-Hydra/Pods-Hydra-acknowledgements.plist \ No newline at end of file diff --git a/RestoMenuToday/Info.plist b/RestoMenuToday/Info.plist index 4a25f15..136fdab 100644 --- a/RestoMenuToday/Info.plist +++ b/RestoMenuToday/Info.plist @@ -13,7 +13,7 @@ CFBundleIcons~ipad CFBundleIdentifier - be.ugent.zeus.Hydra${CUSTOM_IDENTIFIER_SUFFIX}.${PRODUCT_NAME:rfc1034identifier} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -21,11 +21,11 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.0.1 + 2.1 CFBundleSignature ???? CFBundleVersion - 2.0.15 + 2.1.9 NSAppTransportSecurity NSExceptionDomains diff --git a/RestoMenuToday/RestoManager.swift b/RestoMenuToday/RestoManager.swift index d8b0a3b..ab5989b 100644 --- a/RestoMenuToday/RestoManager.swift +++ b/RestoMenuToday/RestoManager.swift @@ -60,7 +60,7 @@ class RestoManager: NSObject { */ func retrieveMenuForDate(date: NSDate, completionHandler: (menu: Menu?, error: NSError?) -> ()) { // Construct the URL for the API request based on the year and week of the given date - let dateComponents = NSCalendar.currentCalendar().components([.NSWeekOfYearCalendarUnit, .NSYearCalendarUnit], fromDate: date) + let dateComponents = NSCalendar.currentCalendar().components([.WeekOfYear, .Year], fromDate: date) let URL = NSURL(string: "https://zeus.ugent.be/hydra/api/1.0/resto/menu/\(dateComponents.year)/\(dateComponents.weekOfYear).json") // We're relying on NSURLCache to cache the data for us when the user is offline diff --git a/RestoMenuToday/TodayViewController.swift b/RestoMenuToday/TodayViewController.swift index 0733620..a5987e3 100644 --- a/RestoMenuToday/TodayViewController.swift +++ b/RestoMenuToday/TodayViewController.swift @@ -85,11 +85,11 @@ class TodayViewController: UIViewController, UITableViewDataSource, UITableViewD // MARK: NCWidgetProviding - func widgetPerformUpdateWithCompletionHandler(_ completionHandler: ((NCUpdateResult) -> Void) = {result in return}) { + func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void) = {result in return}) { let calendar = NSCalendar.currentCalendar() // Call the completion with no data as update result when we already have a menu for the given date - if self.menu != nil && calendar.ordinalityOfUnit(.NSDayCalendarUnit, inUnit: .NSEraCalendarUnit, forDate: self.menu.date) == calendar.ordinalityOfUnit(.NSDayCalendarUnit, inUnit: .NSEraCalendarUnit, forDate: NSDate()){ + if self.menu != nil && calendar.ordinalityOfUnit(.Day, inUnit: .Era, forDate: self.menu.date) == calendar.ordinalityOfUnit(.Day, inUnit: .Era, forDate: NSDate()){ completionHandler(.NoData) return } diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist new file mode 100644 index 0000000..49811ab --- /dev/null +++ b/Settings.bundle/Root.plist @@ -0,0 +1,31 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSToggleSwitchSpecifier + Title + Reset App + Key + reset_app_preference + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + First Launch + Key + first_launch_preference + DefaultValue + + + + + diff --git a/Settings.bundle/nl.lproj/Root.strings b/Settings.bundle/nl.lproj/Root.strings new file mode 100644 index 0000000..1cf11b7 Binary files /dev/null and b/Settings.bundle/nl.lproj/Root.strings differ diff --git a/UGentOAuthConfig.plist.example b/UGentOAuthConfig.plist.example new file mode 100644 index 0000000..a8b0cb9 --- /dev/null +++ b/UGentOAuthConfig.plist.example @@ -0,0 +1,13 @@ + + + + + UGentOAuth2 + + ClientID + *** CLIENT ID *** + ClientSecret + *** SECRET ID *** + + +