From 40f8f72dc2908f58c307ec9161f5673984d7fbcf Mon Sep 17 00:00:00 2001 From: Rehaan <5091484+r3khaan@users.noreply.github.com> Date: Sun, 11 Apr 2021 19:48:38 -0400 Subject: [PATCH 01/18] Updated version to 6.5.6; Added PCA to release version (#386) * Updated version numbers; fixed crashing more page * Updated build version 6561 --- PennMobile.xcodeproj/project.pbxproj | 110 +++++++++--------- .../xcschemes/PennMobile.xcscheme | 2 +- .../Setup + Navigation/ControllerModel.swift | 8 +- PennMobile/Supporting_Files/Info.plist | 4 +- 4 files changed, 64 insertions(+), 60 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 16a2d85b5..4008e6c61 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -234,12 +234,12 @@ F212BE8623B6DA8D00ED46A1 /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F212BE8523B6DA8D00ED46A1 /* PrivacyTableViewCell.swift */; }; F212BE8823B71C7100ED46A1 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F212BE8723B71C7100ED46A1 /* NotificationsTableViewController.swift */; }; F212BE8A23B71C8500ED46A1 /* NotificationsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F212BE8923B71C8500ED46A1 /* NotificationsTableViewCell.swift */; }; - F213CCE223C3EE3E000AD90F /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE123C3EE3E000AD90F /* SwiftPackageProductDependency */; }; - F213CCE523C3F240000AD90F /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE423C3F240000AD90F /* SwiftPackageProductDependency */; }; - F213CCE723C3F240000AD90F /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE623C3F240000AD90F /* SwiftPackageProductDependency */; }; - F213CCEA23C3F5D5000AD90F /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE923C3F5D5000AD90F /* SwiftPackageProductDependency */; }; - F213CCED23C3F6A8000AD90F /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCEC23C3F6A8000AD90F /* SwiftPackageProductDependency */; }; - F213CCF023C3F99B000AD90F /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCEF23C3F99B000AD90F /* SwiftPackageProductDependency */; }; + F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE123C3EE3E000AD90F /* SwiftSoup */; }; + F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE423C3F240000AD90F /* KingfisherSwiftUI */; }; + F213CCE723C3F240000AD90F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE623C3F240000AD90F /* Kingfisher */; }; + F213CCEA23C3F5D5000AD90F /* SCLAlertView in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE923C3F5D5000AD90F /* SCLAlertView */; }; + F213CCED23C3F6A8000AD90F /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCEC23C3F6A8000AD90F /* SwiftyJSON */; }; + F213CCF023C3F99B000AD90F /* ScrollableGraphView in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCEF23C3F99B000AD90F /* ScrollableGraphView */; }; F230FB11225809E900760499 /* AutomatedScreenshotUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F230FB10225809E900760499 /* AutomatedScreenshotUITests.swift */; }; F230FB1A22580ACA00760499 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F230FB1922580ACA00760499 /* SnapshotHelper.swift */; }; F23CC0BB235E3EF10007317A /* DiningVenue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23CC0BA235E3EF10007317A /* DiningVenue.swift */; }; @@ -257,7 +257,7 @@ F2568A692411F20E00561295 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2568A682411F20E00561295 /* NotificationService.swift */; }; F2568A6D2411F20E00561295 /* NotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F2568A662411F20E00561295 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F2568A732411F26500561295 /* NotificationService+ImageCacheing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2568A722411F26500561295 /* NotificationService+ImageCacheing.swift */; }; - F2568A762413534F00561295 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = F2568A752413534F00561295 /* SwiftPackageProductDependency */; }; + F2568A762413534F00561295 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = F2568A752413534F00561295 /* SnapKit */; }; F2568A7824135B6500561295 /* HomeCellHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2568A7724135B6500561295 /* HomeCellHeader.swift */; }; F2568A7A2413637D00561295 /* HomeCellSafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2568A792413637D00561295 /* HomeCellSafeArea.swift */; }; F25AFDDB25C3E5E900B3AC80 /* ZeroCourseAlertsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F25AFDDA25C3E5E900B3AC80 /* ZeroCourseAlertsCell.swift */; }; @@ -591,13 +591,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F213CCED23C3F6A8000AD90F /* BuildFile in Frameworks */, - F2568A762413534F00561295 /* BuildFile in Frameworks */, - F213CCF023C3F99B000AD90F /* BuildFile in Frameworks */, - F213CCE723C3F240000AD90F /* BuildFile in Frameworks */, - F213CCE523C3F240000AD90F /* BuildFile in Frameworks */, - F213CCE223C3EE3E000AD90F /* BuildFile in Frameworks */, - F213CCEA23C3F5D5000AD90F /* BuildFile in Frameworks */, + F213CCED23C3F6A8000AD90F /* SwiftyJSON in Frameworks */, + F2568A762413534F00561295 /* SnapKit in Frameworks */, + F213CCF023C3F99B000AD90F /* ScrollableGraphView in Frameworks */, + F213CCE723C3F240000AD90F /* Kingfisher in Frameworks */, + F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */, + F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */, + F213CCEA23C3F5D5000AD90F /* SCLAlertView in Frameworks */, 56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1569,13 +1569,13 @@ ); name = PennMobile; packageProductDependencies = ( - F213CCE123C3EE3E000AD90F /* SwiftPackageProductDependency */, - F213CCE423C3F240000AD90F /* SwiftPackageProductDependency */, - F213CCE623C3F240000AD90F /* SwiftPackageProductDependency */, - F213CCE923C3F5D5000AD90F /* SwiftPackageProductDependency */, - F213CCEC23C3F6A8000AD90F /* SwiftPackageProductDependency */, - F213CCEF23C3F99B000AD90F /* SwiftPackageProductDependency */, - F2568A752413534F00561295 /* SwiftPackageProductDependency */, + F213CCE123C3EE3E000AD90F /* SwiftSoup */, + F213CCE423C3F240000AD90F /* KingfisherSwiftUI */, + F213CCE623C3F240000AD90F /* Kingfisher */, + F213CCE923C3F5D5000AD90F /* SCLAlertView */, + F213CCEC23C3F6A8000AD90F /* SwiftyJSON */, + F213CCEF23C3F99B000AD90F /* ScrollableGraphView */, + F2568A752413534F00561295 /* SnapKit */, ); productName = PennMobile; productReference = 216640601EBADADA00746B8E /* PennMobile.app */; @@ -1662,12 +1662,12 @@ ); mainGroup = 216640571EBADADA00746B8E; packageReferences = ( - F213CCE023C3EE3E000AD90F /* RemoteSwiftPackageReference */, - F213CCE323C3F240000AD90F /* RemoteSwiftPackageReference */, - F213CCE823C3F5D5000AD90F /* RemoteSwiftPackageReference */, - F213CCEB23C3F6A8000AD90F /* RemoteSwiftPackageReference */, - F213CCEE23C3F99B000AD90F /* RemoteSwiftPackageReference */, - F2568A742413534F00561295 /* RemoteSwiftPackageReference */, + F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */, + F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */, + F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */, + F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */, + F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -2111,8 +2111,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CF_BUNDLE_LONG_VERSION_STRING = 6530; - CF_BUNDLE_SHORT_VERSION_STRING = 6.5.3; + CF_BUNDLE_LONG_VERSION_STRING = 6560; + CF_BUNDLE_SHORT_VERSION_STRING = 6.5.6; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2172,8 +2172,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CF_BUNDLE_LONG_VERSION_STRING = 6530; - CF_BUNDLE_SHORT_VERSION_STRING = 6.5.3; + CF_BUNDLE_LONG_VERSION_STRING = 6560; + CF_BUNDLE_SHORT_VERSION_STRING = 6.5.6; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2233,7 +2233,7 @@ CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(CF_BUNDLE_LONG_VERSION_STRING)"; + CURRENT_PROJECT_VERSION = 6561; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = VU59R57FGM; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; @@ -2243,7 +2243,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "$(CF_BUNDLE_SHORT_VERSION_STRING)"; + MARKETING_VERSION = 6.5.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2317,7 +2317,7 @@ CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(CF_BUNDLE_LONG_VERSION_STRING)"; + CURRENT_PROJECT_VERSION = 6561; DEVELOPMENT_TEAM = VU59R57FGM; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; INFOPLIST_FILE = PennMobile/Supporting_Files/Info.plist; @@ -2326,7 +2326,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "$(CF_BUNDLE_SHORT_VERSION_STRING)"; + MARKETING_VERSION = 6.5.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2541,7 +2541,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - F213CCE023C3EE3E000AD90F /* RemoteSwiftPackageReference */ = { + F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup"; requirement = { @@ -2549,7 +2549,7 @@ minimumVersion = 2.3.0; }; }; - F213CCE323C3F240000AD90F /* RemoteSwiftPackageReference */ = { + F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { @@ -2557,7 +2557,7 @@ minimumVersion = 5.12.0; }; }; - F213CCE823C3F5D5000AD90F /* RemoteSwiftPackageReference */ = { + F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pennlabs/SCLAlertView-Swift/"; requirement = { @@ -2565,7 +2565,7 @@ minimumVersion = 0.8.2; }; }; - F213CCEB23C3F6A8000AD90F /* RemoteSwiftPackageReference */ = { + F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; requirement = { @@ -2573,7 +2573,7 @@ minimumVersion = 5.0.0; }; }; - F213CCEE23C3F99B000AD90F /* RemoteSwiftPackageReference */ = { + F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pennlabs/ScrollableGraphView"; requirement = { @@ -2581,7 +2581,7 @@ minimumVersion = 4.1.0; }; }; - F2568A742413534F00561295 /* RemoteSwiftPackageReference */ = { + F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit"; requirement = { @@ -2592,39 +2592,39 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - F213CCE123C3EE3E000AD90F /* SwiftPackageProductDependency */ = { + F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; - package = F213CCE023C3EE3E000AD90F /* RemoteSwiftPackageReference */; + package = F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - F213CCE423C3F240000AD90F /* SwiftPackageProductDependency */ = { + F213CCE423C3F240000AD90F /* KingfisherSwiftUI */ = { isa = XCSwiftPackageProductDependency; - package = F213CCE323C3F240000AD90F /* RemoteSwiftPackageReference */; + package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = KingfisherSwiftUI; }; - F213CCE623C3F240000AD90F /* SwiftPackageProductDependency */ = { + F213CCE623C3F240000AD90F /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; - package = F213CCE323C3F240000AD90F /* RemoteSwiftPackageReference */; + package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - F213CCE923C3F5D5000AD90F /* SwiftPackageProductDependency */ = { + F213CCE923C3F5D5000AD90F /* SCLAlertView */ = { isa = XCSwiftPackageProductDependency; - package = F213CCE823C3F5D5000AD90F /* RemoteSwiftPackageReference */; + package = F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */; productName = SCLAlertView; }; - F213CCEC23C3F6A8000AD90F /* SwiftPackageProductDependency */ = { + F213CCEC23C3F6A8000AD90F /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; - package = F213CCEB23C3F6A8000AD90F /* RemoteSwiftPackageReference */; + package = F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; - F213CCEF23C3F99B000AD90F /* SwiftPackageProductDependency */ = { + F213CCEF23C3F99B000AD90F /* ScrollableGraphView */ = { isa = XCSwiftPackageProductDependency; - package = F213CCEE23C3F99B000AD90F /* RemoteSwiftPackageReference */; + package = F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */; productName = ScrollableGraphView; }; - F2568A752413534F00561295 /* SwiftPackageProductDependency */ = { + F2568A752413534F00561295 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = F2568A742413534F00561295 /* RemoteSwiftPackageReference */; + package = F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; /* End XCSwiftPackageProductDependency section */ diff --git a/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme b/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme index 0855db3bb..fec27927d 100644 --- a/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme +++ b/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme @@ -78,7 +78,7 @@ - ITSAppUsesNonExemptEncryption - CFBundleDevelopmentRegion en CFBundleDisplayName @@ -36,6 +34,8 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity From 657777f14b41c2878c6cf591d9c5824c7f8da9bc Mon Sep 17 00:00:00 2001 From: r3khaan Date: Sun, 11 Apr 2021 20:23:57 -0400 Subject: [PATCH 02/18] Modified schema to run DEBUG not release; Added build active architecture only for release too --- PennMobile.xcodeproj/project.pbxproj | 3 +++ .../xcshareddata/xcschemes/PennMobile.xcscheme | 2 +- PennMobile/General/Networking + Analytics/UserDBManager.swift | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 4008e6c61..bc610648a 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -2236,6 +2236,7 @@ CURRENT_PROJECT_VERSION = 6561; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = VU59R57FGM; + EXCLUDED_ARCHS = ""; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; INFOPLIST_FILE = PennMobile/Supporting_Files/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -2319,6 +2320,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 6561; DEVELOPMENT_TEAM = VU59R57FGM; + EXCLUDED_ARCHS = ""; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; INFOPLIST_FILE = PennMobile/Supporting_Files/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -2327,6 +2329,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 6.5.6; + ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme b/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme index fec27927d..0855db3bb 100644 --- a/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme +++ b/PennMobile.xcodeproj/xcshareddata/xcschemes/PennMobile.xcscheme @@ -78,7 +78,7 @@ Void)? = nil) { - let url = "\(baseUrl)/notifications/register" + var url = "\(baseUrl)/notifications/register" var params: [String: Any] = ["ios_token": deviceToken] #if DEBUG + #warning("Remove this!") + url = "localhost:5000/notifications/register" params["dev"] = true #endif makePostRequestWithAccessToken(url: url, params: params) { (_, _, _) in From 3ca10eb7b9e2bca46391fc40d1bbfe9a6a746dfb Mon Sep 17 00:00:00 2001 From: r3khaan Date: Mon, 12 Apr 2021 21:28:37 -0400 Subject: [PATCH 03/18] Changed Push Notifications to not point to localhost (accident) --- PennMobile/General/Networking + Analytics/UserDBManager.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PennMobile/General/Networking + Analytics/UserDBManager.swift b/PennMobile/General/Networking + Analytics/UserDBManager.swift index aede0f224..d2a4b31d2 100644 --- a/PennMobile/General/Networking + Analytics/UserDBManager.swift +++ b/PennMobile/General/Networking + Analytics/UserDBManager.swift @@ -433,11 +433,9 @@ extension UserDBManager { // MARK: - Push Notifications extension UserDBManager { func savePushNotificationDeviceToken(deviceToken: String, _ completion: (() -> Void)? = nil) { - var url = "\(baseUrl)/notifications/register" + let url = "\(baseUrl)/notifications/register" var params: [String: Any] = ["ios_token": deviceToken] #if DEBUG - #warning("Remove this!") - url = "localhost:5000/notifications/register" params["dev"] = true #endif makePostRequestWithAccessToken(url: url, params: params) { (_, _, _) in From 265323539dcaddbd26a4e5ca8e087111d5bcb510 Mon Sep 17 00:00:00 2001 From: 01jongmin <49562117+01jongmin@users.noreply.github.com> Date: Mon, 6 Sep 2021 12:11:04 -0400 Subject: [PATCH 04/18] GSR fix complete (#389) --- PennMobile.xcodeproj/project.pbxproj | 71 ++-- PennMobile/Auth/OAuth2NetworkManager.swift | 4 + .../GSR-Booking/Controllers/GSRBookable.swift | 23 +- .../Controllers/GSRController.swift | 234 ++++++------ .../Controllers/GSRDeletable.swift | 14 +- .../GSRGroupConfirmBookingController.swift | 2 +- .../Controllers/GSRLocationsController.swift | 7 +- .../Controllers/GSRLoginController.swift | 8 +- .../GSRReservationsController.swift | 160 ++++----- .../Controllers/GSRTabController.swift | 4 +- .../GSR-Booking/Model/GSRAPIResponse.swift | 21 ++ PennMobile/GSR-Booking/Model/GSRBooking.swift | 95 +++-- .../GSR-Booking/Model/GSRLocation.swift | 9 +- .../GSR-Booking/Model/GSRLocationModel.swift | 27 +- .../GSR-Booking/Model/GSRReservation.swift | 13 +- PennMobile/GSR-Booking/Model/GSRRoom.swift | 117 ++---- .../GSR-Booking/Model/GSRTimeSlot.swift | 53 +-- PennMobile/GSR-Booking/Model/GSRVenue.swift | 20 -- PennMobile/GSR-Booking/Model/locations.json | 64 ---- .../Networking/GSRNetworkManager.swift | 333 +++++++----------- .../GSR-Booking/ViewModel/GSRViewModel.swift | 146 +++----- .../GSR-Booking/Views/GSRLocationCell.swift | 5 +- .../GSR-Booking/Views/GSRTimeCell.swift | 17 +- .../GSR-Booking/Views/ReservationCell.swift | 12 +- PennMobile/GSR-Booking/Views/RoomCell.swift | 68 ++-- PennMobile/General/Extensions.swift | 15 + .../HomeReservationsCellItem.swift | 5 +- .../Home/Cells/GSR/HomeGSRCellItem.swift | 98 +++--- .../Home/Cells/GSR/HomeStudyRoomCell.swift | 2 +- .../HomeViewController + Delegates.swift | 28 +- PennMobile/Home/Map/PennCoordinate.swift | 5 - .../Home/Networking/HomeAPIService.swift | 2 - .../RootViewController.swift | 1 + Podfile.lock | 6 +- 34 files changed, 727 insertions(+), 962 deletions(-) create mode 100644 PennMobile/GSR-Booking/Model/GSRAPIResponse.swift delete mode 100644 PennMobile/GSR-Booking/Model/GSRVenue.swift delete mode 100644 PennMobile/GSR-Booking/Model/locations.json diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index bc610648a..1e7fd824b 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -81,14 +81,11 @@ 2189C08C2027CE2600771C1F /* GSRTimeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0882027CE2600771C1F /* GSRTimeCell.swift */; }; 2189C08D2027CE2600771C1F /* RoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0892027CE2600771C1F /* RoomCell.swift */; }; 2189C08E2027CE2600771C1F /* GSRRangeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C08A2027CE2600771C1F /* GSRRangeSlider.swift */; }; - 2189C08F2027CE2600771C1F /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C08B2027CE2600771C1F /* EmptyView.swift */; }; 2189C0912027CE2E00771C1F /* GSRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0902027CE2E00771C1F /* GSRViewModel.swift */; }; 2189C09B2027CE4100771C1F /* GSRTimeSlot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0922027CE3F00771C1F /* GSRTimeSlot.swift */; }; 2189C09C2027CE4100771C1F /* GSRDateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0932027CE3F00771C1F /* GSRDateHandler.swift */; }; 2189C09D2027CE4100771C1F /* GSRLocationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0942027CE3F00771C1F /* GSRLocationModel.swift */; }; - 2189C09E2027CE4100771C1F /* locations.json in Resources */ = {isa = PBXBuildFile; fileRef = 2189C0952027CE4000771C1F /* locations.json */; }; 2189C09F2027CE4100771C1F /* GSRUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0962027CE4000771C1F /* GSRUser.swift */; }; - 2189C0A02027CE4100771C1F /* GSRVenue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0972027CE4000771C1F /* GSRVenue.swift */; }; 2189C0A12027CE4100771C1F /* GSRBooking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0982027CE4000771C1F /* GSRBooking.swift */; }; 2189C0A22027CE4100771C1F /* GSRRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C0992027CE4000771C1F /* GSRRoom.swift */; }; 2189C0A32027CE4100771C1F /* GSRLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2189C09A2027CE4000771C1F /* GSRLocation.swift */; }; @@ -147,6 +144,9 @@ 66E8ECA52381CAA900945BEA /* TOTPFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA42381CAA800945BEA /* TOTPFetcher.swift */; }; 66E8ECA72381CB5100945BEA /* TwoFactorTokenGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA62381CB5100945BEA /* TwoFactorTokenGenerator.swift */; }; 66E8ECA92381CD2F00945BEA /* TOTPNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA82381CD2F00945BEA /* TOTPNetworkManager.swift */; }; + 6C23AF9526E57903002F60F0 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C23AF9426E57903002F60F0 /* EmptyView.swift */; }; + 6C369A1326E299A900721CA1 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 6C369A1226E299A900721CA1 /* Alamofire */; }; + 6C369A1526E39BC100721CA1 /* GSRAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */; }; 97AA806923D26BC700C23488 /* DiningBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */; }; 97AA806A23D26BC700C23488 /* TransactionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806523D26BC700C23488 /* TransactionTableViewCell.swift */; }; 97AA806B23D26BC700C23488 /* DiningHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806623D26BC700C23488 /* DiningHeaderView.swift */; }; @@ -390,14 +390,11 @@ 2189C0882027CE2600771C1F /* GSRTimeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRTimeCell.swift; sourceTree = ""; }; 2189C0892027CE2600771C1F /* RoomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomCell.swift; sourceTree = ""; }; 2189C08A2027CE2600771C1F /* GSRRangeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRRangeSlider.swift; sourceTree = ""; }; - 2189C08B2027CE2600771C1F /* EmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; }; 2189C0902027CE2E00771C1F /* GSRViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRViewModel.swift; sourceTree = ""; }; 2189C0922027CE3F00771C1F /* GSRTimeSlot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRTimeSlot.swift; sourceTree = ""; }; 2189C0932027CE3F00771C1F /* GSRDateHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRDateHandler.swift; sourceTree = ""; }; 2189C0942027CE3F00771C1F /* GSRLocationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRLocationModel.swift; sourceTree = ""; }; - 2189C0952027CE4000771C1F /* locations.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = locations.json; sourceTree = ""; }; 2189C0962027CE4000771C1F /* GSRUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRUser.swift; sourceTree = ""; }; - 2189C0972027CE4000771C1F /* GSRVenue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRVenue.swift; sourceTree = ""; }; 2189C0982027CE4000771C1F /* GSRBooking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRBooking.swift; sourceTree = ""; }; 2189C0992027CE4000771C1F /* GSRRoom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRRoom.swift; sourceTree = ""; }; 2189C09A2027CE4000771C1F /* GSRLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRLocation.swift; sourceTree = ""; }; @@ -455,6 +452,8 @@ 66E8ECA42381CAA800945BEA /* TOTPFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPFetcher.swift; sourceTree = ""; }; 66E8ECA62381CB5100945BEA /* TwoFactorTokenGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorTokenGenerator.swift; sourceTree = ""; }; 66E8ECA82381CD2F00945BEA /* TOTPNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPNetworkManager.swift; sourceTree = ""; }; + 6C23AF9426E57903002F60F0 /* EmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; }; + 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSRAPIResponse.swift; sourceTree = ""; }; 6DBD80716B161DAEA6274151 /* Pods-AutomatedScreenshotUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutomatedScreenshotUITests.release.xcconfig"; path = "Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests.release.xcconfig"; sourceTree = ""; }; 6F4DC86EE3A48EC9A02145AE /* Pods-PennMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PennMobile.debug.xcconfig"; path = "Target Support Files/Pods-PennMobile/Pods-PennMobile.debug.xcconfig"; sourceTree = ""; }; 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningBalanceCell.swift; sourceTree = ""; }; @@ -591,6 +590,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6C369A1326E299A900721CA1 /* Alamofire in Frameworks */, F213CCED23C3F6A8000AD90F /* SwiftyJSON in Frameworks */, F2568A762413534F00561295 /* SnapKit in Frameworks */, F213CCF023C3F99B000AD90F /* ScrollableGraphView in Frameworks */, @@ -986,16 +986,15 @@ 2189C0752027CD3A00771C1F /* Model */ = { isa = PBXGroup; children = ( + 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */, + 2189C0992027CE4000771C1F /* GSRRoom.swift */, EFE2D6FF239B124D0020F6BF /* GSRGroupUser.swift */, 2189C0982027CE4000771C1F /* GSRBooking.swift */, 21A6B6D122162702003A357D /* GSRReservation.swift */, 2189C0932027CE3F00771C1F /* GSRDateHandler.swift */, 2189C0942027CE3F00771C1F /* GSRLocationModel.swift */, - 2189C0992027CE4000771C1F /* GSRRoom.swift */, 2189C0922027CE3F00771C1F /* GSRTimeSlot.swift */, 2189C0962027CE4000771C1F /* GSRUser.swift */, - 2189C0972027CE4000771C1F /* GSRVenue.swift */, - 2189C0952027CE4000771C1F /* locations.json */, 2189C09A2027CE4000771C1F /* GSRLocation.swift */, 2138D55E22599D4700D67CA2 /* GSRGroup.swift */, ); @@ -1015,13 +1014,13 @@ isa = PBXGroup; children = ( EFE2D6E6239B11050020F6BF /* GSRGroups */, - 2189C08B2027CE2600771C1F /* EmptyView.swift */, F2B8C40B252C58E600922D08 /* GSRClosedView.swift */, 2189C08A2027CE2600771C1F /* GSRRangeSlider.swift */, 2189C0882027CE2600771C1F /* GSRTimeCell.swift */, 2138E1F72252AFB500E4055A /* GSRLocationCell.swift */, 2189C0892027CE2600771C1F /* RoomCell.swift */, 21A6B6D32216824B003A357D /* ReservationCell.swift */, + 6C23AF9426E57903002F60F0 /* EmptyView.swift */, C1D90F1C2220A25700DAB8EE /* NoReservationsCell.swift */, ); path = Views; @@ -1576,6 +1575,7 @@ F213CCEC23C3F6A8000AD90F /* SwiftyJSON */, F213CCEF23C3F99B000AD90F /* ScrollableGraphView */, F2568A752413534F00561295 /* SnapKit */, + 6C369A1226E299A900721CA1 /* Alamofire */, ); productName = PennMobile; productReference = 216640601EBADADA00746B8E /* PennMobile.app */; @@ -1668,6 +1668,7 @@ F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */, F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */, F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, + 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */, ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -1687,7 +1688,6 @@ files = ( 2166406E1EBADADA00746B8E /* LaunchScreen.storyboard in Resources */, B6111DB22018497400DC7877 /* GoogleService-Info.plist in Resources */, - 2189C09E2027CE4100771C1F /* locations.json in Resources */, 2190FD2F1EBBC8BA00EC683C /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1736,12 +1736,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/SimulatorStatusMagic/SimulatorStatusMagic.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SimulatorStatusMagic.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1775,12 +1776,23 @@ buildActionMask = 12; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-PennMobile/Pods-PennMobile-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-PennMobile/Pods-PennMobile-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Base32/Base32.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework", + "${BUILT_PRODUCTS_DIR}/OneTimePassword/OneTimePassword.framework", + "${BUILT_PRODUCTS_DIR}/XLPagerTabStrip/XLPagerTabStrip.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-PennMobile/Pods-PennMobile-frameworks-${CONFIGURATION}-output-files.xcfilelist", + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Base32.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MBProgressHUD.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OneTimePassword.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/XLPagerTabStrip.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1860,9 +1872,9 @@ F23CC0BF235E3F610007317A /* DiningVenue+Codable.swift in Sources */, F299616F23F78D9B00C67A3B /* AppDelegate+NotificationExtension.swift in Sources */, 21B8BBB82223963C00EBC1D0 /* Degree.swift in Sources */, + 6C369A1526E39BC100721CA1 /* GSRAPIResponse.swift in Sources */, 21640D6020105BB4002F33CA /* HomeStudyRoomCell.swift in Sources */, 212B835C222A37D000F835D6 /* HomeFeatureCellItem.swift in Sources */, - 2189C08F2027CE2600771C1F /* EmptyView.swift in Sources */, 2119D26922529A2300693CDB /* HomeGSRLocationsCell.swift in Sources */, 21D5E07623BD45F400B331CC /* KeychainAccessible.swift in Sources */, 21470EBC223DD34200019C10 /* ScheduleEventCell.swift in Sources */, @@ -1954,6 +1966,7 @@ C10A59A42183CDD60059130B /* AboutPageCollectionViewCell.swift in Sources */, EF6329A22409D2CE00E7ED36 /* GSRGroupConfirmBookingController.swift in Sources */, 21D5E07423BCFE0300B331CC /* CoursePrivacyController.swift in Sources */, + 6C23AF9526E57903002F60F0 /* EmptyView.swift in Sources */, B658393B1FABD295009486FC /* LaundryAPIService.swift in Sources */, 21ABE2A9223D7DEA00199D29 /* ScheduleLayout.swift in Sources */, 97E79E042100DA1200D3D606 /* BuildingHeaderCell.swift in Sources */, @@ -1998,7 +2011,6 @@ F2C7E5F5259EE1CA0043A98A /* CourseAlertSettings.swift in Sources */, 211DA1AF204921030065BC2C /* LaundryMachinesView.swift in Sources */, EFE2D6F2239B11050020F6BF /* GroupMemberCell.swift in Sources */, - 2189C0A02027CE4100771C1F /* GSRVenue.swift in Sources */, 215C605D205C88EA00FE271A /* ImageNetworkingManager.swift in Sources */, 2189C0872027CDD700771C1F /* GSRNetworkManager.swift in Sources */, 2138D55522598AA800D67CA2 /* GSRTabController.swift in Sources */, @@ -2239,7 +2251,7 @@ EXCLUDED_ARCHS = ""; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; INFOPLIST_FILE = PennMobile/Supporting_Files/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2323,7 +2335,7 @@ EXCLUDED_ARCHS = ""; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; INFOPLIST_FILE = PennMobile/Supporting_Files/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2544,6 +2556,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.4.3; + }; + }; F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup"; @@ -2595,6 +2615,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 6C369A1226E299A900721CA1 /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */; diff --git a/PennMobile/Auth/OAuth2NetworkManager.swift b/PennMobile/Auth/OAuth2NetworkManager.swift index a854a0ab8..9cc261906 100644 --- a/PennMobile/Auth/OAuth2NetworkManager.swift +++ b/PennMobile/Auth/OAuth2NetworkManager.swift @@ -227,6 +227,10 @@ extension OAuth2NetworkManager { try? secureStore.removeValue(for: secureKey) } + func clearCurrentAccessToken() { + currentAccessToken = nil + } + func hasRefreshToken() -> Bool { return getRefreshToken() != nil } diff --git a/PennMobile/GSR-Booking/Controllers/GSRBookable.swift b/PennMobile/GSR-Booking/Controllers/GSRBookable.swift index e6a570ceb..0c165ff99 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRBookable.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRBookable.swift @@ -12,23 +12,26 @@ import SCLAlertView protocol GSRBookable: IndicatorEnabled {} extension GSRBookable where Self: UIViewController { - func submitBooking(for booking: GSRBooking, _ completion: @escaping (_ success: Bool) -> Void) { + func submitBooking(for booking: GSRBooking) { self.showActivity() - GSRNetworkManager.instance.makeBooking(for: booking) { (success, errorMessage) in + GSRNetworkManager.instance.makeBooking(for: booking) { result in DispatchQueue.main.async { self.hideActivity() let alertView = SCLAlertView() - var result: FirebaseAnalyticsManager.EventResult = .failed - if success { - alertView.showSuccess("Success!", subTitle: "You booked a space in \(booking.location.name). You should receive a confirmation email in the next few minutes.") - result = .success + var firebaseResult: FirebaseAnalyticsManager.EventResult + + switch result { + case .success: + alertView.showSuccess("Success!", subTitle: "You booked a space in \(booking.roomName). You should receive a confirmation email in the next few minutes.") + firebaseResult = .success guard let homeVC = ControllerModel.shared.viewController(for: .home) as? HomeViewController else { return } homeVC.clearCache() - } else if let msg = errorMessage { - alertView.showError("Uh oh!", subTitle: msg) + case .failure: + alertView.showError("Uh oh!", subTitle: "You seem to have exceeded the booking limit for this venue.") + firebaseResult = .failed } - FirebaseAnalyticsManager.shared.trackEvent(action: .attemptBooking, result: result, content: booking.location.name) - completion(success) + + FirebaseAnalyticsManager.shared.trackEvent(action: .attemptBooking, result: firebaseResult, content: booking.roomName) } } } diff --git a/PennMobile/GSR-Booking/Controllers/GSRController.swift b/PennMobile/GSR-Booking/Controllers/GSRController.swift index 9168f16f1..1f1f5b781 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRController.swift @@ -8,17 +8,18 @@ import UIKit import WebKit +import SCLAlertView -class GSRController: GenericViewController, IndicatorEnabled { +class GSRController: GenericViewController, IndicatorEnabled, ShowsAlert { // MARK: UI Elements fileprivate var tableView: UITableView! fileprivate var rangeSlider: GSRRangeSlider! fileprivate var pickerView: UIPickerView! - fileprivate var emptyView: EmptyView! fileprivate var closedView: GSRClosedView! fileprivate var barButton: UIBarButtonItem! fileprivate var bookingsBarButton: UIBarButtonItem! + fileprivate var limitedAccessLabel: UILabel! var group: GSRGroup? @@ -27,8 +28,6 @@ class GSRController: GenericViewController, IndicatorEnabled { var currentDay = Date() fileprivate var viewModel: GSRViewModel! - - var loadingView: UIActivityIndicatorView! var startingLocation: GSRLocation! @@ -39,7 +38,6 @@ class GSRController: GenericViewController, IndicatorEnabled { let index = viewModel.getLocationIndex(startingLocation) self.pickerView.selectRow(index, inComponent: 1, animated: true) - } override func viewWillAppear(_ animated: Bool) { @@ -77,9 +75,8 @@ extension GSRController { preparePickerView() prepareRangeSlider() prepareTableView() - prepareEmptyView() - prepareLoadingView() prepareClosedView() + prepareLimitedAccessLabel() } private func preparePickerView() { @@ -111,14 +108,6 @@ extension GSRController { view.addSubview(tableView) _ = tableView.anchor(rangeSlider.bottomAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, topConstant: 8, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) } - - private func prepareEmptyView() { - emptyView = EmptyView() - emptyView.isHidden = true - - view.addSubview(emptyView) - _ = emptyView.anchor(tableView.topAnchor, left: tableView.leftAnchor, bottom: tableView.bottomAnchor, right: tableView.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) - } private func prepareClosedView() { closedView = GSRClosedView() @@ -127,6 +116,20 @@ extension GSRController { view.addSubview(closedView) _ = closedView.anchor(tableView.topAnchor, left: tableView.leftAnchor, bottom: tableView.bottomAnchor, right: tableView.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) } + + private func prepareLimitedAccessLabel() { + limitedAccessLabel = UILabel() + limitedAccessLabel.isHidden = true + limitedAccessLabel.numberOfLines = 0 + limitedAccessLabel.textAlignment = .center + + view.addSubview(limitedAccessLabel) + limitedAccessLabel.translatesAutoresizingMaskIntoConstraints = false + limitedAccessLabel.topAnchor.constraint(equalTo: rangeSlider.topAnchor, constant: Padding.pad).isActive = true + limitedAccessLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Padding.pad).isActive = true + limitedAccessLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Padding.pad).isActive = true + limitedAccessLabel.heightAnchor.constraint(equalToConstant: 200).isActive = true + } } // MARK: - Prepare View Model @@ -140,21 +143,44 @@ extension GSRController { // MARK: - ViewModelDelegate + Networking extension GSRController: GSRViewModelDelegate { + func resetDataForCell(at indexPath: IndexPath) { + (tableView.cellForRow(at: indexPath) as? RoomCell)?.resetSelection() + } + func fetchData() { let location = viewModel.getSelectedLocation() let date = viewModel.getSelectedDate() + self.showActivity() - self.startLoadingViewAnimation() - - GSRNetworkManager.instance.getAvailability(for: location.lid, date: date) { (rooms) in - + GSRNetworkManager.instance.getAvailability(lid: location.lid, gid: location.gid, startDate: date.string) { result in DispatchQueue.main.async { - let rooms = rooms ?? [] - self.viewModel.updateData(with: rooms) - self.refreshDataUI() - self.rangeSlider.reload() - self.refreshBarButton() - self.stopLoadingViewAnimation() + self.limitedAccessLabel.isHidden = true + self.tableView.isHidden = false + self.hideActivity() + switch result { + case .success(let rooms): + self.viewModel.updateData(with: rooms) + self.refreshDataUI() + self.rangeSlider.reload() + self.refreshBarButton() + case .failure: + if location.gid == 1 { + if !Account.isLoggedIn { + self.limitedAccessLabel.isHidden = false + self.tableView.isHidden = true + self.limitedAccessLabel.text = "You need to log in with a Wharton pennkey to access Wharton GSRs" + return + } + + if Account.isLoggedIn && !UserDefaults.standard.isInWharton() { + self.limitedAccessLabel.isHidden = false + self.tableView.isHidden = true + self.limitedAccessLabel.text = "You need to have a Wharton pennkey to access Wharton GSRs" + return + } + } + self.navigationVC?.addStatusBar(text: .apiError) + } } } } @@ -162,13 +188,8 @@ extension GSRController: GSRViewModelDelegate { func refreshDataUI() { tableView.isHidden = !viewModel.existsTimeSlot() closedView.isHidden = viewModel.existsTimeSlot() - emptyView.isHidden = !viewModel.isEmpty || !viewModel.existsTimeSlot() self.tableView.reloadData() } - - func refreshSelectionUI() { - self.refreshBarButton() - } } // MARK: - UIGestureRecognizerDelegate @@ -181,29 +202,6 @@ extension GSRController: UIGestureRecognizerDelegate { } } -// MARK: - Activity Indicator -extension GSRController { - func prepareLoadingView() { - loadingView = UIActivityIndicatorView(style: .whiteLarge) - loadingView.color = .black - loadingView.isHidden = false - view.addSubview(loadingView) - loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true - loadingView.translatesAutoresizingMaskIntoConstraints = false - } - - func startLoadingViewAnimation() { - if loadingView != nil && !loadingView.isHidden { - loadingView.startAnimating() - } - } - - func stopLoadingViewAnimation() { - self.loadingView.isHidden = true - self.loadingView.stopAnimating() - } -} // MARK: - Bar Button Refresh + Handler extension GSRController: GSRBookable { fileprivate func refreshBarButton() { @@ -212,13 +210,80 @@ extension GSRController: GSRBookable { } @objc fileprivate func handleBarButtonPressed(_ sender: Any) { - if let booking = viewModel.getBooking() { - submitPressed(for: booking) + if let id = viewModel.getSelectedRoomId(), let roomIndexPath = viewModel.getSelectedRoomIdIndexPath() { + let gid = viewModel.getSelectedLocation().gid + let roomName = viewModel.getSelectRoomName() ?? "" + let times = (tableView.cellForRow(at: roomIndexPath) as? RoomCell)?.getSelectTimes() ?? [] + + if times.count == 0 { + showAlert(withMsg: "Please select a timeslot to book.", title: "Empty Selection", completion: nil) + } + + let sorted = times.sorted(by: {$0.startTime < $1.startTime}) + + let first = sorted.first! + let last = sorted.last! + + // wharton prevents booking more than 90 minutes + if gid == 1 && times.count > 3 { + showAlert(withMsg: "You cannot book for more than 90 minutes for Wharton GSRs", title: "Invalid Selection", completion: nil) + } else if times.count > 4 { + showAlert(withMsg: "You cannot book for more than 120 minutes for Library GSRs", title: "Invalid Selection", completion: nil) + } else { + if Account.isLoggedIn { + submitBooking(for: GSRBooking(gid: gid, startTime: first.startTime, endTime: last.endTime, id: id, roomName: roomName)) + } else { + let alertController = UIAlertController(title: "Login Error", message: "Please login to book GSRs", preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: {_ in })) + alertController.addAction(UIAlertAction(title: "Login", style: .default, handler: { _ in + let llc = LabsLoginController { (success) in + DispatchQueue.main.async { + self.submitBooking(for: GSRBooking(gid: gid, startTime: first.startTime, endTime: last.endTime, id: id, roomName: roomName)) + } + } + + let nvc = UINavigationController(rootViewController: llc) + + self.present(nvc, animated: true, completion: nil) + })) + + + present(alertController, animated: true, completion: nil) + } + } } else { - // Alert. Nothing selected. showAlert(withMsg: "Please select a timeslot to book.", title: "Empty Selection", completion: nil) } } + + func getBooking() -> GSRBooking? { + if let id = viewModel.getSelectedRoomId(), let roomIndexPath = viewModel.getSelectedRoomIdIndexPath() { + let gid = viewModel.getSelectedLocation().gid + let roomName = viewModel.getSelectRoomName() ?? "" + let times = (tableView.cellForRow(at: roomIndexPath) as? RoomCell)?.getSelectTimes() ?? [] + + if times.count == 0 { + return nil + } + + let first = times.first! + let last = times.last! + + // wharton prevents booking more than 90 minutes + if gid == 1 && times.count > 3 { + showAlert(withMsg: "You cannot book for more than 90 minutes for Wharton GSRs", title: "Invalid Selection", completion: nil) + return nil + } else if times.count > 4 { + showAlert(withMsg: "You cannot book for more than 120 minutes for Library GSRs", title: "Invalid Selection", completion: nil) + return nil + } + + return GSRBooking(gid: gid, startTime: first.startTime, endTime: last.endTime, id: id, roomName: roomName) + } + + return nil + } @objc fileprivate func handleBookingsBarButtonPressed(_ sender: Any) { let grc = GSRReservationsController() @@ -238,63 +303,6 @@ extension GSRController: GSRBookable { let nvc = UINavigationController(rootViewController: glc) present(nvc, animated: true, completion: nil) } - - private func submitPressed(for booking: GSRBooking) { - if let booking = booking as? GSRGroupBooking { - handleGroupBooking(booking) - return - } - - if GSRNetworkManager.instance.bookingRequestOutstanding { - return - } - - let location = viewModel.getSelectedLocation() - if location.service == "wharton" { - if let sessionId = GSRUser.getSessionID() { - booking.sessionId = sessionId - submitBooking(for: booking) { (success) in - if success { - self.fetchData() - } else { - self.viewModel.clearSelection() - self.refreshDataUI() - } - } - } else { - presentWebviewLoginController { - if let sessionId = GSRUser.getSessionID() { - booking.sessionId = sessionId - self.submitBooking(for: booking) { (success) in - self.fetchData() - } - } - } - } - } else { - if let user = GSRUser.getUser() { - booking.user = user - submitBooking(for: booking) { (success) in - if success { - self.fetchData() - } else { - self.presentLoginController(with: booking) - } - } - } else { - presentLoginController(with: booking) - } - } - } -} - -extension GSRController { - private func handleGroupBooking(_ booking: GSRGroupBooking) { - let confirmController = GSRGroupConfirmBookingController() - confirmController.group = booking.gsrGroup - confirmController.booking = booking - present(confirmController, animated: true, completion: nil) - } } // MARK: - Update For New Day diff --git a/PennMobile/GSR-Booking/Controllers/GSRDeletable.swift b/PennMobile/GSR-Booking/Controllers/GSRDeletable.swift index abccd68b9..c881b0428 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRDeletable.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRDeletable.swift @@ -12,17 +12,17 @@ protocol GSRDeletable: IndicatorEnabled, ShowsAlert {} extension GSRDeletable where Self: UIViewController { - func deleteReservation(_ bookingID: String, _ callback: @escaping (_ success: Bool) -> Void) { + func deleteReservation(_ bookingId: String, _ callback: @escaping (_ success: Bool) -> Void) { confirmDelete { self.showActivity() - let sessionID = GSRUser.getSessionID() - GSRNetworkManager.instance.deleteReservation(bookingID: bookingID, sessionID: sessionID) { (success, errorMsg) in + GSRNetworkManager.instance.deleteReservation(bookingId: bookingId) { result in DispatchQueue.main.async { self.hideActivity() - if success { + switch result { + case .success: callback(true) - } else if let errorMsg = errorMsg { - self.showAlert(withMsg: errorMsg, title: "Uh oh!", completion: nil) + case .failure(let error): + self.showAlert(withMsg: error.rawValue, title: "Uh oh!", completion: nil) callback(false) } } @@ -31,7 +31,7 @@ extension GSRDeletable where Self: UIViewController { } func deleteReservation(_ reservation: GSRReservation, _ callback: @escaping (_ success: Bool) -> Void) { - deleteReservation(reservation.bookingID, callback) + deleteReservation(reservation.bookingId, callback) } func confirmDelete(_ callback: @escaping () -> Void) { diff --git a/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupConfirmBookingController.swift b/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupConfirmBookingController.swift index 125293774..9efd8673b 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupConfirmBookingController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupConfirmBookingController.swift @@ -11,7 +11,7 @@ import UIKit class GSRGroupConfirmBookingController: UIViewController { var group: GSRGroup! - var booking: GSRGroupBooking! +// var booking: GSRGroupBooking! fileprivate var titleLabel: UILabel! fileprivate var groupLabel: UILabel! diff --git a/PennMobile/GSR-Booking/Controllers/GSRLocationsController.swift b/PennMobile/GSR-Booking/Controllers/GSRLocationsController.swift index ca4cd95c4..5ce76b785 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRLocationsController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRLocationsController.swift @@ -19,7 +19,7 @@ class GSRLocationsController: GenericViewController { override func viewDidLoad() { super.viewDidLoad() self.locations = GSRLocationModel.shared.getLocations() - setupUI() + setupTableView() } override func setupNavBar() { @@ -35,10 +35,6 @@ class GSRLocationsController: GenericViewController { // MARK: - Setup UI extension GSRLocationsController { - fileprivate func setupUI() { - setupTableView() - } - fileprivate func setupTableView() { tableView = UITableView() tableView.separatorStyle = .none @@ -50,7 +46,6 @@ extension GSRLocationsController { _ = tableView.anchor(view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) tableView.register(GSRLocationCell.self, forCellReuseIdentifier: GSRLocationCell.identifier) - tableView.register(DiningHeaderView.self, forHeaderFooterViewReuseIdentifier: DiningHeaderView.identifier) } } diff --git a/PennMobile/GSR-Booking/Controllers/GSRLoginController.swift b/PennMobile/GSR-Booking/Controllers/GSRLoginController.swift index f47a8afe4..06cea23c9 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRLoginController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRLoginController.swift @@ -196,13 +196,7 @@ extension GSRLoginController: GSRBookable { let user = GSRUser(firstName: firstName, lastName: lastName, email: email, phone: "2158986533") if booking != nil { - booking.user = user - submitBooking(for: booking) { (success) in - if success { - GSRUser.save(user: user) - } - self.dismiss(animated: true, completion: nil) - } + submitBooking(for: booking) } else { GSRUser.save(user: user) dismiss(animated: true, completion: nil) diff --git a/PennMobile/GSR-Booking/Controllers/GSRReservationsController.swift b/PennMobile/GSR-Booking/Controllers/GSRReservationsController.swift index 58c7913e5..e95dc45d6 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRReservationsController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRReservationsController.swift @@ -9,13 +9,14 @@ import Foundation import UIKit -class GSRReservationsController: UITableViewController, ShowsAlert, IndicatorEnabled { +class GSRReservationsController: GenericTableViewController, ShowsAlert, IndicatorEnabled { fileprivate var reservations: [GSRReservation] = [] fileprivate var barButton: UIBarButtonItem! override func viewDidLoad() { super.viewDidLoad() + self.screenName = "GSRReservation" tableView.dataSource = nil // Don't try to load data initially since reservations will be nil tableView.delegate = self tableView.register(ReservationCell.self, forCellReuseIdentifier: ReservationCell.identifier) @@ -26,20 +27,29 @@ class GSRReservationsController: UITableViewController, ShowsAlert, IndicatorEna self.title = "Your Bookings" self.navigationController?.navigationItem.backBarButtonItem?.title = "Back" - - let sessionID = GSRUser.getSessionID() - let email = GSRUser.getUser()?.email - if sessionID == nil && (email == nil || email!.contains("wharton")) { - self.prepareLoginButton() - self.tableView.dataSource = self - return - } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.fetchData { (success) in - // Handle if not successful + fetchData() + } + + func fetchData() { + if self.reservations.isEmpty { + self.showActivity() + } + GSRNetworkManager.instance.getReservations { result in + DispatchQueue.main.async { + self.hideActivity() + + if let reservations = try? result.get() { + self.reservations = reservations + self.tableView.dataSource = self + self.tableView.reloadData() + } else { + self.navigationVC?.addStatusBar(text: .apiError) + } + } } } } @@ -70,82 +80,74 @@ extension GSRReservationsController { } // MARK: - ReservationCellDelegate -extension GSRReservationsController: ReservationCellDelegate, GSRDeletable { +extension GSRReservationsController: ReservationCellDelegate { func deleteReservation(_ reservation: GSRReservation) { - deleteReservation(reservation) { (success) in - if success { - self.reservations = self.reservations.filter { $0.bookingID != reservation.bookingID } - self.tableView.reloadData() + confirmDelete { + GSRNetworkManager.instance.deleteReservation(bookingId: reservation.bookingId) { result in + DispatchQueue.main.async { + switch result { + case .success: + self.reservations = self.reservations.filter { $0.bookingId != reservation.bookingId } + self.tableView.reloadData() + case .failure(let error): + self.showAlert(withMsg: error.rawValue, title: "Uh oh!", completion: nil) + } + } } } } -} - -// MARK: Login Button -extension GSRReservationsController { - func prepareLoginButton() { - barButton = UIBarButtonItem(title: "Login", style: .done, target: self, action: #selector(handleBarButtonPressed(_:))) - barButton.tintColor = UIColor.navigation - navigationItem.rightBarButtonItem = barButton - } - @objc func handleBarButtonPressed(_ sender: Any) { - let alertController = UIAlertController(title: "Select GSR System", message: "Choose the system to login for.", preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "Library", style: .default, handler: { (_) in - DispatchQueue.main.async { - self.presentLoginFlow(isWharton: false) - } - })) - alertController.addAction(UIAlertAction(title: "Wharton", style: .default, handler: { (_) in + func confirmDelete(_ callback: @escaping () -> Void) { + let alertController = UIAlertController(title: "Are you sure?", message: "Please confirm that you wish to delete this booking.", preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) + alertController.addAction(UIAlertAction(title: "Confirm", style: .default, handler: { (_) in DispatchQueue.main.async { - self.presentLoginFlow(isWharton: true) + callback() } })) present(alertController, animated: true, completion: nil) } - - func presentLoginFlow(isWharton: Bool) { - if isWharton { - let wv = GSRWebviewLoginController() - wv.completion = { - let sessionID = GSRUser.getSessionID() - if sessionID == nil { - self.showAlert(withMsg: "Uh oh!", title: "Login invalid. Please try again.", completion: nil) - return - } - } - let nvc = UINavigationController(rootViewController: wv) - present(nvc, animated: true, completion: nil) - } else { - let glc = GSRLoginController() - let nvc = UINavigationController(rootViewController: glc) - present(nvc, animated: true, completion: nil) - } - } - - func fetchData(_ completion: @escaping (_ success: Bool) -> Void) { - let sessionID = GSRUser.getSessionID() - let email = GSRUser.getUser()?.email - if sessionID == nil && email == nil { - completion(false) - return - } - - if self.reservations.isEmpty { - self.showActivity() - } - GSRNetworkManager.instance.getReservations(sessionID: sessionID, email: email) { (reservations) in - DispatchQueue.main.async { - self.hideActivity() - if let reservations = reservations { - self.reservations = reservations - self.tableView.dataSource = self - self.tableView.reloadData() - completion(true) - } else { - completion(false) - } - } - } - } } + +//// MARK: Login Button +//extension GSRReservationsController { +// func prepareLoginButton() { +// barButton = UIBarButtonItem(title: "Login", style: .done, target: self, action: #selector(handleBarButtonPressed(_:))) +// barButton.tintColor = UIColor.navigation +// navigationItem.rightBarButtonItem = barButton +// } +// +// @objc func handleBarButtonPressed(_ sender: Any) { +// let alertController = UIAlertController(title: "Select GSR System", message: "Choose the system to login for.", preferredStyle: .alert) +// alertController.addAction(UIAlertAction(title: "Library", style: .default, handler: { (_) in +// DispatchQueue.main.async { +// self.presentLoginFlow(isWharton: false) +// } +// })) +// alertController.addAction(UIAlertAction(title: "Wharton", style: .default, handler: { (_) in +// DispatchQueue.main.async { +// self.presentLoginFlow(isWharton: true) +// } +// })) +// present(alertController, animated: true, completion: nil) +// } +// +// func presentLoginFlow(isWharton: Bool) { +// if isWharton { +// let wv = GSRWebviewLoginController() +// wv.completion = { +// let sessionID = GSRUser.getSessionID() +// if sessionID == nil { +// self.showAlert(withMsg: "Uh oh!", title: "Login invalid. Please try again.", completion: nil) +// return +// } +// } +// let nvc = UINavigationController(rootViewController: wv) +// present(nvc, animated: true, completion: nil) +// } else { +// let glc = GSRLoginController() +// let nvc = UINavigationController(rootViewController: glc) +// present(nvc, animated: true, completion: nil) +// } +// } +//} diff --git a/PennMobile/GSR-Booking/Controllers/GSRTabController.swift b/PennMobile/GSR-Booking/Controllers/GSRTabController.swift index dd2c44c55..14f19e346 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRTabController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRTabController.swift @@ -13,6 +13,7 @@ class GSRTabController: ButtonBarPagerTabStripViewController { fileprivate var ownContainerView: UIScrollView! fileprivate var barView: ButtonBarView! + fileprivate var separatorLine: UIView! override func viewDidLoad() { settings.style.buttonBarBackgroundColor = .uiBackground @@ -39,7 +40,7 @@ class GSRTabController: ButtonBarPagerTabStripViewController { let barView = ButtonBarView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) self.barView = barView - let separatorLine = UIView() + separatorLine = UIView() separatorLine.backgroundColor = UIColor.grey1 let containerView = UIScrollView() self.ownContainerView = containerView @@ -89,7 +90,6 @@ class GSRTabController: ButtonBarPagerTabStripViewController { } return [child1, child2] - } } diff --git a/PennMobile/GSR-Booking/Model/GSRAPIResponse.swift b/PennMobile/GSR-Booking/Model/GSRAPIResponse.swift new file mode 100644 index 000000000..2a559d265 --- /dev/null +++ b/PennMobile/GSR-Booking/Model/GSRAPIResponse.swift @@ -0,0 +1,21 @@ +// +// GSRAPIResponse.swift +// PennMobile +// +// Created by CHOI Jongmin on 4/9/2021. +// Copyright © 2021 PennLabs. All rights reserved. +// + +import Foundation + +struct GSRAvailabilityAPIResponse: Codable { + let name: String + let gid: Int + let rooms: [GSRRoom] + + enum CodingKeys: CodingKey { + case name + case gid + case rooms + } +} diff --git a/PennMobile/GSR-Booking/Model/GSRBooking.swift b/PennMobile/GSR-Booking/Model/GSRBooking.swift index c0cfaa025..118cfd6ff 100644 --- a/PennMobile/GSR-Booking/Model/GSRBooking.swift +++ b/PennMobile/GSR-Booking/Model/GSRBooking.swift @@ -7,51 +7,68 @@ // import Foundation - -class GSRBooking { - let location: GSRLocation - let roomId: Int - let start: Date - let end: Date - var user: GSRUser! = nil - var sessionId: String! = nil - var groupName = "Penn Mobile Booking" - var name: String? - - convenience init(location: GSRLocation, roomId: Int, start: Date, end: Date, name: String?) { - self.init(location: location, roomId: roomId, start: start, end: end) - self.name = name - } - - init(location: GSRLocation, roomId: Int, start: Date, end: Date) { - self.location = location - self.roomId = roomId - self.start = start - self.end = end - } +// +struct GSRBooking: Codable { + let gid: Int + let startTime: Date + let endTime: Date + let id: Int + let roomName: String func getLocalTimeString() -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "MMM d, h:mm a" - let dateStringStart = formatter.string(from: self.start) - let dateStringEnd = formatter.string(from: self.end) + let dateStringStart = formatter.string(from: self.startTime) + let dateStringEnd = formatter.string(from: self.endTime) return "\(dateStringStart) -> \(dateStringEnd)" } - - func getRoomName() -> String { - if name != nil { return name! } else { return location.name } - } } -class GSRGroupBooking: GSRBooking { - var gsrGroup: GSRGroup! - - init(location: GSRLocation, roomId: Int, start: Date, end: Date, gsrGroup: GSRGroup) { - super.init(location: location, roomId: roomId, start: start, end: end) - self.gsrGroup = gsrGroup - self.groupName = gsrGroup.name - } -} - -typealias GSRGroupBookings = [GSRGroupBooking] +//class GSRBooking { +// let location: GSRLocation +// let roomId: Int +// let start: Date +// let end: Date +// var user: GSRUser! = nil +// var sessionId: String! = nil +// var groupName = "Penn Mobile Booking" +// var name: String? +// +// convenience init(location: GSRLocation, roomId: Int, start: Date, end: Date, name: String?) { +// self.init(location: location, roomId: roomId, start: start, end: end) +// self.name = name +// } +// +// init(location: GSRLocation, roomId: Int, start: Date, end: Date) { +// self.location = location +// self.roomId = roomId +// self.start = start +// self.end = end +// } +// +// func getLocalTimeString() -> String { +// let formatter = DateFormatter() +// formatter.locale = Locale(identifier: "en_US_POSIX") +// formatter.dateFormat = "MMM d, h:mm a" +// let dateStringStart = formatter.string(from: self.start) +// let dateStringEnd = formatter.string(from: self.end) +// return "\(dateStringStart) -> \(dateStringEnd)" +// } +// +// func getRoomName() -> String { +// if name != nil { return name! } else { return location.name } +// } +//} +// +//class GSRGroupBooking: GSRBooking { +// var gsrGroup: GSRGroup! +// +// init(location: GSRLocation, roomId: Int, start: Date, end: Date, gsrGroup: GSRGroup) { +// super.init(location: location, roomId: roomId, start: start, end: end) +// self.gsrGroup = gsrGroup +// self.groupName = gsrGroup.name +// } +//} +// +//typealias GSRGroupBookings = [GSRGroupBooking] diff --git a/PennMobile/GSR-Booking/Model/GSRLocation.swift b/PennMobile/GSR-Booking/Model/GSRLocation.swift index 890ae9eae..2db048194 100644 --- a/PennMobile/GSR-Booking/Model/GSRLocation.swift +++ b/PennMobile/GSR-Booking/Model/GSRLocation.swift @@ -8,11 +8,10 @@ import Foundation -struct GSRLocation { +struct GSRLocation: Codable, Equatable { let lid: Int - var gid: Int? + let gid: Int let name: String - let service: String + let kind: String + let imageUrl: String } - -extension GSRLocation: Equatable {} diff --git a/PennMobile/GSR-Booking/Model/GSRLocationModel.swift b/PennMobile/GSR-Booking/Model/GSRLocationModel.swift index ee098136b..1e8b04b8c 100644 --- a/PennMobile/GSR-Booking/Model/GSRLocationModel.swift +++ b/PennMobile/GSR-Booking/Model/GSRLocationModel.swift @@ -27,24 +27,17 @@ class GSRLocationModel { return "" } - private func fetchJSON() throws -> JSON { - guard let path = Bundle.main.path(forResource: "locations", ofType: "json") else { - throw NetworkingError.jsonError - } - let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) - return JSON(data) - } - func prepare() { - guard let json = try? fetchJSON() else { return } - let locationsJSONArray = json["locations"].arrayValue - for json in locationsJSONArray { - let lid = json["lid"].intValue - let gid = json["gid"].int - let name = json["name"].stringValue - let service = json["service"].stringValue - let location = GSRLocation(lid: lid, gid: gid, name: name, service: service) - locations.append(location) + DispatchQueue.main.async { + GSRNetworkManager.instance.getLocations { result in + switch result { + case .success(let locations): + self.locations = locations + case .failure: + // TODO handle error + break + } + } } } } diff --git a/PennMobile/GSR-Booking/Model/GSRReservation.swift b/PennMobile/GSR-Booking/Model/GSRReservation.swift index 7881331a7..277877e05 100644 --- a/PennMobile/GSR-Booking/Model/GSRReservation.swift +++ b/PennMobile/GSR-Booking/Model/GSRReservation.swift @@ -13,12 +13,11 @@ enum GSRService: String { case libcal } -struct GSRReservation { +struct GSRReservation: Codable { + let bookingId: String + let gsr: GSRLocation + let roomId: Int let roomName: String - let gid: Int - let lid: Int - let bookingID: String - let startDate: Date - let endDate: Date - let service: GSRService + let start: Date + let end: Date } diff --git a/PennMobile/GSR-Booking/Model/GSRRoom.swift b/PennMobile/GSR-Booking/Model/GSRRoom.swift index 5d7b1b078..26d94fb84 100644 --- a/PennMobile/GSR-Booking/Model/GSRRoom.swift +++ b/PennMobile/GSR-Booking/Model/GSRRoom.swift @@ -8,127 +8,70 @@ import Foundation -// MARK: - Init -class GSRRoom { - let name: String - let roomId: Int - let gid: Int - let imageUrl: String? - let capacity: Int - var timeSlots: [GSRTimeSlot]! = nil - - init(name: String, roomId: Int, gid: Int, imageUrl: String?, capacity: Int, timeSlots: [GSRTimeSlot]) { - self.name = name - self.roomId = roomId - self.gid = gid - self.imageUrl = imageUrl - self.capacity = capacity - self.timeSlots = timeSlots - } -} - -extension GSRRoom: Comparable { - static func <(lhs: GSRRoom, rhs: GSRRoom) -> Bool { -// guard let lhsStart = lhs.timeSlots.first?.startTime, let rhsStart = rhs.timeSlots.first?.startTime else { -// return false -// } -// -// if lhsStart == rhsStart { -// let lhsNumRow = lhs.timeSlots.numberInRow -// let rhsNumRow = rhs.timeSlots.numberInRow -// if lhsNumRow == rhsNumRow { -// return lhs.timeSlots.count > rhs.timeSlots.count -// } -// return lhsNumRow > rhsNumRow -// } else { -// return lhsStart < rhsStart -// } - if lhs.name.contains("F") { - if rhs.name.contains("F") { - return lhs.name < rhs.name - } else { - return true - } - } else if lhs.name.contains("G") { - if rhs.name.contains("G") { - return lhs.name < rhs.name - } else { - return true - } - } else { - return lhs.name < rhs.name - } - } - - static func ==(lhs: GSRRoom, rhs: GSRRoom) -> Bool { - return lhs.roomId == rhs.roomId - } +struct GSRRoom: Codable { + let roomName: String + let id: Int + var availability: [GSRTimeSlot] } extension Array where Element == GSRRoom { - func getMinMaxDates(day: GSRDate) -> (Date?, Date?) { + func getMinMaxDates() -> (Date?, Date?) { var min: Date? = nil var max: Date? = nil for room in self { - if let firstStartTime = room.timeSlots.first?.startTime, min == nil || (firstStartTime < min!) { + if let firstStartTime = room.availability.first?.startTime, min == nil || (firstStartTime < min!) { min = firstStartTime } - if let lastEndTime = room.timeSlots.last?.endTime, max == nil || (lastEndTime > max!) { + if let lastEndTime = room.availability.last?.endTime, max == nil || (lastEndTime > max!) { max = lastEndTime } } + return (min, max) } } extension GSRRoom { - func addMissingTimeslots(minDate: Date, maxDate: Date) { + func addMissingTimeslots(minDate: Date, maxDate: Date) -> [GSRTimeSlot] { var newTimes = [GSRTimeSlot]() - + // Fill in early slots - if let earliestTime = timeSlots.first { + if let earliestTime = availability.first { var currTime = earliestTime let minTime = minDate.add(minutes: 30) while currTime.startTime >= minTime { - let currNewTime = GSRTimeSlot(roomId: roomId, isAvailable: false, startTime: currTime.startTime.add(minutes: -30), endTime: currTime.startTime) - currNewTime.next = currTime - currTime.prev = currNewTime - newTimes.insert(currNewTime, at: 0) - currTime = currNewTime + let newTimeSlot = GSRTimeSlot(startTime: currTime.startTime.add(minutes: -30), endTime: currTime.startTime, isAvailable: false) + newTimes.insert(newTimeSlot, at: 0) + currTime = newTimeSlot } } - + // Fill in middle slots - for i in 0.. Bool { - return lhs.roomId == rhs.roomId && lhs.isAvailable == rhs.isAvailable && lhs.startTime == rhs.startTime && lhs.endTime == rhs.endTime - } - - func getLocalTimeString() -> String { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "MMM d, h:mm a" - let dateStringStart = formatter.string(from: self.startTime) - let dateStringEnd = formatter.string(from: self.endTime) - return "\(dateStringStart) -> \(dateStringEnd)" - } - -} - -extension Array where Element: GSRTimeSlot { - var numberInRow: Int { - if count == 0 { return 0 } - var num = 1 - var currTime: GSRTimeSlot = first! - while currTime.isAvailable && currTime.next != nil { - num += 1 - currTime = currTime.next! - } - return num - } - - func firstTimeslot(duration: Int) -> GSRTimeSlot? { - let numSlots = duration / 30 - for slot in self { - if [slot].numberInRow >= numSlots { - return slot - } - } - return nil + enum CodingKeys: CodingKey { + case startTime + case endTime } } diff --git a/PennMobile/GSR-Booking/Model/GSRVenue.swift b/PennMobile/GSR-Booking/Model/GSRVenue.swift deleted file mode 100644 index e571d0cdb..000000000 --- a/PennMobile/GSR-Booking/Model/GSRVenue.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// GSRVenue.swift -// PennMobile -// -// Created by Zhilei Zheng on 2/2/18. -// Copyright © 2018 PennLabs. All rights reserved. -// - -import Foundation - -public class GSRVenue { - var name: String - var id: Int - var rooms: [GSRRoom]? - - init(name: String, id: Int) { - self.name = name - self.id = id - } -} diff --git a/PennMobile/GSR-Booking/Model/locations.json b/PennMobile/GSR-Booking/Model/locations.json deleted file mode 100644 index ce39e3843..000000000 --- a/PennMobile/GSR-Booking/Model/locations.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "locations": [ - { - "lid": 1, - "gid": 1, - "name": "Huntsman", - "service": "wharton" - }, - { - "lid": 1086, - "gid": 1889, - "name": "Weigle", - "service": "libcal" - }, - { - "lid": 2495, - "gid": 1886, - "name": "Education Commons", - "service": "libcal" - }, - { - "lid": 2587, - "gid": 4368, - "name": "Lippincott", - "service": "libcal" - }, - { - "lid": 4370, - "gid": 7426, - "name": "Perelman Center", - "service": "libcal" - }, - { - "lid": 1086, - "gid": 4660, - "name": "VP Ground Floor", - "service": "libcal" - }, - { - "lid": 1090, - "gid": 1909, - "name": "Levin Building", - "service": "libcal" - }, - { - "lid": 2683, - "gid": 1885, - "name": "Biomedical Library", - "service": "libcal" - }, - { - "lid": 1086, - "gid": 4659, - "name": "VP 3rd Floor", - "service": "libcal" - }, - { - "lid": 1086, - "gid": 1891, - "name": "VP 4th Floor", - "service": "libcal" - } - ] -} diff --git a/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift b/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift index 5bd721780..171b31422 100644 --- a/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift +++ b/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift @@ -13,57 +13,81 @@ class GSRNetworkManager: NSObject, Requestable { static let instance = GSRNetworkManager() - let availUrl = "https://api.pennlabs.org/studyspaces/availability" - let locationsUrl = "https://api.pennlabs.org/studyspaces/locations" - let bookingUrl = "https://api.pennlabs.org/studyspaces/book" - let reservationURL = "https://api.pennlabs.org/studyspaces/reservations" - let cancelURL = "https://api.pennlabs.org/studyspaces/cancel" - let searchUserURL = "https://api.pennlabs.org/studyspaces/user/search?query=" - - var locations:[Int:String] = [:] + let availUrl = "https://studentlife.pennlabs.org/availability/" + let locationsUrl = "https://studentlife.pennlabs.org/locations/" + let bookingUrl = "https://studentlife.pennlabs.org/book/" + let reservationURL = "https://studentlife.pennlabs.org/reservations/" + let cancelURL = "https://studentlife.pennlabs.org/cancel/" + var bookingRequestOutstanding = false - - func getLocations (callback: @escaping (([Int:String]?) -> Void)) { - let url = locationsUrl - getRequest(url: url) { (dict, error, statusCode) in - if let dict = dict { - let json = JSON(dict) - self.locations = self.parseLocations(json: json) - callback(self.locations) - } else { - callback(nil) + func getLocations (completion: @escaping (Result<[GSRLocation], NetworkingError>) -> Void) { + let url = URL(string: self.locationsUrl)! + + let task = URLSession.shared.dataTask(with: url) { (data, response, error) in + if let httpResponse = response as? HTTPURLResponse, let data = data, httpResponse.statusCode == 200 { + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + do { + let gsrLocations = try decoder.decode([GSRLocation].self, from: data) + completion(.success(gsrLocations)) + } catch { + print(error) + completion(.failure(.parsingError)) + } } } + + task.resume() } - - - func getAvailability(for gsrId: Int, date: GSRDate, callback: @escaping ((_ rooms: [GSRRoom]?) -> Void)) { - self.getAvailability(for: gsrId, dateStr: date.string) { (rooms) in - callback(rooms) - } - } - - func getAvailability(for gsrId: Int, dateStr: String, callback: @escaping ((_ rooms: [GSRRoom]?) -> Void)) { - var url = "\(availUrl)/\(gsrId)?date=\(dateStr)" - if let sessionID = GSRUser.getSessionID() { - url = "\(url)&sessionid=\(sessionID)" - } - getRequest(url: url) { (dict, error, statusCode) in - var rooms: [GSRRoom]! - if let dict = dict { - let json = JSON(dict) - rooms = Array(json: json) + func getAvailability(lid: Int, gid: Int, startDate: String? = nil, endDate: String? = nil, completion: @escaping (Result<[GSRRoom], NetworkingError>) -> Void) { + OAuth2NetworkManager.instance.getAccessToken { token in + var url = URL(string: "\(self.availUrl)")! + url.appendPathComponent("\(lid)") + + if let startDate = startDate { + url.appendQueryItem(name: "start", value: startDate) } - if statusCode == 400 && gsrId == 1 { - // If Session ID invalid, clear it and try again without one - GSRUser.clearSessionID() - self.getAvailability(for: gsrId, dateStr: dateStr, callback: callback) - return + if let endDate = endDate { + url.appendQueryItem(name: "end", value: endDate) } - callback(rooms) + + let request = token != nil ? URLRequest(url: url, accessToken: token!) : URLRequest(url: url) + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + if let httpResponse = response as? HTTPURLResponse, let data = data, httpResponse.statusCode == 200 { + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + let response = try decoder.decode([GSRAvailabilityAPIResponse].self, from: data) + + if lid == 1086 { + if let rooms = response.first(where: {$0.gid == gid})?.rooms { + completion(.success(rooms)) + } else { + completion(.success([])) + } + } else { + if let rooms = response.first?.rooms { + completion(.success(rooms)) + } else { + completion(.failure(.serverError)) + } + } + } catch { + completion(.failure(.parsingError)) + } + } else { + completion(.failure(.serverError)) + } + } + + task.resume() } } @@ -79,69 +103,37 @@ class GSRNetworkManager: NSObject, Requestable { return locations } - func makeBooking(for booking: GSRBooking, _ callback: @escaping (_ success: Bool, _ failureMessage: String?) -> Void) { + func makeBooking(for booking: GSRBooking, _ completion: @escaping (Result) -> Void) { OAuth2NetworkManager.instance.getAccessToken { (token) in let url = URL(string: self.bookingUrl)! var request = token != nil ? URLRequest(url: url, accessToken: token!) : URLRequest(url: url) request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: "Content-Type") let deviceID = getDeviceID() request.setValue(deviceID, forHTTPHeaderField: "X-Device-ID") + let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - let start = dateFormatter.string(from: booking.start) - let end = dateFormatter.string(from: booking.end) + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX" - let basicParams = [ - "lid" : String(booking.location.lid), - "room" : String(booking.roomId), - "start" : start, - "end" : end, - ] - let extraParams: [String: String] - if booking.location.service == "wharton" { - let sessionID: String = booking.sessionId - extraParams = [ - "sessionid": sessionID - ] - } else { - let user: GSRUser = booking.user - extraParams = [ - "firstname" : user.firstName, - "lastname" : user.lastName, - "email" : user.email, - "phone" : user.phone, - "groupname" : booking.groupName, - "size" : "2-3" - ] - } + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.dateEncodingStrategy = .formatted(dateFormatter) - let params = basicParams.merging(extraParams, uniquingKeysWith: { (first, _) in first }) - request.httpBody = params.stringFromHttpParameters().data(using: String.Encoding.utf8) + request.httpBody = try? encoder.encode(booking) - self.bookingRequestOutstanding = true let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) in - var success = false - var errorMessage = "Unable to connect to the internet. Please reconnect and try again." if let response = response as? HTTPURLResponse { if response.statusCode == 200 { - if let data = data, let _ = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { - let json = JSON(data) - success = json["results"].boolValue - errorMessage = json["error"].stringValue - } - if errorMessage.contains("\n") { - errorMessage = errorMessage.replacingOccurrences(of: "\n", with: " ") - } + completion(.success(())) } else { - // Session ID is invalid, so clear it - GSRUser.clearSessionID() + completion(.failure(.serverError)) } } - callback(success, errorMessage) - self.bookingRequestOutstanding = false }) + task.resume() } } @@ -149,78 +141,66 @@ class GSRNetworkManager: NSObject, Requestable { // MARK: - Get Reservatoins extension GSRNetworkManager { - func getReservations(sessionID: String?, email: String?, _ callback: @escaping (_ reservations: [GSRReservation]?) -> Void) { - let url: String - if let sessionID = sessionID, let email = email { - url = "\(reservationURL)?sessionid=\(sessionID)&email=\(email)" - } else if let sessionID = sessionID { - url = "\(reservationURL)?sessionid=\(sessionID)" - } else if let email = email { - url = "\(reservationURL)?email=\(email)" - } else { - url = reservationURL - } - getRequest(url: url) { (dict, error, status) in - var reservations: [GSRReservation]? = nil - if let dict = dict { - let json = JSON(dict) - reservations = try? self.parseReservation(json: json) - } - callback(reservations) - } - } - - func parseReservation(json: JSON) throws -> [GSRReservation] { - guard json["error"].string == nil else { - throw NetworkingError.authenticationError - } - return try parseReservationsFromArray(json: json["reservations"]) - } - - func parseReservationsFromArray(json: JSON) throws -> [GSRReservation] { - guard let jsonArray = json.array else { - throw NetworkingError.jsonError - } - - var reservations = [GSRReservation]() - for reservationJSON in jsonArray { - guard let roomName = reservationJSON["name"].string, - let gid = reservationJSON["gid"].int, - let lid = reservationJSON["lid"].int, - let bookingID = reservationJSON["booking_id"].string, - let startDateStr = reservationJSON["fromDate"].string, - let endDateStr = reservationJSON["toDate"].string, - let serviceStr = reservationJSON["service"].string, - let service = GSRService(rawValue: serviceStr) else { - throw NetworkingError.jsonError + func getReservations(_ completion: @escaping (_ reservations: Result<[GSRReservation], NetworkingError>) -> Void) { + OAuth2NetworkManager.instance.getAccessToken { token in + guard let token = token else { + completion(.failure(.authenticationError)) + return } - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - formatter.locale = Locale(identifier: "en_US_POSIX") - guard let startDate = formatter.date(from: startDateStr), - let endDate = formatter.date(from: endDateStr) else { - throw NetworkingError.jsonError + + print(token.value) + let url = URL(string: self.reservationURL)! + let request = URLRequest(url: url, accessToken: token) + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + if let httpResponse = response as? HTTPURLResponse, let data = data, httpResponse.statusCode == 200 { + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + decoder.keyDecodingStrategy = .convertFromSnakeCase + + do { + let reservations = try decoder.decode([GSRReservation].self, from: data) + print(reservations) + completion(.success(reservations)) + } catch { + print(error) + completion(.failure(.parsingError)) + } + } } - let reservation = GSRReservation(roomName: roomName, gid: gid, lid: lid, bookingID: bookingID, startDate: startDate, endDate: endDate, service: service) - reservations.append(reservation) + task.resume() } - return reservations } } // MARK: - Delete Reservation extension GSRNetworkManager { - - func deleteReservation(reservation: GSRReservation, sessionID: String?, callback: @escaping (_ success: Bool, _ errorMsg: String?) -> Void) { - - if reservation.service == .wharton { - guard let _ = sessionID else { - callback(false, "Please log in and try again.") + func deleteReservation(bookingId: String, _ completion: @escaping (Result) -> Void ) { + OAuth2NetworkManager.instance.getAccessToken { token in + guard let token = token else { + completion(.failure(.authenticationError)) return } + + let url = URL(string: self.cancelURL)! + var request = URLRequest(url: url, accessToken: token) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = try? JSONSerialization.data(withJSONObject: ["booking_id": bookingId]) + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + if let httpResponse = response as? HTTPURLResponse, data != nil, httpResponse.statusCode == 200 { + + completion(.success(())) + } else { + completion(.failure(.serverError)) + } + } + + task.resume() } - deleteReservation(bookingID: reservation.bookingID, sessionID: sessionID, callback: callback) } func deleteReservation(bookingID: String, sessionID: String?, callback: @escaping (_ success: Bool, _ errorMsg: String?) -> Void) { @@ -263,67 +243,6 @@ extension GSRNetworkManager { } } -extension Array where Element == GSRRoom { - init(json: JSON) { - self.init() - let roomArray = json["rooms"].arrayValue - for roomJSON in roomArray { - if let room = try? GSRRoom(json: roomJSON) { - self.append(room) - } - } - } -} - -extension GSRRoom { - convenience init(json: JSON) throws { - guard let name = json["name"].string, let roomId = json["room_id"].int, let gid = json["gid"].int else { - throw NetworkingError.jsonError - } - - let capacity = json["capacity"].intValue - let imageUrl = json["thumbnail"].string - - var times = [GSRTimeSlot]() - let jsonTimeArray = json["times"].arrayValue - for timeJSON in jsonTimeArray { - if let time = try? GSRTimeSlot(roomId: roomId, json: timeJSON) { - if let prevTime = times.last, prevTime.endTime == time.startTime { - times.last?.next = time - time.prev = times.last - } - times.append(time) - } - } - self.init(name: name, roomId: roomId, gid: gid, imageUrl: imageUrl, capacity: capacity, timeSlots: times) - } -} - - - -extension GSRTimeSlot { - convenience init(roomId: Int, json: JSON) throws { - guard let isAvailable = json["available"].bool, - let startStr = json["start"].string, - let endStr = json["end"].string else { - throw NetworkingError.jsonError - } - - let startDate = try GSRTimeSlot.extractDate(from: startStr) - let endDate = try GSRTimeSlot.extractDate(from: endStr) - self.init(roomId: roomId, isAvailable: isAvailable, startTime: startDate, endTime: endDate) - } - - private static func extractDate(from dateString: String) throws -> Date { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - guard let date = dateFormatter.date(from: dateString) else { - throw NetworkingError.jsonError - } - return date - } -} - // MARK: - Session ID extension GSRNetworkManager: PennAuthRequestable { diff --git a/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift b/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift index 210ef7a46..a9d16f16e 100644 --- a/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift +++ b/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift @@ -20,8 +20,8 @@ enum GSRState { protocol GSRViewModelDelegate: ShowsAlert { func refreshDataUI() - func refreshSelectionUI() func fetchData() + func resetDataForCell(at indexPath: IndexPath) } class GSRViewModel: NSObject { @@ -35,10 +35,10 @@ class GSRViewModel: NSObject { // MARK: Room Data fileprivate var allRooms = [GSRRoom]() - fileprivate var currentRooms = [GSRRoom]() + fileprivate var filteredRooms = [GSRRoom]() // MARK: Current Selection - fileprivate var currentSelection = [GSRTimeSlot]() + fileprivate var selectedRoomId: Int? = nil // MARK: Delegate var delegate: GSRViewModelDelegate! @@ -47,26 +47,10 @@ class GSRViewModel: NSObject { self.selectedLocation = selectedLocation } - // MARK: GSR State - var state: GSRState { - get { - if let booking = getBooking() { - return .readyToSubmit(booking) - } else { - return GSRUser.hasSavedUser() || GSRUser.getSessionID() != nil ? .loggedIn : .loggedOut - } - } - } - - // MARK: Logged In Flag - var isLoggedIn: Bool { - return GSRUser.hasSavedUser() || GSRUser.getSessionID() != nil - } - // MARK: Empty var isEmpty: Bool { get { - return currentRooms.isEmpty + return filteredRooms.isEmpty } } @@ -109,7 +93,7 @@ extension GSRViewModel: UIPickerViewDataSource, UIPickerViewDelegate { // MARK: - UITableViewDataSource extension GSRViewModel: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - return currentRooms.count + return filteredRooms.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -117,12 +101,13 @@ extension GSRViewModel: UITableViewDataSource { } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return currentRooms[section].name + return filteredRooms[section].roomName } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: RoomCell.identifier, for: indexPath) as! RoomCell - cell.room = currentRooms[indexPath.section] + print(filteredRooms.count) + cell.room = filteredRooms[indexPath.section] cell.contentView.isUserInteractionEnabled = false cell.delegate = self return cell @@ -139,17 +124,17 @@ extension GSRViewModel: UITableViewDelegate { // MARK: - Reload Data extension GSRViewModel { func updateData(with rooms: [GSRRoom]) { - var rooms = rooms - if let gid = selectedLocation.gid { - rooms = rooms.filter { $0.gid == gid } - } - self.allRooms = rooms - self.currentRooms = rooms - self.currentSelection = [] + var populatedRooms = rooms - if let minDate = getMinDate(), let maxDate = getMaxDate() { - rooms.forEach { $0.addMissingTimeslots(minDate: max(minDate, Date()), maxDate: maxDate) } + let (minDate, maxDate) = rooms.getMinMaxDates() + + if let minDate = minDate, let maxDate = maxDate { + populatedRooms = rooms.map({ return GSRRoom(roomName: $0.roomName, id: $0.id, availability: $0.addMissingTimeslots(minDate: minDate, maxDate: maxDate))}) } + + allRooms = populatedRooms + filteredRooms = populatedRooms + selectedRoomId = nil } func updateDates() { @@ -159,53 +144,19 @@ extension GSRViewModel { // MARK: Selection Delegate extension GSRViewModel: GSRSelectionDelegate { - func containsTimeSlot(_ timeSlot: GSRTimeSlot) -> Bool { - return currentSelection.contains(timeSlot) - } - private func isValidAddition(timeSlot: GSRTimeSlot) -> Bool { - if currentSelection.isEmpty { - return true + func handleSelection(for id: Int) { + if (selectedRoomId != nil && selectedRoomId != id) { + let roomCell = filteredRooms.firstIndex(where: {$0.id == selectedRoomId})! + // There is only one row per section (room) + delegate.resetDataForCell(at: IndexPath(item: 0, section: roomCell)) } - let isSameRoom = currentSelection.contains(where: { (otherTimeSlot) -> Bool in - otherTimeSlot.roomId == timeSlot.roomId - }) - - var isBeforeOrAfter = false - for selection in currentSelection { - isBeforeOrAfter = isBeforeOrAfter || timeSlot == selection.prev || timeSlot == selection.next || timeSlot == selection - } - - return isSameRoom && isBeforeOrAfter - } - - func handleSelection(for room: GSRRoom, timeSlot: GSRTimeSlot, action: SelectionType) { - switch action { - case .add: - if currentSelection.contains(timeSlot) { break } - if !isValidAddition(timeSlot: timeSlot) && group == nil { - currentSelection.removeAll() - delegate.refreshDataUI() - } - currentSelection.append(timeSlot) - break - case .remove: - currentSelection.remove(at: currentSelection.firstIndex(of: timeSlot)!) - break - } - - if currentSelection.count == 0 || (currentSelection.count == 1 && action == .add) { - delegate.refreshSelectionUI() - } - } - - func clearSelection() { - currentSelection = [] + selectedRoomId = id } func existsTimeSlot() -> Bool { - let roomsWithTimeSlots = allRooms.filter { $0.timeSlots.count > 0 } + let roomsWithTimeSlots = allRooms.filter { $0.availability.count > 0 } return roomsWithTimeSlots.count > 0 } } @@ -219,15 +170,17 @@ extension GSRViewModel: GSRRangeSliderDelegate { func updateCurrentRooms(startDate: Date, endDate: Date) { var currentRooms = [GSRRoom]() for room in allRooms { - let timeSlots = room.timeSlots.filter { + let timeSlots = room.availability.filter { return $0.startTime >= startDate && $0.endTime <= endDate } + + var filteredRoom = room + filteredRoom.availability = timeSlots if !timeSlots.isEmpty { - let newRoom = GSRRoom(name: room.name, roomId: room.roomId, gid: room.gid, imageUrl: room.imageUrl, capacity: room.capacity, timeSlots: timeSlots) - currentRooms.append(newRoom) + currentRooms.append(filteredRoom) } } - self.currentRooms = currentRooms.sorted() + self.filteredRooms = currentRooms } func parseData(startDate: Date, endDate: Date) { @@ -246,12 +199,12 @@ extension GSRViewModel: GSRRangeSliderDelegate { if selectedDate.day == dates[0].day { return Date().roundedDownToHalfHour } else { - return allRooms.getMinMaxDates(day: selectedDate).0 + return allRooms.getMinMaxDates().0 } } func getMaxDate() -> Date? { - return allRooms.getMinMaxDates(day: selectedDate).1 + return allRooms.getMinMaxDates().1 } } @@ -265,34 +218,21 @@ extension GSRViewModel { return selectedDate } - func getBooking() -> GSRBooking? { - if currentSelection.isEmpty { - return nil - } - let roomId = currentSelection[0].roomId - let startTime: Date - let endTime: Date - var timeSlot = currentSelection[0] - while timeSlot.prev != nil && currentSelection.contains(timeSlot.prev!) { - timeSlot = timeSlot.prev! - } - startTime = timeSlot.startTime - while timeSlot.next != nil && currentSelection.contains(timeSlot.next!) { - timeSlot = timeSlot.next! - } - endTime = timeSlot.endTime - - if let group = group { - return GSRGroupBooking(location: selectedLocation, roomId: roomId, start: startTime, end: endTime, gsrGroup: group) + func getSelectedRoomId() -> Int? { + return selectedRoomId + } + + func getSelectRoomName() -> String? { + if let selectedRoomId = getSelectedRoomId() { + return filteredRooms.first(where: {$0.id == selectedRoomId})?.roomName } - return GSRBooking(location: selectedLocation, roomId: roomId, start: startTime, end: endTime) + return nil } - // TODO: - Logic for generating group bookings - func getGroupBooking() -> GSRGroupBookings? { - if currentSelection.isEmpty { - return nil + func getSelectedRoomIdIndexPath() -> IndexPath? { + if let selectedRoomId = selectedRoomId, let index = filteredRooms.firstIndex(where: {$0.id == selectedRoomId}) { + return IndexPath(item: 0, section: index) } return nil diff --git a/PennMobile/GSR-Booking/Views/GSRLocationCell.swift b/PennMobile/GSR-Booking/Views/GSRLocationCell.swift index 5ecf3a829..cfb34c132 100644 --- a/PennMobile/GSR-Booking/Views/GSRLocationCell.swift +++ b/PennMobile/GSR-Booking/Views/GSRLocationCell.swift @@ -17,9 +17,8 @@ class GSRLocationCell: UITableViewCell { var location: GSRLocation! { didSet { locationLabel.text = location.name - if let url = URL(string: "https://s3.us-east-2.amazonaws.com/labs.api/gsr/lid-\(location.lid)-gid-\(location.gid ?? location.lid).jpg") { - buildingImageView.kf.setImage(with: url) - } + + buildingImageView.kf.setImage(with: URL(string: location.imageUrl)!) } } diff --git a/PennMobile/GSR-Booking/Views/GSRTimeCell.swift b/PennMobile/GSR-Booking/Views/GSRTimeCell.swift index 6e0647988..615f422ce 100644 --- a/PennMobile/GSR-Booking/Views/GSRTimeCell.swift +++ b/PennMobile/GSR-Booking/Views/GSRTimeCell.swift @@ -17,7 +17,22 @@ class GSRTimeCell: UICollectionViewCell { didSet { startLabel.text = format(date: timeSlot.startTime) endLabel.text = format(date: timeSlot.endTime) - backgroundColor = timeSlot.isAvailable ? UIColor.baseGreen : UIColor.labelSecondary + } + } + + override var isSelected: Bool{ + didSet{ + if !timeSlot.isAvailable { + backgroundColor = UIColor.labelSecondary + } else { + UIView.animate(withDuration: 0.15) { + if self.isSelected { + self.backgroundColor = .baseYellow + } else { + self.backgroundColor = .baseGreen + } + } + } } } diff --git a/PennMobile/GSR-Booking/Views/ReservationCell.swift b/PennMobile/GSR-Booking/Views/ReservationCell.swift index f166e67fb..441bc4a2a 100644 --- a/PennMobile/GSR-Booking/Views/ReservationCell.swift +++ b/PennMobile/GSR-Booking/Views/ReservationCell.swift @@ -20,19 +20,17 @@ class ReservationCell: UITableViewCell { var reservation: GSRReservation! { didSet { locationLabel.text = String(reservation.roomName.split(separator: ":").first ?? "") - + let formatter = DateFormatter() formatter.dateFormat = "MMM d, YYYY" - dateLabel.text = formatter.string(from: reservation.startDate) + dateLabel.text = formatter.string(from: reservation.start) formatter.dateFormat = "h:mm a" - let startStr = formatter.string(from: reservation.startDate) - let endStr = formatter.string(from: reservation.endDate) + let startStr = formatter.string(from: reservation.start) + let endStr = formatter.string(from: reservation.end) timeLabel.text = "\(startStr) - \(endStr)" - if let url = URL(string: "https://s3.us-east-2.amazonaws.com/labs.api/gsr/lid-\(reservation.lid)-gid-\(reservation.gid).jpg") { - buildingImageView.kf.setImage(with: url) - } + buildingImageView.kf.setImage(with: URL(string: reservation.gsr.imageUrl)) } } diff --git a/PennMobile/GSR-Booking/Views/RoomCell.swift b/PennMobile/GSR-Booking/Views/RoomCell.swift index 05eaa4596..b9d99861a 100755 --- a/PennMobile/GSR-Booking/Views/RoomCell.swift +++ b/PennMobile/GSR-Booking/Views/RoomCell.swift @@ -9,8 +9,7 @@ import UIKit protocol GSRSelectionDelegate { - func containsTimeSlot(_ timeSlot: GSRTimeSlot) -> Bool - func handleSelection(for room: GSRRoom, timeSlot: GSRTimeSlot, action: SelectionType) + func handleSelection(for id: Int) } class RoomCell: UITableViewCell { @@ -50,23 +49,29 @@ class RoomCell: UITableViewCell { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func resetSelection() { + collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: true) } + } + + func getSelectTimes() -> [GSRTimeSlot] { + (collectionView.indexPathsForSelectedItems ?? []).map( { + return room.availability[$0.item] + }) + } } extension RoomCell: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return room.timeSlots.count + return room.availability.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GSRTimeCell.identifier, for: indexPath) as! GSRTimeCell - let timeSlot = room.timeSlots[indexPath.row] + let timeSlot = room.availability[indexPath.row] cell.timeSlot = timeSlot - if delegate.containsTimeSlot(timeSlot) { - cell.backgroundColor = .baseYellow - } else { - cell.backgroundColor = timeSlot.isAvailable ? UIColor.baseGreen : UIColor.labelSecondary - } + cell.backgroundColor = timeSlot.isAvailable ? UIColor.baseGreen : UIColor.labelSecondary return cell } @@ -75,35 +80,36 @@ extension RoomCell: UICollectionViewDataSource, UICollectionViewDelegate, UIColl return CGSize(width: size, height: size) } - // MARK: - Collection View Delegate Methods - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let timeSlot = room.timeSlots[indexPath.row] - delegate?.handleSelection(for: room, timeSlot: timeSlot, action: SelectionType.add) - let cell = collectionView.cellForItem(at: indexPath) - cell?.backgroundColor = .baseYellow - } - //only enable selection for available rooms func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - let timeSlot = room.timeSlots[indexPath.row] + let timeSlot = room.availability[indexPath.row] return timeSlot.isAvailable } // Deselect this time slot and all select ones that follow it func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - var currTimeSlot = room.timeSlots[indexPath.row] - var currIndex = indexPath - while delegate.containsTimeSlot(currTimeSlot) { - collectionView.deselectItem(at: currIndex, animated: false) - delegate?.handleSelection(for: room, timeSlot: currTimeSlot, action: SelectionType.remove) - let cell = collectionView.cellForItem(at: currIndex) - cell?.backgroundColor = .baseGreen - - currIndex = IndexPath(row: currIndex.row + 1, section: currIndex.section) - if let nextTimeSlot = currTimeSlot.next { - currTimeSlot = nextTimeSlot - } else { - break + let cell = collectionView.cellForItem(at: indexPath) + cell?.backgroundColor = UIColor.baseGreen + + collectionView.indexPathsForSelectedItems?.forEach { + if $0.item > indexPath.item { + collectionView.deselectItem(at: $0, animated: true) + collectionView.reloadItems(at: [$0]) + } + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + delegate.handleSelection(for: room.id) + + let indexPaths = (collectionView.indexPathsForSelectedItems ?? []).sorted(by: {$0.item < $1.item}) + + for i in 1.. HomeCellItem? { guard let json = json else { return nil } do { - let reservations = try GSRNetworkManager.instance.parseReservationsFromArray(json: json) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + decoder.keyDecodingStrategy = .convertFromSnakeCase + let reservations = try decoder.decode([GSRReservation].self, from: json.rawData()) return HomeReservationsCellItem(reservations: reservations) } catch { return nil diff --git a/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift b/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift index 3dc397928..dc576c578 100644 --- a/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift +++ b/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift @@ -48,58 +48,58 @@ extension HomeGSRCellItem: HomeAPIRequestable { let dateString = formatter.string(from: Date().roundedDownToHour) - GSRNetworkManager.instance.getAvailability(for: 1086, dateStr: dateString) { (rooms) in - if let rooms = rooms { - self.filterForTimeConstraints(rooms) - } - completion() - } +// GSRNetworkManager.instance.getAvailability(for: 1086, dateStr: dateString) { (rooms) in +// if let rooms = rooms { +// self.filterForTimeConstraints(rooms) +// } +// completion() +// } } private func filterForTimeConstraints(_ rooms: [GSRRoom]) { - let wicRooms = rooms.filter { $0.gid == 1889 && $0.name.contains("Rm") } - let wicBooths = rooms.filter { $0.gid == 1889 && $0.name.contains("Booth") } - - let gsrLoc = GSRLocation(lid: 1086, gid: 1889, name: "Weigle", service: "libcal") - var roomBookings = [GSRBooking?]() - for i in 1...3 { - let roomSlot = getFirstOpenRoom(wicRooms, duration: 30*i) - roomBookings.append(getBooking(gsrLoc, roomSlot.0, 1, roomSlot.1)) - } - var boothBookings = [GSRBooking?]() - for i in 1...3 { - let roomSlot = getFirstOpenRoom(wicBooths, duration: 30*i) - boothBookings.append(getBooking(gsrLoc, roomSlot.0, 1, roomSlot.1)) - } - self.bookingOptions = [boothBookings, roomBookings] - } - - private func getBooking(_ location: GSRLocation, _ slot: GSRTimeSlot?, _ numSlots: Int, _ room: GSRRoom?) -> GSRBooking? { - guard let slot = slot else { return nil } - var endTime = slot.endTime - if numSlots == 2 { - guard let lastSlot = slot.next else { return nil } - endTime = lastSlot.endTime - } - if numSlots == 3 { - guard let _ = slot.next else { return nil } - guard let lastSlot = slot.next!.next else { return nil } - endTime = lastSlot.endTime - } - return GSRBooking(location: location, roomId: slot.roomId, start: slot.startTime, end: endTime, name: room?.name) +// let wicRooms = rooms.filter { $0.id == 1889 && $0.roomName.contains("Rm") } +// let wicBooths = rooms.filter { $0.id == 1889 && $0.roomName.contains("Booth") } +// +// let gsrLoc = GSRLocation(lid: 1086, gid: 1889, name: "Weigle", kind: "LIBCAL", imageUrl: "URL") +// var roomBookings = [GSRBooking?]() +// for i in 1...3 { +// let roomSlot = getFirstOpenRoom(wicRooms, duration: 30*i) +// roomBookings.append(getBooking(gsrLoc, roomSlot.0, 1, roomSlot.1)) +// } +// var boothBookings = [GSRBooking?]() +// for i in 1...3 { +// let roomSlot = getFirstOpenRoom(wicBooths, duration: 30*i) +// boothBookings.append(getBooking(gsrLoc, roomSlot.0, 1, roomSlot.1)) +// } +// self.bookingOptions = [boothBookings, roomBookings] } - private func getFirstOpenRoom(_ rooms: [GSRRoom], duration: Int) -> (GSRTimeSlot?, GSRRoom?) { - let firstOpenRoom: GSRRoom? = rooms.min { (lhs, rhs) -> Bool in - guard let lhsFirst = lhs.timeSlots.firstTimeslot(duration: duration) else { - return false - } - guard let rhsFirst = rhs.timeSlots.firstTimeslot(duration: duration) else { - return true - } - return lhsFirst.startTime <= rhsFirst.startTime - } - let firstTimeSlot : GSRTimeSlot? = firstOpenRoom?.timeSlots.firstTimeslot(duration: duration) - return (firstTimeSlot, firstOpenRoom) - } +// private func getBooking(_ location: GSRLocation, _ slot: GSRTimeSlot?, _ numSlots: Int, _ room: GSRRoom?) -> GSRBooking? { +// guard let slot = slot else { return nil } +// var endTime = slot.endTime +// if numSlots == 2 { +// guard let lastSlot = slot.next else { return nil } +// endTime = lastSlot.endTime +// } +// if numSlots == 3 { +// guard let _ = slot.next else { return nil } +// guard let lastSlot = slot.next!.next else { return nil } +// endTime = lastSlot.endTime +// } +// return GSRBooking(location: location, roomId: slot.roomId, start: slot.startTime, end: endTime, name: room?.name) +// } +// +// private func getFirstOpenRoom(_ rooms: [GSRRoom], duration: Int) -> (GSRTimeSlot?, GSRRoom?) { +// let firstOpenRoom: GSRRoom? = rooms.min { (lhs, rhs) -> Bool in +// guard let lhsFirst = lhs.timeSlots.firstTimeslot(duration: duration) else { +// return false +// } +// guard let rhsFirst = rhs.timeSlots.firstTimeslot(duration: duration) else { +// return true +// } +// return lhsFirst.startTime <= rhsFirst.startTime +// } +// let firstTimeSlot : GSRTimeSlot? = firstOpenRoom?.timeSlots.firstTimeslot(duration: duration) +// return (firstTimeSlot, firstOpenRoom) +// } } diff --git a/PennMobile/Home/Cells/GSR/HomeStudyRoomCell.swift b/PennMobile/Home/Cells/GSR/HomeStudyRoomCell.swift index 5a6cfd762..9e33e1659 100644 --- a/PennMobile/Home/Cells/GSR/HomeStudyRoomCell.swift +++ b/PennMobile/Home/Cells/GSR/HomeStudyRoomCell.swift @@ -92,7 +92,7 @@ extension HomeStudyRoomCell { for n in 0..<3 { bookingButtons[row][n].booking = bookingOptions?[row][n] if let _ = bookingOptions, bookingOptions![row][n] != nil { - bookingButtonTimeLabels[row][n].text = formatter.string(from: (bookingOptions![row][n]?.start)!) +// bookingButtonTimeLabels[row][n].text = formatter.string(from: (bookingOptions![row][n]?.start)!) } else { bookingButtonTimeLabels[row][n].text = "" } diff --git a/PennMobile/Home/Controllers/HomeViewController + Delegates.swift b/PennMobile/Home/Controllers/HomeViewController + Delegates.swift index 1e055511b..a2b0d4443 100644 --- a/PennMobile/Home/Controllers/HomeViewController + Delegates.swift +++ b/PennMobile/Home/Controllers/HomeViewController + Delegates.swift @@ -18,7 +18,7 @@ extension HomeViewController: GSRDeletable { deleteReservation(bookingID) { (successful) in if successful { guard let reservationItem = self.tableViewModel.getItems(for: [HomeItemTypes.instance.reservations]).first as? HomeReservationsCellItem else { return } - reservationItem.reservations = reservationItem.reservations.filter { $0.bookingID != bookingID } + reservationItem.reservations = reservationItem.reservations.filter { $0.bookingId != bookingID } if reservationItem.reservations.isEmpty { self.removeItem(reservationItem) } else { @@ -29,7 +29,7 @@ extension HomeViewController: GSRDeletable { } func deleteReservation(_ reservation: GSRReservation) { - deleteReservation(reservation.bookingID) + deleteReservation(reservation.bookingId) } } @@ -43,7 +43,7 @@ extension HomeViewController: GSRBookable { } private func confirmBookingWanted(_ booking: GSRBooking) { - let message = "Booking \(booking.getRoomName()) from \(booking.getLocalTimeString())" + let message = "Booking \(booking.roomName) from \(booking.getLocalTimeString())" let alert = UIAlertController(title: "Confirm Booking", message: message, preferredStyle: .alert) @@ -56,17 +56,17 @@ extension HomeViewController: GSRBookable { } private func handleBookingRequested(_ booking: GSRBooking) { - if GSRUser.hasSavedUser() { - booking.user = GSRUser.getUser() - submitBooking(for: booking) { (completion) in - self.fetchCellData(for: [HomeItemTypes.instance.studyRoomBooking]) - } - } else { - let glc = GSRLoginController() - glc.booking = booking - let nvc = UINavigationController(rootViewController: glc) - present(nvc, animated: true, completion: nil) - } +// if GSRUser.hasSavedUser() { +// booking.user = GSRUser.getUser() +// submitBooking(for: booking) { (completion) in +//// self.fetchCellData(for: [HomeItemTypes.instance.studyRoomBooking]) +// } +// } else { +// let glc = GSRLoginController() +// glc.booking = booking +// let nvc = UINavigationController(rootViewController: glc) +// present(nvc, animated: true, completion: nil) +// } } } diff --git a/PennMobile/Home/Map/PennCoordinate.swift b/PennMobile/Home/Map/PennCoordinate.swift index 79de37140..d9d9940f9 100644 --- a/PennMobile/Home/Map/PennCoordinate.swift +++ b/PennMobile/Home/Map/PennCoordinate.swift @@ -45,11 +45,6 @@ class PennCoordinate { func getRegion(for facility: FitnessFacilityName, at scale: PennCoordinateScale) -> MKCoordinateRegion { return MKCoordinateRegion.init(center: getCoordinates(for: facility), latitudinalMeters: scale.rawValue, longitudinalMeters: scale.rawValue) } - - // TODO: Implement a switch statement that matches GSR venue IDs with GPS coords - func getCoordinates(for gsr: GSRVenue) -> CLLocationCoordinate2D { - return getDefault() - } } // MARK: - Placemarks diff --git a/PennMobile/Home/Networking/HomeAPIService.swift b/PennMobile/Home/Networking/HomeAPIService.swift index a2bf53952..2d2b449bd 100644 --- a/PennMobile/Home/Networking/HomeAPIService.swift +++ b/PennMobile/Home/Networking/HomeAPIService.swift @@ -29,8 +29,6 @@ final class HomeAPIService: Requestable { url = "\(url)&groupsEnabled=\(UserDefaults.standard.gsrGroupsEnabled())" - - OAuth2NetworkManager.instance.getAccessToken { (token) in // Make request without access token if one does not exist let url = URL(string: url)! diff --git a/PennMobile/Setup + Navigation/RootViewController.swift b/PennMobile/Setup + Navigation/RootViewController.swift index 1c7719c8b..454278b1a 100644 --- a/PennMobile/Setup + Navigation/RootViewController.swift +++ b/PennMobile/Setup + Navigation/RootViewController.swift @@ -203,6 +203,7 @@ class RootViewController: UIViewController, NotificationRequestable { HTTPCookieStorage.shared.removeCookies(since: Date(timeIntervalSince1970: 0)) UserDefaults.standard.clearAll() OAuth2NetworkManager.instance.clearRefreshToken() + OAuth2NetworkManager.instance.clearCurrentAccessToken() Account.clear() GSRUser.clear() } diff --git a/Podfile.lock b/Podfile.lock index 057041dd9..f4aec5431 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -58,8 +58,8 @@ SPEC REPOS: SPEC CHECKSUMS: Base32: 163c298644d184e89ca4e00a996bad6bf5166390 - Crashlytics: 9220f5bc89e7a618df411b4f639389dbfb0e03d2 - Fabric: ea977e3cd9c20425516d3dafd3bf8c941c51223f + Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df + Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 Firebase: 5ec5e863d269d82d66b4bf56856726f8fb8f0fb3 FirebaseAnalytics: 7ef69e76a5142f643aeb47c780e1cdce4e23632e FirebaseCore: 90cb1c53d69b556f112a1bf72b5fcfaad7650790 @@ -73,4 +73,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 698f270978d4836609b8efd968d02a017d79b97c -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.1 From 2c7a3656ce702f88c88d96b216c98ee2eb6b0f22 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Mon, 6 Sep 2021 12:13:32 -0400 Subject: [PATCH 05/18] Hide course alerts in prod --- PennMobile/Setup + Navigation/ControllerModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PennMobile/Setup + Navigation/ControllerModel.swift b/PennMobile/Setup + Navigation/ControllerModel.swift index ea76b7dba..8b587b903 100644 --- a/PennMobile/Setup + Navigation/ControllerModel.swift +++ b/PennMobile/Setup + Navigation/ControllerModel.swift @@ -79,7 +79,7 @@ class ControllerModel: NSObject { #if DEBUG return [.news, .contacts, .courseSchedule, .courseAlerts, .about] #else - return [.news, .contacts, .courseSchedule, .courseAlerts, .about] + return [.news, .contacts, .courseSchedule, .about] #endif } } From 004522a86ca0de63cede48af1e26a690bcfad0b8 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Mon, 6 Sep 2021 16:38:14 -0400 Subject: [PATCH 06/18] Clean up project to remove all warnings. Reverted migration of some pods to SPM due to compatibility issues. --- PennMobile.xcodeproj/project.pbxproj | 106 +++++------------- .../AutomatedScreenshotUITests.xcscheme | 11 +- .../xcshareddata/xcschemes/Fastlane.xcscheme | 11 +- .../xcschemes/FastlaneUIAutomation.xcscheme | 2 +- .../xcschemes/PennMobile.xcscheme | 2 +- .../Privacy/PrivacyTableViewController.swift | 2 +- PennMobile/Auth/TwoFactorTokenGenerator.swift | 4 +- .../ContactsTableViewController.swift | 2 +- .../Controllers/CourseAlertController.swift | 2 +- .../CourseAlertCreateController.swift | 4 +- .../CourseAlertSettingsController.swift | 2 +- .../CourseAlertNetworkManager.swift | 4 +- ...ningDollarsTransactionViewController.swift | 2 +- .../Controllers/DiningViewController.swift | 2 - .../Dining/Views/DiningHeaderView.swift | 2 +- .../GSRGroupInviteViewController.swift | 2 +- .../ModularTableViewCell.swift | 2 +- .../ModularTableViewItemTypes.swift | 2 +- .../Buildings/Cells/BuildingCell.swift | 2 +- .../Home/Cells/GSR/HomeGSRCellItem.swift | 2 +- .../Home/Controllers/HomeViewController.swift | 2 +- .../Controllers/FitnessViewController.swift | 2 - .../Home/Fitness/Model/FitnessModels.swift | 2 +- PennMobile/Laundry/Cells/AddLaundryCell.swift | 2 +- PennMobile/Laundry/Cells/LaundryCell.swift | 2 +- .../RoomSelectionViewController.swift | 2 +- .../Laundry/Views/RoomSelectionView.swift | 2 +- .../Login/CampusExpressNetworkManager.swift | 4 +- .../NotificationsTableViewController.swift | 2 +- .../Onboarding/OnboardingController.swift | 2 +- PennMobile/Onboarding/SelectionCell.swift | 4 +- Podfile | 9 +- Podfile.lock | 10 +- 33 files changed, 80 insertions(+), 133 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 1e7fd824b..682aa9877 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -145,8 +145,8 @@ 66E8ECA72381CB5100945BEA /* TwoFactorTokenGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA62381CB5100945BEA /* TwoFactorTokenGenerator.swift */; }; 66E8ECA92381CD2F00945BEA /* TOTPNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA82381CD2F00945BEA /* TOTPNetworkManager.swift */; }; 6C23AF9526E57903002F60F0 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C23AF9426E57903002F60F0 /* EmptyView.swift */; }; - 6C369A1326E299A900721CA1 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 6C369A1226E299A900721CA1 /* Alamofire */; }; 6C369A1526E39BC100721CA1 /* GSRAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */; }; + 6C4CC1FA26E6B1720000B4A8 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */; }; 97AA806923D26BC700C23488 /* DiningBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */; }; 97AA806A23D26BC700C23488 /* TransactionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806523D26BC700C23488 /* TransactionTableViewCell.swift */; }; 97AA806B23D26BC700C23488 /* DiningHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806623D26BC700C23488 /* DiningHeaderView.swift */; }; @@ -237,9 +237,6 @@ F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE123C3EE3E000AD90F /* SwiftSoup */; }; F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE423C3F240000AD90F /* KingfisherSwiftUI */; }; F213CCE723C3F240000AD90F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE623C3F240000AD90F /* Kingfisher */; }; - F213CCEA23C3F5D5000AD90F /* SCLAlertView in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCE923C3F5D5000AD90F /* SCLAlertView */; }; - F213CCED23C3F6A8000AD90F /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCEC23C3F6A8000AD90F /* SwiftyJSON */; }; - F213CCF023C3F99B000AD90F /* ScrollableGraphView in Frameworks */ = {isa = PBXBuildFile; productRef = F213CCEF23C3F99B000AD90F /* ScrollableGraphView */; }; F230FB11225809E900760499 /* AutomatedScreenshotUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F230FB10225809E900760499 /* AutomatedScreenshotUITests.swift */; }; F230FB1A22580ACA00760499 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F230FB1922580ACA00760499 /* SnapshotHelper.swift */; }; F23CC0BB235E3EF10007317A /* DiningVenue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23CC0BA235E3EF10007317A /* DiningVenue.swift */; }; @@ -590,14 +587,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6C369A1326E299A900721CA1 /* Alamofire in Frameworks */, - F213CCED23C3F6A8000AD90F /* SwiftyJSON in Frameworks */, F2568A762413534F00561295 /* SnapKit in Frameworks */, - F213CCF023C3F99B000AD90F /* ScrollableGraphView in Frameworks */, + 6C4CC1FA26E6B1720000B4A8 /* SwiftyJSON in Frameworks */, F213CCE723C3F240000AD90F /* Kingfisher in Frameworks */, F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */, F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */, - F213CCEA23C3F5D5000AD90F /* SCLAlertView in Frameworks */, 56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1571,11 +1565,8 @@ F213CCE123C3EE3E000AD90F /* SwiftSoup */, F213CCE423C3F240000AD90F /* KingfisherSwiftUI */, F213CCE623C3F240000AD90F /* Kingfisher */, - F213CCE923C3F5D5000AD90F /* SCLAlertView */, - F213CCEC23C3F6A8000AD90F /* SwiftyJSON */, - F213CCEF23C3F99B000AD90F /* ScrollableGraphView */, F2568A752413534F00561295 /* SnapKit */, - 6C369A1226E299A900721CA1 /* Alamofire */, + 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */, ); productName = PennMobile; productReference = 216640601EBADADA00746B8E /* PennMobile.app */; @@ -1625,12 +1616,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1250; ORGANIZATIONNAME = PennLabs; TargetAttributes = { 2166405F1EBADADA00746B8E = { CreatedOnToolsVersion = 8.3.2; - LastSwiftMigration = 1020; + LastSwiftMigration = 1250; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Keychain = { @@ -1644,6 +1635,7 @@ F230FB0D225809E900760499 = { CreatedOnToolsVersion = 10.1; DevelopmentTeam = VU59R57FGM; + LastSwiftMigration = 1250; ProvisioningStyle = Automatic; TestTargetID = 2166405F1EBADADA00746B8E; }; @@ -1664,11 +1656,8 @@ packageReferences = ( F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */, F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */, - F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */, - F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */, F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, - 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */, + 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON.git" */, ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -1782,6 +1771,8 @@ "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/OneTimePassword/OneTimePassword.framework", + "${BUILT_PRODUCTS_DIR}/SCLAlertView/SCLAlertView.framework", + "${BUILT_PRODUCTS_DIR}/ScrollableGraphView/ScrollableGraphView.framework", "${BUILT_PRODUCTS_DIR}/XLPagerTabStrip/XLPagerTabStrip.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", ); @@ -1791,6 +1782,8 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MBProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OneTimePassword.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SCLAlertView.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ScrollableGraphView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/XLPagerTabStrip.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", ); @@ -2123,8 +2116,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CF_BUNDLE_LONG_VERSION_STRING = 6560; - CF_BUNDLE_SHORT_VERSION_STRING = 6.5.6; + CF_BUNDLE_LONG_VERSION_STRING = 6601; + CF_BUNDLE_SHORT_VERSION_STRING = 6.6.0; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2147,6 +2140,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -2171,7 +2165,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -2184,8 +2178,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CF_BUNDLE_LONG_VERSION_STRING = 6560; - CF_BUNDLE_SHORT_VERSION_STRING = 6.5.6; + CF_BUNDLE_LONG_VERSION_STRING = 6601; + CF_BUNDLE_SHORT_VERSION_STRING = 6.6.0; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2208,6 +2202,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -2226,7 +2221,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -2245,7 +2240,7 @@ CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6561; + CURRENT_PROJECT_VERSION = "$(CF_BUNDLE_LONG_VERSION_STRING"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = VU59R57FGM; EXCLUDED_ARCHS = ""; @@ -2256,7 +2251,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 6.6.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2315,7 +2310,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "PennMobile/Supporting_Files/PennMobile-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -2330,7 +2325,7 @@ CODE_SIGN_ENTITLEMENTS = PennMobile/PennMobile.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6561; + CURRENT_PROJECT_VERSION = "$(CF_BUNDLE_LONG_VERSION_STRING"; DEVELOPMENT_TEAM = VU59R57FGM; EXCLUDED_ARCHS = ""; GCC_PREFIX_HEADER = PennMobile/Supporting_Files/PrefixHeader.pch; @@ -2340,7 +2335,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 6.6.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "$(inherited)", @@ -2399,7 +2394,7 @@ PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "PennMobile/Supporting_Files/PennMobile-Bridging-Header.h"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -2556,12 +2551,12 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON.git" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Alamofire/Alamofire"; + repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.4.3; + minimumVersion = 5.0.1; }; }; F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { @@ -2580,30 +2575,6 @@ minimumVersion = 5.12.0; }; }; - F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pennlabs/SCLAlertView-Swift/"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.8.2; - }; - }; - F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.0.0; - }; - }; - F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pennlabs/ScrollableGraphView"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.1.0; - }; - }; F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit"; @@ -2615,10 +2586,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 6C369A1226E299A900721CA1 /* Alamofire */ = { + 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; - package = 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; + package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON.git" */; + productName = SwiftyJSON; }; F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; @@ -2635,21 +2606,6 @@ package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - F213CCE923C3F5D5000AD90F /* SCLAlertView */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */; - productName = SCLAlertView; - }; - F213CCEC23C3F6A8000AD90F /* SwiftyJSON */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */; - productName = SwiftyJSON; - }; - F213CCEF23C3F99B000AD90F /* ScrollableGraphView */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */; - productName = ScrollableGraphView; - }; F2568A752413534F00561295 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; package = F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */; diff --git a/PennMobile.xcodeproj/xcshareddata/xcschemes/AutomatedScreenshotUITests.xcscheme b/PennMobile.xcodeproj/xcshareddata/xcschemes/AutomatedScreenshotUITests.xcscheme index c69369d5b..ddf551443 100644 --- a/PennMobile.xcodeproj/xcshareddata/xcschemes/AutomatedScreenshotUITests.xcscheme +++ b/PennMobile.xcodeproj/xcshareddata/xcschemes/AutomatedScreenshotUITests.xcscheme @@ -1,6 +1,6 @@ - - - - diff --git a/PennMobile.xcodeproj/xcshareddata/xcschemes/Fastlane.xcscheme b/PennMobile.xcodeproj/xcshareddata/xcschemes/Fastlane.xcscheme index 532ac4ba7..e81c9d512 100644 --- a/PennMobile.xcodeproj/xcshareddata/xcschemes/Fastlane.xcscheme +++ b/PennMobile.xcodeproj/xcshareddata/xcschemes/Fastlane.xcscheme @@ -1,6 +1,6 @@ - - - - diff --git a/PennMobile.xcodeproj/xcshareddata/xcschemes/FastlaneUIAutomation.xcscheme b/PennMobile.xcodeproj/xcshareddata/xcschemes/FastlaneUIAutomation.xcscheme index a0658f02d..3a03fe6cc 100644 --- a/PennMobile.xcodeproj/xcshareddata/xcschemes/FastlaneUIAutomation.xcscheme +++ b/PennMobile.xcodeproj/xcshareddata/xcschemes/FastlaneUIAutomation.xcscheme @@ -1,6 +1,6 @@ Bool { let genericPwdQueryable = GenericPasswordQueryable(service: "PennWebLogin") let secureStore = SecureStore(secureStoreQueryable: genericPwdQueryable) - if let _ = try? secureStore.getValue(for: "TOTPSecret") { + if let _ = ((try? secureStore.getValue(for: "TOTPSecret")) as String??) { return true } else { diff --git a/PennMobile/Contacts/ContactsTableViewController.swift b/PennMobile/Contacts/ContactsTableViewController.swift index d490b1ede..a291a7111 100644 --- a/PennMobile/Contacts/ContactsTableViewController.swift +++ b/PennMobile/Contacts/ContactsTableViewController.swift @@ -8,7 +8,7 @@ import UIKit -protocol ContactCellDelegate: class { +protocol ContactCellDelegate: AnyObject { func call(number: String) } diff --git a/PennMobile/Course Alerts/Controllers/CourseAlertController.swift b/PennMobile/Course Alerts/Controllers/CourseAlertController.swift index 9a809f411..662a88b3e 100644 --- a/PennMobile/Course Alerts/Controllers/CourseAlertController.swift +++ b/PennMobile/Course Alerts/Controllers/CourseAlertController.swift @@ -112,7 +112,7 @@ extension CourseAlertController { } func setupLoadingView() { - loadingView = UIActivityIndicatorView(style: .whiteLarge) + loadingView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.large) loadingView.color = .black loadingView.isHidden = false view.addSubview(loadingView) diff --git a/PennMobile/Course Alerts/Controllers/CourseAlertCreateController.swift b/PennMobile/Course Alerts/Controllers/CourseAlertCreateController.swift index 7cd7f199e..1e9b0c355 100644 --- a/PennMobile/Course Alerts/Controllers/CourseAlertCreateController.swift +++ b/PennMobile/Course Alerts/Controllers/CourseAlertCreateController.swift @@ -8,7 +8,7 @@ import UIKit import Foundation -protocol FetchPCADataProtocol: class { +protocol FetchPCADataProtocol { func fetchAlerts() func fetchSettings() } @@ -27,7 +27,7 @@ class CourseAlertCreateController: GenericViewController { fileprivate var sectionToAlert: CourseSection? - weak var delegate: FetchPCADataProtocol? + var delegate: FetchPCADataProtocol? override func viewDidLoad() { super.viewDidLoad() diff --git a/PennMobile/Course Alerts/Controllers/CourseAlertSettingsController.swift b/PennMobile/Course Alerts/Controllers/CourseAlertSettingsController.swift index 9f2f1780d..d1606ee29 100644 --- a/PennMobile/Course Alerts/Controllers/CourseAlertSettingsController.swift +++ b/PennMobile/Course Alerts/Controllers/CourseAlertSettingsController.swift @@ -10,7 +10,7 @@ import Foundation import UIKit -protocol CourseAlertSettingsChangedPreference: class { +protocol CourseAlertSettingsChangedPreference: AnyObject { func allowChange() -> Bool func changed(option: PCAOption, toValue: Bool) func requestChange(option: PCAOption, toValue: Bool) diff --git a/PennMobile/Course Alerts/Networking/CourseAlertNetworkManager.swift b/PennMobile/Course Alerts/Networking/CourseAlertNetworkManager.swift index 26d64563a..7987778aa 100644 --- a/PennMobile/Course Alerts/Networking/CourseAlertNetworkManager.swift +++ b/PennMobile/Course Alerts/Networking/CourseAlertNetworkManager.swift @@ -85,7 +85,7 @@ class CourseAlertNetworkManager: NSObject, Requestable { return } - guard let data = data else { + guard data != nil else { callback(false, "", error) return } @@ -137,7 +137,7 @@ class CourseAlertNetworkManager: NSObject, Requestable { return } - guard let data = data else { + guard data != nil else { callback(false, error) return } diff --git a/PennMobile/Dining/Controllers/DiningDollarsTransactionViewController.swift b/PennMobile/Dining/Controllers/DiningDollarsTransactionViewController.swift index 5429ccfcf..08cdbcaa0 100644 --- a/PennMobile/Dining/Controllers/DiningDollarsTransactionViewController.swift +++ b/PennMobile/Dining/Controllers/DiningDollarsTransactionViewController.swift @@ -8,7 +8,7 @@ import Foundation -protocol TransactionCellDelegate: class { +protocol TransactionCellDelegate: AnyObject { func userDidSelect() } diff --git a/PennMobile/Dining/Controllers/DiningViewController.swift b/PennMobile/Dining/Controllers/DiningViewController.swift index 12d67634a..48a5a7cd2 100755 --- a/PennMobile/Dining/Controllers/DiningViewController.swift +++ b/PennMobile/Dining/Controllers/DiningViewController.swift @@ -69,7 +69,6 @@ class DiningViewController: GenericTableViewController { //Mark: Networking to retrieve today's times extension DiningViewController { fileprivate func fetchDiningHours() { - UIApplication.shared.isNetworkActivityIndicatorVisible = true DiningAPI.instance.fetchDiningHours { (success, error) in DispatchQueue.main.async { if !success { @@ -82,7 +81,6 @@ extension DiningViewController { self.viewModel.venues = DiningDataStore.shared.getSectionedVenues() self.tableView.reloadData() self.refreshControl?.endRefreshing() - UIApplication.shared.isNetworkActivityIndicatorVisible = false } } } diff --git a/PennMobile/Dining/Views/DiningHeaderView.swift b/PennMobile/Dining/Views/DiningHeaderView.swift index 157fb9902..56194e996 100644 --- a/PennMobile/Dining/Views/DiningHeaderView.swift +++ b/PennMobile/Dining/Views/DiningHeaderView.swift @@ -97,7 +97,7 @@ extension DiningHeaderView { } func prepareLoadingView() { - loadingView = UIActivityIndicatorView(style: .white) + loadingView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.medium) loadingView.color = .black loadingView.translatesAutoresizingMaskIntoConstraints = false diff --git a/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupInviteViewController.swift b/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupInviteViewController.swift index fb3f5d050..f84c5703b 100644 --- a/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupInviteViewController.swift +++ b/PennMobile/GSR-Booking/Controllers/GSRGroups/GSRGroupInviteViewController.swift @@ -269,7 +269,7 @@ extension GSRGroupInviteViewController { extension GSRGroupInviteViewController { func prepareLoadingView() { - loadingView = UIActivityIndicatorView(style: .whiteLarge) + loadingView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.large) loadingView.color = .black loadingView.isHidden = true diff --git a/PennMobile/General/ModularTableView/ModularTableViewCell.swift b/PennMobile/General/ModularTableView/ModularTableViewCell.swift index b80e9f60e..4693ea9cd 100644 --- a/PennMobile/General/ModularTableView/ModularTableViewCell.swift +++ b/PennMobile/General/ModularTableView/ModularTableViewCell.swift @@ -10,7 +10,7 @@ import Foundation protocol ModularTableViewCellDelegate {} -protocol ModularTableViewCell: class { +protocol ModularTableViewCell: AnyObject { static var identifier: String { get } static func getCellHeight(for item: ModularTableViewItem) -> CGFloat var item: ModularTableViewItem! { get set } diff --git a/PennMobile/General/ModularTableView/ModularTableViewItemTypes.swift b/PennMobile/General/ModularTableView/ModularTableViewItemTypes.swift index 1e607733f..60f196f7e 100644 --- a/PennMobile/General/ModularTableView/ModularTableViewItemTypes.swift +++ b/PennMobile/General/ModularTableView/ModularTableViewItemTypes.swift @@ -8,7 +8,7 @@ import Foundation -protocol ModularTableViewItemTypes: class { +protocol ModularTableViewItemTypes: AnyObject { func registerCells(for tableView: UITableView) } diff --git a/PennMobile/Home/Buildings/Buildings/Cells/BuildingCell.swift b/PennMobile/Home/Buildings/Buildings/Cells/BuildingCell.swift index e825a8470..516e22daf 100644 --- a/PennMobile/Home/Buildings/Buildings/Cells/BuildingCell.swift +++ b/PennMobile/Home/Buildings/Buildings/Cells/BuildingCell.swift @@ -8,7 +8,7 @@ import UIKit -protocol CellUpdateDelegate: class { +protocol CellUpdateDelegate: AnyObject { func cellRequiresNewLayout(with height: CGFloat, for cell: String) } diff --git a/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift b/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift index dc576c578..8a653ed8f 100644 --- a/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift +++ b/PennMobile/Home/Cells/GSR/HomeGSRCellItem.swift @@ -46,7 +46,7 @@ extension HomeGSRCellItem: HomeAPIRequestable { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" - let dateString = formatter.string(from: Date().roundedDownToHour) + _ = formatter.string(from: Date().roundedDownToHour) // GSRNetworkManager.instance.getAvailability(for: 1086, dateStr: dateString) { (rooms) in // if let rooms = rooms { diff --git a/PennMobile/Home/Controllers/HomeViewController.swift b/PennMobile/Home/Controllers/HomeViewController.swift index f4e155114..cc6eed2c0 100644 --- a/PennMobile/Home/Controllers/HomeViewController.swift +++ b/PennMobile/Home/Controllers/HomeViewController.swift @@ -146,7 +146,7 @@ extension HomeViewController { } func prepareLoadingView() { - loadingView = UIActivityIndicatorView(style: .whiteLarge) + loadingView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.large) loadingView.color = .black loadingView.isHidden = false view.addSubview(loadingView) diff --git a/PennMobile/Home/Fitness/Controllers/FitnessViewController.swift b/PennMobile/Home/Fitness/Controllers/FitnessViewController.swift index 8ea2928c5..efb2cb89b 100644 --- a/PennMobile/Home/Fitness/Controllers/FitnessViewController.swift +++ b/PennMobile/Home/Fitness/Controllers/FitnessViewController.swift @@ -42,7 +42,6 @@ class FitnessViewController: GenericTableViewController { //Mark: Networking to retrieve today's times extension FitnessViewController { fileprivate func fetchFitnessHours() { - UIApplication.shared.isNetworkActivityIndicatorVisible = true FitnessAPI.instance.fetchFitnessHours { (success, error) in DispatchQueue.main.async { if !success { @@ -54,7 +53,6 @@ extension FitnessViewController { } self.tableView.reloadData() self.refreshControl?.endRefreshing() - UIApplication.shared.isNetworkActivityIndicatorVisible = false } } } diff --git a/PennMobile/Home/Fitness/Model/FitnessModels.swift b/PennMobile/Home/Fitness/Model/FitnessModels.swift index c513f384d..8f1e8b8db 100644 --- a/PennMobile/Home/Fitness/Model/FitnessModels.swift +++ b/PennMobile/Home/Fitness/Model/FitnessModels.swift @@ -32,7 +32,7 @@ struct FitnessSchedule: Codable { self.name = try container.decodeIfPresent(FitnessFacilityName.self, forKey: .name) ?? .unknown } catch { self.name = .unknown - if let unknownName = try? container.decodeIfPresent(String.self, forKey: .name) { + if let unknownName = ((try? container.decodeIfPresent(String.self, forKey: .name)) as String??) { print("ERROR: Unknown fitness facility name - \(unknownName ?? "")") } } diff --git a/PennMobile/Laundry/Cells/AddLaundryCell.swift b/PennMobile/Laundry/Cells/AddLaundryCell.swift index de3dc0cf5..275d8b8c9 100644 --- a/PennMobile/Laundry/Cells/AddLaundryCell.swift +++ b/PennMobile/Laundry/Cells/AddLaundryCell.swift @@ -8,7 +8,7 @@ import UIKit -protocol AddLaundryCellDelegate: class { +protocol AddLaundryCellDelegate: AnyObject { func addPressed() } diff --git a/PennMobile/Laundry/Cells/LaundryCell.swift b/PennMobile/Laundry/Cells/LaundryCell.swift index 7f24a114b..52caa9390 100644 --- a/PennMobile/Laundry/Cells/LaundryCell.swift +++ b/PennMobile/Laundry/Cells/LaundryCell.swift @@ -11,7 +11,7 @@ import ScrollableGraphView // MARK: - Laundry Cell Delegate -protocol LaundryCellDelegate: class, LaundryMachineCellTappable { +protocol LaundryCellDelegate: AnyObject, LaundryMachineCellTappable { func deleteLaundryCell(for room: LaundryRoom) } diff --git a/PennMobile/Laundry/Controllers/RoomSelectionViewController.swift b/PennMobile/Laundry/Controllers/RoomSelectionViewController.swift index dce98f992..bfe473d9b 100644 --- a/PennMobile/Laundry/Controllers/RoomSelectionViewController.swift +++ b/PennMobile/Laundry/Controllers/RoomSelectionViewController.swift @@ -8,7 +8,7 @@ import UIKit -protocol RoomSelectionVCDelegate: class { +protocol RoomSelectionVCDelegate: AnyObject { func saveSelection(for rooms: [LaundryRoom]) } diff --git a/PennMobile/Laundry/Views/RoomSelectionView.swift b/PennMobile/Laundry/Views/RoomSelectionView.swift index 47f3b4520..375f42607 100644 --- a/PennMobile/Laundry/Views/RoomSelectionView.swift +++ b/PennMobile/Laundry/Views/RoomSelectionView.swift @@ -6,7 +6,7 @@ // Copyright © 2017 PennLabs. All rights reserved. // -protocol RoomSelectionViewDelegate: class { +protocol RoomSelectionViewDelegate: AnyObject { func updateSelectedRooms(for rooms: [LaundryRoom]) func handleFailureToLoadDictionary() } diff --git a/PennMobile/Login/CampusExpressNetworkManager.swift b/PennMobile/Login/CampusExpressNetworkManager.swift index b4989301e..19147133e 100644 --- a/PennMobile/Login/CampusExpressNetworkManager.swift +++ b/PennMobile/Login/CampusExpressNetworkManager.swift @@ -31,7 +31,7 @@ extension CampusExpressNetworkManager: PennAuthRequestable { func updateHousingData(_ completion: ((_ success: Bool) -> Void)? = nil) { makeAuthRequest(targetUrl: housingUrl, shibbolethUrl: shibbolethUrl) { (data, response, error) in if let data = data, let html = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { - if let doc = try? SwiftSoup.parse(html as String), let htmlStr = try? doc.body()?.html(), let html = htmlStr { + if let doc = try? SwiftSoup.parse(html as String), let htmlStr = ((try? doc.body()?.html()) as String??), let html = htmlStr { UserDBManager.shared.saveHousingData(html: html) { (result) in completion?(result != nil) } @@ -45,7 +45,7 @@ extension CampusExpressNetworkManager: PennAuthRequestable { func getDiningBalanceHTML(callback: @escaping (_ html: String?, _ error: Error?) -> Void) { makeAuthRequest(targetUrl: diningUrl, shibbolethUrl: shibbolethUrl) { (data, response, error) in if let data = data, let html = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { - if let doc = try? SwiftSoup.parse(html as String), let htmlStr = try? doc.body()?.html() { + if let doc = try? SwiftSoup.parse(html as String), let htmlStr = ((try? doc.body()?.html()) as String??) { callback(htmlStr, error) } } else { diff --git a/PennMobile/Notifications/NotificationsTableViewController.swift b/PennMobile/Notifications/NotificationsTableViewController.swift index a6dfd6518..77277e880 100644 --- a/PennMobile/Notifications/NotificationsTableViewController.swift +++ b/PennMobile/Notifications/NotificationsTableViewController.swift @@ -8,7 +8,7 @@ import UIKit -protocol NotificationViewControllerChangedPreference: class { +protocol NotificationViewControllerChangedPreference: AnyObject { func allowChange() -> Bool func changed(option: NotificationOption, toValue: Bool) func requestChange(option: NotificationOption, toValue: Bool) diff --git a/PennMobile/Onboarding/OnboardingController.swift b/PennMobile/Onboarding/OnboardingController.swift index 88d03b581..459b5e40e 100644 --- a/PennMobile/Onboarding/OnboardingController.swift +++ b/PennMobile/Onboarding/OnboardingController.swift @@ -8,7 +8,7 @@ import UIKit -protocol OnboardingDelegate: class { +protocol OnboardingDelegate: AnyObject { func handleFinishedOnboarding() } diff --git a/PennMobile/Onboarding/SelectionCell.swift b/PennMobile/Onboarding/SelectionCell.swift index 41308690c..aaa8b9735 100644 --- a/PennMobile/Onboarding/SelectionCell.swift +++ b/PennMobile/Onboarding/SelectionCell.swift @@ -8,7 +8,7 @@ import UIKit -protocol SelectionCellDelegate: class { +protocol SelectionCellDelegate: AnyObject { func handleCancel() func saveSelection(for rooms: [LaundryRoom]) } @@ -25,7 +25,7 @@ class SelectionCell: UICollectionViewCell, RoomSelectionViewDelegate { backgroundColor = .white navigationBar = NavigationBar(frame: .zero) - navigationBar.customHeight = 44 + UIApplication.shared.statusBarFrame.height + navigationBar.customHeight = 44 + (window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0) navigationBar.frame.size = navigationBar.sizeThatFits(CGSize(width: UIScreen.main.bounds.size.width, height: navigationBar.customHeight)) selectionView = RoomSelectionView(frame: .zero) diff --git a/Podfile b/Podfile index 01d6292e6..64c6405de 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,7 @@ # Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' +#platform :ios + +inhibit_all_warnings! target 'PennMobile' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks @@ -8,6 +10,8 @@ target 'PennMobile' do # Pods for PennMobile pod 'MBProgressHUD', '~> 0.8' # old objc library, should be replaced +pod 'SCLAlertView' +pod 'ScrollableGraphView' pod 'Fabric', '~> 1.10.2' # Required by Firebase. pod 'Crashlytics', '~> 3.14.0' # Required by Firebase. @@ -30,7 +34,8 @@ end post_install do |pi| pi.pods_project.targets.each do |t| t.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = "YES" end end end diff --git a/Podfile.lock b/Podfile.lock index f4aec5431..1e75c0380 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -28,6 +28,8 @@ PODS: - nanopb/encode (0.3.9011) - OneTimePassword (3.2.0): - Base32 (~> 1.1.2) + - SCLAlertView (0.8) + - ScrollableGraphView (4.0.6) - SimulatorStatusMagic (2.4.1) - XLPagerTabStrip (9.0.0) @@ -37,6 +39,8 @@ DEPENDENCIES: - Firebase (~> 4.7) - MBProgressHUD (~> 0.8) - OneTimePassword (~> 3.2) + - SCLAlertView + - ScrollableGraphView - SimulatorStatusMagic - XLPagerTabStrip (~> 9.0) @@ -53,6 +57,8 @@ SPEC REPOS: - MBProgressHUD - nanopb - OneTimePassword + - SCLAlertView + - ScrollableGraphView - SimulatorStatusMagic - XLPagerTabStrip @@ -68,9 +74,11 @@ SPEC CHECKSUMS: MBProgressHUD: 1569cf7ace17a8bac47aabfbb8580a49690386d1 nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd OneTimePassword: c00ecc908e67e6c5bfda9d50b0b2e4696d0b066e + SCLAlertView: 6a77bb2edfc65e04dbe57725546cb4107a506b85 + ScrollableGraphView: 4af35d4ea9ecdec665d34a74da51da161ddff8ac SimulatorStatusMagic: 28d4a9d1a500ac7cea0b2b5a43c1c6ddb40ba56c XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 698f270978d4836609b8efd968d02a017d79b97c +PODFILE CHECKSUM: 5614727a7fd3c150fce595bdb18a15fc6b091712 COCOAPODS: 1.10.1 From 89aec0a78c6ca18de35c607171748f1f9863ee9c Mon Sep 17 00:00:00 2001 From: bsk42 <43442311+bsk42@users.noreply.github.com> Date: Mon, 6 Sep 2021 16:49:29 -0400 Subject: [PATCH 07/18] Update MoreViewController.swift (#381) * Update MoreViewController.swift Added link to air table for feedback * Changed feedback tab description Co-authored-by: Jong Min Choi <01jongminchoi@gmail.com> --- PennMobile/New Group/MoreViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PennMobile/New Group/MoreViewController.swift b/PennMobile/New Group/MoreViewController.swift index a623e5811..52dbd6eee 100644 --- a/PennMobile/New Group/MoreViewController.swift +++ b/PennMobile/New Group/MoreViewController.swift @@ -84,7 +84,8 @@ class MoreViewController: GenericTableViewController, ShowsAlert, KeychainAccess PennLink(title: "CampusExpress", url: "https://prod.campusexpress.upenn.edu"), PennLink(title: "Canvas", url: "https://canvas.upenn.edu"), PennLink(title: "PennInTouch", url: "https://pennintouch.apps.upenn.edu"), - PennLink(title: "PennPortal", url: "https://portal.apps.upenn.edu/penn_portal")] + PennLink(title: "PennPortal", url: "https://portal.apps.upenn.edu/penn_portal"), + PennLink(title: "Share Your Feedback", url: "https://airtable.com/shrS98E3rj5Nw1wy6")] } From 66acead1b31fd571c9e2b1a679ea85939a478a04 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Mon, 6 Sep 2021 17:41:34 -0400 Subject: [PATCH 08/18] Removed print statement --- PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift | 4 ---- PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift | 1 - PennMobile/Home/Fitness/Networking/FitnessAPI.swift | 1 - .../AppDelegate+NotificationExtension.swift | 3 --- 4 files changed, 9 deletions(-) diff --git a/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift b/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift index 171b31422..1642bc5cc 100644 --- a/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift +++ b/PennMobile/GSR-Booking/Networking/GSRNetworkManager.swift @@ -34,7 +34,6 @@ class GSRNetworkManager: NSObject, Requestable { let gsrLocations = try decoder.decode([GSRLocation].self, from: data) completion(.success(gsrLocations)) } catch { - print(error) completion(.failure(.parsingError)) } } @@ -148,7 +147,6 @@ extension GSRNetworkManager { return } - print(token.value) let url = URL(string: self.reservationURL)! let request = URLRequest(url: url, accessToken: token) @@ -161,10 +159,8 @@ extension GSRNetworkManager { do { let reservations = try decoder.decode([GSRReservation].self, from: data) - print(reservations) completion(.success(reservations)) } catch { - print(error) completion(.failure(.parsingError)) } } diff --git a/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift b/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift index a9d16f16e..0a6524f65 100644 --- a/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift +++ b/PennMobile/GSR-Booking/ViewModel/GSRViewModel.swift @@ -106,7 +106,6 @@ extension GSRViewModel: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: RoomCell.identifier, for: indexPath) as! RoomCell - print(filteredRooms.count) cell.room = filteredRooms[indexPath.section] cell.contentView.isUserInteractionEnabled = false cell.delegate = self diff --git a/PennMobile/Home/Fitness/Networking/FitnessAPI.swift b/PennMobile/Home/Fitness/Networking/FitnessAPI.swift index a31894d06..cc7a83a38 100644 --- a/PennMobile/Home/Fitness/Networking/FitnessAPI.swift +++ b/PennMobile/Home/Fitness/Networking/FitnessAPI.swift @@ -50,7 +50,6 @@ extension FitnessFacilityData { let decodedSchedules = try decoder.decode(FitnessSchedules.self, from: json.rawData()) self.load(inputSchedules: decodedSchedules) } catch { - print(error) return false } diff --git a/PennMobile/Setup + Navigation/AppDelegate+NotificationExtension.swift b/PennMobile/Setup + Navigation/AppDelegate+NotificationExtension.swift index 08da1d9ff..0382e89e4 100644 --- a/PennMobile/Setup + Navigation/AppDelegate+NotificationExtension.swift +++ b/PennMobile/Setup + Navigation/AppDelegate+NotificationExtension.swift @@ -70,9 +70,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { // Share the GSR Booking with the iOS share sheet let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - - print(startDateString) - print(endDateString) var text = "Upcoming GSR in \(roomName) " if let startDate = dateFormatter.date(from: startDateString), let endDate = dateFormatter.date(from: endDateString) { From 383a593b4b27847b22d1f1419ae2c93b267c7583 Mon Sep 17 00:00:00 2001 From: 01jongmin <49562117+01jongmin@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:37:09 -0400 Subject: [PATCH 09/18] Segment control dining (#388) * tab bar added * added a flag * added basic structure for displaying lists of dining stat cards * fixed balance view + added JSON decoding for dining balance * finished initial codable implementation * fixed codable, set up card generation skeleton * completed integration of FrequentLocationsView * created RecentTransactionsView and hooked it to the model * added dining balance headers * finished dining balance view implementation * Starting to integrate daily average graph * more prgress on daily average view, added graph path * continued work on daily avg graph * completed implementation of daily averages view * polish and fixes for dining stat cards * polish, cleaned up API decoding structs * renaming dining stats to insights, cleaning up code * renamed to diningInsights, other polish * refined struct structure ;) * integrated variable step graph view. oof * progress with predictions graph card * added axis label generation based on semester dates and max dining balance * added dining dollars card, added switch to determine balance type * slightly buggy implementation of graph prediction line (computed based on a zero-balance date given by server) * added todo reminders * fully implemented line graph slope for prediction cards * changes to adapt to adam's api * updated json * added handling for extra balance at end of semester * added comments for jong min * Initial commit for diningInsights networking * finished datastore code, moved cache logic to the diningAPI * updating hosting view after networking request * using try catch logic for error response * Initial error handling for diningInsights * Complete error handling fo diningInsights * not using cached value when request is successful * updated api url * updated podfile * undoing pbxproj changes * Finished networking for diningInsights. Started migrating dining storage to diningAPI * Dining API cleanup + dining redesign partial completion * Resolve merge conflicts * DiningVenueDetailView 2/3 selectors complete * Added dining menus * Dining Redesign 1.0 finish. * Dining Redesign beta complete * Deleted accidentally created files * delete accidently created files * Deleted unused files + preparation for release * Fix error from merge-conflict resolution Co-authored-by: Elizabeth Powell Co-authored-by: dominic --- PennMobile.xcodeproj/project.pbxproj | 246 +++- .../Assets.xcassets/Dining/Contents.json | 6 +- .../Dining/Dining Menu Icons/Contents.json | 6 + .../Farm to Fork.imageset/Contents.json | 21 + .../Farm to Fork.imageset/Farm to Fork.png | Bin 0 -> 1200 bytes .../Halal.imageset/Contents.json | 21 + .../Halal.imageset/Halal.png | Bin 0 -> 1127 bytes .../Humane.imageset/Contents.json | 21 + .../Humane.imageset/Humane.png | Bin 0 -> 1133 bytes .../Jain.imageset/Contents.json | 21 + .../Dining Menu Icons/Jain.imageset/Jain.png | Bin 0 -> 722 bytes .../Contents.json | 21 + .../test.png | Bin 0 -> 1556 bytes .../Vegan.imageset/Contents.json | 21 + .../Vegan.imageset/Vegan.png | Bin 0 -> 1736 bytes .../Vegetarian.imageset/Contents.json | 21 + .../Vegetarian.imageset/Vegetarian.png | Bin 0 -> 1353 bytes .../Controllers/DiningViewController.swift | 14 +- .../Dining/Controllers/DiningViewModel.swift | 4 +- .../Model/DiningInsightsAPIResponse.swift | 156 +++ PennMobile/Dining/Model/DiningMenu.swift | 112 ++ .../Dining/Model/DiningVenue+Extensions.swift | 143 ++- PennMobile/Dining/Model/DiningVenue.swift | 4 +- .../Dining/Networking + Cache/DiningAPI.swift | 147 ++- .../SwiftUI/DiningViewControllerSwiftUI.swift | 38 + .../SwiftUI/DiningViewModelSwiftUI.swift | 102 ++ .../Dining/SwiftUI/Views/DiningView.swift | 32 + .../SwiftUI/Views/DiningViewHeader.swift | 93 ++ .../Views/Insights/Cards/CardHeaderView.swift | 64 + .../Views/Insights/Cards/CardView.swift | 35 + .../Views/Insights/DailyAverageView.swift | 236 ++++ .../Views/Insights/DiningBalanceView.swift | 78 ++ .../Views/Insights/DiningInsightsView.swift | 104 ++ .../Insights/FrequentLocationsView.swift | 145 +++ .../PredictionsGraphView+AxisLabels.swift | 48 + .../PredictionsGraphView+SmoothedData.swift | 44 + .../PredictionsGraphView.swift | 117 ++ ...eStepLineGraphView+GraphEndpointPath.swift | 37 + ...tepLineGraphView+PredictionSlopePath.swift | 45 + ...pLineGraphView+VariableStepGraphPath.swift | 44 + .../VariableStepLineGraphView.swift | 111 ++ .../Insights/RecentTransactionsView.swift | 70 + .../DiningVenueDetailHoursView.swift | 57 + .../DiningVenueDetailLocationView.swift | 43 + .../DiningVenueDetailMenuView.swift | 38 + .../Detail View/DiningVenueDetailView.swift | 242 ++++ .../Detail View/MenuDisclosureGroup.swift | 164 +++ .../Views/Venue/Detail View/mock_menu.json | 1067 ++++++++++++++++ .../SwiftUI/Views/Venue/DiningVenueRow.swift | 137 ++ .../SwiftUI/Views/Venue/DiningVenueView.swift | 70 + .../Views/Venue/sample-dining-venue.json | 1122 +++++++++++++++++ PennMobile/General/Colors.swift | 71 ++ PennMobile/General/Extensions.swift | 27 + .../LocalJSONStore.swift | 1 + .../General/SwiftUI Views/AlertModifier.swift | 86 ++ .../SwiftUI Views/FadingScrollView.swift | 93 ++ .../General/SwiftUI Views/UIKit Views.swift | 70 + .../General/UserDefaults + Helpers.swift | 35 + .../Cells/Dining/HomeDiningCellItem.swift | 8 +- .../Home/Cells/Example/HomeExampleCell.swift | 73 ++ .../Cells/Example/HomeExampleCellItem.swift | 42 + .../DiningCellSettingsController.swift | 2 +- .../Home/Controllers/HomeViewController.swift | 6 +- PennMobile/Home/Map/PennCoordinate.swift | 52 + .../Setup + Navigation/ControllerModel.swift | 6 +- .../RootViewController.swift | 1 + .../Setup + Navigation/TabBarController.swift | 10 + .../Base.lproj/LaunchScreen.storyboard | 4 +- 68 files changed, 5894 insertions(+), 61 deletions(-) create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Farm to Fork.png create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Halal.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Halal.imageset/Halal.png create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Humane.png create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Jain.png create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/test.png create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Vegan.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Vegan.imageset/Vegan.png create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Vegetarian.imageset/Contents.json create mode 100644 PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Vegetarian.imageset/Vegetarian.png create mode 100644 PennMobile/Dining/Model/DiningInsightsAPIResponse.swift create mode 100644 PennMobile/Dining/Model/DiningMenu.swift create mode 100644 PennMobile/Dining/SwiftUI/DiningViewControllerSwiftUI.swift create mode 100644 PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/DiningView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/DiningViewHeader.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardHeaderView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/DailyAverageView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/DiningBalanceView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/DiningInsightsView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/FrequentLocationsView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+AxisLabels.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+SmoothedData.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+GraphEndpointPath.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+PredictionSlopePath.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+VariableStepGraphPath.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Insights/RecentTransactionsView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailHoursView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailLocationView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailMenuView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/Detail View/mock_menu.json create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueRow.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift create mode 100644 PennMobile/Dining/SwiftUI/Views/Venue/sample-dining-venue.json rename PennMobile/{Dining/Networking + Cache => General}/LocalJSONStore.swift (97%) create mode 100644 PennMobile/General/SwiftUI Views/AlertModifier.swift create mode 100644 PennMobile/General/SwiftUI Views/FadingScrollView.swift create mode 100644 PennMobile/General/SwiftUI Views/UIKit Views.swift create mode 100644 PennMobile/Home/Cells/Example/HomeExampleCell.swift create mode 100644 PennMobile/Home/Cells/Example/HomeExampleCellItem.swift diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 682aa9877..2af8e426b 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -144,9 +144,44 @@ 66E8ECA52381CAA900945BEA /* TOTPFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA42381CAA800945BEA /* TOTPFetcher.swift */; }; 66E8ECA72381CB5100945BEA /* TwoFactorTokenGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA62381CB5100945BEA /* TwoFactorTokenGenerator.swift */; }; 66E8ECA92381CD2F00945BEA /* TOTPNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E8ECA82381CD2F00945BEA /* TOTPNetworkManager.swift */; }; - 6C23AF9526E57903002F60F0 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C23AF9426E57903002F60F0 /* EmptyView.swift */; }; - 6C369A1526E39BC100721CA1 /* GSRAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */; }; + 6C23AF9526E57903002F60F0 /* (null) in Sources */ = {isa = PBXBuildFile; }; + 6C369A1526E39BC100721CA1 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6C4CC1FA26E6B1720000B4A8 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */; }; + 6C6035F326E722890025FBC7 /* DiningVenueDetailLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035ED26E722890025FBC7 /* DiningVenueDetailLocationView.swift */; }; + 6C6035F426E722890025FBC7 /* DiningVenueDetailMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035EE26E722890025FBC7 /* DiningVenueDetailMenuView.swift */; }; + 6C6035F526E722890025FBC7 /* MenuDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035EF26E722890025FBC7 /* MenuDisclosureGroup.swift */; }; + 6C6035F626E722890025FBC7 /* DiningVenueDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035F026E722890025FBC7 /* DiningVenueDetailView.swift */; }; + 6C6035F726E722890025FBC7 /* DiningVenueDetailHoursView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035F126E722890025FBC7 /* DiningVenueDetailHoursView.swift */; }; + 6C6035F826E722890025FBC7 /* mock_menu.json in Resources */ = {isa = PBXBuildFile; fileRef = 6C6035F226E722890025FBC7 /* mock_menu.json */; }; + 6C6035FA26E722E60025FBC7 /* GSRAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035F926E722E60025FBC7 /* GSRAPIResponse.swift */; }; + 6C6035FC26E723240025FBC7 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6035FB26E723240025FBC7 /* EmptyView.swift */; }; + 6C84D9E526293E680039C57F /* UIKit Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C84D9E426293E680039C57F /* UIKit Views.swift */; }; + 6C88FE9425337476006F4587 /* DiningViewControllerSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C88FE9325337476006F4587 /* DiningViewControllerSwiftUI.swift */; }; + 6CAE4351253370B300BD0200 /* FadingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE434E253370B200BD0200 /* FadingScrollView.swift */; }; + 6CAE4352253370B300BD0200 /* AlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE434F253370B200BD0200 /* AlertModifier.swift */; }; + 6CAE437D253370E500BD0200 /* DiningViewModelSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE435A253370E500BD0200 /* DiningViewModelSwiftUI.swift */; }; + 6CAE4380253370E500BD0200 /* FrequentLocationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE435F253370E500BD0200 /* FrequentLocationsView.swift */; }; + 6CAE4381253370E500BD0200 /* RecentTransactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4360253370E500BD0200 /* RecentTransactionsView.swift */; }; + 6CAE4382253370E500BD0200 /* DiningBalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4361253370E500BD0200 /* DiningBalanceView.swift */; }; + 6CAE4383253370E500BD0200 /* DailyAverageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4362253370E500BD0200 /* DailyAverageView.swift */; }; + 6CAE4384253370E500BD0200 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4364253370E500BD0200 /* CardView.swift */; }; + 6CAE4385253370E600BD0200 /* CardHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4365253370E500BD0200 /* CardHeaderView.swift */; }; + 6CAE4386253370E600BD0200 /* VariableStepLineGraphView+GraphEndpointPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4367253370E500BD0200 /* VariableStepLineGraphView+GraphEndpointPath.swift */; }; + 6CAE4387253370E600BD0200 /* PredictionsGraphView+SmoothedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4368253370E500BD0200 /* PredictionsGraphView+SmoothedData.swift */; }; + 6CAE4388253370E600BD0200 /* PredictionsGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4369253370E500BD0200 /* PredictionsGraphView.swift */; }; + 6CAE4389253370E600BD0200 /* VariableStepLineGraphView+PredictionSlopePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE436A253370E500BD0200 /* VariableStepLineGraphView+PredictionSlopePath.swift */; }; + 6CAE438A253370E600BD0200 /* PredictionsGraphView+AxisLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE436B253370E500BD0200 /* PredictionsGraphView+AxisLabels.swift */; }; + 6CAE438B253370E600BD0200 /* VariableStepLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE436C253370E500BD0200 /* VariableStepLineGraphView.swift */; }; + 6CAE438C253370E600BD0200 /* VariableStepLineGraphView+VariableStepGraphPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE436D253370E500BD0200 /* VariableStepLineGraphView+VariableStepGraphPath.swift */; }; + 6CAE438D253370E600BD0200 /* DiningInsightsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE436E253370E500BD0200 /* DiningInsightsView.swift */; }; + 6CAE438E253370E600BD0200 /* DiningViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE436F253370E500BD0200 /* DiningViewHeader.swift */; }; + 6CAE438F253370E600BD0200 /* DiningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4370253370E500BD0200 /* DiningView.swift */; }; + 6CAE4390253370E600BD0200 /* DiningVenueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4372253370E500BD0200 /* DiningVenueView.swift */; }; + 6CAE4391253370E600BD0200 /* DiningVenueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE4373253370E500BD0200 /* DiningVenueRow.swift */; }; + 6CAE4392253370E600BD0200 /* sample-dining-venue.json in Resources */ = {isa = PBXBuildFile; fileRef = 6CAE4374253370E500BD0200 /* sample-dining-venue.json */; }; + 6CAE43C62533731000BD0200 /* DiningInsightsAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE43C52533731000BD0200 /* DiningInsightsAPIResponse.swift */; }; + 6CAE43D3253373B900BD0200 /* DiningMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE43D2253373B900BD0200 /* DiningMenu.swift */; }; + 6CAE43D82533740300BD0200 /* LocalJSONStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE43D72533740300BD0200 /* LocalJSONStore.swift */; }; 97AA806923D26BC700C23488 /* DiningBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */; }; 97AA806A23D26BC700C23488 /* TransactionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806523D26BC700C23488 /* TransactionTableViewCell.swift */; }; 97AA806B23D26BC700C23488 /* DiningHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806623D26BC700C23488 /* DiningHeaderView.swift */; }; @@ -245,7 +280,6 @@ F23CC0C1235E3F8A0007317A /* DiningVenue+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23CC0C0235E3F8A0007317A /* DiningVenue+Extensions.swift */; }; F23CC0C3235E6D2B0007317A /* DiningDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23CC0C2235E6D2B0007317A /* DiningDataStore.swift */; }; F23CC0C5235EB30E0007317A /* StorageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23CC0C4235EB30E0007317A /* StorageType.swift */; }; - F23CC0C7235ED0C60007317A /* LocalJSONStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23CC0C6235ED0C60007317A /* LocalJSONStore.swift */; }; F2562A262551F6D40021C92F /* CourseAlertSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2562A252551F6D40021C92F /* CourseAlertSettingsController.swift */; }; F2562A2B255830AB0021C92F /* CourseAlertSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2562A2A255830AB0021C92F /* CourseAlertSettingsCell.swift */; }; F2562A302558330D0021C92F /* CourseAlertSettingsOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2562A2F2558330D0021C92F /* CourseAlertSettingsOptions.swift */; }; @@ -443,16 +477,49 @@ 21FBC23F228514ED00B432D8 /* FeedAnalyticsEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedAnalyticsEngine.swift; sourceTree = ""; }; 21FBC241228B774E00B432D8 /* PennCashNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PennCashNetworkManager.swift; sourceTree = ""; }; 2F8A024230F1E8D7BA9410B4 /* Pods-AutomatedScreenshotUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutomatedScreenshotUITests.debug.xcconfig"; path = "Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests.debug.xcconfig"; sourceTree = ""; }; + 407FC3CFC29C6496A17C99C2 /* Pods-AutomatedScreenshotUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutomatedScreenshotUITests.release.xcconfig"; path = "Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests.release.xcconfig"; sourceTree = ""; }; 665F1FC423D261B100DC023E /* TwoFactorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorCell.swift; sourceTree = ""; }; 665F1FC623D2628800DC023E /* TwoFactorWebviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorWebviewController.swift; sourceTree = ""; }; 667EC29523DB9C4800B0DF66 /* TwoFactorEnableController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorEnableController.swift; sourceTree = ""; }; 66E8ECA42381CAA800945BEA /* TOTPFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPFetcher.swift; sourceTree = ""; }; 66E8ECA62381CB5100945BEA /* TwoFactorTokenGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorTokenGenerator.swift; sourceTree = ""; }; 66E8ECA82381CD2F00945BEA /* TOTPNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPNetworkManager.swift; sourceTree = ""; }; - 6C23AF9426E57903002F60F0 /* EmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; }; - 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSRAPIResponse.swift; sourceTree = ""; }; - 6DBD80716B161DAEA6274151 /* Pods-AutomatedScreenshotUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutomatedScreenshotUITests.release.xcconfig"; path = "Target Support Files/Pods-AutomatedScreenshotUITests/Pods-AutomatedScreenshotUITests.release.xcconfig"; sourceTree = ""; }; - 6F4DC86EE3A48EC9A02145AE /* Pods-PennMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PennMobile.debug.xcconfig"; path = "Target Support Files/Pods-PennMobile/Pods-PennMobile.debug.xcconfig"; sourceTree = ""; }; + 6C6035ED26E722890025FBC7 /* DiningVenueDetailLocationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningVenueDetailLocationView.swift; sourceTree = ""; }; + 6C6035EE26E722890025FBC7 /* DiningVenueDetailMenuView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningVenueDetailMenuView.swift; sourceTree = ""; }; + 6C6035EF26E722890025FBC7 /* MenuDisclosureGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuDisclosureGroup.swift; sourceTree = ""; }; + 6C6035F026E722890025FBC7 /* DiningVenueDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningVenueDetailView.swift; sourceTree = ""; }; + 6C6035F126E722890025FBC7 /* DiningVenueDetailHoursView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningVenueDetailHoursView.swift; sourceTree = ""; }; + 6C6035F226E722890025FBC7 /* mock_menu.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mock_menu.json; sourceTree = ""; }; + 6C6035F926E722E60025FBC7 /* GSRAPIResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GSRAPIResponse.swift; sourceTree = ""; }; + 6C6035FB26E723240025FBC7 /* EmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; }; + 6C84D9E426293E680039C57F /* UIKit Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIKit Views.swift"; sourceTree = ""; }; + 6C88FE9325337476006F4587 /* DiningViewControllerSwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningViewControllerSwiftUI.swift; sourceTree = ""; }; + 6CAE434E253370B200BD0200 /* FadingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FadingScrollView.swift; sourceTree = ""; }; + 6CAE434F253370B200BD0200 /* AlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertModifier.swift; sourceTree = ""; }; + 6CAE435A253370E500BD0200 /* DiningViewModelSwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningViewModelSwiftUI.swift; sourceTree = ""; }; + 6CAE435F253370E500BD0200 /* FrequentLocationsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrequentLocationsView.swift; sourceTree = ""; }; + 6CAE4360253370E500BD0200 /* RecentTransactionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentTransactionsView.swift; sourceTree = ""; }; + 6CAE4361253370E500BD0200 /* DiningBalanceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningBalanceView.swift; sourceTree = ""; }; + 6CAE4362253370E500BD0200 /* DailyAverageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DailyAverageView.swift; sourceTree = ""; }; + 6CAE4364253370E500BD0200 /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; + 6CAE4365253370E500BD0200 /* CardHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardHeaderView.swift; sourceTree = ""; }; + 6CAE4367253370E500BD0200 /* VariableStepLineGraphView+GraphEndpointPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VariableStepLineGraphView+GraphEndpointPath.swift"; sourceTree = ""; }; + 6CAE4368253370E500BD0200 /* PredictionsGraphView+SmoothedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PredictionsGraphView+SmoothedData.swift"; sourceTree = ""; }; + 6CAE4369253370E500BD0200 /* PredictionsGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionsGraphView.swift; sourceTree = ""; }; + 6CAE436A253370E500BD0200 /* VariableStepLineGraphView+PredictionSlopePath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VariableStepLineGraphView+PredictionSlopePath.swift"; sourceTree = ""; }; + 6CAE436B253370E500BD0200 /* PredictionsGraphView+AxisLabels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PredictionsGraphView+AxisLabels.swift"; sourceTree = ""; }; + 6CAE436C253370E500BD0200 /* VariableStepLineGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableStepLineGraphView.swift; sourceTree = ""; }; + 6CAE436D253370E500BD0200 /* VariableStepLineGraphView+VariableStepGraphPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VariableStepLineGraphView+VariableStepGraphPath.swift"; sourceTree = ""; }; + 6CAE436E253370E500BD0200 /* DiningInsightsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningInsightsView.swift; sourceTree = ""; }; + 6CAE436F253370E500BD0200 /* DiningViewHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningViewHeader.swift; sourceTree = ""; }; + 6CAE4370253370E500BD0200 /* DiningView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningView.swift; sourceTree = ""; }; + 6CAE4372253370E500BD0200 /* DiningVenueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningVenueView.swift; sourceTree = ""; }; + 6CAE4373253370E500BD0200 /* DiningVenueRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningVenueRow.swift; sourceTree = ""; }; + 6CAE4374253370E500BD0200 /* sample-dining-venue.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "sample-dining-venue.json"; sourceTree = ""; }; + 6CAE43C52533731000BD0200 /* DiningInsightsAPIResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningInsightsAPIResponse.swift; sourceTree = ""; }; + 6CAE43D2253373B900BD0200 /* DiningMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningMenu.swift; sourceTree = ""; }; + 6CAE43D72533740300BD0200 /* LocalJSONStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalJSONStore.swift; sourceTree = ""; }; + 72787B1E070BFCDF84D8C3CA /* Pods-PennMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PennMobile.debug.xcconfig"; path = "Target Support Files/Pods-PennMobile/Pods-PennMobile.debug.xcconfig"; sourceTree = ""; }; 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningBalanceCell.swift; sourceTree = ""; }; 97AA806523D26BC700C23488 /* TransactionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionTableViewCell.swift; sourceTree = ""; }; 97AA806623D26BC700C23488 /* DiningHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningHeaderView.swift; sourceTree = ""; }; @@ -553,7 +620,6 @@ F23CC0C0235E3F8A0007317A /* DiningVenue+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiningVenue+Extensions.swift"; sourceTree = ""; }; F23CC0C2235E6D2B0007317A /* DiningDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningDataStore.swift; sourceTree = ""; }; F23CC0C4235EB30E0007317A /* StorageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageType.swift; sourceTree = ""; }; - F23CC0C6235ED0C60007317A /* LocalJSONStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalJSONStore.swift; sourceTree = ""; }; F2562A252551F6D40021C92F /* CourseAlertSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseAlertSettingsController.swift; sourceTree = ""; }; F2562A2A255830AB0021C92F /* CourseAlertSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseAlertSettingsCell.swift; sourceTree = ""; }; F2562A2F2558330D0021C92F /* CourseAlertSettingsOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseAlertSettingsOptions.swift; sourceTree = ""; }; @@ -758,16 +824,16 @@ 21B8BBBB2224F98B00EBC1D0 /* Login */, 216640881EBADBA000746B8E /* Setup + Navigation */, B62875E82112591700FB2873 /* Buildings */, + 21640D4220103C8A002F33CA /* Home */, 97E79E0A2100DBC300D3D606 /* Fitness */, B62875E921152FF500FB2873 /* Map */, - 21640D4220103C8A002F33CA /* Home */, 2166408B1EBADBF800746B8E /* Dining */, 2189C0732027CD2F00771C1F /* GSR-Booking */, B654BAE81F801F030038B9D5 /* Laundry */, 21D5E06523B68F0E00B331CC /* Housing */, 212304C52054ED9900958CE0 /* Spring Fling */, - 216640EE1EBB867600746B8E /* Contacts */, 216640D31EBB80FD00746B8E /* News */, + 216640EE1EBB867600746B8E /* Contacts */, CF1CE9B8205D5BF100C51F46 /* More Tab */, 216640F91EBB898700746B8E /* About */, 216640831EBADB7000746B8E /* Supporting_Files */, @@ -804,6 +870,7 @@ 2166408B1EBADBF800746B8E /* Dining */ = { isa = PBXGroup; children = ( + 6CAE4357253370E500BD0200 /* SwiftUI */, 97AA806323D26BC700C23488 /* Views */, 2166408E1EBADC0700746B8E /* Controllers */, 2166408D1EBADC0100746B8E /* Model */, @@ -815,11 +882,13 @@ 2166408D1EBADC0100746B8E /* Model */ = { isa = PBXGroup; children = ( + F23CC0BC235E3F390007317A /* DiningAPIResponse.swift */, + 6CAE43C52533731000BD0200 /* DiningInsightsAPIResponse.swift */, C11B38E222508A8D009B752A /* DiningBalance.swift */, F23CC0BA235E3EF10007317A /* DiningVenue.swift */, F23CC0BE235E3F610007317A /* DiningVenue+Codable.swift */, F23CC0C0235E3F8A0007317A /* DiningVenue+Extensions.swift */, - F23CC0BC235E3F390007317A /* DiningAPIResponse.swift */, + 6CAE43D2253373B900BD0200 /* DiningMenu.swift */, ); path = Model; sourceTree = ""; @@ -849,9 +918,11 @@ 216640C71EBADC9D00746B8E /* General */ = { isa = PBXGroup; children = ( + 6CAE434D253370B200BD0200 /* SwiftUI Views */, 217A7839204D977E004F1227 /* ModularTableView */, 216640C01EBADC8000746B8E /* Generics */, 216640C91EBADCA400746B8E /* Extensions.swift */, + 6CAE43D72533740300BD0200 /* LocalJSONStore.swift */, F2F4C91F23611EB900816456 /* Colors.swift */, 2190FD341EC625BB00EC683C /* Protocols.swift */, 2188C6D6223C7E8300F18D90 /* SecureStore.swift */, @@ -867,7 +938,6 @@ children = ( 211DFE531F36A02C00CB73E4 /* DiningAPI.swift */, F23CC0C4235EB30E0007317A /* StorageType.swift */, - F23CC0C6235ED0C60007317A /* LocalJSONStore.swift */, F23CC0C2235E6D2B0007317A /* DiningDataStore.swift */, ); path = "Networking + Cache"; @@ -980,7 +1050,7 @@ 2189C0752027CD3A00771C1F /* Model */ = { isa = PBXGroup; children = ( - 6C369A1426E39BC100721CA1 /* GSRAPIResponse.swift */, + 6C6035F926E722E60025FBC7 /* GSRAPIResponse.swift */, 2189C0992027CE4000771C1F /* GSRRoom.swift */, EFE2D6FF239B124D0020F6BF /* GSRGroupUser.swift */, 2189C0982027CE4000771C1F /* GSRBooking.swift */, @@ -1014,8 +1084,8 @@ 2138E1F72252AFB500E4055A /* GSRLocationCell.swift */, 2189C0892027CE2600771C1F /* RoomCell.swift */, 21A6B6D32216824B003A357D /* ReservationCell.swift */, - 6C23AF9426E57903002F60F0 /* EmptyView.swift */, C1D90F1C2220A25700DAB8EE /* NoReservationsCell.swift */, + 6C6035FB26E723240025FBC7 /* EmptyView.swift */, ); path = Views; sourceTree = ""; @@ -1150,6 +1220,98 @@ path = Auth; sourceTree = ""; }; + 6C6035EC26E722890025FBC7 /* Detail View */ = { + isa = PBXGroup; + children = ( + 6C6035ED26E722890025FBC7 /* DiningVenueDetailLocationView.swift */, + 6C6035EE26E722890025FBC7 /* DiningVenueDetailMenuView.swift */, + 6C6035EF26E722890025FBC7 /* MenuDisclosureGroup.swift */, + 6C6035F026E722890025FBC7 /* DiningVenueDetailView.swift */, + 6C6035F126E722890025FBC7 /* DiningVenueDetailHoursView.swift */, + 6C6035F226E722890025FBC7 /* mock_menu.json */, + ); + path = "Detail View"; + sourceTree = ""; + }; + 6CAE434D253370B200BD0200 /* SwiftUI Views */ = { + isa = PBXGroup; + children = ( + 6CAE434E253370B200BD0200 /* FadingScrollView.swift */, + 6CAE434F253370B200BD0200 /* AlertModifier.swift */, + 6C84D9E426293E680039C57F /* UIKit Views.swift */, + ); + path = "SwiftUI Views"; + sourceTree = ""; + }; + 6CAE4357253370E500BD0200 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 6C88FE9325337476006F4587 /* DiningViewControllerSwiftUI.swift */, + 6CAE435A253370E500BD0200 /* DiningViewModelSwiftUI.swift */, + 6CAE435D253370E500BD0200 /* Views */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 6CAE435D253370E500BD0200 /* Views */ = { + isa = PBXGroup; + children = ( + 6CAE4371253370E500BD0200 /* Venue */, + 6CAE435E253370E500BD0200 /* Insights */, + 6CAE436F253370E500BD0200 /* DiningViewHeader.swift */, + 6CAE4370253370E500BD0200 /* DiningView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 6CAE435E253370E500BD0200 /* Insights */ = { + isa = PBXGroup; + children = ( + 6CAE435F253370E500BD0200 /* FrequentLocationsView.swift */, + 6CAE4360253370E500BD0200 /* RecentTransactionsView.swift */, + 6CAE4361253370E500BD0200 /* DiningBalanceView.swift */, + 6CAE4362253370E500BD0200 /* DailyAverageView.swift */, + 6CAE4363253370E500BD0200 /* Cards */, + 6CAE4366253370E500BD0200 /* PredictionsGraph */, + 6CAE436E253370E500BD0200 /* DiningInsightsView.swift */, + ); + path = Insights; + sourceTree = ""; + }; + 6CAE4363253370E500BD0200 /* Cards */ = { + isa = PBXGroup; + children = ( + 6CAE4364253370E500BD0200 /* CardView.swift */, + 6CAE4365253370E500BD0200 /* CardHeaderView.swift */, + ); + path = Cards; + sourceTree = ""; + }; + 6CAE4366253370E500BD0200 /* PredictionsGraph */ = { + isa = PBXGroup; + children = ( + 6CAE4367253370E500BD0200 /* VariableStepLineGraphView+GraphEndpointPath.swift */, + 6CAE4368253370E500BD0200 /* PredictionsGraphView+SmoothedData.swift */, + 6CAE4369253370E500BD0200 /* PredictionsGraphView.swift */, + 6CAE436A253370E500BD0200 /* VariableStepLineGraphView+PredictionSlopePath.swift */, + 6CAE436B253370E500BD0200 /* PredictionsGraphView+AxisLabels.swift */, + 6CAE436C253370E500BD0200 /* VariableStepLineGraphView.swift */, + 6CAE436D253370E500BD0200 /* VariableStepLineGraphView+VariableStepGraphPath.swift */, + ); + path = PredictionsGraph; + sourceTree = ""; + }; + 6CAE4371253370E500BD0200 /* Venue */ = { + isa = PBXGroup; + children = ( + 6CAE4372253370E500BD0200 /* DiningVenueView.swift */, + 6CAE4373253370E500BD0200 /* DiningVenueRow.swift */, + 6C6035EC26E722890025FBC7 /* Detail View */, + 6CAE4374253370E500BD0200 /* sample-dining-venue.json */, + ); + path = Venue; + sourceTree = ""; + }; 6D2C4E8629C0A2D81734AD99 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1533,9 +1695,9 @@ isa = PBXGroup; children = ( 2F8A024230F1E8D7BA9410B4 /* Pods-AutomatedScreenshotUITests.debug.xcconfig */, - 6DBD80716B161DAEA6274151 /* Pods-AutomatedScreenshotUITests.release.xcconfig */, - 6F4DC86EE3A48EC9A02145AE /* Pods-PennMobile.debug.xcconfig */, 059FD41D74734ECD9DE8209C /* Pods-PennMobile.release.xcconfig */, + 407FC3CFC29C6496A17C99C2 /* Pods-AutomatedScreenshotUITests.release.xcconfig */, + 72787B1E070BFCDF84D8C3CA /* Pods-PennMobile.debug.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1657,7 +1819,7 @@ F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */, F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */, F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, - 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON.git" */, + 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -1675,7 +1837,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6CAE4392253370E600BD0200 /* sample-dining-venue.json in Resources */, 2166406E1EBADADA00746B8E /* LaunchScreen.storyboard in Resources */, + 6C6035F826E722890025FBC7 /* mock_menu.json in Resources */, B6111DB22018497400DC7877 /* GoogleService-Info.plist in Resources */, 2190FD2F1EBBC8BA00EC683C /* Assets.xcassets in Resources */, ); @@ -1816,6 +1980,7 @@ EFE2D6F0239B11050020F6BF /* GroupSettingsCell.swift in Sources */, B650C5681FA44C2100922E98 /* LaundryMachineCell.swift in Sources */, C8C96F10241407000069D56A /* CourseScheduleViewController.swift in Sources */, + 6CAE4389253370E600BD0200 /* VariableStepLineGraphView+PredictionSlopePath.swift in Sources */, EFE2D6F5239B11050020F6BF /* GroupHeaderCell.swift in Sources */, 219F01F41FB7CCFA006BBC4E /* SAConfettiView.swift in Sources */, 219F01FB1FBE973A006BBC4E /* LaundryNotificationCenter.swift in Sources */, @@ -1832,17 +1997,20 @@ 2138E1F82252AFB500E4055A /* GSRLocationCell.swift in Sources */, B654BAF21F802A050038B9D5 /* LaundryCell.swift in Sources */, 2138D55322597FCB00D67CA2 /* GSRLocationsController.swift in Sources */, + 6C6035F426E722890025FBC7 /* DiningVenueDetailMenuView.swift in Sources */, 21B649222236F2FC00AC5B7F /* HomeCoursesCellItem.swift in Sources */, EF389EC421860B7000E29C6A /* StatusBar.swift in Sources */, 212304C72054EDB900958CE0 /* FlingViewController.swift in Sources */, 211DA1AB20490E4D0065BC2C /* HomeCellConformable.swift in Sources */, 2189C0AA2027CE4B00771C1F /* TextLayer.swift in Sources */, + 6CAE43D3253373B900BD0200 /* DiningMenu.swift in Sources */, CF1CE9C1205D683400C51F46 /* HeaderViewCell.swift in Sources */, C11B38E322508A8D009B752A /* DiningBalance.swift in Sources */, 21640FD7204A5309008DB6E8 /* LaundryMachineCellTappable.swift in Sources */, 217A7838204D2E2B004F1227 /* HomeGSRCellItem.swift in Sources */, 2119D26722529A0600693CDB /* HomeGSRLocationsCellItem.swift in Sources */, EF23946423EF4117005BA55F /* GSRGroupInviteCell.swift in Sources */, + 6CAE4382253370E500BD0200 /* DiningBalanceView.swift in Sources */, 97E79E122100DE0C00D3D606 /* FitnessModels.swift in Sources */, 2189C0AB2027CE4B00771C1F /* TrackLayer.swift in Sources */, 2108CF211F3F73FF00CEC3F4 /* ContactsTableViewController.swift in Sources */, @@ -1857,6 +2025,7 @@ EF389EC621861B8500E29C6A /* HomeNavigationController.swift in Sources */, 21879B1E1FB3FF54003CF552 /* Storage.swift in Sources */, 2180D2352013F65C008C94CF /* HomeAPIService.swift in Sources */, + 6C6035F726E722890025FBC7 /* DiningVenueDetailHoursView.swift in Sources */, F2568A7A2413637D00561295 /* HomeCellSafeArea.swift in Sources */, CF29A1771FB788820067D946 /* OnboardingController.swift in Sources */, 21B8BBB4222318BE00EBC1D0 /* PennInTouchNetworkManager.swift in Sources */, @@ -1864,8 +2033,14 @@ 21D5E06723B68F3100B331CC /* Model.swift in Sources */, F23CC0BF235E3F610007317A /* DiningVenue+Codable.swift in Sources */, F299616F23F78D9B00C67A3B /* AppDelegate+NotificationExtension.swift in Sources */, + 6CAE4387253370E600BD0200 /* PredictionsGraphView+SmoothedData.swift in Sources */, + 6CAE438B253370E600BD0200 /* VariableStepLineGraphView.swift in Sources */, 21B8BBB82223963C00EBC1D0 /* Degree.swift in Sources */, - 6C369A1526E39BC100721CA1 /* GSRAPIResponse.swift in Sources */, + 6CAE4390253370E600BD0200 /* DiningVenueView.swift in Sources */, + 21640D6020105BB4002F33CA /* HomeStudyRoomCell.swift in Sources */, + 212B835C222A37D000F835D6 /* HomeFeatureCellItem.swift in Sources */, + 6CAE4383253370E500BD0200 /* DailyAverageView.swift in Sources */, + 6C369A1526E39BC100721CA1 /* (null) in Sources */, 21640D6020105BB4002F33CA /* HomeStudyRoomCell.swift in Sources */, 212B835C222A37D000F835D6 /* HomeFeatureCellItem.swift in Sources */, 2119D26922529A2300693CDB /* HomeGSRLocationsCell.swift in Sources */, @@ -1886,17 +2061,23 @@ 21640D542010526B002F33CA /* HomeTableViewModel.swift in Sources */, F2B8C40C252C58E600922D08 /* GSRClosedView.swift in Sources */, F2568A7824135B6500561295 /* HomeCellHeader.swift in Sources */, + 6C6035FC26E723240025FBC7 /* EmptyView.swift in Sources */, 21640FD3204A296D008DB6E8 /* LaundryGraphView.swift in Sources */, C8C96F11241407000069D56A /* CourseScheduleTableViewModel.swift in Sources */, 97E6E1F0239D74F500C07D7A /* GSRGroupIconView.swift in Sources */, F212BE8423B6C8A200ED46A1 /* NotificationPreference.swift in Sources */, 21A6B6D022162652003A357D /* GSRReservationsController.swift in Sources */, + 6C6035F326E722890025FBC7 /* DiningVenueDetailLocationView.swift in Sources */, F2562A3A2558E7B80021C92F /* CourseAlertCreateCell.swift in Sources */, B654BAEE1F801F830038B9D5 /* LaundryTableViewController.swift in Sources */, + 6C6035F526E722890025FBC7 /* MenuDisclosureGroup.swift in Sources */, F2F4C92023611EB900816456 /* Colors.swift in Sources */, + 6CAE4380253370E500BD0200 /* FrequentLocationsView.swift in Sources */, F27AA01823BC6CEC00276C4F /* PermissionView.swift in Sources */, + 6CAE43C62533731000BD0200 /* DiningInsightsAPIResponse.swift in Sources */, 2189C0852027CDB800771C1F /* GSRController.swift in Sources */, 21ABE2AA223D7DEA00199D29 /* ScheduleTable.swift in Sources */, + 6C84D9E526293E680039C57F /* UIKit Views.swift in Sources */, CF1CE9BA205D5C3F00C51F46 /* MoreViewController.swift in Sources */, 21B649242236FFD300AC5B7F /* BuildingMapWebviewController.swift in Sources */, 216640951EBADC1700746B8E /* DiningViewController.swift in Sources */, @@ -1905,17 +2086,21 @@ EF30077223EE1380006C9CF0 /* HomeGroupInvitesCellItem.swift in Sources */, 217A7841204D9802004F1227 /* ModularTableViewItemTypes.swift in Sources */, 212B8360222A37FE00F835D6 /* FeatureAnnouncement.swift in Sources */, + 6CAE438F253370E600BD0200 /* DiningView.swift in Sources */, 21737FD723A1DEE7002DCD07 /* LabsLoginController.swift in Sources */, 97E79E182100E4AA00D3D606 /* FitnessFacilityData.swift in Sources */, F2770AE02545E311001EA1DD /* CourseAlert.swift in Sources */, CF29A1781FB788820067D946 /* PageCell.swift in Sources */, + 6CAE438D253370E600BD0200 /* DiningInsightsView.swift in Sources */, 210AC14520684F9B0050D837 /* HomeCellProtocols.swift in Sources */, 97AA806B23D26BC700C23488 /* DiningHeaderView.swift in Sources */, 2189C09F2027CE4100771C1F /* GSRUser.swift in Sources */, B9F8423123DDFD6700814975 /* GroupInviteUserCell.swift in Sources */, 2189C09D2027CE4100771C1F /* GSRLocationModel.swift in Sources */, + 6C6035FA26E722E60025FBC7 /* GSRAPIResponse.swift in Sources */, C8C96F15241407420069D56A /* PacCodeViewController.swift in Sources */, B62875ED2115433100FB2873 /* PennCoordinate.swift in Sources */, + 6CAE438A253370E600BD0200 /* PredictionsGraphView+AxisLabels.swift in Sources */, EFE2D6F6239B11050020F6BF /* GSRColorCell.swift in Sources */, F212BE8823B71C7100ED46A1 /* NotificationsTableViewController.swift in Sources */, 21F5F8F320538A90005B143F /* HomeFlingCell.swift in Sources */, @@ -1925,8 +2110,10 @@ 21B8BBB2222318A600EBC1D0 /* Course.swift in Sources */, 97E79E1A2100E51A00D3D606 /* FitnessFacilityName.swift in Sources */, 97E79E082100DA1200D3D606 /* BuildingHoursCell.swift in Sources */, + 6CAE4352253370B300BD0200 /* AlertModifier.swift in Sources */, 21F5F8F520538AFC005B143F /* FlingPerformer.swift in Sources */, 21E6A109224BDFEB00DC457A /* HomeViewController + Delegates.swift in Sources */, + 6CAE4385253370E600BD0200 /* CardHeaderView.swift in Sources */, 217A7832204D2C7E004F1227 /* HomeCellItem.swift in Sources */, 2189C08D2027CE2600771C1F /* RoomCell.swift in Sources */, 2150816A220D24C4002F7EA1 /* NewsArticle.swift in Sources */, @@ -1938,6 +2125,7 @@ 21640D40200EF881002F33CA /* LaundryCell + Graph.swift in Sources */, 21EB4D30203D330C0029A460 /* UserDBManager.swift in Sources */, 2188C6D7223C7E8400F18D90 /* SecureStore.swift in Sources */, + 6CAE4388253370E600BD0200 /* PredictionsGraphView.swift in Sources */, F23CC0BD235E3F390007317A /* DiningAPIResponse.swift in Sources */, 216640641EBADADA00746B8E /* AppDelegate.swift in Sources */, B6111DB420184C4A00DC7877 /* AboutViewController.swift in Sources */, @@ -1959,15 +2147,17 @@ C10A59A42183CDD60059130B /* AboutPageCollectionViewCell.swift in Sources */, EF6329A22409D2CE00E7ED36 /* GSRGroupConfirmBookingController.swift in Sources */, 21D5E07423BCFE0300B331CC /* CoursePrivacyController.swift in Sources */, - 6C23AF9526E57903002F60F0 /* EmptyView.swift in Sources */, + 6C23AF9526E57903002F60F0 /* (null) in Sources */, B658393B1FABD295009486FC /* LaundryAPIService.swift in Sources */, + 6C6035F626E722890025FBC7 /* DiningVenueDetailView.swift in Sources */, 21ABE2A9223D7DEA00199D29 /* ScheduleLayout.swift in Sources */, 97E79E042100DA1200D3D606 /* BuildingHeaderCell.swift in Sources */, + 6CAE4386253370E600BD0200 /* VariableStepLineGraphView+GraphEndpointPath.swift in Sources */, 97AA806923D26BC700C23488 /* DiningBalanceCell.swift in Sources */, - F23CC0C7235ED0C60007317A /* LocalJSONStore.swift in Sources */, B62875EB2115302200FB2873 /* MapViewController.swift in Sources */, 214C23B21F2FEE150004487C /* Networking.swift in Sources */, 2189C0A92027CE4B00771C1F /* RangeSlider.swift in Sources */, + 6CAE4391253370E600BD0200 /* DiningVenueRow.swift in Sources */, F2562A262551F6D40021C92F /* CourseAlertSettingsController.swift in Sources */, 217A7845204DB800004F1227 /* HomeAsynchronousFetching.swift in Sources */, C11DFA2F219F8A65000FC573 /* CalendarAPI.swift in Sources */, @@ -1981,6 +2171,9 @@ 21508168220D24A1002F7EA1 /* HomeNewsCell.swift in Sources */, 21640D6220114A29002F33CA /* ControllerModel.swift in Sources */, 216640C41EBADC9300746B8E /* GenericControllers.swift in Sources */, + 6CAE437D253370E500BD0200 /* DiningViewModelSwiftUI.swift in Sources */, + 6CAE438C253370E600BD0200 /* VariableStepLineGraphView+VariableStepGraphPath.swift in Sources */, + 6CAE438E253370E600BD0200 /* DiningViewHeader.swift in Sources */, 97AA807623D26C8E00C23488 /* DiningViewModel.swift in Sources */, EFE2D6F4239B11050020F6BF /* GroupCell.swift in Sources */, 21640D3E200ED954002F33CA /* LaundryMachine.swift in Sources */, @@ -1992,6 +2185,7 @@ 97E79E072100DA1200D3D606 /* BuildingImageCell.swift in Sources */, 217A783F204D97F8004F1227 /* ModularTableViewItem.swift in Sources */, 212209A71EC7CD2F0038FB7F /* MoveableTableViewController.swift in Sources */, + 6CAE4351253370B300BD0200 /* FadingScrollView.swift in Sources */, 21A9D942224087980000444D /* CourseMeetingTime.swift in Sources */, 139139772388923F00C6C298 /* DiningDollarsTransactionViewController.swift in Sources */, B6040A361F8F24D900E4B783 /* AddLaundryCell.swift in Sources */, @@ -2028,11 +2222,14 @@ F2562A302558330D0021C92F /* CourseAlertSettingsOptions.swift in Sources */, F2770ADB2545E2EB001EA1DD /* CourseAlertCell.swift in Sources */, 97E79E162100E33100D3D606 /* FitnessAPI.swift in Sources */, + 6C88FE9425337476006F4587 /* DiningViewControllerSwiftUI.swift in Sources */, + 6CAE43D82533740300BD0200 /* LocalJSONStore.swift in Sources */, 21B556192224FDF700D80F61 /* SplashViewController.swift in Sources */, B650C5641FA43FC600922E98 /* LaundryRoom.swift in Sources */, EF30077423EE139F006C9CF0 /* HomeGroupInvitesCell.swift in Sources */, 217C6DD920139AC500F07C3D /* AsynchronousOperation.swift in Sources */, 21B8BBBD2224F9AA00EBC1D0 /* LoginController.swift in Sources */, + 6CAE4381253370E500BD0200 /* RecentTransactionsView.swift in Sources */, F2562A3525583C5C0021C92F /* ButtonWithImage.swift in Sources */, 97E79E102100DD5000D3D606 /* FitnessHourCell.swift in Sources */, EFE2D6FB239B12270020F6BF /* GSRGroupNewIntialController.swift in Sources */, @@ -2043,6 +2240,7 @@ 2108CF231F3F762500CEC3F4 /* ContactCell.swift in Sources */, 2189C09B2027CE4100771C1F /* GSRTimeSlot.swift in Sources */, 21F5F8F120538A87005B143F /* HomeFlingCellItem.swift in Sources */, + 6CAE4384253370E500BD0200 /* CardView.swift in Sources */, 2189C08E2027CE2600771C1F /* GSRRangeSlider.swift in Sources */, B64830A2213C939200FD2B17 /* FitnessHeaderView.swift in Sources */, C11DFA3521BB5782000FC573 /* UniversityNotificationCell.swift in Sources */, @@ -2232,7 +2430,7 @@ }; 2166407E1EBADADA00746B8E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6F4DC86EE3A48EC9A02145AE /* Pods-PennMobile.debug.xcconfig */; + baseConfigurationReference = 72787B1E070BFCDF84D8C3CA /* Pods-PennMobile.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -2430,7 +2628,7 @@ }; F230FB17225809E900760499 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6DBD80716B161DAEA6274151 /* Pods-AutomatedScreenshotUITests.release.xcconfig */; + baseConfigurationReference = 407FC3CFC29C6496A17C99C2 /* Pods-AutomatedScreenshotUITests.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -2551,7 +2749,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON.git" */ = { + 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git"; requirement = { @@ -2588,7 +2786,7 @@ /* Begin XCSwiftPackageProductDependency section */ 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; - package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON.git" */; + package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { diff --git a/PennMobile/Assets.xcassets/Dining/Contents.json b/PennMobile/Assets.xcassets/Dining/Contents.json index da4a164c9..73c00596a 100644 --- a/PennMobile/Assets.xcassets/Dining/Contents.json +++ b/PennMobile/Assets.xcassets/Dining/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Contents.json b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Contents.json b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Contents.json new file mode 100644 index 000000000..0e7c4ef43 --- /dev/null +++ b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Farm to Fork.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Farm to Fork.png b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Farm to Fork.imageset/Farm to Fork.png new file mode 100644 index 0000000000000000000000000000000000000000..a06d34ad44273ed80c9a67bab9ec97ec9abf16af GIT binary patch literal 1200 zcmV;h1W)^kP)l8(K8ZewPaPzb_(8=QCoh68J_yB!JV+o=v<-zonFS>!Y7l2hi^%pPSnM_pX=>KZ zW{(fEGjnJ5P7?IMkezeSJ^%mtxaUrUEy{^fahJeRRfZ7yQQeKug9?Z)gH%!G5T*sb zb3K1?D}H5j;6k4$!zywN=teEO6%dUSqSQer5IOC7e&s(Aa7x737K zG1sAq@TDpvV`YD3%LJTK@kvD|LG~sb+jba$0;oNt3h?rn=U>^BfODYutcrdQ(wiBj z1w1>PNNS>b$SqfX&Lq%|ybRJC0hm_$KYoWKV9+%(5iL8VLJ!JMiL_g38qRGJ^@j%a zF#9FYS3m|_&;J`BANf-mMc5lU-}&$(Z13HfffC4_KNnb8x`PNj`a}UFmu;-d!j%dw zrGVexV!T zecO6X+1HR{Y`a!p<>L6)7W8;u7Z! zYJj4)#!3-M<8^>{?%yWxLQYkNV(VnPiy2-zHL1ul8Hn*W1dq)Q<`DWV*nvSi17bQg z?Xm_>Iy+fM7W(s|x(AZX;`+u)F`aIZek(8A-4X2FomXTB(k_q1!*j>q;;BO~b%3nZ z>wNp!NJ96(_8q+Op=;%Jd8W+l`Ljv?qP_X>MJ-XODv$R)ZE*+x+`AhPFvxb@*G=Eh zVXOVhVvX6Uv$3MxQ+Z%npvM-yh;U~3UFK)XR;TG)-& z6OrjG#tG`_yC2KDO|rhAf*zR80q0QaiGdOBP1nsbV8lk+JpXCGkq=!_n98}HUlU;> zoUZ1S4d;NgfubkWZ7>HCcIFf7$HzQ>CA@x5quMlUc6aeMx2{)dtga^N9oJHWS`z%O z-MGr_h3ip}iS^Wepn=M$nHZc>@e76b5~;J*-4?D*HrOk|r>^Ipgv95zo)nmmoTlsD zQ6CF<)+J=1 z=Wu@5Y*G)(O;n#Z!8-^9Br2CwWKe>CCXy1ieRQPEi-UO0EQNxo&LQk~%az~KF1tEF zfFG{jx;^;Vt}~zyf(*n&)3Y`G!9uz!4H0z_dDHd$nhk5y0y?F_K1CdX*MV&j$Oga) zD+1%dsO$NEY~+8-9psdXy(ouO7y|l%J;p7%1XKm)fN9`@>-o!D@%u045B-toiuKC? O0000l>&~9UK7x*6d0>hZ%mS${SfY(0)oC5}Ig<9h#aE4&u zT6F^Cs?PzZw&=JJpI{gtSnWo!0I%N#dDq_y#Q_rO4AFakOWqm>=FflBb?%N1Ghy-Pea9mH-1-PcV9pbt0!n?X2%5Tx`p04u98 zP)b}8LXgakd$1mtTc71)i6nshEoHxn2`Rwq?*D=4Km~Rjv?6bKYRAm0{$R$H}miX|@XVB7AY_T4v z`$#`yzCjfGqh^Pw3fwLf;Bu<4s|lKS6r0pLY2MNJUmcs6Q-Il8&9((xSAeTPw363Z zjS&T|Dg*=bz>Qk^f3O~h$O_#5r?{nb^ZLF)FfebP0u6&#%%E~P)pDuyVemQY)XfXj zfVyn$QHHXVN4Y?nFz<-pCF+;|XtYYeC1@ zRZ&2ex&_NSzXF`j9iIQ8!y$tX&lTWlpsNBI)nE$wEKijdAVui3ANUF(8i1i1IX{nK zoaC0K7k1c{X2*b!0nZj4{{Wn-zBJ1h;XoD@?*NB@-8L3~A&b+Cz-58~i&srG0`kT5 t10%>1y$9&b_ADXG^J~cR{C1^N{tL#3GTV-6h=c$D002ovPDHLkV1kjQ5j6k+ literal 0 HcmV?d00001 diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Contents.json b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Contents.json new file mode 100644 index 000000000..92c5864c8 --- /dev/null +++ b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Humane.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Humane.png b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Humane.imageset/Humane.png new file mode 100644 index 0000000000000000000000000000000000000000..3520aada950c6fdcd2e5dab60523ffe2b38ad8fc GIT binary patch literal 1133 zcmV-z1d{uSP)4=Y=KTJibMHC#MohT2SMoCw7lB1!9_Rt5O7SkR0StgO!|u!$>syof)dYdxkyr&T z0aN2OEq3<;<2K@Yw-`S7UjTMi@)?6daMiF3$;AkP;((RlG4BBQUb1W5VtsF706YEs zZG#^LxpUEe)6Xoos)&?Ks_}Ty0<}!_D@xbXC=HW%WPmZpZ2-{FQ65W$t1Dn(Y?1Qz&af zUZ+#Km>UN*)_hAwFWI-{tO@8Pvd$dSAAQbID?8mARiL~g{MqpSBDcL33Rh|~l?bt! zq1Wbk_55;O#73+KZhVDon>znCTw;1kbCHU)t1F5=ifj&Io<)2`ieTky79ozlX{#by}HWRYv16qjCOr6RcBye?g?%I#kjx|1Bifh zfF)qjU>>6 literal 0 HcmV?d00001 diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Contents.json b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Contents.json new file mode 100644 index 000000000..9cf87da16 --- /dev/null +++ b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Jain.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Jain.png b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Jain.imageset/Jain.png new file mode 100644 index 0000000000000000000000000000000000000000..5d5421ca0c88242009d1af70e2a47be4e6643725 GIT binary patch literal 722 zcmV;@0xkWCP)< zu@P*_Hw^wPq}@n zV~&~kogL=;-Z9WbdQLwu0ZalXfl`b$j>L_7beBOZn!My~~;2mbtmFwiw|Hu)t)LR_Feds}e;%A52}&a!`M z5iu|hggUbZfCcF}`8or@6lu=fpt|dDM|&DD0^F}LAU$Ugc$8+9f8?(8oMOd5J{>Gr)FR&0x2>YqJk?sHDa>eKZ30UhY9u18mn{A!?7!#dk1=OPE6?#Nks|*I!^* z2ifsI6KtfuNY8E8Uj*n$3_b;J(X2G@Zqs{og4~(gm7!pLncuH&VpxSV;_F1ahx$-j z1U{j%TPWkV6%rwF)}fdMxh1kk$7FvYuJdQ+*AG#~O))-s7~0O3asmTngb2 zz;rWVZ377-gTNJ3gc}12K>WbiLY?tfRB3-5UG28~1(Bn+oE@=YlK=n!07*qoM6N<$ Ef_v*ixc~qF literal 0 HcmV?d00001 diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/Contents.json b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/Contents.json new file mode 100644 index 000000000..835852311 --- /dev/null +++ b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "test.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/test.png b/PennMobile/Assets.xcassets/Dining/Dining Menu Icons/Made without Gluten-Containing Ingredients.imageset/test.png new file mode 100644 index 0000000000000000000000000000000000000000..df2bb14bf160d8de42f153dfa2c1d86c8a025198 GIT binary patch literal 1556 zcmV+v2J88WP)eS zb2)x^-zINvuJgc?C(m&fO zK&F7(!qNCpT>#d}IaqRD8SHbVD!@|$o5RsWF%m_tm2-`YNH=P!p4Z*W?Bpof+3C7@ z7yHNUZ^6^F0>mJjPLiFQrjXCqx+h0P^xAMVkp?gwSa*v^_mWisf>+#tVR|_I%ePfA z5Wv^gLFbko1bc48yW#>wN>bVR8B)i8Vj{7R$;5uAehJ)z++RsArZ$yNK*7IrUT*rB9H!*+aB$J8#725h#EE3uq zj>bn!0Q;RAl}@~^?d0ZXs+@P-^Z;vady?G3EF&Mi$VB{0&_b15^M#j_ojbGSlu`o4 z1fBsNGGmd@Dpl@;^8AW`r#VPIlV<5o00cVLvijD?C}cAn+WQ2l6F-;L0jHhR__3P% zmb~tWMM4o%(QP8)aiSDJ3=_9MKt8)ryc3{w_1%vcE`E=_Lu&kI2e0y!AZ5!^39~); z72RqI(pQZUTpllm+lv-Tp*73~f<0T%LZ0z&KBk)FKbMso0)3`hAC@XIea&U(0%&Ys zjmzU>cH%d(b7!hTf9@n3Oq8xlYzmm(CPV}g1JvR-1cCr8Oi!X(ac2NeppC}%ORC%? z$9^QAU943@x=ayms&p?1E_VX}hR0hhHi-35w2()N306C&(018%T)FdkRJE)oeErgP z=KncaX5WS#FsnZz)@-;vsHz3TX5}+;s48xM3)Om%{No76KG|JdcW&8&kk4iStom;6Ws-k0e|j9#*UXCUjn-ySr=(vTPI#3(1yvn05QY(}@L&%S$fc72 zG_${I$sayPL|C)+3B+`l=}Bwk>EDOR%uLz(0sJdF@dqzL)h@ghz6E9x z1_dBC5PDDH-leDoE?&pR$M;ewFEz50jbs$9~R=svx3ohogx{EdY_;9#uIkQtA&nx9sH7?E{Diqp^1w{pu~JX_k`g zZfs@6>TB?|bzqncsOHI~XIPk?VCK&eGO1HkzA#l55w7Tu#z%|0gOSkd0#7*|^xXX% z7i`?d&>Ihv96Ma=jMR8qb*Ln(@JfF){wz3p08!nA)x8!4fBEcH(o=shJMn8Bqvz5q z+On#{0?*g95Q~H^NA(~`a4Fq}%Z29iHG9HZ{c*k7R-OblhokY)x<`l&gf^<^mx2V( z;dslTNriBKG=9XnS=EMx_r!+;HY*%0J)X18|4pho3~a7+UK0R7IGPv{^jeTTislrt ztEY2T$J#m6xn2Zb0omjXyOYj(i(-+`WvD)*!VXmY<#~1d6RQQY`^i~V{Q!~O{dF8K z8-N2ytEz4lkv^0SDs%}1Z1yxT20Dy#0Fl9PG@e?H-G2dVKv|*YMO1140000VMP)g+Ib$s|Wzi2>M^@~pU#wR9!Hyq_LxPzWcd^h+0tFipLZoc65(?q9 zk%Uc1LS8nzd;IY1KKtylAwkc~o%`H#&OP^k&&z%86{bn9z3w7}XMl~sa-amrkBbL^ zZlE1#Qt@@zB7LA+LLUF=NddUL?k83G*uJ=3DUy?fwC&J$3FsK*tGT`Qf6w43EOIN{E{-Mj+XV(`7y+YyAfPBk(fodCcg1{;c(QC+-@CtM52 z%W)EkMY%P6o4~DZS}va@7`hG+0V+ct-vvk9uIKjMJI1SPUL+EWa=5FNp@}ghLp}>o zQ(VQ5YG2{Z^-g~E$pHYim9OR}n|4xMI3IvWG{QtQg2Tun+mUU8SLIuO`57N{wK{;; zft?Q5UU!kI*4-Ju%aMzxc(!6aTgtw~fsSL=p3`Bxuyg~eN`0UiRN1xmYwWIhnMf?k zpW6KF|MVj|dap1Zogh2QL21D}YKoV!sdObrFSUUx3Oih0_iGM>X9URsXb3d3ZAmpR zRBoWY^LWM_%5&thblFh!=P*8o` zKqTK@%m26$t5j5#nt3Evhn@N!)8C_@sd7DYMOag6V;{W^6^Yxw0S^_Kj_%34&F1A%71Oh{E_X#B1<-(E4lcfU%- z+=VGoKYb&}4?cX$8XLf1qSO*F8f;-m7%QIj2&*2e5oS1Y(|$bW zDA$pL(~*nQk;|;ynN${)Qu6RzJDvdt(5*_9Sh0p+=sKVFT;vP$%BU(TrTssbsH>hfzO9Ss4B$;a}n#JKpcYd&G&z9ek+vEevH=dyiI~(dP=$tpgsMJSD-%7 zf{0L8QET=~YXPY5XhB5~fq(UMAtF>4SEa;0Ay0`nAn`sar`+4krF7o9%=z9cma{+n>@?xX1RF|Mve@-#%KXGeGF(oQ!4jGb75u7mKpU)j7%gEyGZM!JQooUUtSEUseH9z9= zVc>|-@A37C&;W{{NW4x3^Q;#UURtq*n8rBRaXdvXC_ip_mkWJYshVF-(<{Gbdqp)f z9NCs!;x#`j$7zMYT#NTY9^YW1484f#0Fq@X@Yk+Zei0tUnLUFO-R;~MzMXVvyt@+~ z;ko^L_`|l>SYNV&cb?zP*u*%OLpK?j7{y^YnD@|Zit=4{?}tJ60b~J;Hg)wnpI$Tv z;mh_Yg`*L $~q${)}Ax!8X*V@yXT#yJpZ=FE*Cu~-ymb`Fo^yKv%3@m`+mUK6l**ms|XHfi)qIe++C_viQCI ztDt?>#Xg=v$z#gaPa@Yu)@f4T;-Ni)Y)>qn;(-LYyzVaohk-fw!2kcK2i32H_V~}+ znMS%+$m2V$unxHIlj?y;wHbmWGy8K;4oG7a@0i7m=5#B_2Ddh3@ zW>T2W<8%@5I_M6Ryi8A?d!I-$jPf3$`$8W7rTf{R5`Yaz0m^11<$0dCw+&!>o&!fh e9^c?}?EV+OwB1L#c{Pjx0000I*6qDPvm;OnY3+ zH{W-^M@zw*+_~Sq=iGD7J?DSUy(28Kb=j%43j2UI&=!z7pxPnlfia*D=m9;Ev3n<$ zB3B;A&}pZd1rCAkSBUwys3RgY&I)oIIFh!r!%t;Emz}BsjshP7mS5pw&Bs#!z5ouV z?d;T&4d}E}O~ALH4MiqF4Z-4kD{8EGUNC>1{UZu*W$f(8V-4uCUwK`T?|{T2E`FME z0(5W2?(JRB0O#c23z`Vqwvh0p)3j(rYsT&!4H@9X{8G{FPjb423|w%17)Yh<>Q8#&T_!O|La;mCoiN|YP1ekP%Mj&01Q3l0S1HJ+k z&)U*v<<*^U^WHn3vpiPGm1`G`6NY(r?%2-Caz2s=B(-bz`fo0rzj@bk zmJb!Z5#>S{7&?RM98KHzP$uPsn`cmSvI7yJKi3^_y-#Mj!a)G5%TBEY-Ctx;#7R1H z%;*1NZ0skht5;E9zr&zobfq@&92++#$>(o#W9(9h?w@m@%T6UMg?);|)XC~n+;Qi{ zv47ws0L{q`gO0B!o7w?kaOe#4^Mw%I?4XsP9TvsfP7t~3cNxQ%&rv8mVB6N+#FkYU z^inAjnv?AS^yj*bLKIAA6Xeonfwp+=_HgTWneyPl6u*rAKt)A0n>V$(>m`4!dv*(J z*3@(R&UJ2$j|Rfzmt};w#S)|rL#+fXHKSGp7QB?~aA_s3=43k}LN0eQus`bCm0f2k zRI3qBF9^PPp}^H^7nzxvq@m$8;_;R4`9&<6w(ntnexBUmDT4U$@Z^`LXpQAr<&n&b z1Y=&9D=;{8ma?*CY;SziHMZ`Bm#L{)%dfxv%>Dc03+yK(4+IOyF9w!i(aE{l(C%`Y zxv##W6Ko+pla`=kCJ4h|@qhpR#{l>4{mJtiTS%;YhGpe38n?Ca=+P|0m(NA)T@-|4 zmLh$g#bP+lk8`)HGB9`w%d%+P_9idZ?<5|tVRZCI{+<2D%_DzJG4BH(2>UFc#{q<) zyc8>MX0G-RoI+LEvgr-BY;Fahf1ulSag-#DoYPYdoB(D8sffN(!&mUc#0_rV8ezkR zCTeRpFf%jBm8%ycqP`Fg0CWoEw3V^56M`NOD>IqHRV|n6CKij6NUURU=qz({4;N7w zRR`o)+Ro-nX?O%E`0s@k&x3;;;m*VY7A3auGCI~deg7F+ DiningVenue.VenueType { diff --git a/PennMobile/Dining/Model/DiningInsightsAPIResponse.swift b/PennMobile/Dining/Model/DiningInsightsAPIResponse.swift new file mode 100644 index 000000000..44eccba17 --- /dev/null +++ b/PennMobile/Dining/Model/DiningInsightsAPIResponse.swift @@ -0,0 +1,156 @@ +// +// DiningInsightsAPIResponse.swift +// PennMobile +// +// Created by Dominic Holmes on 2/21/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation + +// MARK: Full API Response +struct DiningInsightsAPIResponse: Codable { + + static let directory = "diningInsights.json" + + let swipes: Int? + let diningDollars: Double? + let guestSwipes: Int? + + let startOfSemester: Date + let endOfSemester: Date + + let cards: CardData + + struct CardData: Codable { + // These cards are defined in extensions of CardData to keep this struct definition small + let recentTransactions: RecentTransactionsCardData? + let frequentLocations: FrequentLocationsCardData? + let dailyAverage: DailyAverageCardData? + let predictionsGraphSwipes: PredictionsGraphCardData? + let predictionsGraphDollars: PredictionsGraphCardData? + + enum CodingKeys: String, CodingKey { + case recentTransactions = "recent-transactions" + case frequentLocations = "frequent-locations" + case dailyAverage = "daily-average" + case predictionsGraphSwipes = "predictions-graph-swipes" + case predictionsGraphDollars = "predictions-graph-dollars" + } + } + + enum CodingKeys: String, CodingKey { + case swipes = "swipes" + case diningDollars = "dining-dollars" + case guestSwipes = "guest-swipes" + case startOfSemester = "start-of-semester" + case endOfSemester = "end-of-semester" + case cards = "cards" + } +} + +// MARK: Dining Transaction +struct DiningTransaction: Codable, Hashable { + let location: String + let date: Date + let balance: Double + let amount: Double + + var formattedAmount: String { + let amountString = String(format: "%.2f", amount) + return (self.amount > 0 ? "+" : "") + amountString + } + + var formattedBalance: String { + return String(format: "%.2f", balance) + } + + var formattedDate: String { + let formatter = DateFormatter() + formatter.dateFormat = "h:mm a EEEE, MMM d" + return formatter.string(from: self.date) + } +} + +// MARK: Frequent Location +struct FrequentLocation: Codable { + let location: String + let week: Double + let month: Double + let semester: Double +} + + +// MARK: Recent Transactions Card +extension DiningInsightsAPIResponse.CardData { + struct RecentTransactionsCardData: Codable { + let type: String + let data: [DiningTransaction] + } +} + +// MARK: Frequent Locations Card +extension DiningInsightsAPIResponse.CardData { + struct FrequentLocationsCardData: Codable { + let type: String + let data: [FrequentLocation] + } +} + +// MARK: Predictions Graph Card +extension DiningInsightsAPIResponse.CardData { + struct PredictionsGraphCardData: Codable { + let type: String + let data: [DiningBalance] + let startOfSemester: Date + let endOfSemester: Date + let predictedZeroDate: Date + let semesterEndBalance: Double? + + enum CodingKeys: String, CodingKey { + case type = "type" + case data = "data" + case startOfSemester = "start-of-semester" + case endOfSemester = "end-of-semester" + case predictedZeroDate = "predicted-zero-date" + case semesterEndBalance = "balance-at-semester-end" + } + + struct DiningBalance: Codable { + let date: Date + let balance: Double + } + + enum BalanceType: String { + case dollars + case swipes + } + } +} + +// MARK: Daily Average Card +extension DiningInsightsAPIResponse.CardData { + struct DailyAverageCardData: Codable { + let type: String + let data: DailyAverageTuple + + struct DailyAverageTuple: Codable { + let thisWeek: [DailyAverage] + let lastWeek: [DailyAverage] + + enum CodingKeys: String, CodingKey { + case thisWeek = "this-week" + case lastWeek = "last-week" + } + + struct DailyAverage: Codable, Comparable { + let date: Date + let average: Double + + static func < (lhs: DiningInsightsAPIResponse.CardData.DailyAverageCardData.DailyAverageTuple.DailyAverage, rhs: DiningInsightsAPIResponse.CardData.DailyAverageCardData.DailyAverageTuple.DailyAverage) -> Bool { + return lhs.average < rhs.average + } + } + } + } +} diff --git a/PennMobile/Dining/Model/DiningMenu.swift b/PennMobile/Dining/Model/DiningMenu.swift new file mode 100644 index 000000000..26aecef89 --- /dev/null +++ b/PennMobile/Dining/Model/DiningMenu.swift @@ -0,0 +1,112 @@ +// +// DiningMenu.swift +// PennMobile +// +// Created by CHOI Jongmin on 26/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation + +struct DiningMenuAPIResponse: Codable { + static let directory = "diningMenus.json" + + let document: Document + + enum CodingKeys: String, CodingKey { + case document = "Document" + } + + struct Document: Codable { + let dateString: String + let menuDocument: MenuDocument + + enum CodingKeys: String, CodingKey { + case dateString = "menudate" + case menuDocument = "tblMenu" + } + } +} + +struct MenuDocument: Codable { + let menus: [DiningMenu] + + enum CodingKeys: String, CodingKey { + case menus = "tblDayPart" + } +} + +struct DiningMenu: Codable, Hashable { + + let mealType: String + let diningStations: [DiningStation] + + enum CodingKeys: String, CodingKey { + case mealType = "txtDayPartDescription" + case diningStations = "tblStation" + } +} + +struct DiningStation: Codable, Hashable { + let stationDescription: String + let diningStationItems: [DiningStationItem] + + enum CodingKeys: String, CodingKey { + case stationDescription = "txtStationDescription" + case diningStationItems = "tblItem" + } +} + +struct DiningStationItem: Codable, Hashable { + + let tableAttribute: Attribute + let title: String + let description: String + + enum CodingKeys: String, CodingKey { + case tableAttribute = "tblAttributes" + case title = "txtTitle" + case description = "txtDescription" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let data = try? container.decode(Attribute.self, forKey: .tableAttribute) { + self.tableAttribute = data + } else { + self.tableAttribute = Attribute() + } + +// self.tblFarmToFork = try! container.decode(String.self, forKey: .tblFarmToFork) + self.title = try container.decode(String.self, forKey: .title) + self.description = try container.decode(String.self, forKey: .description) + } +} + +struct Attribute: Codable, Hashable { + init() { + attributeDescriptions = [] + } + + let attributeDescriptions: [AttributeDescription] + + enum CodingKeys: String, CodingKey { + case attributeDescriptions = "txtAttribute" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let data = try? container.decode(AttributeDescription.self, forKey: .attributeDescriptions) { + self.attributeDescriptions = [data] + } else { + let data = try container.decode([AttributeDescription].self, forKey: .attributeDescriptions) + self.attributeDescriptions = data + } + } +} + +struct AttributeDescription: Codable, Hashable { + let description: String +} diff --git a/PennMobile/Dining/Model/DiningVenue+Extensions.swift b/PennMobile/Dining/Model/DiningVenue+Extensions.swift index dd4f8629b..95f84b81c 100644 --- a/PennMobile/Dining/Model/DiningVenue+Extensions.swift +++ b/PennMobile/Dining/Model/DiningVenue+Extensions.swift @@ -30,14 +30,30 @@ extension DiningVenue { return self.meals[DiningVenue.dateFormatter.string(from: Date())] } - var currentMeal: DiningVenue.MealsForDate.Meal? { - guard let mealsToday = mealsToday else { return nil } + var isOpen: Bool { + guard let mealsToday = mealsToday else { return false } for meal in mealsToday.meals { if meal.isCurrentlyServing { - return meal + return true } } - return nil + return false + } + + var currentMeal: MealsForDate.Meal? { + return self.mealsToday?.meals.first(where: { $0.isCurrentlyServing }) ?? nil + } + + var currentMealType: String? { + return self.currentMeal?.type ?? nil + } + + var isClosingSoon: Bool { + return Date().minutesFrom(date: currentMeal?.close ?? Date()) < 15 + } + + var timeLeft: String { + return Date().humanReadableDistanceFrom(currentMeal?.close ?? Date()) } var nextMeal: MealsForDate.Meal? { @@ -46,10 +62,40 @@ extension DiningVenue { return mealsToday.meals.first(where: { $0.open > now }) } + var currentMealIndex: Int? { + return self.mealsToday?.meals.firstIndex(where: { $0.isCurrentlyServing }) + } + + var currentOrNearestMealIndex: Int { + return self.mealsToday?.meals.firstIndex(where: { $0.isCurrentlyServing }) ?? self.mealsToday?.meals.firstIndex(where: { $0.open > Date() }) ?? 0 + } + var hasMealsToday: Bool { return mealsToday != nil } + var nextOpenedDayOfTheWeek: String { + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + + let sortedMeals = self.meals.sorted(by: { + formatter.date(from: $0.key)! < formatter.date(from: $1.key)! + }) + + for meal in sortedMeals { + if formatter.date(from: meal.key) ?? Date() > Date() && meal.value.meals.count != 0 { + return "until \(formatter.date(from: meal.key)?.dayOfWeek ?? "N/A")" + } + } + + return "Indefinitely" + } + + var isMainDiningTimes: Bool { + return (currentMealType == "Breakfast" || currentMealType == "Lunch" || currentMealType == "Dinner") + } + // MARK: - Formatted Hours var humanFormattedHoursStringForToday: String { guard let _ = mealsToday else { return "" } @@ -103,6 +149,95 @@ extension DiningVenue { } return timesString } + + var humanFormattedHoursArrayForToday: [String] { + guard let _ = mealsToday else { return [] } + return formattedHoursArrayFor(Date()) + } + + func formattedHoursArrayFor(_ date: Date) -> [String] { + let dateString = DiningVenue.dateFormatter.string(from: date) + return formattedHoursArrayFor(dateString) + } + + func formattedHoursArrayFor(_ dateString: String) -> [String] { + + var formattedHoursArray = [String]() + + guard let meals = self.meals[dateString]?.meals else { return [] } + + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(abbreviation: "EST") + formatter.dateFormat = "h:mm" + formatter.amSymbol = "am" + formatter.pmSymbol = "pm" + + let moreThanOneMeal = meals.count > 1 + + for m in meals { + if m.open.minutes == 0 { + formatter.dateFormat = moreThanOneMeal ? "h" : "h" + } else { + formatter.dateFormat = moreThanOneMeal ? "h:mm" : "h:mm" + } + let open = formatter.string(from: m.open) + + if m.close.minutes == 0 { + formatter.dateFormat = moreThanOneMeal ? "h" : "ha" + } else { + formatter.dateFormat = moreThanOneMeal ? "h:mm" : "h:mma" + } + let close = formatter.string(from: m.close) + + formattedHoursArray.append("\(open) - \(close)") + } + + return formattedHoursArray + } + + var statusString : String { + if hasMealsToday { + if isOpen { + if isClosingSoon { + return "Closes \(timeLeft)" + } else { + switch venueType { + case .dining: + return currentMealType! + default: + return "Open" + } + + } + } else if let nextMeal = nextMeal { + switch venueType { + case .dining: + return "\(nextMeal.type) \(Date().humanReadableDistanceFrom(nextMeal.open))" + default: + return "Opens \(Date().humanReadableDistanceFrom(nextMeal.open))" + } + } else { + return "Closed \(nextOpenedDayOfTheWeek)" + } + } else { + return "Closed \(nextOpenedDayOfTheWeek)" + } + } + + var statusImageString: String { + if hasMealsToday { + if isOpen { + return "circle.fill" + } else if nextMeal != nil { + return "pause.circle.fill" + } else { + return "xmark.circle.fill" + } + } else { + return "xmark.circle.fill" + } + } } // MARK: - Meal diff --git a/PennMobile/Dining/Model/DiningVenue.swift b/PennMobile/Dining/Model/DiningVenue.swift index 249be49eb..b0c9af128 100644 --- a/PennMobile/Dining/Model/DiningVenue.swift +++ b/PennMobile/Dining/Model/DiningVenue.swift @@ -8,7 +8,9 @@ import Foundation -struct DiningVenue: Codable, Equatable { +struct DiningVenue: Codable, Equatable, Identifiable { + + static let directory = "diningVenue.json" let id: Int let name: String diff --git a/PennMobile/Dining/Networking + Cache/DiningAPI.swift b/PennMobile/Dining/Networking + Cache/DiningAPI.swift index f23570fac..a9c03ce04 100644 --- a/PennMobile/Dining/Networking + Cache/DiningAPI.swift +++ b/PennMobile/Dining/Networking + Cache/DiningAPI.swift @@ -14,33 +14,92 @@ class DiningAPI: Requestable { static let instance = DiningAPI() let diningUrl = "https://api.pennlabs.org/dining/venues" + let diningMenuUrl = "https://api.pennlabs.org/dining/daily_menu/" let diningPrefs = "https://api.pennlabs.org/dining/preferences" let diningBalanceUrl = "https://api.pennlabs.org/dining/balance" - - func fetchDiningHours(_ completion: @escaping (_ success: Bool, _ error: Bool) -> Void) { - + let diningInsightsUrl = "https://studentlife.pennlabs.org/dining/" + + func fetchDiningHours(_ completion: @escaping (_ result: Result) -> Void) { getRequestData(url: diningUrl) { (data, error, statusCode) in if statusCode == nil { - completion(false, false) - return + return completion(.failure(.noInternet)) } if statusCode != 200 { - completion(false, true) - return + return completion(.failure(.serverError)) } - guard let data = data else { completion(false, true); return } + guard let data = data else { return completion(.failure(.other)) } if let diningAPIResponse = try? JSONDecoder().decode(DiningAPIResponse.self, from: data) { - DiningDataStore.shared.store(response: diningAPIResponse) - completion(true, false) + self.saveToCache(diningAPIResponse.document.venues) + return completion(.success(diningAPIResponse)) } else { - completion(false, true) + return completion(.failure(.parsingError)) } } } + func fetchDiningMenu(for id: Int, _ completion: @escaping (_ result: Result) -> Void) { + getRequestData(url: diningMenuUrl + "\(id)") { (data, error, statusCode) in + if statusCode == nil { + return completion(.failure(.noInternet)) + } + + if statusCode != 200 { + return completion(.failure(.serverError)) + } + + guard let data = data else { return completion(.failure(.other)) } + + if let diningMenuAPIResponse = try? JSONDecoder().decode(DiningMenuAPIResponse.self, from: data) { + self.saveToCache(id: id, diningMenuAPIResponse) + return completion(.success(diningMenuAPIResponse)) + } else { + return completion(.failure(.parsingError)) + } + } + } + + + func fetchDiningInsights(_ completion: @escaping (_ result: Result) -> Void ) { + OAuth2NetworkManager.instance.getAccessToken { (token) in + print("token:" + token!.value) + guard let token = token else { + // TODO: - Add network error handling for OAuth2 + completion(.failure(.noInternet)) + return + } + + let url = URL(string: self.diningInsightsUrl)! + var request = URLRequest(url: url, accessToken: token) + request.httpMethod = "GET" + + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + guard let data = data else { + if let error = error as? NetworkingError { + completion(.failure(error)) + } else { + completion(.failure(.other)) + } + return + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + if let diningInsightsAPIResponse = try? decoder.decode(DiningInsightsAPIResponse.self, from: data) { + self.saveToCache(diningInsightsAPIResponse) + completion(.success(diningInsightsAPIResponse)) + } else { + completion(.failure(.parsingError)) + } + } + task.resume() + } + + } + func fetchDetailPageHTML(for venue: DiningVenue, _ completion: @escaping (_ html: String?) -> Void) { DispatchQueue.global(qos: .background).async { guard let url = venue.facilityURL else { return } @@ -86,3 +145,69 @@ extension DiningAPI { } } } + +// Dining Data Storage +extension DiningAPI { + + // MARK: - Get Methods + func getVenues() -> [DiningVenue] { + if Storage.fileExists(DiningVenue.directory, in: .caches) { + return Storage.retrieve(DiningVenue.directory, from: .caches, as: [DiningVenue].self) + } else { + return [] + } + } + + func getSectionedVenues() -> [DiningVenue.VenueType : [DiningVenue]] { + var venuesDict = [DiningVenue.VenueType : [DiningVenue]]() + for type in DiningVenue.VenueType.allCases { + venuesDict[type] = getVenues().filter({ $0.venueType == type }) + } + return venuesDict + } + + func getVenues(with ids: Set) -> [DiningVenue] { + return getVenues().filter({ ids.contains($0.id) }) + } + + func getVenues(with ids: [Int]) -> [DiningVenue] { + return getVenues().filter({ ids.contains($0.id) }) + } + + func getInsights() -> DiningInsightsAPIResponse? { + if Storage.fileExists(DiningInsightsAPIResponse.directory, in: .caches) { + return Storage.retrieve(DiningInsightsAPIResponse.directory, from: .caches, as: DiningInsightsAPIResponse.self) + } else { + return nil + } + } + + func getMenus() -> [Int: DiningMenuAPIResponse] { + if Storage.fileExists(DiningMenuAPIResponse.directory, in: .caches) { + return Storage.retrieve(DiningMenuAPIResponse.directory, from: .caches, as: [Int:DiningMenuAPIResponse].self) + } else { + return [:] + } + } + + // MARK: - Cache Methods + func saveToCache(_ venues: [DiningVenue]) { + Storage.store(venues, to: .caches, as: DiningVenue.directory) + } + + func saveToCache(_ insights: DiningInsightsAPIResponse) { + Storage.store(insights, to: .caches, as: DiningInsightsAPIResponse.directory) + } + + func saveToCache(id: Int, _ menu: DiningMenuAPIResponse) { + if Storage.fileExists(DiningMenuAPIResponse.directory, in: .caches) { + var menus = Storage.retrieve(DiningMenuAPIResponse.directory, from: .caches, as: [Int:DiningMenuAPIResponse].self) + + menus[id] = menu + + Storage.store(menus, to: .caches, as: DiningMenuAPIResponse.directory) + } else { + Storage.store([id: menu], to: .caches, as: DiningMenuAPIResponse.directory) + } + } +} diff --git a/PennMobile/Dining/SwiftUI/DiningViewControllerSwiftUI.swift b/PennMobile/Dining/SwiftUI/DiningViewControllerSwiftUI.swift new file mode 100644 index 000000000..100eb1f5d --- /dev/null +++ b/PennMobile/Dining/SwiftUI/DiningViewControllerSwiftUI.swift @@ -0,0 +1,38 @@ +// +// DiningTestView.swift +// PennMobile +// +// Created by CHOI Jongmin on 4/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +@available(iOS 14, *) +class DiningViewControllerSwiftUI: GenericViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let hostingView = UIHostingController(rootView: DiningView()) + + view.backgroundColor = .uiBackground + self.screenName = "Dining SwiftUI" + + addChild(hostingView) + view.addSubview(hostingView.view) + hostingView.didMove(toParent: self) + + hostingView.view.translatesAutoresizingMaskIntoConstraints = false + hostingView.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true + hostingView.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true + hostingView.view.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true + hostingView.view.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true + + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.tabBarController?.title = "Dining" + } +} diff --git a/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift b/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift new file mode 100644 index 000000000..896c67dea --- /dev/null +++ b/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift @@ -0,0 +1,102 @@ +// +// DiningViewModelSwiftUI.swift +// PennMobile +// +// Created by CHOI Jongmin on 4/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +@available(iOS 14, *) +class DiningViewModelSwiftUI: ObservableObject { + + @Published var diningVenues: [DiningVenue.VenueType : [DiningVenue]] = DiningAPI.instance.getSectionedVenues() + @Published var diningInsights = DiningAPI.instance.getInsights() + @Published var diningMenus = DiningAPI.instance.getMenus() + + @Published var diningVenuesIsLoading = false + @Published var diningInsightsIsLoading = false + + @Published var alertType: NetworkingError? = nil + + @Published var swipes = 0 + @Published var diningDollars = 0.0 + + // MARK:- Venue Methods + let ordering: [DiningVenue.VenueType] = [.dining, .retail] + + init() { + refreshVenues() + refreshBalance() + } + + func refreshVenues() { + let lastRequest = UserDefaults.standard.getLastDiningHoursRequest() + + if lastRequest == nil || !lastRequest!.isToday { + self.diningVenuesIsLoading = true + + DiningAPI.instance.fetchDiningHours { result in + self.diningVenuesIsLoading = false + + switch result { + case .success(let diningVenues): + UserDefaults.standard.setLastDiningHoursRequest() + var venuesDict = [DiningVenue.VenueType : [DiningVenue]]() + for type in DiningVenue.VenueType.allCases { + venuesDict[type] = diningVenues.document.venues.filter({ $0.venueType == type }) + } + self.diningVenues = venuesDict + case .failure(let error): + self.alertType = error + self.diningVenuesIsLoading = false + } + } + } + } + + func refreshMenu(for id: Int) { + let lastRequest = UserDefaults.standard.getLastMenuRequest(id: id) + if lastRequest == nil || !lastRequest!.isToday { + DiningAPI.instance.fetchDiningMenu(for: id) { result in + switch result { + case .success(let diningMenu): + withAnimation { + self.diningMenus[id] = diningMenu + } + case .failure(let error): + self.alertType = error + } + } + } + } + + func refreshBalance() { + if (UserDefaults.standard.hasDiningPlan()) { + DiningAPI.instance.fetchDiningBalance { diningBalance in + if let diningBalance = diningBalance { + self.swipes = diningBalance.visits + self.diningDollars = Double(diningBalance.diningDollars) + } + } + } + } + + // MARK: - Insights + func refreshInsights() { + diningInsightsIsLoading = true + + DiningAPI.instance.fetchDiningInsights { (result) in + self.diningInsightsIsLoading = false + + switch result { + case .success(let diningInsights): + self.diningInsights = diningInsights + case .failure(let error): + self.alertType = error + self.diningInsights = nil + } + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/DiningView.swift b/PennMobile/Dining/SwiftUI/Views/DiningView.swift new file mode 100644 index 000000000..d72000110 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/DiningView.swift @@ -0,0 +1,32 @@ +// +// DiningView.swift +// PennMobile +// +// Created by CHOI Jongmin on 4/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +@available(iOS 14, *) +struct DiningView: View { + @StateObject var diningVM = DiningViewModelSwiftUI() + + var body: some View { + return + VStack(spacing: 0) { + DiningViewHeader() + .padding() + + DiningVenueView() + } + .environmentObject(diningVM) + } +} + +@available(iOS 14, *) +struct DiningView_Previews: PreviewProvider { + static var previews: some View { + DiningView() + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/DiningViewHeader.swift b/PennMobile/Dining/SwiftUI/Views/DiningViewHeader.swift new file mode 100644 index 000000000..0de08a678 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/DiningViewHeader.swift @@ -0,0 +1,93 @@ +// +// DiningViewHeader.swift +// PennMobile +// +// Created by CHOI Jongmin on 4/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct DiningViewHeader: View { + + @EnvironmentObject var diningVM: DiningViewModelSwiftUI + + var body: some View { + HStack { + DiningViewHeaderDate() + + Spacer() + + DiningViewHeaderBalance() + .environmentObject(diningVM) + } + } +} + +@available(iOS 14, *) +struct DiningViewHeaderDate: View { + var dateString: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE, MMMM d" + return dateFormatter.string(from: Date()).uppercased() + } + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text(dateString) + .font(.system(.caption)) + .fontWeight(.bold) + .foregroundColor(.gray) + + Text("Dining") + .font(.system(.title)) + .fontWeight(.bold) + } + } +} + +@available(iOS 14, *) +struct DiningViewHeaderBalance: View { + @EnvironmentObject var diningVM: DiningViewModelSwiftUI + + var body: some View { + VStack(alignment: .trailing, spacing: 5) { + Label("\(diningVM.swipes)", systemImage: "creditcard.fill") + .labelStyle(BalanceLabelStyle()) + + Label("\(String(format: "%.2f", diningVM.diningDollars))", systemImage: "dollarsign.circle.fill") + .labelStyle(BalanceLabelStyle()) + } + } +} + +@available(iOS 14, *) +struct BalanceLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.title + .font(.system(size: 17, weight: .bold, design: .rounded)) + configuration.icon + .frame(width:20, height: 20) + } + } +} + +@available(iOS 14, *) +struct DiningViewHeader_Previews: PreviewProvider { + static var previews: some View { + VStack { + Spacer() + HStack { + DiningViewHeaderDate() + .padding() + + Spacer() + } + Spacer() + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardHeaderView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardHeaderView.swift new file mode 100644 index 000000000..3ade7409a --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardHeaderView.swift @@ -0,0 +1,64 @@ +// +// CardHeaderView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/23/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct CardHeaderView: View { + + let color: Color + let icon: CardHeaderTitleView.IconType + let title: String + let subtitle: String + + var body: some View { + VStack(alignment: .leading) { + CardHeaderTitleView(color: color, icon: icon, title: title) + Text(subtitle) + .fontWeight(.medium) + } + } +} + +@available(iOS 14, *) +struct CardHeaderTitleView: View { + enum IconType { + case dollars, swipes, predictions + } + + let color: Color + let icon: IconType + let title: String + + private func imageName(for icon: IconType) -> String { + switch icon { + case .dollars: return "dollarsign.circle.fill" + case .swipes: return "creditcard.fill" + case .predictions: return "wand.and.rays" + } + } + + var body: some View { + HStack { + Image(systemName: imageName(for: icon)) + Text(title) + } + .font(Font.body.weight(.medium)) + .foregroundColor(color) + } +} + +@available(iOS 14, *) +struct CardHeaderView_Previews: PreviewProvider { + static var previews: some View { + CardHeaderView(color: .blue, icon: .predictions, title: "Predictions", subtitle: "These are your predictions! Pretty cool that they even wrap onto new lines.") + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardView.swift new file mode 100644 index 000000000..18adc4eee --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/Cards/CardView.swift @@ -0,0 +1,35 @@ +// +// CardView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/21/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct CardView : View where Content : View { + let content: () -> Content + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.uiCardBackground) + .shadow(color: Color.black.opacity(0.2), radius: 4, x: 2, y: 2) + self.content() + } + } +} + +@available(iOS 14, *) +struct CardView_Previews: PreviewProvider { + static var previews: some View { + CardView { + Text("Hello World") + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/DailyAverageView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/DailyAverageView.swift new file mode 100644 index 000000000..e98ff9fd3 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/DailyAverageView.swift @@ -0,0 +1,236 @@ +// +// AverageDiningPerDayView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/23/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct DailyAverageView: View { + + init(config: DiningInsightsAPIResponse.CardData.DailyAverageCardData) { + self.config = config + + self.maxSpent = max((config.data.thisWeek.min()?.average ?? 0.0) * -1, (config.data.lastWeek.min()?.average ?? 0.0) * -1) + if maxSpent == 0 { maxSpent = 1 } + + // Make a local copy of maxSpent (for init/compiler reasons) + let maxSpent = self.maxSpent + self.thisWeekDollarData = config.data.thisWeek.map({CGFloat(($0.average * -1) / maxSpent)}).reversed() + self.lastWeekDollarData = config.data.lastWeek.map({CGFloat(($0.average * -1) / maxSpent)}).reversed() + + let dayFormatter = DateFormatter() + dayFormatter.dateFormat = "EEEEE" + + dayOfWeek = config.data.thisWeek.map({ dayFormatter.string(from: $0.date) }).reversed() + } + + let config: DiningInsightsAPIResponse.CardData.DailyAverageCardData + @State private var selectedDataPoint: Int? = nil + + private var maxSpent: Double + private let thisWeekDollarData: [CGFloat] + private let lastWeekDollarData: [CGFloat] + + private let timeFrames = ["This Week", "Last Week"] + @State private var timeFrame = "This Week" + @State private var axisOffset: CGFloat = 0.0 + + private var data: [CGFloat] { + return timeFrame == "This Week" ? thisWeekDollarData : lastWeekDollarData + } + + private var dayOfWeek: [String] + + private var spacingForDollarData: CGFloat { + return self.data.count <= 7 ? 6 : (self.data.count <= 33 ? 2 : 0) + } + + private var averageDollar: CGFloat { + return (self.data.reduce(0, +) / CGFloat(self.data.count)) + } + + private var formattedAverage: String { + let spec = "%.2f" + return String(format: "$\(spec)", Double(averageDollar) * maxSpent) + } + + private var formattedAverageForDay: String { + let spec = "%.2f" + if selectedDataPoint == nil { + return String(format: "$\(spec)", Double(averageDollar) * maxSpent) + } else { + return String(format: "$\(spec)", Double(self.data[self.selectedDataPoint!]) * maxSpent) + } + } + + private var formattedDay: String { + if selectedDataPoint == nil { + return "Average" + } else { + let df = DateFormatter() + df.dateFormat = "EEEE M/d" + let date = timeFrame == "This Week" ? config.data.thisWeek.reversed()[selectedDataPoint!].date : + config.data.lastWeek.reversed()[selectedDataPoint!].date + return df.string(from: date) + } + } + + var body: some View { + VStack(alignment: .leading) { + // Top labels + Group { + CardHeaderTitleView(color: .green, icon: .dollars, title: "Daily Averages") + Text("Over the \(self.timeFrame == "This Week" ? "last 7 days" : "7 days before that"), you spent an average of \(formattedAverage) per day.") + .fontWeight(.medium) + Divider() + .padding([.top, .bottom]) + } + + // Graph view + Spacer() + ZStack { + HStack(alignment: .bottom) { + ZStack(alignment: .leading) { + Spacer() + .frame(width: 120.0, height: 110.0) + VStack(alignment: .leading) { + Text(self.selectedDataPoint == nil ? "Average" : formattedDay) .font(Font.caption.weight(.bold)).foregroundColor(self.selectedDataPoint == nil ? .gray : .yellow) + .offset(x: 0, y: self.axisOffset - 13) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(formattedAverageForDay) + .font(Font.system(.title, design: .rounded).bold()) + .offset(x: 0, y: self.axisOffset - 13) + Text("\(self.selectedDataPoint == nil ? "/ day" : "")").font(Font.caption.weight(.bold)).foregroundColor(.gray) + .offset(x: 0, y: self.axisOffset - 13) + + } + .padding(.top, 8) + } + .frame(height: 110) + + } + // Graph pillars and caption + HStack(alignment: .bottom, spacing: self.spacingForDollarData) { + ForEach(self.data.indices, id: \.self) { i in + VStack { + Spacer() + RoundedRectangle(cornerRadius: 4).frame(height: 110.0 * self.data[i]) + .foregroundColor( + self.selectedDataPoint == i ? Color.yellow : Color.gray.opacity(0.3)) + .onTapGesture { + if self.selectedDataPoint == i { + self.selectedDataPoint = nil + } else { + self.selectedDataPoint = i + } + withAnimation { + self.axisOffset = (self.selectedDataPoint == nil ? ((0.5 - self.averageDollar) * 110) : ((0.5 - self.data[self.selectedDataPoint!]) * 110)) + } + } + Text(self.dayOfWeek[i]) + .font(.caption) + .opacity(0.5) + } + } + } + .animation(.default) + } + GraphPath(data: [0.5, 0.5, 0.5]).stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round)) + .foregroundColor(self.selectedDataPoint == nil ? .green : Color.gray.opacity(0.5)) + .offset(x: 0, y: self.axisOffset - 5) + .animation(.default) + } + + // Footer + Picker("Pick a time frame", selection: self.$timeFrame.onChange({ _ in self.selectedDataPoint = nil})) { + ForEach(self.timeFrames, id: \.self) { time in + Text(time) + } + } + .pickerStyle(SegmentedPickerStyle()) + .onReceive([self.timeFrame].publisher.first()) { (output) in + withAnimation { + self.axisOffset = (self.selectedDataPoint == nil ? ((0.5 - self.averageDollar) * 110) : ((0.5 - self.data[self.selectedDataPoint!]) * 110)) + } + } +//// Fix for ad-hoc bindinding created +// .onChange(of: timeFrame) { +// self.selectedDataPoint = nil +// } + .padding(.top) + } + .frame(height: 318) + .padding() + } +} + +@available(iOS 14, *) +struct GraphPath: Shape, Animatable { + @State var data: [CGFloat] + + var animatableData: [CGFloat] { + get { return data } + set { data = newValue } + } + + func path(in rect: CGRect) -> Path { + var path = Path() + + guard data.count > 2 else { return path } + + func point(at n: Int) -> CGPoint { + return CGPoint(x: CGFloat(n) * (rect.maxX / CGFloat(data.count - 1)), y: rect.maxY - (rect.maxY * data[n])) + } + + path.move(to: point(at: 0)) + + for i in 1 ..< data.count { + path.addLine(to: point(at: i)) + } + + return path + } +} + + +@available(iOS 14, *) +struct DailyAverageView_Previews: PreviewProvider { + + static let path = Bundle.main.path(forResource: "example-dining-stats", ofType: "json") + static let data = try! Data(contentsOf: URL(fileURLWithPath: path!), options: .mappedIfSafe) + static var decoder : JSONDecoder { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + } + + static let diningInsights = try! decoder.decode(DiningInsightsAPIResponse.self, from: data) + + static var previews: some View { + CardView { DailyAverageView(config: diningInsights.cards.dailyAverage!) } + + } +} + + +// Ad-hoc solution to onChange effect of State +// Will be replaced using .onChange modifier for state +@available(iOS 14, *) +extension Binding { + func onChange(_ handler: @escaping (Value) -> Void) -> Binding { + return Binding( + get: { self.wrappedValue }, + set: { selection in + self.wrappedValue = selection + handler(selection) + }) + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/DiningBalanceView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/DiningBalanceView.swift new file mode 100644 index 000000000..cf4366e65 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/DiningBalanceView.swift @@ -0,0 +1,78 @@ +// +// DiningBalanceView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/21/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14.0, *) +struct DiningBalanceView: View { + let description: String + let image: Image + var balance: Double + + // By default, remove trailing zeros + var specifier: String = "%g" + var color: Color = .blue + + var formattedBalance: String { + let b: Double = balance + return String(format: "\(self.specifier)", b) + } + + var body: some View { + CardView { + VStack(alignment: .trailing) { + HStack { + self.image.font(Font.system(size: 24).weight(.bold)) + Spacer() + Text(self.formattedBalance) + .font(.system(size: 24, design: .rounded)) + .fontWeight(.bold) + } + .foregroundColor(self.color) + Text(self.description) + .font(.subheadline) + .opacity(0.5) + } + .padding() + } + } +} + +@available(iOS 14.0, *) +struct BlankDiningBalanceView: View { + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.gray.opacity(0.0)) + .shadow(color: Color.black.opacity(0.0), radius: 4, x: 2, y: 2) + VStack(alignment: .trailing) { + HStack { + Image(systemName:" ").font(Font.system(size: 24).weight(.bold)) + Spacer() + Text(" ") + .font(.system(size: 24, design: .rounded)) + .fontWeight(.bold) + } + Text(" ") + .font(.subheadline) + .opacity(0.5) + } + .padding() + } + } +} + +@available(iOS 14.0, *) +struct DiningBalanceView_Previews: PreviewProvider { + static var previews: some View { + DiningBalanceView(description: "Dining Dollars", image: Image(systemName: "dollarsign.circle.fill"), balance: 427.84, specifier: "%.2f") + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/DiningInsightsView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/DiningInsightsView.swift new file mode 100644 index 000000000..fd88c97c0 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/DiningInsightsView.swift @@ -0,0 +1,104 @@ +// +// DiningInsightsView.swift +// PennMobile +// +// Created by CHOI Jongmin on 9/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct DiningInsightsView: View { + + @EnvironmentObject var diningVM: DiningViewModelSwiftUI + + @Binding var pickerIndex : Int + @State var isPresentingLoginSheet = false + @State var loginFailure = false + + var body: some View { + GeometryReader { geo in + ZStack { + ScrollView { + VStack { + VStack { + HStack { + if self.diningVM.diningInsights?.diningDollars != nil { + DiningBalanceView(description: "Dining Dollars", image: Image(systemName: "dollarsign.circle.fill"), balance: self.diningVM.diningInsights!.diningDollars!, specifier: "%.2f", color: .green) + } + + if self.diningVM.diningInsights?.swipes != nil { + DiningBalanceView(description: "Swipes", image: Image(systemName: "creditcard.fill"), balance: Double(self.diningVM.diningInsights!.swipes!), specifier: "%g", color: .blue) + } + }.padding(.bottom) + + HStack { + if self.diningVM.diningInsights?.guestSwipes != nil { + DiningBalanceView(description: "Guest Swipes", image: Image(systemName: "creditcard.fill"), balance: Double(self.diningVM.diningInsights!.guestSwipes!), specifier: "%g", color: .purple) + } + + BlankDiningBalanceView() + }.padding(.bottom) + } + + + if self.diningVM.diningInsights?.cards.predictionsGraphSwipes != nil { + CardView { PredictionsGraphView(config: self.diningVM.diningInsights!.cards.predictionsGraphSwipes!) } + .padding(.bottom) + } + + if self.diningVM.diningInsights?.cards.predictionsGraphDollars != nil { + CardView { PredictionsGraphView(config: self.diningVM.diningInsights!.cards.predictionsGraphDollars!) } + .padding(.bottom) + } + + if self.diningVM.diningInsights?.cards.recentTransactions != nil { + CardView { RecentTransactionsView(config: self.diningVM.diningInsights!.cards.recentTransactions!) } + .padding(.bottom) + } + + if self.diningVM.diningInsights?.cards.frequentLocations != nil { + CardView { FrequentLocationsView(config: self.diningVM.diningInsights!.cards.frequentLocations!) } + .padding(.bottom) + } + + if self.diningVM.diningInsights?.cards.dailyAverage != nil { + CardView { DailyAverageView(config: self.diningVM.diningInsights!.cards.dailyAverage!) } + .padding(.bottom) + } + + } + .padding() + .frame(width: geo.size.width) + } + .onAppear(perform: { + diningVM.refreshInsights() + }) + .alert(isPresented: .constant(!Account.isLoggedIn)) { + Alert(title: Text("Login Error"), message: Text("Please Login to Use this Feature"), + primaryButton: .default(Text("Login")) { + self.isPresentingLoginSheet.toggle() + }, secondaryButton: .cancel{ + self.pickerIndex = 0 + }) + } + .sheet(isPresented: self.$isPresentingLoginSheet) { + LabsLoginControllerSwiftUI(isShowing: self.$isPresentingLoginSheet, loginFailure: self.$loginFailure, handleError: { self.pickerIndex = 0 }) + .environmentObject(diningVM) + } + + + // Ad-hoc method to adding acitivity indicator + // Will be replaced using Progress View after iOS 14 release + ActivityIndicatorView(animating: self.$diningVM.diningInsightsIsLoading, style: .large) + .alert(isPresented: self.$loginFailure) { + Alert(title: Text("Login Failure"), message: Text("Login failed please try again later"), dismissButton: .default(Text("Do something"))) + } + } + + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/FrequentLocationsView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/FrequentLocationsView.swift new file mode 100644 index 000000000..22e4f7119 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/FrequentLocationsView.swift @@ -0,0 +1,145 @@ +// +// FrequentLocationsView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/21/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct FrequentLocationsView: View { + + enum LengthOfTime: Int, CaseIterable { + case week, month, semester + } + + init(config: DiningInsightsAPIResponse.CardData.FrequentLocationsCardData) { + data = config.data + _portions = State(initialValue: FrequentLocationsView.computeTotal(with: config.data, for: 0)) + } + + private var data: [FrequentLocation] + @State private var portions: [Double] + + @State private var colors: [Color] = [.orange, .yellow, .green, .blue, .pink, .purple, .red, .orange, .yellow, .green, .blue, .pink, .purple, .red, .orange, .yellow, .green, .blue, .pink, .purple, .red] + @State private var lengthOfTime: Int = 0 + + static func computeTotal(with data: [FrequentLocation], for lengthOfTime: Int) -> [Double] { + var sum = data.reduce(0.0) { (result, freq) -> Double in + result + spending(at: freq, in: lengthOfTime) + } + + if sum == 0 { + sum = 1 + } + + var values = [Double]() + for freq in data { + values.append(spending(at: freq, in: lengthOfTime) / sum) + } + return values + } + + private static func spending(at location: FrequentLocation, in timeLength: Int) -> Double { + spending(at: location, in: LengthOfTime(rawValue: timeLength) ?? .week) + } + + private static func spending(at location: FrequentLocation, in timeLength: LengthOfTime) -> Double { + switch timeLength { + case .week: return location.week + case .month: return location.month + case .semester: return location.semester + } + } + + private static func formattedSpending(at location: FrequentLocation, in timeLength: Int) -> String { + let s = spending(at: location, in: LengthOfTime(rawValue: timeLength) ?? .week) + return String(format: "$%.2f", s) + } + + var body: some View { + VStack(alignment: .leading) { + Group { + CardHeaderTitleView(color: .green, icon: .dollars, title: "Frequent Locations") + Text("Your dining dollar totals for each location over the last \(["week", "month", "semester"][lengthOfTime]).") + .fontWeight(.medium) + .lineLimit(nil) + .frame(height: 44) + } + + Divider() + .padding([.top, .bottom]) + + PortionView(portions: self.$portions, colors: self.$colors) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + .frame(height: 20) + .padding(.bottom) + + VStack(alignment: .leading) { + ForEach(self.data.indices, id: \.self) { index in + HStack { + Image(systemName: "circle.fill") + .foregroundColor(self.colors[index]) + Text(self.data[index].location) + Spacer() + Text(FrequentLocationsView.formattedSpending(at: self.data[index], in: self.lengthOfTime)) + } + } + } + + Picker("Pick a time frame", selection: $lengthOfTime) { + ForEach(0...2, id: \.self) { index in + Text(["This Week", "This Month", "This Semester"][index]) + } + } + .labelsHidden() + .pickerStyle(SegmentedPickerStyle()) + .padding(.top) + .onReceive([self.lengthOfTime].publisher.first()) { (output) in + withAnimation { + self.portions = FrequentLocationsView.computeTotal(with: self.data, for: self.lengthOfTime) + } + } + }.padding() + } + + struct PortionView: View { + @Binding var portions: [Double] + @Binding var colors: [Color] + + var body: some View { + GeometryReader { geometry in + HStack(spacing: 0) { + ForEach(self.portions.indices, id: \.self) { + Rectangle() + .frame(width: geometry.size.width * CGFloat(self.portions[$0])) + .foregroundColor(self.colors[$0]) + } + } + } + } + } +} + +@available(iOS 14.0, *) +struct FrequentLocationsView_Previews: PreviewProvider { + static let path = Bundle.main.path(forResource: "example-dining-stats", ofType: "json") + static let data = try! Data(contentsOf: URL(fileURLWithPath: path!), options: .mappedIfSafe) + static var decoder : JSONDecoder { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + } + + static let diningInsights = try! decoder.decode(DiningInsightsAPIResponse.self, from: data) + + static var previews: some View { + CardView { FrequentLocationsView(config: diningInsights.cards.frequentLocations!) } + + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+AxisLabels.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+AxisLabels.swift new file mode 100644 index 000000000..1fabc5075 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+AxisLabels.swift @@ -0,0 +1,48 @@ +// +// PredictionsGraphView+AxisLabels.swift +// PennMobile +// +// Created by Dominic Holmes on 2/29/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +extension PredictionsGraphView { + + static let xAxisLabelCount = 4 + static let yAxisLabelCount = 5 + + // Compute axis labels + static func getAxisLabelsYX(from trans: [DiningInsightsAPIResponse.CardData.PredictionsGraphCardData.DiningBalance], startOfSemester sos: Date, endOfSemester eos: Date) -> ([String], [String]) { + + var xLabels: [String] = [] + var yLabels: [String] = [] + + guard sos < eos else { return ([" "],[" "]) } + + // Generate Y Axis Labels + let maxDollarValue = trans.max(by: { $0.balance < $1.balance })?.balance ?? 1.0 + let dollarStep = (maxDollarValue / Double(yAxisLabelCount - 1)) + for i in 0 ..< yAxisLabelCount { + let yAxisLabel = "\(Int(dollarStep * Double(yAxisLabelCount - i - 1)))" + yLabels.append(yAxisLabel) + } + + // Generate X Axis Labels + let semester = sos.distance(to: eos) + let semesterStep = semester / Double(xAxisLabelCount - 1) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "M/d" + for i in 0 ..< xAxisLabelCount { + let dateForLabel = sos.advanced(by: semesterStep * Double(i)) + xLabels.append(dateFormatter.string(from: dateForLabel)) + } + + return (yLabels, xLabels) + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+SmoothedData.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+SmoothedData.swift new file mode 100644 index 000000000..46fc12e5e --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView+SmoothedData.swift @@ -0,0 +1,44 @@ +// +// PredictionsGraphView+SmoothedData.swift +// PennMobile +// +// Created by Dominic Holmes on 2/28/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +extension PredictionsGraphView { + + // Compute graph data + static func getSmoothedData(from trans: [DiningInsightsAPIResponse.CardData.PredictionsGraphCardData.DiningBalance], startOfSemester sos: Date, endOfSemester eos: Date) -> [YXDataPoint] { + + guard sos < eos else { return [] } + + let totalLength = eos.distance(to: sos) + let maxDollarValue = trans.max(by: { $0.balance < $1.balance })?.balance ?? 1.0 + let yxPoints: [YXDataPoint] = trans.map { (t) -> YXDataPoint in + let xPoint = t.date.distance(to: sos) / totalLength + return YXDataPoint(y: CGFloat(t.balance / maxDollarValue), x: CGFloat(xPoint)) + } + return yxPoints + } + + static func getPredictionZeroPoint(from trans: [DiningInsightsAPIResponse.CardData.PredictionsGraphCardData.DiningBalance], startOfSemester sos: Date, endOfSemester eos: Date, predictedZeroDate zpd: Date) -> PredictionsGraphView.YXDataPoint { + + guard sos < eos else { return .init(y: 0.0, x: 0.0) } + + let fullSemester = sos.distance(to: eos) + let fullZeroDistance = sos.distance(to: zpd) + + // x value, may be > 1 if the zero date is past end of semester + let x = (fullZeroDistance / fullSemester) + + // y value is always zero + return .init(y: 0.0, x: CGFloat(x)) + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView.swift new file mode 100644 index 000000000..531a3eac0 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/PredictionsGraphView.swift @@ -0,0 +1,117 @@ +// +// PredictionsGraphView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/28/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +// TODO: - Move these protocols to Protocols.swift +protocol ClampableRange { + associatedtype Bound : Comparable + var upperBound: Bound { get } + var lowerBound: Bound { get } +} +extension ClampableRange { + func clamp(_ value: Bound) -> Bound { + return min(max(lowerBound, value), upperBound) + } +} +extension Range: ClampableRange {} +extension ClosedRange: ClampableRange {} +// END TODO + +//VariableStepLineGraphView.getSmoothedData(from: DiningTransaction.sampleData) +@available(iOS 14, *) +struct PredictionsGraphView: View { + + init(config: DiningInsightsAPIResponse.CardData.PredictionsGraphCardData) { + + self.config = config + data = PredictionsGraphView.getSmoothedData(from: config.data, startOfSemester: config.startOfSemester, endOfSemester: config.endOfSemester) + + axisLabelsYX = PredictionsGraphView.getAxisLabelsYX(from: config.data, startOfSemester: config.startOfSemester, endOfSemester: config.endOfSemester) + + balanceType = config.type.contains("swipes") ? .swipes : .dollars + + predictedZeroPoint = PredictionsGraphView.getPredictionZeroPoint(from: config.data, startOfSemester: config.startOfSemester, endOfSemester: config.endOfSemester, predictedZeroDate: config.predictedZeroDate) + } + + struct YXDataPoint { + var y: CGFloat // Bound between 0 and 1 + var x: CGFloat // Bound between 0 and 1 + } + + var config: DiningInsightsAPIResponse.CardData.PredictionsGraphCardData + var data: [YXDataPoint] + var axisLabelsYX: ([String], [String]) + var balanceType: DiningInsightsAPIResponse.CardData.PredictionsGraphCardData.BalanceType + var predictedZeroPoint: YXDataPoint + + var formattedZeroDate: String { + let formatter = DateFormatter() + formatter.dateFormat = "MMM. d" + return formatter.string(from: self.config.predictedZeroDate) + } + + var displayZeroDate: Bool { + if config.predictedZeroDate > config.endOfSemester && config.semesterEndBalance != nil { + return false + } + return true + } + + var formattedBalance: String { + let b: Double = config.semesterEndBalance ?? 0 + return String(format: balanceType == .swipes ? "%g" : "%.2f", b) + } + + var helpText: String { + if displayZeroDate { + return "Based on your current balance and past behavior, we project you'll run out on this date." + } else { + return "Based on your past behavior, we project you'll end the semester with \(balanceType == .swipes ? "swipes" : "dollars") to spare." + } + } + + var body: some View { + VStack(alignment: .leading) { + Group { + CardHeaderTitleView(color: balanceType == .swipes ? .blue : .green, icon: .predictions, title: "\(balanceType == .swipes ? "Swipes" : "Dining Dollars") Predictions") + Text("Log into Penn Mobile often to get more accurate predictions.") + .fontWeight(.medium) + .lineLimit(nil) + .frame(height: 44) + } + Divider() + .padding([.top, .bottom]) + VariableStepLineGraphView(data: self.data, lastPointPosition: self.data.last?.x ?? 0, xAxisLabels: axisLabelsYX.1, yAxisLabels: axisLabelsYX.0, lineColor: balanceType == .swipes ? .blue : .green, predictedZeroPoint: self.predictedZeroPoint) + Divider() + .padding([.top, .bottom]) + + HStack { + VStack(alignment: .leading) { + // "Leftover" Dollars, wasted dollars + Text(displayZeroDate ? ("Out of \(balanceType == .swipes ? "Swipes" : "Dollars")") : "Extra Balance") + .font(.caption) + Text(displayZeroDate ? "\(self.formattedZeroDate)" : "\(formattedBalance)\(balanceType == .swipes ? " Swipes" : " Dollars")") + .font(Font.system(size: 21, weight: .bold, design: .rounded)) + Spacer() + } + .padding(.trailing) + VStack { + Text(helpText) + .font(.caption) + .foregroundColor(.gray) + Spacer() + } + }.frame(height: 60) + } + .padding() + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+GraphEndpointPath.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+GraphEndpointPath.swift new file mode 100644 index 000000000..d48ec7553 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+GraphEndpointPath.swift @@ -0,0 +1,37 @@ +// +// VariableStepLineGraphView+GraphEndpointPath.swift +// PennMobile +// +// Created by Dominic Holmes on 2/28/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +extension VariableStepLineGraphView { + + struct GraphEndpointPath: Shape { + // The x value of the path (between 0 and 1) + @State var x: CGFloat + + func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint( + x: x * rect.maxX, + y: rect.maxY + )) + + path.addLine(to: CGPoint( + x: x * rect.maxX, + y: rect.minY + )) + + return path + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+PredictionSlopePath.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+PredictionSlopePath.swift new file mode 100644 index 000000000..2a133397f --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+PredictionSlopePath.swift @@ -0,0 +1,45 @@ +// +// VariableStepLineGraphView+PredictionSlopePath.swift +// PennMobile +// +// Created by Dominic Holmes on 2/28/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +extension VariableStepLineGraphView { + + struct PredictionSlopePath: Shape, Animatable { + // This should be the last data point before prediction line begins + @State var lastDataPoint: PredictionsGraphView.YXDataPoint + + // Calculated day balance will reach 0, computed by the server. X may exceed 1.0 if the zero date is past the end of the semester + @State var predictionZeroPoint: PredictionsGraphView.YXDataPoint + + var animatableData: PredictionsGraphView.YXDataPoint { + get { return lastDataPoint } + set { lastDataPoint = newValue } + } + + func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint( + x: lastDataPoint.x * rect.maxX, + y: rect.maxY - (rect.maxY * lastDataPoint.y) + )) + + path.addLine(to: CGPoint( + x: predictionZeroPoint.x * rect.maxX, + y: rect.maxY - (rect.maxY * predictionZeroPoint.y) + )) + + return path + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+VariableStepGraphPath.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+VariableStepGraphPath.swift new file mode 100644 index 000000000..746117d7d --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView+VariableStepGraphPath.swift @@ -0,0 +1,44 @@ +// +// PredictionsGraph+VariableStepGraph.swift +// PennMobile +// +// Created by Dominic Holmes on 2/28/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +extension VariableStepLineGraphView { + struct VariableStepGraphPath: Shape, Animatable { + @State var data: [PredictionsGraphView.YXDataPoint] + + var animatableData: [PredictionsGraphView.YXDataPoint] { + get { return data } + set { data = newValue } + } + + func path(in rect: CGRect) -> Path { + var path = Path() + + guard data.count > 2 else { return path } + + func point(at n: Int) -> CGPoint { + return CGPoint( + x: data[n].x * rect.maxX, + y: rect.maxY - (rect.maxY * data[n].y)) + } + + path.move(to: point(at: 0)) + + for i in 1 ..< data.count { + path.addLine(to: point(at: i)) + } + + return path + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView.swift new file mode 100644 index 000000000..0eaa5fab8 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/PredictionsGraph/VariableStepLineGraphView.swift @@ -0,0 +1,111 @@ +// +// VariableStepLineGraphView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/28/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct VariableStepLineGraphView: View { + + private let graphHeight: CGFloat = 160.0 + + @Environment(\.colorScheme) private var colorScheme: ColorScheme + @State private var trimEnd: CGFloat = 0.0 + @GestureState private var dragActive = false + var data: [PredictionsGraphView.YXDataPoint] + var lastPointPosition: CGFloat = 0.0 + var xAxisLabels: [String] + var yAxisLabels: [String] + var lineColor: Color + var predictedZeroPoint: PredictionsGraphView.YXDataPoint + + var body: some View { + VStack(alignment: .leading) { + Spacer() + .frame(height: 20) + + HStack { + // Y-Axis labels + VStack(alignment: .leading) { + ForEach(0 ..< yAxisLabels.count) { num in + if num != 0 { Spacer().frame(width: 40) } + Text(self.yAxisLabels[num]) + .font(.subheadline) + .opacity(0.5) + } + } + .frame(width: 40, height: self.graphHeight) + + GeometryReader { geometry in + ZStack { + + VariableStepGraphPath(data: self.data).trim(from: 0, to: self.trimEnd).stroke( + style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round) + ) + .foregroundColor(self.lineColor) + .frame(height: self.graphHeight) + .animation(.default) + .onAppear { + self.trimEnd = 1.0 + } + + + PredictionSlopePath(lastDataPoint: self.data.last!, predictionZeroPoint: self.predictedZeroPoint).stroke( + style: StrokeStyle(lineWidth: 2.0, lineCap: .round, lineJoin: .round, dash: [5], dashPhase: 5) + ) + .foregroundColor(.gray) + .frame(height: self.graphHeight) + .animation(.default) + .onAppear { + self.trimEnd = 1.0 + } + .clipped() + + Group { + Group { + HStack(alignment: .center) { + Spacer() + Text("End of Term") + Image(systemName: "circle.fill") + } + .foregroundColor(.red) + .font(.caption) + } + .frame(width: 140) + .offset(x: -70 + 5.5 + ((1.0 - 0.5) * geometry.size.width), y: -6 - geometry.size.height/2) + + GraphEndpointPath(x: 1.0).stroke( + style: StrokeStyle(lineWidth: 2.0, lineCap: .round, lineJoin: .round) + ) + .foregroundColor(.red) + .frame(height: self.graphHeight) + } + } + } + .frame(height: self.graphHeight) + + Spacer() + .frame(width: 10) + } + // X-Axis labels + HStack { + Spacer() + .frame(width: 40) + ForEach(0 ..< xAxisLabels.count) { num in + if num != 0 { Spacer() } + Text(self.xAxisLabels[num]) + .font(.subheadline) + .opacity(0.5) + } + } + .frame(height: 20) + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Insights/RecentTransactionsView.swift b/PennMobile/Dining/SwiftUI/Views/Insights/RecentTransactionsView.swift new file mode 100644 index 000000000..1a9643468 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Insights/RecentTransactionsView.swift @@ -0,0 +1,70 @@ +// +// RecentTransactionsView.swift +// PennMobile +// +// Created by Dominic Holmes on 2/21/20. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import Foundation +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct RecentTransactionsView: View { + + init(config: DiningInsightsAPIResponse.CardData.RecentTransactionsCardData) { +// self.config = config + data = config.data + } + +// let config: DiningInsightsAPIResponse.CardData.RecentTransactionsCardData + var data: [DiningTransaction] + + var body: some View { + VStack(alignment: .leading) { + + CardHeaderView(color: .green, icon: .dollars, title: "Transactions", subtitle: "Your recent dining dollar transactions.") + + Divider() + .padding(.top) + + VStack { + ForEach(self.data, id: \.self) { trans in + VStack { + RecentTransactionsViewRow(transaction: trans) + Divider() + } + } + } + }.padding() + } + + struct RecentTransactionsViewRow: View { + + var transaction: DiningTransaction + + var body: some View { + HStack { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 10, height: 10) + .foregroundColor(.orange) + VStack(alignment: .leading) { + Text(transaction.location) + Text(transaction.formattedDate) + .font(.caption).foregroundColor(.gray) + } + Spacer() + VStack(alignment: .trailing) { + Text(transaction.formattedAmount) + .fontWeight(.medium) + .foregroundColor(transaction.amount > 0 ? .green : .red) + Text(transaction.formattedBalance) + .font(.caption).foregroundColor(.gray) + } + } + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailHoursView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailHoursView.swift new file mode 100644 index 000000000..02ae8a0fd --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailHoursView.swift @@ -0,0 +1,57 @@ +// +// DiningVenueDetailHoursView.swift +// PennMobile +// +// Created by CHOI Jongmin on 23/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +@available(iOS 14, *) +struct DiningVenueDetailHoursView: View { + + init(for venue: DiningVenue) { + self.venue = venue + } + + let venue: DiningVenue + + var body: some View { + // TODO: Add level of business using public APIs Penn Dining will provide + VStack(alignment: .leading, spacing: 7) { + ForEach(0..<7) { duration in + let dateInt = (7 - Date().integerDayOfWeek + duration) % 7 + let date = Date().dateIn(days: dateInt) + + Text("\(date.dayOfWeek)") + .font(duration == Date().integerDayOfWeek ? .system(size: 18, weight: .bold): .system(size: 18, weight: .regular)) + + HStack { + ForEach(venue.formattedHoursArrayFor(date), id: \.self) { hours in + Text(hours) + .padding(.vertical, 3) + .padding(.horizontal, 4) + .font(.system(size: 14, weight: .light, design: .default)) + .background(Color.grey5) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + + Spacer() + }.offset(y: -4) + } + } + } +} + +@available(iOS 14, *) +struct DiningVenueDetailHoursView_Previews: PreviewProvider { + static var previews: some View { + let path = Bundle.main.path(forResource: "sample-dining-venue", ofType: "json") + let data = try! Data(contentsOf: URL(fileURLWithPath: path!), options: .mappedIfSafe) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let diningVenues = try! decoder.decode(DiningAPIResponse.self, from: data) + return DiningVenueDetailHoursView(for: diningVenues.document.venues[0]).padding() + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailLocationView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailLocationView.swift new file mode 100644 index 000000000..1d7ecdc14 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailLocationView.swift @@ -0,0 +1,43 @@ +// +// DiningVenueDetailLocationView.swift +// PennMobile +// +// Created by CHOI Jongmin on 23/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI +import MapKit + +@available(iOS 14.0, *) +struct DiningVenueDetailLocationView: View { + + @State private var region: MKCoordinateRegion + let venue: DiningVenue + let mapHeight: CGFloat + + init(for venue: DiningVenue, screenHeight: CGFloat) { + self.venue = venue + mapHeight = screenHeight - 20 + _region = .init(initialValue: PennCoordinate.shared.getRegion(for: venue, at: .mid)) + } + + var body: some View { + Map(coordinateRegion: $region, annotationItems: [venue]) { venue in + MapMarker(coordinate: PennCoordinate.shared.getCoordinates(for: venue)) + }.clipShape(RoundedRectangle(cornerRadius: /*@START_MENU_TOKEN@*/25.0/*@END_MENU_TOKEN@*/)) + .frame(height: mapHeight) + } +} + +@available(iOS 14.0, *) +struct DiningVenueDetailLocationView_Previews: PreviewProvider { + static var previews: some View { + let path = Bundle.main.path(forResource: "sample-dining-venue", ofType: "json") + let data = try! Data(contentsOf: URL(fileURLWithPath: path!), options: .mappedIfSafe) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let diningVenues = try! decoder.decode(DiningAPIResponse.self, from: data) + return DiningVenueDetailLocationView(for: diningVenues.document.venues[0], screenHeight: 600).padding(.horizontal, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/) + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailMenuView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailMenuView.swift new file mode 100644 index 000000000..50df8de11 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailMenuView.swift @@ -0,0 +1,38 @@ +// +// DiningVenueDetailMenuView.swift +// PennMobile +// +// Created by CHOI Jongmin on 23/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct DiningVenueDetailMenuView: View { + var menus: [DiningMenu] + + var body: some View { + ForEach(menus, id: \.self) { menu in + DiningMenuRow(for: menu) + .transition(.opacity) + } + } +} + +@available(iOS 14.0, *) +struct DiningVenueDetailMenuView_Previews: PreviewProvider { + let diningVenues: DiningMenuAPIResponse = Bundle.main.decode("mock_menu.json") + + static var previews: some View { + return NavigationView { + ScrollView { + VStack { + DiningVenueDetailMenuView(menus: []) + Spacer() + } + }.navigationTitle("Dining") + .padding() + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift new file mode 100644 index 000000000..e5a630646 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift @@ -0,0 +1,242 @@ +// +// DiningVenueDetailView.swift +// PennMobile +// +// Created by CHOI Jongmin on 6/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +#if canImport(SwiftUI) +import SwiftUI +import KingfisherSwiftUI +#endif + +@available(iOS 14, *) +struct DiningVenueDetailView: View { + + let safeFrameHeight: CGFloat + let customNavBarHeight: CGFloat + + init(for venue: DiningVenue) { + self.venue = venue + + let window = UIApplication.shared.windows[0] + safeFrameHeight = window.safeAreaLayoutGuide.layoutFrame.minY + customNavBarHeight = 44 + safeFrameHeight + } + + private let venue: DiningVenue + private let sectionTitle = ["Menu", "Hours", "Location"] + + @Environment(\.presentationMode) var presentationMode: Binding + @EnvironmentObject var diningVM: DiningViewModelSwiftUI + @State private var pickerIndex = 0 + + @State private var headerImageHeight: CGFloat = 300 + + var body: some View { + GeometryReader { fullGeo in + ScrollView { + image + .zIndex(2) + + Group { + Picker("Section", selection: self.$pickerIndex) { + ForEach(0 ..< self.sectionTitle.count) { + Text(self.sectionTitle[$0]) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.top, 5) + + Divider() + .padding(.vertical, 5) + + VStack { + if self.pickerIndex == 0 { + DiningVenueDetailMenuView(menus: diningVM.diningMenus[venue.id]?.document.menuDocument.menus ?? []) + } else if self.pickerIndex == 1 { + DiningVenueDetailHoursView(for: venue) + } else { + DiningVenueDetailLocationView(for: venue, screenHeight: fullGeo.size.width) + } + + Spacer() + }.frame(minHeight: fullGeo.size.height - 80) + }.padding(.horizontal) + + } + .edgesIgnoringSafeArea(.top) + .navigationBarHidden(true) + .onAppear(perform: { + diningVM.refreshMenu(for: venue.id) + headerImageHeight = fullGeo.frame(in: .global).height * 4/9 + }) + } + } + + var image : some View { + GeometryReader { geometry in + ZStack(alignment: .bottomLeading) { + KFImage(self.venue.imageURL) + .resizable() + .scaledToFill() + .frame(width: geometry.size.width, height: getHeightForHeaderImage(geometry)) + .offset(x: 0, y: getParallaxOffset(geometry)) + .clipped() + .overlay(LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .center, endPoint: .bottom)) + .allowsHitTesting(false) + + VStack(alignment: .leading) { + Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Image(systemName: "chevron.left") + .font(.system(size: 20, weight: .light)) + } + .foregroundColor(.white) + .padding(10) + .background(Circle().opacity(0.8).foregroundColor(.black)) + .opacity(getOpacity(geometry)) + .position(x: 40, y: customNavBarHeight - 20) + .offset(x: 0, y: getBackButtonYOffset(geometry)) + + Spacer() + + Text(venue.name) + .padding() + .foregroundColor(.white) + .font(.system(size: 40, weight: .bold)) + .minimumScaleFactor(0.2) + .lineLimit(1) + .opacity(getOpacity(geometry)) + + } + + DefaultNavigationBar(presentationMode: _presentationMode, height: customNavBarHeight, width: geometry.size.width, title: venue.name) + .offset(x:0, y:getOffsetForNavBar(geometry)) + .opacity(getOpacityForNavBar(geometry)) + } + .offset(x: 0, y: getOffsetForHeaderImage(geometry)) + } + .frame(height: headerImageHeight) + } +} + + +// MARK: - Calculations for offsets + opacity +@available(iOS 14, *) +extension DiningVenueDetailView { + + private func getBackButtonYOffset(_ geometry: GeometryProxy) -> CGFloat { + let offset = getOffset(geometry) + + return offset < 0 ? -offset : 0 + } + + private func getOffset(_ geometry: GeometryProxy) -> CGFloat { + return geometry.frame(in: .global).minY + } + + private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { + let offset = getOffset(geometry) + + return offset > 0 ? -offset : 0 + } + + private func getParallaxOffset(_ geometry: GeometryProxy) -> CGFloat { + let offset = getOffset(geometry) + + return offset < 0 ? -offset/1.3 : 0 + } + + private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { + let offset = getOffset(geometry) + + return offset > 0 ? headerImageHeight + offset : headerImageHeight + } + + private func getOpacity(_ geometry: GeometryProxy) -> Double { + let offset = getOffset(geometry) + + return offset > 0 ? Double(1 - offset/headerImageHeight * 4) : 1.0 + } + + private func getOffsetForNavBar(_ geometry: GeometryProxy) -> CGFloat { + return -getOffset(geometry) - headerImageHeight + customNavBarHeight + } + + private func getOpacityForNavBar(_ geometry: GeometryProxy) -> Double { + let offset = getOffset(geometry) + + if -offset > 0.6 * headerImageHeight { + return Double((-offset/headerImageHeight - 0.6) * 8) + } + + return 0.0 + } +} + +@available(iOS 14.0, *) +struct DefaultNavigationBar: View { + + @Environment(\.presentationMode) var presentationMode: Binding + + var height: CGFloat + var width: CGFloat + var title: String + + var body: some View { + ZStack(alignment: .bottom) { + VisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) + + VStack { + Spacer() + + HStack { + Button("Back") { + self.presentationMode.wrappedValue.dismiss() + } + .frame(width: 70, height: 44) + .contentShape(Rectangle()) + + Spacer() + } + } + + VStack { + Spacer() + Text(title) + .frame(height: 44) + } + } + .frame(width: width, height: height) + } +} + +@available(iOS 14, *) +struct DiningVenueDetailView_Previews: PreviewProvider { + static var previews: some View { + let path = Bundle.main.path(forResource: "sample-dining-venue", ofType: "json") + let data = try! Data(contentsOf: URL(fileURLWithPath: path!), options: .mappedIfSafe) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let diningVenues = try! decoder.decode(DiningAPIResponse.self, from: data) + + return + NavigationView { + DiningVenueDetailView(for: diningVenues.document.venues[0]) + .preferredColorScheme(.dark) + .environmentObject(DiningViewModelSwiftUI()) + } + } +} + +// Hack to enable swipe from left while disabling navigation title +// TODO: find a more natural fix in future releases +extension UINavigationController { + override open func viewDidLoad() { + super.viewDidLoad() + interactivePopGestureRecognizer?.delegate = nil + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift new file mode 100644 index 000000000..899aeaf51 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift @@ -0,0 +1,164 @@ +// +// MenuDisclosureGroup.swift +// PennMobile +// +// Created by CHOI Jongmin on 18/1/2021. +// Copyright © 2021 PennLabs. All rights reserved. +// + +import SwiftUI + +struct DiningMenuSectionRow: View { + @Binding var isExpanded: Bool + let title: String + + init(isExpanded: Binding, title: String) { + self.title = title.capitalizeMainWords() + self._isExpanded = isExpanded + } + + var body: some View { + HStack { + Text(title) + + Spacer() + + Image(systemName: "chevron.right.circle") + .rotationEffect(.degrees(isExpanded ? -90 : 90)) + .frame(width:28, alignment: .center) + } + .contentShape(Rectangle()) + .padding(.bottom) + .onTapGesture { + withAnimation { + isExpanded.toggle() + } + } + } +} + +struct DiningMenuRow: View { + + init (for diningMenu: DiningMenu) { + self.diningMenu = diningMenu + } + + @State var isExpanded = false + let diningMenu: DiningMenu + + var body: some View { + VStack { + DiningMenuSectionRow(isExpanded: $isExpanded, title: diningMenu.mealType) + .font(.system(size: 21, weight: .medium)) + + if isExpanded { + ForEach(diningMenu.diningStations, id: \.self) { diningStation in + DiningStationRow(for: diningStation) + } + .padding(.leading) + .transition(.moveAndFade) + } + }.clipped() + } +} + +struct DiningStationRow: View { + + init (for diningStation: DiningStation) { + self.diningStation = diningStation + } + + @State var isExpanded = false + let diningStation: DiningStation + + var body: some View { + VStack { + DiningMenuSectionRow(isExpanded: $isExpanded, title: diningStation.stationDescription) + .font(Font.system(size: 17)) + + if isExpanded { + ForEach(diningStation.diningStationItems, id: \.self) { diningStationItem in + DiningStationItemRow(for: diningStationItem) + .padding(.leading) + } + .transition(.moveAndFade) + } + }.clipped() + + } +} + +struct DiningStationItemRow: View { + + init (for diningStationItem: DiningStationItem) { + self.diningStationItem = diningStationItem + } + + let diningStationItem: DiningStationItem + + var body: some View { + VStack(alignment: .leading){ + HStack(alignment: .center) { + Text(diningStationItem.title.capitalizeMainWords()) + .font(Font.system(size: 17)) + + + ForEach(diningStationItem.tableAttribute.attributeDescriptions, id: \.self) { attribute in + //Unlike UIKit, image will simply not appear if it doesn't exist in assets + Image(attribute.description) + .resizable() + .scaledToFit() + .frame(width: 20.0,height:20) + + } + Spacer() + }.padding(.bottom, 3) + + Text(diningStationItem.description) + .font(.system(size: 17, weight: .thin)) + .fixedSize(horizontal: false, vertical: true) + }.padding(.bottom) + } +} + + +extension AnyTransition { + static var moveAndFade: AnyTransition { + let insertion = AnyTransition + .opacity.animation(.easeInOut(duration: 0.7)) + .combined(with: .move(edge: .top)).animation(.easeInOut) + + let removal = AnyTransition + .opacity.animation(.easeInOut(duration: 0.1)) + .combined(with: .move(edge: .top)).animation(.easeInOut) + + return .asymmetric(insertion: insertion, removal: removal) + } +} + +@available(iOS 14, *) +struct MenuDisclosureGroup_Previews: PreviewProvider { + static var previews: some View { + let diningVenues: DiningMenuAPIResponse = Bundle.main.decode("mock_menu.json") + + return NavigationView { + ScrollView { + VStack { + DiningVenueDetailMenuView(menus: diningVenues.document.menuDocument.menus) + Spacer() + } + }.navigationTitle("Dining") + .padding() + } + } +} + +extension String { + func capitalizeMainWords() -> String { + let nonCaptializingSet: Set = [ + "a", "an", "the", "for", "and", "nor", "but", "or", "yet", "so", "with", "at", "around", "by", "after", "along", "for", "from", "of", "on", "to", "with", "without" + ] + + return self.split(separator: " ").map({nonCaptializingSet.contains(String($0)) ? $0.lowercased() : $0.capitalized}).joined(separator: " ") + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/mock_menu.json b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/mock_menu.json new file mode 100644 index 000000000..ffb321f1e --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/mock_menu.json @@ -0,0 +1,1067 @@ +{ + "Document": { + "location": "1920 Commons", + "menudate": "1/17/2021", + "tblMenu": { + "tblDayPart": [ + { + "tblStation": [ + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "herb and garlic roasted vegetables", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "roasted root vegetable hash" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegetarian" + }, + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "lightly seasoned scrambled cage free eggs", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "scrambled eggs" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegetarian" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "vanilla custard dipped thick cut Texas toast griddle toasted golden brown", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "vanilla French toast" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Made without Gluten-Containing Ingredients" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "broiled pork sausage", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "zesty pork breakfast sausage" + } + ], + "txtStationDescription": "breakfast grill" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "sauteed yellow squash with zucchini, onion, garlic and basil", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "squash medley" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "steamed brown rice saut\u00e9ed with carrot, celery, and onion", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "brown rice pilaf" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Halal" + }, + { + "description": "Farm to Fork" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "oven-roasted honey, paprika, garlic, and lime marinated Murray's chicken", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "honey lime chicken" + } + ], + "txtStationDescription": "SimplyOASIS" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Halal" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "roasted chicken with arugula, tomato, sriracha mayo on ciabatta roll", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "sriracha chicken sandwich" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "whole wheat tortilla filled with roasted garlic hummus, roasted squash, mushrooms, peppers, onions and arugula", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "roasted vegetable with arugula wrap" + } + ], + "txtStationDescription": "commons' deli" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "breakfast porridge of simmered oats", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "old-fashioned oatmeal" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegetarian" + }, + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Farm to Fork" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "hard-boiled, cage free eggs sourced from The Common Market", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "boiled eggs" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "lean roasted turkey breast, hearty chicken broth, celery, carrot, onion, herbs and finished with steamed rice", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "turkey with rice" + } + ], + "txtStationDescription": "kettles" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a selection of bread and bagels served with your choice of peanut butter, jelly, butter and cream cheese on the side", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "breads" + } + ], + "txtStationDescription": "breads and bagels" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "house-made pizza dough, topped with house-made pizza sauce and shredded mozzarella cheese", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "classic cheese pizza" + }, + { + "tblAttributes": "", + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "house-made pizza dough, topped with house-made pizza sauce shredded mozzarella cheese and slice pork pepperoni", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "classic pepperoni pizza" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "al dente pasta, chunky house made sauce pomodoro, chopped basil", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "pasta pomodoro" + } + ], + "txtStationDescription": "pizza & pasta" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "steamed ancient grain, shredded coconut, diced fruit, dried fruit medley, infused agave", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "quinoa brunch bowl" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "seasoned crumbled tofu enhanced with bell peppers", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "breakfast tofu scramble" + } + ], + "txtStationDescription": "very veggie" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Humane" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "savory turkey burger" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Humane" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "angus beef frank" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "vegan burger patty" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "seasoned fries" + } + ], + "txtStationDescription": "grill" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "arcadian mixed greens with carrot, tomato and cucumber and balsamic vinaigrette", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "mixed green salad" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Farm to Fork" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "chopped romaine with house-made parmesan croutons, Murray's grilled chicken breast, parmesan cheese and house-made caesar dressing", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "grilled chicken caesar salad" + } + ], + "txtStationDescription": "salad bar" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of fresh hand cut fruit", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "fresh fruit cups" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of flavored yogurt cups", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "yogurt cups" + } + ], + "txtStationDescription": "fruit and yogurt" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "smooth garbanzo hummus with sesame tahini, lemon and garlic served with garlic toasted pita triangles", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "hummus with toasted pita" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "artichoke hearts marinated in a basil vinaigrette accented with roasted red peppers", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "herb marinated artichokes" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegetarian" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "feta cheese with extra virgin olive oil and fresh basil", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "herbed feta cheese" + } + ], + "txtStationDescription": "mezze" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of cereal flavors to choose from with milk served on the side", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "breakfast cereals" + } + ], + "txtStationDescription": "cereal" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of classic continental breakfast pastries", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "breakfast pastries" + } + ], + "txtStationDescription": "sweets and treats" + } + ], + "txtDayPartDescription": "Brunch" + }, + { + "tblStation": [ + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "crisp steamed seasonal vegetable", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "steamed broccoli" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "steamed lightly seasoned potatoes, finished with fresh chopped Italian parsley", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "red potatoes" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Humane" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "tender cubes of slow roasted beef eye round braised with garlic, herbs, Spanish onion, and hearty beef stock", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "cubed beef braised onions" + } + ], + "txtStationDescription": "comfort" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "sauteed yellow squash with zucchini, onion, garlic and basil", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "squash medley" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "steamed brown rice saut\u00e9ed with carrot, celery, and onion", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "brown rice pilaf" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Halal" + }, + { + "description": "Farm to Fork" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "oven-roasted honey, paprika, garlic, and lime marinated Murray's chicken", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "honey lime chicken" + } + ], + "txtStationDescription": "SimplyOASIS" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Halal" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "roasted chicken with arugula, tomato, sriracha mayo on ciabatta roll", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "sriracha chicken sandwich" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "whole wheat tortilla filled with roasted garlic hummus, roasted squash, mushrooms, peppers, onions and arugula", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "roasted vegetable with arugula wrap" + } + ], + "txtStationDescription": "commons' deli" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "corn, lima beans, carrots, diced tomato, chopped spinach, hearty vegetable stock and quinoa", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "mixed vegetable with quinoa" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Humane" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "lean roasted turkey breast, hearty chicken broth, celery, carrot, onion, herbs and finished with steamed rice", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "turkey with rice" + } + ], + "txtStationDescription": "kettles" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a selection of bread and bagels served with your choice of peanut butter, jelly, butter and cream cheese on the side", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "breads" + } + ], + "txtStationDescription": "breads and bagels" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "house-made pizza dough, topped with house-made pizza sauce and shredded mozzarella cheese", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "classic cheese pizza" + }, + { + "tblAttributes": "", + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "house-made pizza dough, topped with house-made pizza sauce shredded mozzarella cheese and slice pork pepperoni", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "classic pepperoni pizza" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "al dente pasta, chunky house made sauce pomodoro, chopped basil", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "pasta pomodoro" + } + ], + "txtStationDescription": "pizza & pasta" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + }, + { + "description": "Jain" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "snowpeas, edamame, peppers, zucchini and bok choy, flash sauteed with steamed rice noodles finished with agave sweetened soy", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "vegetable stir fry with rice noodles" + } + ], + "txtStationDescription": "very veggie" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Humane" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "savory turkey burger" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Humane" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "angus beef frank" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "vegan burger patty" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "seasoned fries" + } + ], + "txtStationDescription": "grill" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "arcadian mixed greens with carrot, tomato and cucumber and balsamic vinaigrette", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "mixed green salad" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Farm to Fork" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "chopped romaine with house-made parmesan croutons, Murray's grilled chicken breast, parmesan cheese and house-made caesar dressing", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "grilled chicken caesar salad" + } + ], + "txtStationDescription": "salad bar" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of fresh hand cut fruit", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "fresh fruit cups" + }, + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of flavored yogurt cups", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "yogurt cups" + } + ], + "txtStationDescription": "fruit and yogurt" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegan" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "smooth garbanzo hummus with sesame tahini, lemon and garlic served with garlic toasted pita triangles", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "hummus with toasted pita" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegan" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "artichoke hearts marinated in a basil vinaigrette accented with roasted red peppers", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "herb marinated artichokes" + }, + { + "tblAttributes": { + "txtAttribute": [ + { + "description": "Vegetarian" + }, + { + "description": "Made without Gluten-Containing Ingredients" + } + ] + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "feta cheese with extra virgin olive oil and fresh basil", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "herbed feta cheese" + } + ], + "txtStationDescription": "mezze" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of cereal flavors to choose from with milk served on the side", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "breakfast cereals" + } + ], + "txtStationDescription": "cereal" + }, + { + "tblItem": [ + { + "tblAttributes": { + "txtAttribute": { + "description": "Vegetarian" + } + }, + "tblFarmToFork": "", + "tblSide": "", + "txtDescription": "a variety of pies and cakes and house-made desserts", + "txtNutritionInfo": "", + "txtPrice": "", + "txtTitle": "desserts" + } + ], + "txtStationDescription": "sweets and treats" + } + ], + "txtDayPartDescription": "Dinner" + } + ] + } + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueRow.swift b/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueRow.swift new file mode 100644 index 000000000..8a1e9e1ab --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueRow.swift @@ -0,0 +1,137 @@ +// +// DiningVenueRow.swift +// PennMobile +// +// Created by CHOI Jongmin on 5/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI +import KingfisherSwiftUI + +@available(iOS 14, *) +struct DiningVenueRow: View { + + init(for venue: DiningVenue) { + self.venue = venue + } + + let venue: DiningVenue + + var body: some View { + HStack(spacing: 13) { + KFImage(venue.imageURL) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 64) + .background(Color.grey1) + .clipShape(RoundedRectangle(cornerRadius: 7)) + + VStack(alignment: .leading, spacing: 3) { + Label(venue.statusString, systemImage: venue.statusImageString) + .labelStyle(VenueStatusLabelStyle()) + .modifier(StatusColorModifier(for: venue)) + + Text(venue.name) + .font(.system(size: 17, weight: .medium)) + .minimumScaleFactor(0.2) + .lineLimit(1) + + GeometryReader { geo in + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { proxy in + HStack(spacing: 6) { + ForEach(Array(venue.humanFormattedHoursArrayForToday.enumerated()), id: \.offset) { (index, time) in + Text(time) + .font(.system(size: 14, weight: .light, design: .default)) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .foregroundColor(index == venue.currentMealIndex ? Color.white : Color.labelPrimary) + .background(index == venue.currentMealIndex ? (venue.isClosingSoon ? Color.redLight : Color.greenLight) : Color.grey5) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .id(index) + .frame(height: geo.frame(in: .global).height) + }.onAppear { + withAnimation { + proxy.scrollTo(venue.currentOrNearestMealIndex) + } + } + } + } + } + } + } + .frame(height: 64) + } + } +} + +// MARK: - ViewModifiers +@available(iOS 14, *) +struct StatusColorModifier: ViewModifier { + + init(for venue: DiningVenue) { + self.venue = venue + } + + let venue: DiningVenue + + func body(content: Content) -> some View { + if venue.hasMealsToday && venue.isOpen { + if venue.isClosingSoon { + return content.foregroundColor(Color.red) + } else { + switch venue.venueType { + case .dining: + if venue.isMainDiningTimes { + return content.foregroundColor(Color.green) + } else { + return content.foregroundColor(Color.yellow) + } + default: + return content.foregroundColor(Color.green) + } + } + } else { + return content.foregroundColor(Color.gray) + } + } +} + +@available(iOS 14, *) +struct VenueStatusLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 4) { + configuration.icon.font(.system(size: 9, weight: .semibold)) + configuration.title.font(.system(size: 11, weight: .semibold)) + Spacer() + } + } +} + +@available(iOS 14, *) +struct DiningVenueRow_Previews: PreviewProvider { + static var previews: some View { + let diningVenues: DiningAPIResponse = Bundle.main.decode("sample-dining-venue.json") + + return NavigationView { + List { + NavigationLink(destination: Text("dfs")) { + DiningVenueRow(for: diningVenues.document.venues[0]) + } + NavigationLink(destination: Text("dfs")) { + DiningVenueRow(for: diningVenues.document.venues[1]) + } + NavigationLink(destination: Text("dfs")) { + DiningVenueRow(for: diningVenues.document.venues[2]) + } + NavigationLink(destination: Text("dfs")) { + DiningVenueRow(for: diningVenues.document.venues[3]) + } + } + } + .preferredColorScheme(.light) + } +} + + diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift new file mode 100644 index 000000000..6e85875a3 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/DiningVenueView.swift @@ -0,0 +1,70 @@ +// +// DiningVenueView.swift +// PennMobile +// +// Created by CHOI Jongmin on 9/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +@available(iOS 14, *) +struct DiningVenueView: View { + @EnvironmentObject var diningVM: DiningViewModelSwiftUI + + // Hack to deselect cells after popping navigation view + // Will be removed once SwiftUI is Fixed + @State private var selectedItem: String? + @State private var listViewId = UUID() + + var body: some View { + List { + ForEach(diningVM.ordering, id: \.self) { venueType in + Section(header: CustomHeader(name: venueType.fullDisplayName)) { + ForEach(diningVM.diningVenues[venueType] ?? []) { venue in + NavigationLink(destination: DiningVenueDetailView(for: venue).environmentObject(diningVM), tag: "\(venue.id)", selection: $selectedItem) { + DiningVenueRow(for: venue) + .padding(.vertical, 4) + } + } + } + } + } + // Hack to deselect items + .id(listViewId) + .onAppear { + diningVM.refreshVenues() + diningVM.refreshBalance() + if selectedItem != nil { + selectedItem = nil + listViewId = UUID() + } + } + } +} + +@available(iOS 14, *) +struct CustomHeader: View { + + let name: String + + var body: some View { + HStack { + Text(name) + .font(.system(size: 21, weight: .semibold)) + Spacer() + } + .padding() + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .background(Color(UIColor.uiBackground)) + //Default Text Case for Header is Caps Lock + .textCase(nil) + } +} + +@available(iOS 14, *) +struct DiningVenueView_Previews: PreviewProvider { + static var previews: some View { + CustomHeader(name: "new") + } +} diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/sample-dining-venue.json b/PennMobile/Dining/SwiftUI/Views/Venue/sample-dining-venue.json new file mode 100644 index 000000000..b76dc9280 --- /dev/null +++ b/PennMobile/Dining/SwiftUI/Views/Venue/sample-dining-venue.json @@ -0,0 +1,1122 @@ +{ + "document": { + "venue": [ + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/593", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "21:30:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-04", + "meal": [ + { + "close": "14:00:00", + "open": "09:00:00", + "type": "Brunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-05", + "meal": [ + { + "close": "14:00:00", + "open": "09:00:00", + "type": "Brunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-06", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "09:00:00", + "open": "07:30:00", + "type": "Closed" + }, + { + "close": "14:00:00", + "open": "09:00:00", + "type": "Brunch" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "21:30:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "21:30:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/1920-commons/", + "id": 593, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/1920-commons.jpg", + "name": "1920 Commons", + "venueType": "residential", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/593" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/636", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "21:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "19:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-04", + "meal": [ + { + "close": "14:00:00", + "open": "10:00:00", + "type": "Brunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-05", + "meal": [ + { + "close": "14:00:00", + "open": "10:00:00", + "type": "Brunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-06", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "10:00:00", + "open": "07:30:00", + "type": "Closed" + }, + { + "close": "14:00:00", + "open": "10:00:00", + "type": "Brunch" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "21:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "10:30:00", + "open": "07:30:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "21:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/hill-house/", + "id": 636, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/Hill+House.jpg", + "name": "Hill House", + "venueType": "residential", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/636" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/637", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "10:30:00", + "open": "08:00:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "10:30:00", + "open": "08:00:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "10:30:00", + "open": "08:00:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "10:30:00", + "open": "08:00:00", + "type": "Breakfast" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/kings-court-english-house/", + "id": 637, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/kceh.jpg", + "name": "English House", + "venueType": "residential", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/637" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/638", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "14:00:00", + "open": "11:30:00", + "type": "Lunch" + }, + { + "close": "19:30:00", + "open": "17:30:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "14:00:00", + "open": "11:30:00", + "type": "Lunch" + }, + { + "close": "20:00:00", + "open": "17:30:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-06", + "meal": [ + { + "close": "14:00:00", + "open": "11:30:00", + "type": "Lunch" + }, + { + "close": "14:00:00", + "open": "11:30:00", + "type": "Closed" + }, + { + "close": "19:30:00", + "open": "17:30:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "14:00:00", + "open": "11:30:00", + "type": "Lunch" + }, + { + "close": "19:30:00", + "open": "17:30:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "14:00:00", + "open": "11:30:00", + "type": "Lunch" + }, + { + "close": "19:30:00", + "open": "17:30:00", + "type": "Dinner" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/falk-dining-commons/", + "id": 638, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/hillel.jpg", + "name": "Falk Kosher Dining", + "venueType": "residential", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/638" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/639", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "19:00:00", + "open": "08:00:00", + "type": "The Market Caf\u00e9" + }, + { + "close": "19:00:00", + "open": "11:00:00", + "type": "Houston Market" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "17:00:00", + "open": "08:00:00", + "type": "The Market Caf\u00e9" + }, + { + "close": "17:00:00", + "open": "11:00:00", + "type": "Houston Market" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "19:00:00", + "open": "08:00:00", + "type": "The Market Caf\u00e9" + }, + { + "close": "19:00:00", + "open": "11:00:00", + "type": "Houston Market" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "19:00:00", + "open": "08:00:00", + "type": "The Market Caf\u00e9" + }, + { + "close": "19:00:00", + "open": "11:00:00", + "type": "Houston Market" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/houston-market/", + "id": 639, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/houston.jpg", + "name": "Houston Market", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/639" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/641", + "dateHours": [ + { + "date": "2021-09-03", + "meal": [ + { + "close": "14:00:00", + "open": "08:00:00", + "type": "Accenture" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "17:00:00", + "open": "08:00:00", + "type": "Accenture" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "17:00:00", + "open": "08:00:00", + "type": "Accenture" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/accenture-cafe/", + "id": 641, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/accenture.png", + "name": "Accenture Caf\u00e9", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/641" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/642", + "dateHours": [ + { + "date": "2021-09-03", + "meal": [ + { + "close": "13:30:00", + "open": "08:00:00", + "type": "Joe" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "17:00:00", + "open": "08:00:00", + "type": "Joe" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "17:00:00", + "open": "08:00:00", + "type": "Joe" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/joes-cafe/", + "id": 642, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/Penn.JoesCafe-Int5.72W.jpg", + "name": "Joe's Caf\u00e9", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/642" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/747", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "20:00:00", + "open": "11:00:00", + "type": "Lunch" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "14:00:00", + "open": "11:00:00", + "type": "Lunch" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "20:00:00", + "open": "11:00:00", + "type": "Lunch" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "20:00:00", + "open": "11:00:00", + "type": "Lunch" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/mcclelland/", + "id": 747, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/mcclelland.jpg", + "name": "McClelland Express", + "venueType": "residential", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/747" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1057", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "23:59:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "21:00:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + }, + { + "date": "2021-09-04", + "meal": [ + { + "close": "18:00:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + }, + { + "date": "2021-09-05", + "meal": [ + { + "close": "18:00:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + }, + { + "date": "2021-09-06", + "meal": [ + { + "close": "23:59:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "23:59:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "23:59:00", + "open": "11:00:00", + "type": "Gourmet Grocer" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/1920-gourmet-grocer/", + "id": 1057, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/gourmetgrocer.jpg", + "name": "1920 Gourmet Grocer", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1057" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1163", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "23:59:00", + "open": "08:00:00", + "type": "Starbucks" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "21:00:00", + "open": "08:00:00", + "type": "Starbucks" + } + ] + }, + { + "date": "2021-09-04", + "meal": [ + { + "close": "21:00:00", + "open": "08:00:00", + "type": "Starbucks" + } + ] + }, + { + "date": "2021-09-05", + "meal": [ + { + "close": "18:00:00", + "open": "11:00:00", + "type": "Starbucks" + } + ] + }, + { + "date": "2021-09-06", + "meal": [ + { + "close": "23:59:00", + "open": "11:00:00", + "type": "Starbucks" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "23:59:00", + "open": "08:00:00", + "type": "Starbucks" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "23:59:00", + "open": "08:00:00", + "type": "Starbucks" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/1920-starbucks/", + "id": 1163, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/starbucks.jpg", + "name": "1920 Starbucks", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1163" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1442", + "dateHours": [ + { + "date": "2021-09-07", + "meal": [ + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/new-college-house/", + "id": 1442, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/nch.jpg", + "name": "Lauder College House", + "venueType": "residential", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1442" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1731", + "dateHours": [ + { + "date": "2021-09-07", + "meal": [ + { + "close": "23:59:00", + "open": "20:30:00", + "type": "late night" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "23:59:00", + "open": "20:30:00", + "type": "late night" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/nch-retail/", + "id": 1731, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/nch.jpg", + "name": "LCH Retail", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1731" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1732", + "dateHours": [ + { + "date": "2021-09-07", + "meal": [ + { + "close": "16:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "16:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/pret-a-manger-upper/", + "id": 1732, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/MBA+Cafe.jpg", + "name": "Pret a Manger MBA", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1732" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1733", + "dateHours": [ + { + "date": "2021-09-02", + "meal": [ + { + "close": "20:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-03", + "meal": [ + { + "close": "16:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-04", + "meal": [ + { + "close": "16:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-05", + "meal": [ + { + "close": "16:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-06", + "meal": [ + { + "close": "16:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "20:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "20:00:00", + "open": "08:00:00", + "type": "Pret a Manger" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/pret-a-manger-lower/", + "id": 1733, + "imageURL": "https://s3.us-east-2.amazonaws.com/labs.api/dining/Pret+A+Manger.jpg", + "name": "Pret a Manger Locust Walk", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1733" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1464004", + "dateHours": [ + { + "date": "2021-09-03", + "meal": [ + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "20:00:00", + "open": "17:00:00", + "type": "Dinner" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/quaker-kitchen/", + "id": 1464004, + "imageURL": null, + "name": "Quaker Kitchen", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1464004" + }, + { + "dailyMenuURL": "http://www.cafebonappetit.com/feeds/daily/1464009", + "dateHours": [ + { + "date": "2021-09-03", + "meal": [ + { + "close": "14:00:00", + "open": "08:00:00", + "type": "Breakfast" + } + ] + }, + { + "date": "2021-09-07", + "meal": [ + { + "close": "14:00:00", + "open": "08:00:00", + "type": "Breakfast" + }, + { + "close": "23:30:00", + "open": "20:30:00", + "type": "Late Night" + } + ] + }, + { + "date": "2021-09-08", + "meal": [ + { + "close": "14:00:00", + "open": "08:00:00", + "type": "Breakfast" + }, + { + "close": "23:30:00", + "open": "20:30:00", + "type": "Late Night" + } + ] + } + ], + "facilityURL": "http://university-of-pennsylvania.cafebonappetit.com/cafe/cafe-west/", + "id": 1464009, + "imageURL": null, + "name": "Cafe West", + "venueType": "retail", + "weeklyMenuURL": "http://www.cafebonappetit.com/feeds/weekly/1464009" + } + ] + } +} diff --git a/PennMobile/General/Colors.swift b/PennMobile/General/Colors.swift index 804ff43d2..a055774e7 100644 --- a/PennMobile/General/Colors.swift +++ b/PennMobile/General/Colors.swift @@ -97,3 +97,74 @@ extension UIColor { return (red, green, blue, alpha) } } + + +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +extension Color { + // MARK: - UI Palette + static let navigation = Color("navigation") + static let uiCardBackground = Color("uiCardBackground") + static let uiGroupedBackground = Color("uiGroupedBackground") + static let uiGroupedBackgroundSecondary = Color("uiGroupedBackgroundSecondary") + static let uiBackground = Color("uiBackground") + static let uiBackgroundSecondary = Color("uiBackgroundSecondary") + static let labelPrimary = Color("labelPrimary") + static let labelSecondary = Color("labelSecondary") + static let labelTertiary = Color("labelTertiary") + static let labelQuaternary = Color("labelQuaternary") + + // MARK: - Primary Palette + static var baseDarkBlue = Color("baseDarkBlue") + static let baseLabsBlue = Color("baseLabsBlue") + + // MARK: - Neutral Palette + static var grey1 = Color("grey1") + static var grey2 = Color("grey2") + static var grey3 = Color("grey3") + static var grey4 = Color("grey4") + static var grey5 = Color("grey5") + static var grey6 = Color("grey6") + + // MARK: - Secondary Palette + static var baseBlue = Color("baseBlue") + static var baseGreen = Color("baseGreen") + static var baseOrange = Color("baseOrange") + static var basePurple = Color("basePurple") + static var baseRed = Color("baseRed") + static var baseYellow = Color("baseYellow") + + // MARK: - Extended Palette + static var blueLight = Color("blueLighter") + static var blueLighter = Color("blueLighter") + static var blueDark = Color("blueDark") + static var blueDarker = Color("blueDarker") + + static var greenLight = Color("greenLighter") + static var greenLighter = Color("greenLighter") + static var greenDark = Color("greenDark") + static var greenDarker = Color("greenDarker") + + static var orangeLight = Color("orangeLighter") + static var orangeLighter = Color("orangeLighter") + static var orangeDark = Color("orangeDark") + static var orangeDarker = Color("orangeDarker") + + static var purpleLight = Color("purpleLighter") + static var purpleLighter = Color("purpleLighter") + static var purpleDark = Color("purpleDark") + static var purpleDarker = Color("purpleDarker") + + static var redLight = Color("redLight") + static var redLighter = Color("redLighter") + static var redDark = Color("redDark") + static var redDarker = Color("redDarker") + + static var yellowLight = Color("yellowLighter") + static var yellowLighter = Color("yellowLighter") + static var yellowDark = Color("yellowDark") + static var yellowDarker = Color("yellowDarker") +} diff --git a/PennMobile/General/Extensions.swift b/PennMobile/General/Extensions.swift index c72251530..c81988c88 100755 --- a/PennMobile/General/Extensions.swift +++ b/PennMobile/General/Extensions.swift @@ -509,6 +509,33 @@ extension NSMutableAttributedString { } } +// Decodes .json data for SwiftUI Previews +// https://www.hackingwithswift.com/books/ios-swiftui/using-generics-to-load-any-kind-of-codable-data +extension Bundle { + func decode(_ file: String, dateFormat: String? = nil) -> T { + guard let url = self.url(forResource: file, withExtension: nil) else { + fatalError("unable to find data") + } + + guard let data = try? Data(contentsOf: url) else { + fatalError("Failed to load \(file) from bundle") + } + + let decoder = JSONDecoder() + + let formatter = DateFormatter() + formatter.dateFormat = dateFormat + + decoder.dateDecodingStrategy = .formatted(formatter) + + guard let decoded = try? decoder.decode(T.self, from: data) else { + fatalError("Data does not conform to desired structure") + } + + return decoded + } +} + extension URL { mutating func appendQueryItem(name: String, value: String?) { guard var urlComponents = URLComponents(string: absoluteString) else { return } diff --git a/PennMobile/Dining/Networking + Cache/LocalJSONStore.swift b/PennMobile/General/LocalJSONStore.swift similarity index 97% rename from PennMobile/Dining/Networking + Cache/LocalJSONStore.swift rename to PennMobile/General/LocalJSONStore.swift index 618d5a7e4..272c43ff3 100644 --- a/PennMobile/Dining/Networking + Cache/LocalJSONStore.swift +++ b/PennMobile/General/LocalJSONStore.swift @@ -10,6 +10,7 @@ import Foundation // MARK: - LocalJSONStore // This class can be used by any Codable struct as a simple cacheing layer. Check out DiningDataStore.swift for an example. +// "filename" is just the name of the cache file class LocalJSONStore where T : Codable { let storageType: DataStoreType let filename: String diff --git a/PennMobile/General/SwiftUI Views/AlertModifier.swift b/PennMobile/General/SwiftUI Views/AlertModifier.swift new file mode 100644 index 000000000..90252a372 --- /dev/null +++ b/PennMobile/General/SwiftUI Views/AlertModifier.swift @@ -0,0 +1,86 @@ +// +// AlertModifier.swift +// PennMobile +// +// Created by CHOI Jongmin on 11/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +#if canImport(SwiftUI) +import SwiftUI +#endif + +// https://trailingclosure.com/notification-banner-using-swiftui/ + +@available(iOS 14, *) +struct AlertBanner: View { + init(for type: NetworkingError) { + self.type = type + } + + var type: NetworkingError + + var alertDescription: String { + switch type { + case .noInternet: + return "No Internet Connection" + default: + return "Unable to connect to the API.\nPlease refresh and try again." + } + } + + var body: some View { + GeometryReader { geo in + Text(self.alertDescription) + .frame(width: geo.size.width, height: 70, alignment: .center) + .font(Font(UIFont.primaryInformationFont)) + .foregroundColor(.white) + .background(Color.baseRed) + } + } +} + +@available(iOS 14, *) +struct AlertModifier: ViewModifier { + @Binding var type: NetworkingError? + + var alertDescription: String { + switch type { + case .noInternet: + return "No Internet Connection" + default: + return "Unable to connect to the API.\nPlease refresh and try again." + } + } + + func body(content: Content) -> some View { + ZStack { + content + + if let type = type { + AlertBanner(for: type) + .animation(.easeInOut(duration: 1.0)) + .transition(AnyTransition.move(edge: .top)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + self.type = nil + } + } + } + .onTapGesture { + withAnimation { + self.type = nil + } + } + } + } + } +} + +@available(iOS 14, *) +extension View { + func alert(show type: Binding) -> some View { + self.modifier(AlertModifier(type: type)) + } +} diff --git a/PennMobile/General/SwiftUI Views/FadingScrollView.swift b/PennMobile/General/SwiftUI Views/FadingScrollView.swift new file mode 100644 index 000000000..9aed98987 --- /dev/null +++ b/PennMobile/General/SwiftUI Views/FadingScrollView.swift @@ -0,0 +1,93 @@ +// +// FadingScrollView.swift +// PennMobile +// +// Created by CHOI Jongmin on 5/7/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import SwiftUI + +struct FadingScrollView: View { + let fadeDistance: CGFloat + let axes: Axis.Set + let showsIndicators: Bool + let content: Content + + init( + fadeDistance: CGFloat, + _ axes: Axis.Set = .horizontal, + showsIndicators: Bool = true, + @ViewBuilder content: () -> Content + ) { + self.fadeDistance = fadeDistance + self.axes = axes + self.showsIndicators = showsIndicators + self.content = content() + } + + var body: some View { + ZStack(alignment: .leading) { + ScrollView(axes, showsIndicators: showsIndicators) { + // Pad the content depending on the axes so that bottom or trailing + // part of the content isn't faded when scrolling all the way to the end. + + if axes == .vertical { + Spacer(minLength: fadeDistance) + + content + + Spacer(minLength: fadeDistance) + } else if axes == .horizontal { + HStack(spacing: 0) { + Spacer(minLength: fadeDistance) + + content + + Spacer(minLength: fadeDistance) + } + } + } + + if axes.contains(.vertical) { + VStack { + fadeGradient(for: .vertical, startPoint: .top, endPoint: .bottom) + .frame(height: fadeDistance) + // SwiftUI internally not working + .allowsHitTesting(false) + + Spacer() + + fadeGradient(for: .vertical, startPoint: .bottom, endPoint: .top) + .frame(height: fadeDistance) + // SwiftUI internally not working + .allowsHitTesting(false) + } + } + + + if axes.contains(.horizontal) { + HStack { + fadeGradient(for: .horizontal, startPoint: .leading, endPoint: .trailing) + .frame(width: fadeDistance) + .allowsHitTesting(false) + Spacer() + fadeGradient(for: .horizontal, startPoint: .trailing, endPoint: .leading) + .frame(width: fadeDistance) + .allowsHitTesting(false) + } + } + }.offset(x: -fadeDistance, y: 0) + } + + private func fadeGradient(for axis: Axis, startPoint: UnitPoint, endPoint: UnitPoint) -> some View { + return LinearGradient( + gradient: Gradient(colors: [ + Color(.systemBackground).opacity(1), + Color(.systemBackground).opacity(0) + ]), + startPoint: startPoint, + endPoint: endPoint + ) + } +} diff --git a/PennMobile/General/SwiftUI Views/UIKit Views.swift b/PennMobile/General/SwiftUI Views/UIKit Views.swift new file mode 100644 index 000000000..c6bdc29b9 --- /dev/null +++ b/PennMobile/General/SwiftUI Views/UIKit Views.swift @@ -0,0 +1,70 @@ +// +// UIKit Views.swift +// PennMobile +// +// Created by CHOI Jongmin on 3/6/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +#if canImport(SwiftUI) +import SwiftUI +#endif + +@available(iOS 14, *) +struct ActivityIndicatorView: UIViewRepresentable { + + @Binding var animating: Bool + let style: UIActivityIndicatorView.Style + + func makeUIView(context: Context) -> UIActivityIndicatorView { + let indicator = UIActivityIndicatorView(style: style) + indicator.color = .labelPrimary + return indicator + } + + func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) { + animating ? uiView.startAnimating() : uiView.stopAnimating() + } +} + +@available(iOS 14, *) +struct LabsLoginControllerSwiftUI: UIViewControllerRepresentable { + + @EnvironmentObject var diningVM: DiningViewModelSwiftUI + @Binding var isShowing: Bool + @Binding var loginFailure: Bool + var handleError: () -> Void + + func makeUIViewController(context: Context) -> UINavigationController { + let llc = LabsLoginController { (success) in + DispatchQueue.main.async { + self.loginCompletion(success) + } + } + + llc.handleCancel = { + self.handleError() + } + + return UINavigationController(rootViewController: llc) + } + + func updateUIViewController(_ navigationController: UINavigationController, context: Context) {} + + fileprivate func loginCompletion(_ successful: Bool) { + isShowing = false + + if successful { + diningVM.refreshInsights() + } else { + loginFailure = true + } + } +} + +@available(iOS 14, *) +struct VisualEffectView: UIViewRepresentable { + var effect: UIVisualEffect? + func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() } + func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = effect } +} diff --git a/PennMobile/General/UserDefaults + Helpers.swift b/PennMobile/General/UserDefaults + Helpers.swift index 8cc4c87d5..23a98b086 100644 --- a/PennMobile/General/UserDefaults + Helpers.swift +++ b/PennMobile/General/UserDefaults + Helpers.swift @@ -36,6 +36,8 @@ extension UserDefaults { case PCAPreferences case gsrGroupsEnabled case totpEnabledDate + case lastDiningHoursRequest + case lastMenuRequest } func clearAll() { @@ -570,3 +572,36 @@ extension UserDefaults { UserDefaults.standard.set(date, forKey: UserDefaultsKeys.totpEnabledDate.rawValue) } } + +// MARK: - DiningHours +extension UserDefaults { + func setLastDiningHoursRequest() { + UserDefaults.standard.set(Date(), forKey: UserDefaultsKeys.lastDiningHoursRequest.rawValue) + } + + func getLastDiningHoursRequest() -> Date? { + return UserDefaults.standard.value(forKey: UserDefaultsKeys.lastDiningHoursRequest.rawValue) as? Date + } +} + + +// MARK: - MenuRequest +extension UserDefaults { + func setLastMenuRequest(id: Int) { + let dict: [Int: Date]? = UserDefaults.standard.value(forKey: UserDefaultsKeys.lastMenuRequest.rawValue) as? [Int: Date] + + if var menuDateDict = dict { + menuDateDict[id] = Date() + UserDefaults.standard.set(menuDateDict, forKey: UserDefaultsKeys.lastMenuRequest.rawValue) + } else { + let menuDateDictInit = [id: Date()] + UserDefaults.standard.set(menuDateDictInit, forKey: UserDefaultsKeys.lastMenuRequest.rawValue) + } + } + + func getLastMenuRequest(id: Int) -> Date? { + let dict = UserDefaults.standard.value(forKey: UserDefaultsKeys.lastMenuRequest.rawValue) as? [Int:Date] + + return dict?[id] + } +} diff --git a/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift b/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift index e1d0170ab..230040317 100644 --- a/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift +++ b/PennMobile/Home/Cells/Dining/HomeDiningCellItem.swift @@ -43,7 +43,7 @@ extension HomeDiningCellItem { guard let ids = json["venues"].arrayObject as? [Int] else { throw NetworkingError.jsonError } - let venues: [DiningVenue] = DiningDataStore.shared.getVenues(for: ids) + let venues: [DiningVenue] = DiningAPI.instance.getVenues(with: ids) self.init(venues: venues, venueIds: ids) } } @@ -51,8 +51,10 @@ extension HomeDiningCellItem { // MARK: - API Fetching extension HomeDiningCellItem: HomeAPIRequestable { func fetchData(_ completion: @escaping () -> Void) { - DiningAPI.instance.fetchDiningHours { _,_ in - self.venues = DiningDataStore.shared.getVenues(for: self.venueIds) + DiningAPI.instance.fetchDiningHours { _ in + if self.venues.isEmpty { + self.venues = DiningAPI.instance.getVenues() + } completion() } } diff --git a/PennMobile/Home/Cells/Example/HomeExampleCell.swift b/PennMobile/Home/Cells/Example/HomeExampleCell.swift new file mode 100644 index 000000000..c2c8a7b8a --- /dev/null +++ b/PennMobile/Home/Cells/Example/HomeExampleCell.swift @@ -0,0 +1,73 @@ +// +// HomeExampleCell.swift +// PennMobile +// +// Created by CHOI Jongmin on 14/2/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import UIKit + +final class HomeExampleCell: UITableViewCell, HomeCellConformable { + + static var identifier: String = "exampleCell" + + static func getCellHeight(for item: ModularTableViewItem) -> CGFloat { + return 100.0 + } + + var item: ModularTableViewItem! { + didSet { + guard let item = item as? HomeExampleCellItem else { return } + setupCell(with: item) + } + } + + var delegate: ModularTableViewCellDelegate! + + var cardView: UIView! = UIView() + + var titleLabel: UILabel! + var myLabel: UILabel! + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + prepareHomeCell() + prepareUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension HomeExampleCell { + fileprivate func prepareUI() { + prepareTitleLabel() + prepareMyLabel() + } + + private func prepareTitleLabel() { + titleLabel = UILabel() + titleLabel.text = "Example Cell" + + cardView.addSubview(titleLabel) + _ = titleLabel.anchor(cardView.topAnchor, left: cardView.leftAnchor, bottom: nil, right: nil, topConstant: 12, leftConstant: 20, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0) + } + + private func prepareMyLabel() { + myLabel = UILabel() + + cardView.addSubview(myLabel) + _ = myLabel.anchor(nil, left: cardView.leftAnchor, bottom: cardView.bottomAnchor, right: nil, topConstant: 0, leftConstant: 20, bottomConstant: 10, rightConstant: 0, widthConstant: 0, heightConstant: 0) + + } + +} + +// MARK: - Setup Item +extension HomeExampleCell { + func setupCell(with item: HomeExampleCellItem) { + self.myLabel.text = item.myData + } +} diff --git a/PennMobile/Home/Cells/Example/HomeExampleCellItem.swift b/PennMobile/Home/Cells/Example/HomeExampleCellItem.swift new file mode 100644 index 000000000..3acae9a7b --- /dev/null +++ b/PennMobile/Home/Cells/Example/HomeExampleCellItem.swift @@ -0,0 +1,42 @@ +// +// HomeExampleCellItem.swift +// PennMobile +// +// Created by CHOI Jongmin on 14/2/2020. +// Copyright © 2020 PennLabs. All rights reserved. +// + +import UIKit +import SwiftyJSON + +final class HomeExampleCellItem: HomeCellItem { + + static var associatedCell: ModularTableViewCell.Type { + return HomeExampleCell.self + } + + var myData: String + + init(myData: String) { + self.myData = myData + } + + func equals(item: HomeCellItem) -> Bool { + guard let item = item as? HomeExampleCellItem else { return false } + return myData == item.myData + } + + static var jsonKey: String { + return "example" // Does not matter for this step + } + + static func getItem(for json: JSON?) -> HomeCellItem? { + return HomeExampleCellItem(myData: "My example string") + } + + func equals(item: ModularTableViewItem) -> Bool { + guard let item = item as? HomeCoursesCellItem else { return false } + return true + } + +} diff --git a/PennMobile/Home/Controllers/DiningCellSettingsController.swift b/PennMobile/Home/Controllers/DiningCellSettingsController.swift index 74651683a..b517034e4 100644 --- a/PennMobile/Home/Controllers/DiningCellSettingsController.swift +++ b/PennMobile/Home/Controllers/DiningCellSettingsController.swift @@ -17,7 +17,7 @@ class DiningCellSettingsController: UITableViewController { var delegate: DiningCellSettingsDelegate? var chosenVenueIds = Set() - let allVenues = DiningDataStore.shared.getVenues() + let allVenues = DiningAPI.instance.getVenues() override func viewDidLoad() { super.viewDidLoad() diff --git a/PennMobile/Home/Controllers/HomeViewController.swift b/PennMobile/Home/Controllers/HomeViewController.swift index cc6eed2c0..50bda399a 100644 --- a/PennMobile/Home/Controllers/HomeViewController.swift +++ b/PennMobile/Home/Controllers/HomeViewController.swift @@ -262,11 +262,9 @@ extension HomeViewController : DiningCellSettingsDelegate { func saveSelection(for venueIds: [Int]) { guard let diningItem = self.tableViewModel.getItems(for: [HomeItemTypes.instance.dining]).first as? HomeDiningCellItem else { return } if venueIds.count == 0 { - diningItem.venues = DiningDataStore.shared.getVenues(for: DiningVenue.defaultVenueIds) - diningItem.venueIds = DiningVenue.defaultVenueIds + diningItem.venues = DiningAPI.instance.getVenues(with: DiningVenue.defaultVenueIds) } else { - diningItem.venues = DiningDataStore.shared.getVenues(for: venueIds) - diningItem.venueIds = venueIds + diningItem.venues = DiningAPI.instance.getVenues(with: venueIds) } reloadItem(diningItem) diff --git a/PennMobile/Home/Map/PennCoordinate.swift b/PennMobile/Home/Map/PennCoordinate.swift index d9d9940f9..dfe053ef1 100644 --- a/PennMobile/Home/Map/PennCoordinate.swift +++ b/PennMobile/Home/Map/PennCoordinate.swift @@ -45,6 +45,58 @@ class PennCoordinate { func getRegion(for facility: FitnessFacilityName, at scale: PennCoordinateScale) -> MKCoordinateRegion { return MKCoordinateRegion.init(center: getCoordinates(for: facility), latitudinalMeters: scale.rawValue, longitudinalMeters: scale.rawValue) } + + func getCoordinates(for dining: DiningVenue) -> CLLocationCoordinate2D { + switch dining.id { + case 593: + //1920 Commons + return CLLocationCoordinate2D(latitude: 39.952275, longitude: -75.199560) + case 636: + //Hill House + return CLLocationCoordinate2D(latitude: 39.953040, longitude: -75.190589) + case 637: + //English House + return CLLocationCoordinate2D(latitude: 39.954242, longitude: -75.194351) + case 638: + //Falk Kosher Dining + return CLLocationCoordinate2D(latitude: 39.953117, longitude: -75.200075) + case 639: + //Houston Market + return CLLocationCoordinate2D(latitude: 39.950920, longitude: -75.193892) + case 641: + //"Accenture Caf\u00e9" + return CLLocationCoordinate2D(latitude: 39.951827, longitude: -75.191315) + case 642: + //"Joe's Caf\u00e9" + return CLLocationCoordinate2D(latitude: 39.951818, longitude: -75.196089) + case 1442: + //"Lauder College House" + return CLLocationCoordinate2D(latitude: 39.953907, longitude: -75.191733) + case 747: + //"McClelland Express" + return CLLocationCoordinate2D(latitude: 39.950378, longitude: -75.197151) + case 1057: + //"1920 Gourmet Grocer" + return CLLocationCoordinate2D(latitude: 39.952115, longitude: -75.199492) + case 1163: + //"1920 Starbucks" + return CLLocationCoordinate2D(latitude: 39.952361, longitude: -75.199466) + case 1731: + //"LCH Retail" + return CLLocationCoordinate2D(latitude: 39.953907, longitude: -75.191733) + case 1732: + //Pret a Manger MBA + return CLLocationCoordinate2D(latitude: 39.952591, longitude: -75.198326) + default: + // case 1733: + // "Pret a Manger Locust Walk", + return CLLocationCoordinate2D(latitude: 39.952591, longitude: -75.198326) + } + } + + func getRegion(for dining: DiningVenue, at scale: PennCoordinateScale) -> MKCoordinateRegion { + return MKCoordinateRegion.init(center: getCoordinates(for: dining), latitudinalMeters: scale.rawValue, longitudinalMeters: scale.rawValue) + } } // MARK: - Placemarks diff --git a/PennMobile/Setup + Navigation/ControllerModel.swift b/PennMobile/Setup + Navigation/ControllerModel.swift index 8b587b903..8928eb3cd 100644 --- a/PennMobile/Setup + Navigation/ControllerModel.swift +++ b/PennMobile/Setup + Navigation/ControllerModel.swift @@ -41,7 +41,11 @@ class ControllerModel: NSObject { func prepare() { vcDictionary = [Feature: UIViewController]() vcDictionary[.home] = HomeViewController() - vcDictionary[.dining] = DiningViewController() + if #available(iOS 14, *) { + vcDictionary[.dining] = DiningViewControllerSwiftUI() + } else { + vcDictionary[.dining] = DiningViewController() + } vcDictionary[.studyRoomBooking] = GSRTabController() vcDictionary[.laundry] = LaundryTableViewController() vcDictionary[.more] = MoreViewController() diff --git a/PennMobile/Setup + Navigation/RootViewController.swift b/PennMobile/Setup + Navigation/RootViewController.swift index 454278b1a..b796ecd57 100644 --- a/PennMobile/Setup + Navigation/RootViewController.swift +++ b/PennMobile/Setup + Navigation/RootViewController.swift @@ -206,6 +206,7 @@ class RootViewController: UIViewController, NotificationRequestable { OAuth2NetworkManager.instance.clearCurrentAccessToken() Account.clear() GSRUser.clear() + Storage.remove(DiningInsightsAPIResponse.directory, from: .caches) } private func animateFadeTransition(to new: UIViewController, completion: (() -> Void)? = nil) { diff --git a/PennMobile/Setup + Navigation/TabBarController.swift b/PennMobile/Setup + Navigation/TabBarController.swift index 6246c73db..d0b256ba3 100644 --- a/PennMobile/Setup + Navigation/TabBarController.swift +++ b/PennMobile/Setup + Navigation/TabBarController.swift @@ -77,6 +77,15 @@ extension DiningViewController: TabBarShowable { } } +@available(iOS 14, *) +extension DiningViewControllerSwiftUI: TabBarShowable { + func getTabBarItem() -> UITabBarItem { + let normalImage = UIImage(named: "Dining_Grey") + let selectedImage = UIImage(named: "Dining_Blue") + return UITabBarItem(title: "Dining", image: normalImage, selectedImage: selectedImage) + } +} + extension GSRController: TabBarShowable { func getTabBarItem() -> UITabBarItem { let normalImage = UIImage(named: "GSR_Grey") @@ -93,6 +102,7 @@ extension GSRLocationsController: TabBarShowable { } } + extension GSRTabController: TabBarShowable { func getTabBarItem() -> UITabBarItem { let normalImage = UIImage(named: "GSR_Grey") diff --git a/PennMobile/Supporting_Files/Base.lproj/LaunchScreen.storyboard b/PennMobile/Supporting_Files/Base.lproj/LaunchScreen.storyboard index 577746a34..5b9a0f511 100644 --- a/PennMobile/Supporting_Files/Base.lproj/LaunchScreen.storyboard +++ b/PennMobile/Supporting_Files/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,9 @@ - + - + From 3eb225353f05c0db1117f64260b14ababc1ed0af Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 00:55:39 -0400 Subject: [PATCH 10/18] Add Sherie to about page --- PennMobile/About/Controllers/AboutViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PennMobile/About/Controllers/AboutViewController.swift b/PennMobile/About/Controllers/AboutViewController.swift index 0d17cd01e..33a1cc7c8 100644 --- a/PennMobile/About/Controllers/AboutViewController.swift +++ b/PennMobile/About/Controllers/AboutViewController.swift @@ -76,6 +76,7 @@ class AboutViewController : UIViewController, UICollectionViewDelegateFlowLayout let daniel = Member(firstName: "Daniel", lastName: "Duan", image: "daniel.jpg") let raunaq = Member(firstName: "Raunaq", lastName: "Singh", image: "raunaq.jpeg") let benK = Member(firstName: "Ben", lastName: "Kaufman", image: "benk.jpg") + let sherie = Member(firstName: "Sherie", lastName: "Pan", image: "sherie.jpeg") var currentMembers = [Member]() @@ -83,7 +84,7 @@ class AboutViewController : UIViewController, UICollectionViewDelegateFlowLayout //fill the arrays with members, and sort alphabetically pastMembers += [marta, grace, ben, tiff, zhilei, laura, adel, yagil, josh, dom, carin, salib, liz] - currentMembers += [rehaan, henrique, lucy, matthew, hassan, jongmin, adam, justin, raunaq, daniel, benK] + currentMembers += [rehaan, henrique, lucy, matthew, hassan, jongmin, adam, justin, raunaq, daniel, benK, sherie] pastMembers.sort(by: {$0 < $1}) currentMembers.sort(by: {$0 < $1}) From 1e1e1eaa4fbba68cf56c6d2cf9b04e0c2a24c88c Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 18:50:29 -0400 Subject: [PATCH 11/18] Enable dining detail view navigation from home dining cell --- .../SwiftUI/DiningViewModelSwiftUI.swift | 1 + .../Dining/SwiftUI/Views/DiningView.swift | 2 +- .../HomeViewController + Delegates.swift | 22 ++++++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift b/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift index 896c67dea..86afe351e 100644 --- a/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift +++ b/PennMobile/Dining/SwiftUI/DiningViewModelSwiftUI.swift @@ -10,6 +10,7 @@ import SwiftUI @available(iOS 14, *) class DiningViewModelSwiftUI: ObservableObject { + static let instance = DiningViewModelSwiftUI() @Published var diningVenues: [DiningVenue.VenueType : [DiningVenue]] = DiningAPI.instance.getSectionedVenues() @Published var diningInsights = DiningAPI.instance.getInsights() diff --git a/PennMobile/Dining/SwiftUI/Views/DiningView.swift b/PennMobile/Dining/SwiftUI/Views/DiningView.swift index d72000110..f70fd5e39 100644 --- a/PennMobile/Dining/SwiftUI/Views/DiningView.swift +++ b/PennMobile/Dining/SwiftUI/Views/DiningView.swift @@ -10,7 +10,7 @@ import SwiftUI @available(iOS 14, *) struct DiningView: View { - @StateObject var diningVM = DiningViewModelSwiftUI() + @StateObject var diningVM = DiningViewModelSwiftUI.instance var body: some View { return diff --git a/PennMobile/Home/Controllers/HomeViewController + Delegates.swift b/PennMobile/Home/Controllers/HomeViewController + Delegates.swift index a2b0d4443..ebb711746 100644 --- a/PennMobile/Home/Controllers/HomeViewController + Delegates.swift +++ b/PennMobile/Home/Controllers/HomeViewController + Delegates.swift @@ -9,6 +9,7 @@ import Foundation import WebKit import SafariServices +import SwiftUI extension HomeViewController: HomeViewModelDelegate {} @@ -95,14 +96,19 @@ extension HomeViewController { // MARK: - Dining Delegate extension HomeViewController { - func handleVenueSelected(_ venue: DiningVenue) { - if let url = venue.facilityURL { - let vc = UIViewController() - let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) - webView.load(URLRequest(url: url)) - vc.view.addSubview(webView) - vc.title = venue.name - self.navigationController?.pushViewController(vc, animated: true) + func handleVenueSelected(_ venue: DiningVenue) { + if #available(iOS 14, *) { + let hostingView = UIHostingController(rootView: DiningVenueDetailView(for: venue).environmentObject(DiningViewModelSwiftUI.instance)) + self.navigationController?.pushViewController(hostingView, animated: true) + } else { + if let url = venue.facilityURL { + let vc = UIViewController() + let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) + webView.load(URLRequest(url: url)) + vc.view.addSubview(webView) + vc.title = venue.name + self.navigationController?.pushViewController(vc, animated: true) + } } } From 8f8cce2461b510d46aa6f8768703efa57fd17051 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 22:09:49 -0400 Subject: [PATCH 12/18] Fix project set up, migrate firebase to SPM --- PennMobile.xcodeproj/project.pbxproj | 186 ++++++++------------------- Podfile | 4 - 2 files changed, 53 insertions(+), 137 deletions(-) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 2af8e426b..763e1d045 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ 212B8355222A31B500F835D6 /* HomePostCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B8354222A31B500F835D6 /* HomePostCellItem.swift */; }; 212B8357222A31C300F835D6 /* HomePostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B8356222A31C300F835D6 /* HomePostCell.swift */; }; 212B8359222A331D00F835D6 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B8358222A331D00F835D6 /* Post.swift */; }; - 212B835C222A37D000F835D6 /* HomeFeatureCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B835B222A37D000F835D6 /* HomeFeatureCellItem.swift */; }; 212B835E222A37DD00F835D6 /* HomeFeatureCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B835D222A37DD00F835D6 /* HomeFeatureCell.swift */; }; 212B8360222A37FE00F835D6 /* FeatureAnnouncement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B835F222A37FE00F835D6 /* FeatureAnnouncement.swift */; }; 2130A5062238C2F000DFEEC7 /* PennLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2130A5052238C2F000DFEEC7 /* PennLoginController.swift */; }; @@ -44,7 +43,6 @@ 21640D542010526B002F33CA /* HomeTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D532010526B002F33CA /* HomeTableViewModel.swift */; }; 21640D5C20105B98002F33CA /* HomeDiningCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5B20105B98002F33CA /* HomeDiningCell.swift */; }; 21640D5E20105BAC002F33CA /* HomeLaundryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5D20105BAC002F33CA /* HomeLaundryCell.swift */; }; - 21640D6020105BB4002F33CA /* HomeStudyRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D5F20105BB4002F33CA /* HomeStudyRoomCell.swift */; }; 21640D6220114A29002F33CA /* ControllerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640D6120114A28002F33CA /* ControllerModel.swift */; }; 21640FD3204A296D008DB6E8 /* LaundryGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640FD2204A296D008DB6E8 /* LaundryGraphView.swift */; }; 21640FD7204A5309008DB6E8 /* LaundryMachineCellTappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21640FD6204A5309008DB6E8 /* LaundryMachineCellTappable.swift */; }; @@ -182,6 +180,10 @@ 6CAE43C62533731000BD0200 /* DiningInsightsAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE43C52533731000BD0200 /* DiningInsightsAPIResponse.swift */; }; 6CAE43D3253373B900BD0200 /* DiningMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE43D2253373B900BD0200 /* DiningMenu.swift */; }; 6CAE43D82533740300BD0200 /* LocalJSONStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAE43D72533740300BD0200 /* LocalJSONStore.swift */; }; + 6CE12F9026E82DC600284D9F /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE12F8F26E82DC600284D9F /* FirebaseAnalytics */; }; + 6CE12F9226E82DC600284D9F /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE12F9126E82DC600284D9F /* FirebaseCrashlytics */; }; + 6CFA06F626E8352F00944B8E /* HomeStudyRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA06F526E8352F00944B8E /* HomeStudyRoomCell.swift */; }; + 6CFA06F826E8355400944B8E /* HomeFeatureCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA06F726E8355400944B8E /* HomeFeatureCellItem.swift */; }; 97AA806923D26BC700C23488 /* DiningBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */; }; 97AA806A23D26BC700C23488 /* TransactionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806523D26BC700C23488 /* TransactionTableViewCell.swift */; }; 97AA806B23D26BC700C23488 /* DiningHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AA806623D26BC700C23488 /* DiningHeaderView.swift */; }; @@ -356,7 +358,6 @@ 212B8354222A31B500F835D6 /* HomePostCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePostCellItem.swift; sourceTree = ""; }; 212B8356222A31C300F835D6 /* HomePostCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePostCell.swift; sourceTree = ""; }; 212B8358222A331D00F835D6 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; - 212B835B222A37D000F835D6 /* HomeFeatureCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeatureCellItem.swift; sourceTree = ""; }; 212B835D222A37DD00F835D6 /* HomeFeatureCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeatureCell.swift; sourceTree = ""; }; 212B835F222A37FE00F835D6 /* FeatureAnnouncement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureAnnouncement.swift; sourceTree = ""; }; 2130A5052238C2F000DFEEC7 /* PennLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PennLoginController.swift; sourceTree = ""; }; @@ -379,7 +380,6 @@ 21640D532010526B002F33CA /* HomeTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTableViewModel.swift; sourceTree = ""; }; 21640D5B20105B98002F33CA /* HomeDiningCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeDiningCell.swift; sourceTree = ""; }; 21640D5D20105BAC002F33CA /* HomeLaundryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeLaundryCell.swift; sourceTree = ""; }; - 21640D5F20105BB4002F33CA /* HomeStudyRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeStudyRoomCell.swift; sourceTree = ""; }; 21640D6120114A28002F33CA /* ControllerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerModel.swift; sourceTree = ""; }; 21640FD2204A296D008DB6E8 /* LaundryGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaundryGraphView.swift; sourceTree = ""; }; 21640FD6204A5309008DB6E8 /* LaundryMachineCellTappable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaundryMachineCellTappable.swift; sourceTree = ""; }; @@ -519,6 +519,8 @@ 6CAE43C52533731000BD0200 /* DiningInsightsAPIResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningInsightsAPIResponse.swift; sourceTree = ""; }; 6CAE43D2253373B900BD0200 /* DiningMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningMenu.swift; sourceTree = ""; }; 6CAE43D72533740300BD0200 /* LocalJSONStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalJSONStore.swift; sourceTree = ""; }; + 6CFA06F526E8352F00944B8E /* HomeStudyRoomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeStudyRoomCell.swift; sourceTree = ""; }; + 6CFA06F726E8355400944B8E /* HomeFeatureCellItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeFeatureCellItem.swift; sourceTree = ""; }; 72787B1E070BFCDF84D8C3CA /* Pods-PennMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PennMobile.debug.xcconfig"; path = "Target Support Files/Pods-PennMobile/Pods-PennMobile.debug.xcconfig"; sourceTree = ""; }; 97AA806423D26BC700C23488 /* DiningBalanceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiningBalanceCell.swift; sourceTree = ""; }; 97AA806523D26BC700C23488 /* TransactionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionTableViewCell.swift; sourceTree = ""; }; @@ -653,10 +655,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6CE12F9026E82DC600284D9F /* FirebaseAnalytics in Frameworks */, F2568A762413534F00561295 /* SnapKit in Frameworks */, 6C4CC1FA26E6B1720000B4A8 /* SwiftyJSON in Frameworks */, F213CCE723C3F240000AD90F /* Kingfisher in Frameworks */, F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */, + 6CE12F9226E82DC600284D9F /* FirebaseCrashlytics in Frameworks */, F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */, 56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */, ); @@ -712,7 +716,7 @@ 212B835A222A37BD00F835D6 /* Feature */ = { isa = PBXGroup; children = ( - 212B835B222A37D000F835D6 /* HomeFeatureCellItem.swift */, + 6CFA06F726E8355400944B8E /* HomeFeatureCellItem.swift */, 212B835D222A37DD00F835D6 /* HomeFeatureCell.swift */, 212B835F222A37FE00F835D6 /* FeatureAnnouncement.swift */, ); @@ -1007,7 +1011,7 @@ isa = PBXGroup; children = ( 217A7837204D2E2B004F1227 /* HomeGSRCellItem.swift */, - 21640D5F20105BB4002F33CA /* HomeStudyRoomCell.swift */, + 6CFA06F526E8352F00944B8E /* HomeStudyRoomCell.swift */, B6FE7A01207BE29000F60838 /* HomeGSRBookingButton.swift */, B6FC5B4E20795584002AF335 /* Views */, ); @@ -1713,9 +1717,9 @@ 2166405C1EBADADA00746B8E /* Sources */, 2166405D1EBADADA00746B8E /* Frameworks */, 2166405E1EBADADA00746B8E /* Resources */, - EF1CF2F322177EED002D912D /* ShellScript */, F2568A6E2411F20E00561295 /* Embed App Extensions */, CAF133B2272BB899064B2A46 /* [CP] Embed Pods Frameworks */, + 6CE12F9326E82DED00284D9F /* ShellScript */, ); buildRules = ( ); @@ -1729,6 +1733,8 @@ F213CCE623C3F240000AD90F /* Kingfisher */, F2568A752413534F00561295 /* SnapKit */, 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */, + 6CE12F8F26E82DC600284D9F /* FirebaseAnalytics */, + 6CE12F9126E82DC600284D9F /* FirebaseCrashlytics */, ); productName = PennMobile; productReference = 216640601EBADADA00746B8E /* PennMobile.app */; @@ -1820,6 +1826,7 @@ F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */, F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -1924,6 +1931,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 6CE12F9326E82DED00284D9F /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n${BUILD_DIR%Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\n"; + }; CAF133B2272BB899064B2A46 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 12; @@ -1932,44 +1956,26 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-PennMobile/Pods-PennMobile-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Base32/Base32.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/OneTimePassword/OneTimePassword.framework", "${BUILT_PRODUCTS_DIR}/SCLAlertView/SCLAlertView.framework", "${BUILT_PRODUCTS_DIR}/ScrollableGraphView/ScrollableGraphView.framework", "${BUILT_PRODUCTS_DIR}/XLPagerTabStrip/XLPagerTabStrip.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Base32.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MBProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OneTimePassword.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SCLAlertView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ScrollableGraphView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/XLPagerTabStrip.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PennMobile/Pods-PennMobile-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - EF1CF2F322177EED002D912D /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "$(SRCROOT)/PennMobile/Supporting_Files/Info.plist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${PODS_ROOT}/Fabric/run\"\n"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -2037,12 +2043,8 @@ 6CAE438B253370E600BD0200 /* VariableStepLineGraphView.swift in Sources */, 21B8BBB82223963C00EBC1D0 /* Degree.swift in Sources */, 6CAE4390253370E600BD0200 /* DiningVenueView.swift in Sources */, - 21640D6020105BB4002F33CA /* HomeStudyRoomCell.swift in Sources */, - 212B835C222A37D000F835D6 /* HomeFeatureCellItem.swift in Sources */, 6CAE4383253370E500BD0200 /* DailyAverageView.swift in Sources */, 6C369A1526E39BC100721CA1 /* (null) in Sources */, - 21640D6020105BB4002F33CA /* HomeStudyRoomCell.swift in Sources */, - 212B835C222A37D000F835D6 /* HomeFeatureCellItem.swift in Sources */, 2119D26922529A2300693CDB /* HomeGSRLocationsCell.swift in Sources */, 21D5E07623BD45F400B331CC /* KeychainAccessible.swift in Sources */, 21470EBC223DD34200019C10 /* ScheduleEventCell.swift in Sources */, @@ -2140,6 +2142,7 @@ C10A59A62183CE120059130B /* AboutPageCollectionViewHeader.swift in Sources */, B67881D8211CBF2A000DA750 /* MenuTableView.swift in Sources */, EFE2D700239B124D0020F6BF /* GSRGroupUser.swift in Sources */, + 6CFA06F626E8352F00944B8E /* HomeStudyRoomCell.swift in Sources */, 21FBC240228514ED00B432D8 /* FeedAnalyticsEngine.swift in Sources */, 21ABE2AF223D7E2B00199D29 /* Time.swift in Sources */, 21A6B6D42216824C003A357D /* ReservationCell.swift in Sources */, @@ -2202,6 +2205,7 @@ 2189C0872027CDD700771C1F /* GSRNetworkManager.swift in Sources */, 2138D55522598AA800D67CA2 /* GSRTabController.swift in Sources */, 211DFE541F36A02C00CB73E4 /* DiningAPI.swift in Sources */, + 6CFA06F826E8355400944B8E /* HomeFeatureCellItem.swift in Sources */, 21640D5E20105BAC002F33CA /* HomeLaundryCell.swift in Sources */, F27AA01A23BC6D1400276C4F /* PrivacyPermissionDelegate.swift in Sources */, F212BE8A23B71C8500ED46A1 /* NotificationsTableViewCell.swift in Sources */, @@ -2450,58 +2454,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 6.6.0; - OTHER_LDFLAGS = ( - "$(inherited)", - "-ObjC", - "-l\"c++\"", - "-l\"sqlite3\"", - "-l\"xml2\"", - "-l\"z\"", - "-framework", - "\"Base32\"", - "-framework", - "\"CoreGraphics\"", - "-framework", - "\"Crashlytics\"", - "-framework", - "\"Fabric\"", - "-framework", - "\"FirebaseAnalytics\"", - "-framework", - "\"FirebaseCore\"", - "-framework", - "\"FirebaseCoreDiagnostics\"", - "-framework", - "\"FirebaseInstanceID\"", - "-framework", - "\"FirebaseNanoPB\"", - "-framework", - "\"Foundation\"", - "-framework", - "\"GoogleToolboxForMac\"", - "-framework", - "\"MBProgressHUD\"", - "-framework", - "\"OneTimePassword\"", - "-framework", - "\"Security\"", - "-framework", - "\"StoreKit\"", - "-framework", - "\"SystemConfiguration\"", - "-framework", - "\"UIKit\"", - "-framework", - "\"XLPagerTabStrip\"", - "-framework", - "\"nanopb\"", - "-weak_framework", - CryptoKit, - "-weak_framework", - Combine, - "-weak_framework", - SwiftUI, - ); + OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = org.pennlabs.PennMobile; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2535,58 +2488,7 @@ ); MARKETING_VERSION = 6.6.0; ONLY_ACTIVE_ARCH = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - "-ObjC", - "-l\"c++\"", - "-l\"sqlite3\"", - "-l\"xml2\"", - "-l\"z\"", - "-framework", - "\"Base32\"", - "-framework", - "\"CoreGraphics\"", - "-framework", - "\"Crashlytics\"", - "-framework", - "\"Fabric\"", - "-framework", - "\"FirebaseAnalytics\"", - "-framework", - "\"FirebaseCore\"", - "-framework", - "\"FirebaseCoreDiagnostics\"", - "-framework", - "\"FirebaseInstanceID\"", - "-framework", - "\"FirebaseNanoPB\"", - "-framework", - "\"Foundation\"", - "-framework", - "\"GoogleToolboxForMac\"", - "-framework", - "\"MBProgressHUD\"", - "-framework", - "\"OneTimePassword\"", - "-framework", - "\"Security\"", - "-framework", - "\"StoreKit\"", - "-framework", - "\"SystemConfiguration\"", - "-framework", - "\"UIKit\"", - "-framework", - "\"XLPagerTabStrip\"", - "-framework", - "\"nanopb\"", - "-weak_framework", - CryptoKit, - "-weak_framework", - Combine, - "-weak_framework", - SwiftUI, - ); + OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = org.pennlabs.PennMobile; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2757,6 +2659,14 @@ minimumVersion = 5.0.1; }; }; + 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.6.1; + }; + }; F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup"; @@ -2789,6 +2699,16 @@ package = 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; + 6CE12F8F26E82DC600284D9F /* FirebaseAnalytics */ = { + isa = XCSwiftPackageProductDependency; + package = 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalytics; + }; + 6CE12F9126E82DC600284D9F /* FirebaseCrashlytics */ = { + isa = XCSwiftPackageProductDependency; + package = 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseCrashlytics; + }; F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; package = F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */; diff --git a/Podfile b/Podfile index 64c6405de..d2423e0e3 100644 --- a/Podfile +++ b/Podfile @@ -13,10 +13,6 @@ pod 'MBProgressHUD', '~> 0.8' # old objc library, should be replaced pod 'SCLAlertView' pod 'ScrollableGraphView' -pod 'Fabric', '~> 1.10.2' # Required by Firebase. -pod 'Crashlytics', '~> 3.14.0' # Required by Firebase. -pod 'Firebase', '~> 4.7' # Firebase not yet supported by SPM. May be a while. - pod 'XLPagerTabStrip', '~> 9.0' # Only used for GSR, should be deleted # WKZombie should be moved to SPM eventually, but something is broken with the current SPM implementation From 55d5d0977a67dc06b47464bc22c7892e3609c210 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 22:11:23 -0400 Subject: [PATCH 13/18] Enable tracking --- .../General/Generics/GenericControllers.swift | 27 ++++++++++--------- .../FirebaseAnalyticsManager.swift | 4 ++- .../Setup + Navigation/AppDelegate.swift | 3 +-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/PennMobile/General/Generics/GenericControllers.swift b/PennMobile/General/Generics/GenericControllers.swift index 046253d2d..f2947c9a5 100755 --- a/PennMobile/General/Generics/GenericControllers.swift +++ b/PennMobile/General/Generics/GenericControllers.swift @@ -8,7 +8,7 @@ import UIKit -@objc class GenericTableViewController: UITableViewController, Trackable { +class GenericTableViewController: UITableViewController, Trackable { var navigationVC: HomeNavigationController? @@ -45,7 +45,14 @@ import UIKit } var screenName: String? - var trackScreen: Bool = false + + var trackScreen: Bool { + #if DEBUG + return false + #else + return true + #endif + } } class GenericViewController: UIViewController, Trackable { @@ -87,18 +94,14 @@ class GenericViewController: UIViewController, Trackable { } var screenName: String? - var trackScreen: Bool = false - - var isPanEnabled: Bool = true { - didSet { - //pan(enabled: isPanEnabled) - } + var trackScreen: Bool { + #if DEBUG + return false + #else + return true + #endif } - /*private func pan(enabled: Bool) { - revealViewController()?.panGestureRecognizer().isEnabled = enabled - }*/ - func removeMenuButton() { self.navigationItem.leftBarButtonItem = nil } diff --git a/PennMobile/General/Networking + Analytics/FirebaseAnalyticsManager.swift b/PennMobile/General/Networking + Analytics/FirebaseAnalyticsManager.swift index 92293c6da..f51e128d1 100644 --- a/PennMobile/General/Networking + Analytics/FirebaseAnalyticsManager.swift +++ b/PennMobile/General/Networking + Analytics/FirebaseAnalyticsManager.swift @@ -13,7 +13,9 @@ class FirebaseAnalyticsManager: NSObject { static let shared = FirebaseAnalyticsManager() private override init() {} - @objc func trackScreen(_ name: String) { + func trackScreen(_ name: String) { + Analytics.logEvent(AnalyticsEventScreenView, + parameters: [AnalyticsParameterScreenName: name]) } func trackEvent(action: EventAction, result: EventResult, content: Any) { diff --git a/PennMobile/Setup + Navigation/AppDelegate.swift b/PennMobile/Setup + Navigation/AppDelegate.swift index 321a99456..eca74b59a 100644 --- a/PennMobile/Setup + Navigation/AppDelegate.swift +++ b/PennMobile/Setup + Navigation/AppDelegate.swift @@ -8,8 +8,7 @@ import UIKit import UserNotifications -import FirebaseCore -import FirebaseInstanceID +import Firebase import StoreKit @UIApplicationMain From ae3c8b4b6c1c6e3be6f45dccf4e26e864bdf1b55 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 22:12:41 -0400 Subject: [PATCH 14/18] Improved tracking --- .../SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift | 1 + PennMobile/Home/Cells/News/HomeNewsCell.swift | 1 + PennMobile/Home/Controllers/HomeViewController.swift | 2 +- PennMobile/Laundry/Controllers/LaundryTableViewController.swift | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift index 899aeaf51..064293336 100644 --- a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/MenuDisclosureGroup.swift @@ -30,6 +30,7 @@ struct DiningMenuSectionRow: View { .contentShape(Rectangle()) .padding(.bottom) .onTapGesture { + FirebaseAnalyticsManager.shared.trackEvent(action: "Open Menu", result: title, content: "") withAnimation { isExpanded.toggle() } diff --git a/PennMobile/Home/Cells/News/HomeNewsCell.swift b/PennMobile/Home/Cells/News/HomeNewsCell.swift index 5a8b60d97..4fee45165 100644 --- a/PennMobile/Home/Cells/News/HomeNewsCell.swift +++ b/PennMobile/Home/Cells/News/HomeNewsCell.swift @@ -110,6 +110,7 @@ extension HomeNewsCell { @objc fileprivate func handleTapped(_ sender: Any) { guard let delegate = delegate as? URLSelectable else { return } + FirebaseAnalyticsManager.shared.trackEvent(action: "News Cell Pressed", result: article.title, content: "") delegate.handleUrlPressed(urlStr: article.articleUrl, title: article.source, item: self.item, shouldLog: true) } } diff --git a/PennMobile/Home/Controllers/HomeViewController.swift b/PennMobile/Home/Controllers/HomeViewController.swift index 50bda399a..84264f478 100644 --- a/PennMobile/Home/Controllers/HomeViewController.swift +++ b/PennMobile/Home/Controllers/HomeViewController.swift @@ -28,8 +28,8 @@ class HomeViewController: GenericViewController { override func viewDidLoad() { super.viewDidLoad() self.title = "Home" + screenName = "Home" view.backgroundColor = .uiBackground - trackScreen = true prepareLoadingView() prepareTableView() diff --git a/PennMobile/Laundry/Controllers/LaundryTableViewController.swift b/PennMobile/Laundry/Controllers/LaundryTableViewController.swift index efa5f77a4..75609a662 100644 --- a/PennMobile/Laundry/Controllers/LaundryTableViewController.swift +++ b/PennMobile/Laundry/Controllers/LaundryTableViewController.swift @@ -20,6 +20,7 @@ class LaundryTableViewController: GenericTableViewController, IndicatorEnabled, override func viewDidLoad() { super.viewDidLoad() + screenName = "Laundry" tableView.backgroundView = nil tableView.separatorStyle = .none tableView.dataSource = self From 83fa6a0ddf9937e11df3dddc9f3fdbefae9c3e77 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 22:18:17 -0400 Subject: [PATCH 15/18] Final fixes for Dining Redesign --- .../Detail View/DiningVenueDetailView.swift | 196 ++++++------------ PennMobile/General/Colors.swift | 2 - Podfile.lock | 46 +--- 3 files changed, 67 insertions(+), 177 deletions(-) diff --git a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift index e5a630646..9ce6f95e2 100644 --- a/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift +++ b/PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift @@ -6,23 +6,15 @@ // Copyright © 2020 PennLabs. All rights reserved. // -#if canImport(SwiftUI) import SwiftUI import KingfisherSwiftUI -#endif +import FirebaseAnalytics @available(iOS 14, *) struct DiningVenueDetailView: View { - let safeFrameHeight: CGFloat - let customNavBarHeight: CGFloat - init(for venue: DiningVenue) { self.venue = venue - - let window = UIApplication.shared.windows[0] - safeFrameHeight = window.safeAreaLayoutGuide.layoutFrame.minY - customNavBarHeight = 44 + safeFrameHeight } private let venue: DiningVenue @@ -31,26 +23,73 @@ struct DiningVenueDetailView: View { @Environment(\.presentationMode) var presentationMode: Binding @EnvironmentObject var diningVM: DiningViewModelSwiftUI @State private var pickerIndex = 0 - - @State private var headerImageHeight: CGFloat = 300 - + var body: some View { GeometryReader { fullGeo in + let imageHeight = fullGeo.size.height * 4/9 + let statusBarHeight = fullGeo.safeAreaInsets.top + ScrollView { - image - .zIndex(2) + GeometryReader { geometry in + let minY = geometry.frame(in: .global).minY + let remain = imageHeight + minY + + ZStack(alignment: .bottomLeading) { + KFImage(self.venue.imageURL) + .resizable() + .scaledToFill() + .frame(width: geometry.size.width, height: imageHeight + max(0, minY)) + .offset(y: min(0, minY) * -2/3) + .allowsHitTesting(false) + .clipped() + + LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .center, endPoint: .bottom) + + VStack(alignment: .leading) { + Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Image(systemName: "chevron.left") + .font(.system(size: 20, weight: .light)) + } + .foregroundColor(.white) + .padding(10) + .background(Circle().opacity(0.8).foregroundColor(.black)) + .position(x: 40, y: statusBarHeight + 22) + .offset(y: -min(0, minY)) + + Spacer() + + Text(venue.name) + .padding() + .foregroundColor(.white) + .font(.system(size: 40, weight: .bold)) + .minimumScaleFactor(0.2) + .lineLimit(1) + }.opacity(1 - Double(minY)/60) + + VStack { + DefaultNavigationBar(title: venue.name) + .frame(height: 44 + statusBarHeight) + .opacity(Double(-1/20 * (remain - (64 + statusBarHeight)))) + + Spacer() + }.offset(y: -min(0, minY)) + } + .offset(y: -max(0, minY)) + } + .frame(height: imageHeight) + .zIndex(2) - Group { + VStack(spacing: 10) { Picker("Section", selection: self.$pickerIndex) { ForEach(0 ..< self.sectionTitle.count) { Text(self.sectionTitle[$0]) } } .pickerStyle(SegmentedPickerStyle()) - .padding(.top, 5) Divider() - .padding(.vertical, 5) VStack { if self.pickerIndex == 0 { @@ -64,126 +103,22 @@ struct DiningVenueDetailView: View { Spacer() }.frame(minHeight: fullGeo.size.height - 80) }.padding(.horizontal) - } - .edgesIgnoringSafeArea(.top) + .edgesIgnoringSafeArea(.all) .navigationBarHidden(true) - .onAppear(perform: { + .onAppear { + FirebaseAnalyticsManager.shared.trackScreen("Venue Detail View") diningVM.refreshMenu(for: venue.id) - headerImageHeight = fullGeo.frame(in: .global).height * 4/9 - }) - } - } - - var image : some View { - GeometryReader { geometry in - ZStack(alignment: .bottomLeading) { - KFImage(self.venue.imageURL) - .resizable() - .scaledToFill() - .frame(width: geometry.size.width, height: getHeightForHeaderImage(geometry)) - .offset(x: 0, y: getParallaxOffset(geometry)) - .clipped() - .overlay(LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .center, endPoint: .bottom)) - .allowsHitTesting(false) - - VStack(alignment: .leading) { - Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Image(systemName: "chevron.left") - .font(.system(size: 20, weight: .light)) - } - .foregroundColor(.white) - .padding(10) - .background(Circle().opacity(0.8).foregroundColor(.black)) - .opacity(getOpacity(geometry)) - .position(x: 40, y: customNavBarHeight - 20) - .offset(x: 0, y: getBackButtonYOffset(geometry)) - - Spacer() - - Text(venue.name) - .padding() - .foregroundColor(.white) - .font(.system(size: 40, weight: .bold)) - .minimumScaleFactor(0.2) - .lineLimit(1) - .opacity(getOpacity(geometry)) - - } - - DefaultNavigationBar(presentationMode: _presentationMode, height: customNavBarHeight, width: geometry.size.width, title: venue.name) - .offset(x:0, y:getOffsetForNavBar(geometry)) - .opacity(getOpacityForNavBar(geometry)) } - .offset(x: 0, y: getOffsetForHeaderImage(geometry)) } - .frame(height: headerImageHeight) - } -} - - -// MARK: - Calculations for offsets + opacity -@available(iOS 14, *) -extension DiningVenueDetailView { - - private func getBackButtonYOffset(_ geometry: GeometryProxy) -> CGFloat { - let offset = getOffset(geometry) - - return offset < 0 ? -offset : 0 - } - - private func getOffset(_ geometry: GeometryProxy) -> CGFloat { - return geometry.frame(in: .global).minY - } - - private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { - let offset = getOffset(geometry) - - return offset > 0 ? -offset : 0 - } - - private func getParallaxOffset(_ geometry: GeometryProxy) -> CGFloat { - let offset = getOffset(geometry) - - return offset < 0 ? -offset/1.3 : 0 - } - - private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { - let offset = getOffset(geometry) - - return offset > 0 ? headerImageHeight + offset : headerImageHeight - } - - private func getOpacity(_ geometry: GeometryProxy) -> Double { - let offset = getOffset(geometry) - - return offset > 0 ? Double(1 - offset/headerImageHeight * 4) : 1.0 - } - - private func getOffsetForNavBar(_ geometry: GeometryProxy) -> CGFloat { - return -getOffset(geometry) - headerImageHeight + customNavBarHeight - } - - private func getOpacityForNavBar(_ geometry: GeometryProxy) -> Double { - let offset = getOffset(geometry) - - if -offset > 0.6 * headerImageHeight { - return Double((-offset/headerImageHeight - 0.6) * 8) - } - - return 0.0 } } @available(iOS 14.0, *) struct DefaultNavigationBar: View { - @Environment(\.presentationMode) var presentationMode: Binding - - var height: CGFloat - var width: CGFloat + @Environment(\.presentationMode) var presentationMode + var title: String var body: some View { @@ -194,11 +129,13 @@ struct DefaultNavigationBar: View { Spacer() HStack { - Button("Back") { + Button(action: { self.presentationMode.wrappedValue.dismiss() + }) { + Text("Back") + .frame(width: 75, height: 44, alignment: .center) + .contentShape(Rectangle()) } - .frame(width: 70, height: 44) - .contentShape(Rectangle()) Spacer() } @@ -210,7 +147,6 @@ struct DefaultNavigationBar: View { .frame(height: 44) } } - .frame(width: width, height: height) } } diff --git a/PennMobile/General/Colors.swift b/PennMobile/General/Colors.swift index a055774e7..dab18db87 100644 --- a/PennMobile/General/Colors.swift +++ b/PennMobile/General/Colors.swift @@ -99,9 +99,7 @@ extension UIColor { } -#if canImport(SwiftUI) import SwiftUI -#endif @available(iOS 14, *) extension Color { diff --git a/Podfile.lock b/Podfile.lock index 1e75c0380..14376c527 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,31 +1,6 @@ PODS: - Base32 (1.1.2) - - Crashlytics (3.14.0): - - Fabric (~> 1.10.2) - - Fabric (1.10.2) - - Firebase (4.13.0): - - Firebase/Core (= 4.13.0) - - Firebase/Core (4.13.0): - - FirebaseAnalytics (= 4.2.0) - - FirebaseCore (= 4.0.20) - - FirebaseAnalytics (4.2.0): - - FirebaseCore (~> 4.0) - - FirebaseInstanceID (~> 2.0) - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - nanopb (~> 0.3) - - FirebaseCore (4.0.20): - - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - - FirebaseInstanceID (2.0.10): - - FirebaseCore (~> 4.0) - - GoogleToolboxForMac/Defines (2.2.2) - - "GoogleToolboxForMac/NSData+zlib (2.2.2)": - - GoogleToolboxForMac/Defines (= 2.2.2) - MBProgressHUD (0.9.2) - - nanopb (0.3.9011): - - nanopb/decode (= 0.3.9011) - - nanopb/encode (= 0.3.9011) - - nanopb/decode (0.3.9011) - - nanopb/encode (0.3.9011) - OneTimePassword (3.2.0): - Base32 (~> 1.1.2) - SCLAlertView (0.8) @@ -34,9 +9,6 @@ PODS: - XLPagerTabStrip (9.0.0) DEPENDENCIES: - - Crashlytics (~> 3.14.0) - - Fabric (~> 1.10.2) - - Firebase (~> 4.7) - MBProgressHUD (~> 0.8) - OneTimePassword (~> 3.2) - SCLAlertView @@ -47,15 +19,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - Base32 - - Crashlytics - - Fabric - - Firebase - - FirebaseAnalytics - - FirebaseCore - - FirebaseInstanceID - - GoogleToolboxForMac - MBProgressHUD - - nanopb - OneTimePassword - SCLAlertView - ScrollableGraphView @@ -64,21 +28,13 @@ SPEC REPOS: SPEC CHECKSUMS: Base32: 163c298644d184e89ca4e00a996bad6bf5166390 - Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df - Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 - Firebase: 5ec5e863d269d82d66b4bf56856726f8fb8f0fb3 - FirebaseAnalytics: 7ef69e76a5142f643aeb47c780e1cdce4e23632e - FirebaseCore: 90cb1c53d69b556f112a1bf72b5fcfaad7650790 - FirebaseInstanceID: 8d20d890d65c917f9f7d9950b6e10a760ad34321 - GoogleToolboxForMac: 800648f8b3127618c1b59c7f97684427630c5ea3 MBProgressHUD: 1569cf7ace17a8bac47aabfbb8580a49690386d1 - nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd OneTimePassword: c00ecc908e67e6c5bfda9d50b0b2e4696d0b066e SCLAlertView: 6a77bb2edfc65e04dbe57725546cb4107a506b85 ScrollableGraphView: 4af35d4ea9ecdec665d34a74da51da161ddff8ac SimulatorStatusMagic: 28d4a9d1a500ac7cea0b2b5a43c1c6ddb40ba56c XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 -PODFILE CHECKSUM: 5614727a7fd3c150fce595bdb18a15fc6b091712 +PODFILE CHECKSUM: 9e5e28b991a790112963ed77c95535421e3a89ff COCOAPODS: 1.10.1 From 7524420b0c9eb5cf143a5f0d6a62328811ea6301 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 22:21:22 -0400 Subject: [PATCH 16/18] Added Eli to about page --- PennMobile/About/Controllers/AboutViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PennMobile/About/Controllers/AboutViewController.swift b/PennMobile/About/Controllers/AboutViewController.swift index 33a1cc7c8..8a0c85b7e 100644 --- a/PennMobile/About/Controllers/AboutViewController.swift +++ b/PennMobile/About/Controllers/AboutViewController.swift @@ -77,6 +77,7 @@ class AboutViewController : UIViewController, UICollectionViewDelegateFlowLayout let raunaq = Member(firstName: "Raunaq", lastName: "Singh", image: "raunaq.jpeg") let benK = Member(firstName: "Ben", lastName: "Kaufman", image: "benk.jpg") let sherie = Member(firstName: "Sherie", lastName: "Pan", image: "sherie.jpeg") + let eli = Member(firstName: "Eli", lastName: "Nathan", image: "eli.jpeg") var currentMembers = [Member]() @@ -84,7 +85,7 @@ class AboutViewController : UIViewController, UICollectionViewDelegateFlowLayout //fill the arrays with members, and sort alphabetically pastMembers += [marta, grace, ben, tiff, zhilei, laura, adel, yagil, josh, dom, carin, salib, liz] - currentMembers += [rehaan, henrique, lucy, matthew, hassan, jongmin, adam, justin, raunaq, daniel, benK, sherie] + currentMembers += [rehaan, henrique, lucy, matthew, hassan, jongmin, adam, justin, raunaq, daniel, benK, sherie, eli] pastMembers.sort(by: {$0 < $1}) currentMembers.sort(by: {$0 < $1}) From 4c1fb1b7bf51d5dd116390826db7ed9ab39bb025 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Tue, 7 Sep 2021 23:09:18 -0400 Subject: [PATCH 17/18] Revert "Merge branch 'master' into development" This reverts commit 0bd2224b61c5d23949e12478132a86208f351428, reversing changes made to 7524420b0c9eb5cf143a5f0d6a62328811ea6301. --- Gemfile.lock | 4 +- PennMobile.xcodeproj/project.pbxproj | 123 --------------------------- 2 files changed, 2 insertions(+), 125 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9b232c38b..444f80dba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,8 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.0) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.6.0) + public_suffix (>= 2.0.2, < 4.0) atomos (0.1.3) babosa (1.0.2) claide (1.0.2) diff --git a/PennMobile.xcodeproj/project.pbxproj b/PennMobile.xcodeproj/project.pbxproj index 153b6bdc5..763e1d045 100644 --- a/PennMobile.xcodeproj/project.pbxproj +++ b/PennMobile.xcodeproj/project.pbxproj @@ -662,16 +662,6 @@ F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */, 6CE12F9226E82DC600284D9F /* FirebaseCrashlytics in Frameworks */, F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */, -======= - 6C369A1326E299A900721CA1 /* Alamofire in Frameworks */, - F213CCED23C3F6A8000AD90F /* SwiftyJSON in Frameworks */, - F2568A762413534F00561295 /* SnapKit in Frameworks */, - F213CCF023C3F99B000AD90F /* ScrollableGraphView in Frameworks */, - F213CCE723C3F240000AD90F /* Kingfisher in Frameworks */, - F213CCE523C3F240000AD90F /* KingfisherSwiftUI in Frameworks */, - F213CCE223C3EE3E000AD90F /* SwiftSoup in Frameworks */, - F213CCEA23C3F5D5000AD90F /* SCLAlertView in Frameworks */, ->>>>>>> master 56D74230B1B43DAF260BCCBE /* Pods_PennMobile.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1098,7 +1088,6 @@ 2138E1F72252AFB500E4055A /* GSRLocationCell.swift */, 2189C0892027CE2600771C1F /* RoomCell.swift */, 21A6B6D32216824B003A357D /* ReservationCell.swift */, - 6C23AF9426E57903002F60F0 /* EmptyView.swift */, C1D90F1C2220A25700DAB8EE /* NoReservationsCell.swift */, 6C6035FB26E723240025FBC7 /* EmptyView.swift */, ); @@ -1746,13 +1735,6 @@ 6C4CC1F926E6B1720000B4A8 /* SwiftyJSON */, 6CE12F8F26E82DC600284D9F /* FirebaseAnalytics */, 6CE12F9126E82DC600284D9F /* FirebaseCrashlytics */, -======= - F213CCE923C3F5D5000AD90F /* SCLAlertView */, - F213CCEC23C3F6A8000AD90F /* SwiftyJSON */, - F213CCEF23C3F99B000AD90F /* ScrollableGraphView */, - F2568A752413534F00561295 /* SnapKit */, - 6C369A1226E299A900721CA1 /* Alamofire */, ->>>>>>> master ); productName = PennMobile; productReference = 216640601EBADADA00746B8E /* PennMobile.app */; @@ -1845,13 +1827,6 @@ F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, 6C4CC1F826E6B1720000B4A8 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 6CE12F8E26E82DC600284D9F /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, -======= - F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */, - F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */, - F2568A742413534F00561295 /* XCRemoteSwiftPackageReference "SnapKit" */, - 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */, ->>>>>>> master ); productRefGroup = 216640611EBADADA00746B8E /* Products */; projectDirPath = ""; @@ -2514,62 +2489,6 @@ MARKETING_VERSION = 6.6.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = "-ObjC"; -======= - MARKETING_VERSION = 6.5.6; - ONLY_ACTIVE_ARCH = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - "-ObjC", - "-l\"c++\"", - "-l\"sqlite3\"", - "-l\"xml2\"", - "-l\"z\"", - "-framework", - "\"Base32\"", - "-framework", - "\"CoreGraphics\"", - "-framework", - "\"Crashlytics\"", - "-framework", - "\"Fabric\"", - "-framework", - "\"FirebaseAnalytics\"", - "-framework", - "\"FirebaseCore\"", - "-framework", - "\"FirebaseCoreDiagnostics\"", - "-framework", - "\"FirebaseInstanceID\"", - "-framework", - "\"FirebaseNanoPB\"", - "-framework", - "\"Foundation\"", - "-framework", - "\"GoogleToolboxForMac\"", - "-framework", - "\"MBProgressHUD\"", - "-framework", - "\"OneTimePassword\"", - "-framework", - "\"Security\"", - "-framework", - "\"StoreKit\"", - "-framework", - "\"SystemConfiguration\"", - "-framework", - "\"UIKit\"", - "-framework", - "\"XLPagerTabStrip\"", - "-framework", - "\"nanopb\"", - "-weak_framework", - CryptoKit, - "-weak_framework", - Combine, - "-weak_framework", - SwiftUI, - ); ->>>>>>> master PRODUCT_BUNDLE_IDENTIFIER = org.pennlabs.PennMobile; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2749,9 +2668,6 @@ }; }; F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { -======= - F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { ->>>>>>> master isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/scinfu/SwiftSoup"; requirement = { @@ -2760,9 +2676,6 @@ }; }; F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { -======= - F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */ = { ->>>>>>> master isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { @@ -2810,42 +2723,6 @@ isa = XCSwiftPackageProductDependency; package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; -======= - 6C369A1226E299A900721CA1 /* Alamofire */ = { - isa = XCSwiftPackageProductDependency; - package = 6C369A1126E299A900721CA1 /* XCRemoteSwiftPackageReference "Alamofire" */; - productName = Alamofire; - }; - F213CCE123C3EE3E000AD90F /* SwiftSoup */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCE023C3EE3E000AD90F /* XCRemoteSwiftPackageReference "SwiftSoup" */; - productName = SwiftSoup; - }; - F213CCE423C3F240000AD90F /* KingfisherSwiftUI */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = KingfisherSwiftUI; - }; - F213CCE623C3F240000AD90F /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCE323C3F240000AD90F /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; - F213CCE923C3F5D5000AD90F /* SCLAlertView */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCE823C3F5D5000AD90F /* XCRemoteSwiftPackageReference "SCLAlertView-Swift" */; - productName = SCLAlertView; - }; - F213CCEC23C3F6A8000AD90F /* SwiftyJSON */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCEB23C3F6A8000AD90F /* XCRemoteSwiftPackageReference "SwiftyJSON" */; - productName = SwiftyJSON; - }; - F213CCEF23C3F99B000AD90F /* ScrollableGraphView */ = { - isa = XCSwiftPackageProductDependency; - package = F213CCEE23C3F99B000AD90F /* XCRemoteSwiftPackageReference "ScrollableGraphView" */; - productName = ScrollableGraphView; ->>>>>>> master }; F2568A752413534F00561295 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; From 1cc8ed2329736f45668743886f7be53ec68748c5 Mon Sep 17 00:00:00 2001 From: Jong Min Choi <01jongminchoi@gmail.com> Date: Wed, 8 Sep 2021 09:58:37 -0400 Subject: [PATCH 18/18] Home screen bug fix --- PennMobile/Home/Controllers/HomeViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PennMobile/Home/Controllers/HomeViewController.swift b/PennMobile/Home/Controllers/HomeViewController.swift index 84264f478..3c27a5745 100644 --- a/PennMobile/Home/Controllers/HomeViewController.swift +++ b/PennMobile/Home/Controllers/HomeViewController.swift @@ -43,6 +43,8 @@ class HomeViewController: GenericViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + self.navigationController?.isNavigationBarHidden = false + if tableViewModel == nil { self.startLoadingViewAnimation() }