From 4aa03ccf0c52ad8e735b1306f421c43bf5df64e3 Mon Sep 17 00:00:00 2001 From: durualayli Date: Wed, 11 Feb 2026 15:58:45 -0500 Subject: [PATCH 1/2] complete calendar (and ticketing ui) --- score-ios.xcodeproj/project.pbxproj | 14 ++- score-ios/Info.plist | 8 +- score-ios/ViewModels/CalendarViewModel.swift | 77 ++++++++++++++++ score-ios/Views/DetailedViews/GameView.swift | 94 ++++++++++---------- 4 files changed, 139 insertions(+), 54 deletions(-) create mode 100644 score-ios/ViewModels/CalendarViewModel.swift diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj index f8a2cf3..7d48417 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 13E348FB2F3D212D0014EC63 /* CalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E348FA2F3D21280014EC63 /* CalendarViewModel.swift */; }; 1C87865D2D8CD76900EBDF74 /* TrailingFadeGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */; }; 1C87865F2D8CDADC00EBDF74 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */; }; 2384C7B81B22428D94240957 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840304A20FA141C291346BA8 /* Highlight.swift */; }; @@ -130,6 +131,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 13E348FA2F3D21280014EC63 /* CalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModel.swift; sourceTree = ""; }; 1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingFadeGradient.swift; sourceTree = ""; }; 1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 2C1375CA2E7233390089EBC7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; @@ -502,6 +504,7 @@ D87787C62CFFAE3D00EA79E1 /* ViewModels */ = { isa = PBXGroup; children = ( + 13E348FA2F3D21280014EC63 /* CalendarViewModel.swift */, D87787C72CFFAE5200EA79E1 /* GamesViewModel.swift */, 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */, D864B5AA2D793A7400A3A50E /* PastGameViewModel.swift */, @@ -743,6 +746,7 @@ FD5A38DB2D8F2BDD00CF5E30 /* GameLoadingView.swift in Sources */, B136701ECD164EE9AC64667F /* Article.swift in Sources */, 64005CCECEAC4FD4BA8F51D2 /* YouTubeVideo.swift in Sources */, + 13E348FB2F3D212D0014EC63 /* CalendarViewModel.swift in Sources */, 2384C7B81B22428D94240957 /* Highlight.swift in Sources */, CE8ED4FC2D6BF47C00A274DE /* DummyData.swift in Sources */, CE335CD92C9244230037F572 /* Game.swift in Sources */, @@ -946,16 +950,19 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 26; DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\""; - DEVELOPMENT_TEAM = W7U2WA4D54; + DEVELOPMENT_TEAM = H5ZTDCQ89H; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "score-ios/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Score; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; + INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Allow calendar access to add Cornell games that aren't in your calendar."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -980,16 +987,19 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 26; DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\""; - DEVELOPMENT_TEAM = W7U2WA4D54; + DEVELOPMENT_TEAM = H5ZTDCQ89H; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "score-ios/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Score; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; + INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Allow calendar access to add Cornell games that aren't in your calendar."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/score-ios/Info.plist b/score-ios/Info.plist index 5fd6b04..082f441 100644 --- a/score-ios/Info.plist +++ b/score-ios/Info.plist @@ -2,12 +2,6 @@ - UIUserInterfaceStyle - Light - UIViewControllerBasedStatusBarAppearance - - ITSAppUsesNonExemptEncryption - SCORE_DEV_URL $(SCORE_DEV_URL) SCORE_PROD_URL @@ -19,5 +13,7 @@ Poppins-Bold.ttf Poppins-Regular.ttf + UIViewControllerBasedStatusBarAppearance + diff --git a/score-ios/ViewModels/CalendarViewModel.swift b/score-ios/ViewModels/CalendarViewModel.swift new file mode 100644 index 0000000..99685d1 --- /dev/null +++ b/score-ios/ViewModels/CalendarViewModel.swift @@ -0,0 +1,77 @@ +// +// CalendarViewModel.swift +// score-ios +// +// Created by Duru Alayli on 2/11/26. +// + +import EventKit +import Foundation +import UIKit + +final class CalendarViewModel: ObservableObject{ + static let shared = CalendarViewModel() + private let eventStore = EKEventStore() + @Published var showAlert: Bool = false + @Published var alertTitle: String = "" + @Published var alertMessage: String = "" + + private init() {} + + func requestAccessandAdd(event: Game) { + eventStore.requestFullAccessToEvents{ [weak self] (granted, error) in + guard let self = self else { return } + if granted && error == nil { + + let title = "Cornell vs. \(event.opponent.name) \(event.sex) \(event.sport)" + let existing = self.eventStore.predicateForEvents( + withStart: event.date, + end: event.date.addingTimeInterval(7200), + calendars: [self.eventStore.defaultCalendarForNewEvents].compactMap { $0 } + ) + let existingEvents = self.eventStore.events(matching: existing) + + if existingEvents.contains(where: { $0.title == title && $0.startDate == event.date }) { + DispatchQueue.main.async { + self.alertTitle = "Game already added." + self.alertMessage = "This game is already added to your calendar." + self.showAlert = true + } + return + } + + let calendarEvent = EKEvent(eventStore: self.eventStore) + calendarEvent.title = "Cornell vs. \(event.opponent.name) \(event.sex) \(event.sport)" + calendarEvent.startDate = event.date + calendarEvent.endDate = event.date.addingTimeInterval(7200) + calendarEvent.location = event.address + calendarEvent.calendar = self.eventStore.defaultCalendarForNewEvents + + do { + try eventStore.save(calendarEvent, span: .thisEvent) + DispatchQueue.main.async { + self.alertTitle = "Game added." + self.alertMessage = "Cornell vs. \(event.opponent.name) has been successfully added to your calendar." + self.showAlert = true + + if let url = URL(string: "calshow:\(event.date.timeIntervalSinceReferenceDate)") { + UIApplication.shared.open(url) + } + } + } catch { + DispatchQueue.main.async { + self.alertTitle = "Game can't be added." + self.alertMessage = "There was an error adding Cornell vs. \(event.opponent.name) to your calendar." + self.showAlert = true + } + } + } else { + DispatchQueue.main.async { + self.alertTitle = "Game can't be added." + self.alertMessage = "Can't access to the calendar. The request was denied." + self.showAlert = true + } + } + } + } +} diff --git a/score-ios/Views/DetailedViews/GameView.swift b/score-ios/Views/DetailedViews/GameView.swift index dcbba14..bdcda4c 100644 --- a/score-ios/Views/DetailedViews/GameView.swift +++ b/score-ios/Views/DetailedViews/GameView.swift @@ -10,6 +10,7 @@ import SwiftUI struct GameView : View { var game : Game @ObservedObject var viewModel: PastGameViewModel + @StateObject var calendarViewModel = CalendarViewModel.shared @State var viewState: Int = 0 @State var dayFromNow: Int = 0 @State var hourFromNow: Int = 0 @@ -119,14 +120,14 @@ extension GameView { Text("\(game.sex.description) \(game.sport.description)") .font(Constants.Fonts.subheader) .foregroundStyle(Constants.Colors.black) - + ScrollView(.horizontal, showsIndicators: false){ Text("Cornell vs. " + game.opponent.name.removingUniversityPrefix()) .font(Constants.Fonts.header) .foregroundStyle(Constants.Colors.black) } .withTrailingFadeGradient() - + HStack(spacing: 10) { HStack { Image("Location-g") @@ -176,25 +177,50 @@ extension GameView { .padding(.top, 8) } .padding(.top, 20) - - // Ticketing Link Button - if let link = game.ticketLink, - let url = URL(string: link) { + HStack (spacing: 16){ + // Ticketing Link Button + if let link = game.ticketLink, + let url = URL(string: link) { + Button(action: { + UIApplication.shared.open(url) + }) { + HStack (spacing: 9){ + Image("Ticket") + .resizable() + .frame(width: 22, height: 22) + Text("Buy Tickets") + .foregroundStyle(Constants.Colors.white) + .font(.system(size: 16, weight: .medium)) + .font(Constants.Fonts.buttonLabel) + } + .foregroundColor(.white) + .padding(12) + .background( + Constants.Colors.primary_red + ) + .overlay( + RoundedRectangle(cornerRadius: 30) + .stroke(Color.black.opacity(0.1), lineWidth: 1) + .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 30)) + } + } + + // Calendar Button Button(action: { - UIApplication.shared.open(url) + calendarViewModel.requestAccessandAdd(event:game) }) { - HStack { - Image("Ticket") + HStack (spacing: 8){ + Image("Calendar") .resizable() - .frame(width: 25, height: 25) - Text("Buy Tickets") - .foregroundStyle(Constants.Colors.white) - .font(.system(size: 16, weight: .medium)) + .frame(width: 24, height: 24) + Text("Add to Calendar") .font(Constants.Fonts.buttonLabel) + .foregroundStyle(Constants.Colors.white) } .foregroundColor(.white) - .padding(.horizontal, 20) - .padding(.vertical, 15) + .padding(12) .background( Constants.Colors.primary_red ) @@ -203,41 +229,17 @@ extension GameView { .stroke(Color.black.opacity(0.1), lineWidth: 1) .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2) ) - .clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners + .clipShape(RoundedRectangle(cornerRadius: 30)) + } + .alert(isPresented: $calendarViewModel.showAlert) { + Alert(title: Text(calendarViewModel.alertTitle), message: Text(calendarViewModel.alertMessage)) } - .padding(.top, 80) } - - // Calendar Button -// TODO: make this back when we have login -// Button(action: { -// // TODO: action -// }) { -// HStack { -// Image("Calendar") -// .resizable() -// .frame(width: 24, height: 24) -// Text("Add to Calendar") -// .font(Constants.Fonts.buttonLabel) -// .foregroundStyle(Constants.Colors.white) -// } -// .foregroundColor(.white) -// .padding(.horizontal, 16) -// .padding(.vertical, 10) -// .background( -// Constants.Colors.primary_red -// ) -// .overlay( -// RoundedRectangle(cornerRadius: 30) -// .stroke(Color.black.opacity(0.1), lineWidth: 1) -// .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2) -// ) -// .clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners -// } -// .padding(.top, 68) -// } + .padding(.top, 80) } + } + private var summaryTab: some View { NavigationLink(destination: ScoringSummary(game: game)) { From f544ec1907aacd2b9609be2abb6d9c1180f4e35d Mon Sep 17 00:00:00 2001 From: durualayli Date: Sun, 22 Feb 2026 13:34:25 -0500 Subject: [PATCH 2/2] Small fixes --- score-ios/ViewModels/CalendarViewModel.swift | 126 +++++++++---------- score-ios/Views/DetailedViews/GameView.swift | 21 +++- 2 files changed, 81 insertions(+), 66 deletions(-) diff --git a/score-ios/ViewModels/CalendarViewModel.swift b/score-ios/ViewModels/CalendarViewModel.swift index 99685d1..28187de 100644 --- a/score-ios/ViewModels/CalendarViewModel.swift +++ b/score-ios/ViewModels/CalendarViewModel.swift @@ -10,68 +10,66 @@ import Foundation import UIKit final class CalendarViewModel: ObservableObject{ - static let shared = CalendarViewModel() - private let eventStore = EKEventStore() - @Published var showAlert: Bool = false - @Published var alertTitle: String = "" - @Published var alertMessage: String = "" - - private init() {} - - func requestAccessandAdd(event: Game) { - eventStore.requestFullAccessToEvents{ [weak self] (granted, error) in - guard let self = self else { return } - if granted && error == nil { - - let title = "Cornell vs. \(event.opponent.name) \(event.sex) \(event.sport)" - let existing = self.eventStore.predicateForEvents( - withStart: event.date, - end: event.date.addingTimeInterval(7200), - calendars: [self.eventStore.defaultCalendarForNewEvents].compactMap { $0 } - ) - let existingEvents = self.eventStore.events(matching: existing) - - if existingEvents.contains(where: { $0.title == title && $0.startDate == event.date }) { - DispatchQueue.main.async { - self.alertTitle = "Game already added." - self.alertMessage = "This game is already added to your calendar." - self.showAlert = true - } - return - } - - let calendarEvent = EKEvent(eventStore: self.eventStore) - calendarEvent.title = "Cornell vs. \(event.opponent.name) \(event.sex) \(event.sport)" - calendarEvent.startDate = event.date - calendarEvent.endDate = event.date.addingTimeInterval(7200) - calendarEvent.location = event.address - calendarEvent.calendar = self.eventStore.defaultCalendarForNewEvents - - do { - try eventStore.save(calendarEvent, span: .thisEvent) - DispatchQueue.main.async { - self.alertTitle = "Game added." - self.alertMessage = "Cornell vs. \(event.opponent.name) has been successfully added to your calendar." - self.showAlert = true - - if let url = URL(string: "calshow:\(event.date.timeIntervalSinceReferenceDate)") { - UIApplication.shared.open(url) - } - } - } catch { - DispatchQueue.main.async { - self.alertTitle = "Game can't be added." - self.alertMessage = "There was an error adding Cornell vs. \(event.opponent.name) to your calendar." - self.showAlert = true - } - } - } else { - DispatchQueue.main.async { - self.alertTitle = "Game can't be added." - self.alertMessage = "Can't access to the calendar. The request was denied." - self.showAlert = true - } - } - } - } + static let shared = CalendarViewModel() + private let eventStore = EKEventStore() + @Published var showAlert: Bool = false + @Published var openSettings: Bool = false + @Published var alertTitle: String = "" + @Published var alertMessage: String = "" + + private init() {} + + func requestAccessandAdd(event: Game) { + eventStore.requestFullAccessToEvents{ [weak self] (granted, error) in + guard let self = self else { return } + if granted && error == nil { + + let title = "Cornell vs. \(event.opponent.name) \(event.sex) \(event.sport)" + let existing = self.eventStore.predicateForEvents( + withStart: event.date, + end: event.date.addingTimeInterval(7200), + calendars: [self.eventStore.defaultCalendarForNewEvents].compactMap { $0 } + ) + let existingEvents = self.eventStore.events(matching: existing) + + if existingEvents.contains(where: { $0.title == title && $0.startDate == event.date }) { + DispatchQueue.main.async { + self.alertTitle = "Game already added." + self.alertMessage = "This game is already added to your calendar." + self.showAlert = true + } + return + } + + let calendarEvent = EKEvent(eventStore: self.eventStore) + calendarEvent.title = title + calendarEvent.startDate = event.date + calendarEvent.endDate = event.date.addingTimeInterval(7200) + calendarEvent.location = event.address + calendarEvent.calendar = self.eventStore.defaultCalendarForNewEvents + + do { + try eventStore.save(calendarEvent, span: .thisEvent) + DispatchQueue.main.async { + if let url = URL(string: "calshow:\(event.date.timeIntervalSinceReferenceDate)") { + UIApplication.shared.open(url) + } + } + } catch { + DispatchQueue.main.async { + self.alertTitle = "Game can't be added." + self.alertMessage = "There was an error adding Cornell vs. \(event.opponent.name) to your calendar." + self.showAlert = true + } + } + } else { + DispatchQueue.main.async { + self.alertTitle = "Game can't be added." + self.alertMessage = "Calendar access denied. Please enable full calendar access in Settings." + self.showAlert = true + self.openSettings = true + } + } + } + } } diff --git a/score-ios/Views/DetailedViews/GameView.swift b/score-ios/Views/DetailedViews/GameView.swift index bdcda4c..fa1332d 100644 --- a/score-ios/Views/DetailedViews/GameView.swift +++ b/score-ios/Views/DetailedViews/GameView.swift @@ -10,7 +10,7 @@ import SwiftUI struct GameView : View { var game : Game @ObservedObject var viewModel: PastGameViewModel - @StateObject var calendarViewModel = CalendarViewModel.shared + @ObservedObject var calendarViewModel = CalendarViewModel.shared @State var viewState: Int = 0 @State var dayFromNow: Int = 0 @State var hourFromNow: Int = 0 @@ -232,7 +232,24 @@ extension GameView { .clipShape(RoundedRectangle(cornerRadius: 30)) } .alert(isPresented: $calendarViewModel.showAlert) { - Alert(title: Text(calendarViewModel.alertTitle), message: Text(calendarViewModel.alertMessage)) + if calendarViewModel.openSettings { + Alert( + title: Text(calendarViewModel.alertTitle), + message: Text(calendarViewModel.alertMessage), + primaryButton: .default(Text("Go to settings")) { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + }, + secondaryButton: .cancel() + ) + } + else { + Alert( + title: Text(calendarViewModel.alertTitle), + message: Text(calendarViewModel.alertMessage) + ) + } } } .padding(.top, 80)