diff --git a/.gitignore b/.gitignore
index 5a5060e..336547f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,9 @@
## Secrets
ScoreSecrets/*
+## API
+GameAPI/**
+
## User settings
xcuserdata/
diff --git a/README.md b/README.md
index 2f288ff..6fdcb6c 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,19 @@
-# score-ios
-SCORE is a centralized sports information App for the Cornell community to discover and stay informed on athletic games.
+# Score
-Score aims to become the Cornell community’s go-to for all sports events information, with the following features (in order of implementation):
+

-1. Centralized hub for sports game information discovery
+Score is Cornell’s all-in-one sports discovery hub that makes Cornell sports easily accessible and engaging for the entire community. A free and open-source app, Score helps facilitate the seamless discovery, tracking, and engagement of Cornell sports, allowing students to conveniently stay informed on sports games and feel motivated to actively participate in the campus sports culture.
-2. Real-time game status and score updates
-3. Personalized game attendance planning
+## Getting Started
-(Taken from Notion, please update when finalized)
+1. Clone the repository.
+2. Download and uncompress the `ScoreSecrets/` folder, which AppDev members can find it pinned in the `#score-ios` Slack channel. Once done, drag the following folder into the root of the project through **FINDER (NOT Xcode)**. The folder should contain the following files:
+ - `GoogleService-Info.plist`
+ - `Keys.plist`
+3. Build the project and you should be good to go! Select the `score-ios` schema to use our development server and `score-ios-prod` to use our production server.
+
+## Common Issues
+
+- If you're running into missing package errors, add the gameAPI folder as a package dependency. Go to File -> Add Package Dependencies -> Add Local -> select the folder
+ - The .zip folder can also found pinned in the `#score-ios` Slack channel.
diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh
index d958093..27d9254 100755
--- a/ci_scripts/ci_post_clone.sh
+++ b/ci_scripts/ci_post_clone.sh
@@ -11,6 +11,7 @@ echo "Downloading Secrets"
brew install wget
cd $CI_PRIMARY_REPOSITORY_PATH/ci_scripts
mkdir ../ScoreSecrets
+wget -O ../ScoreSecrets/GoogleService-Info.plist "$GOOGLE_SERVICE_INFO"
wget -O ../ScoreSecrets/Keys.xcconfig "$KEYS"
wget -O ../ScoreSecrets/apollo-codegen-config-dev.json "$CODEGEN_DEV"
wget -O ../ScoreSecrets/apollo-codegen-config-prod.json "$CODEGEN_PROD"
diff --git a/gameAPI/.DS_Store b/gameAPI/.DS_Store
deleted file mode 100644
index 4d3e227..0000000
Binary files a/gameAPI/.DS_Store and /dev/null differ
diff --git a/gameAPI/Package.swift b/gameAPI/Package.swift
deleted file mode 100644
index 141f37d..0000000
--- a/gameAPI/Package.swift
+++ /dev/null
@@ -1,28 +0,0 @@
-// swift-tools-version:5.9
-
-import PackageDescription
-
-let package = Package(
- name: "GameAPI",
- platforms: [
- .iOS(.v12),
- .macOS(.v10_14),
- .tvOS(.v12),
- .watchOS(.v5),
- ],
- products: [
- .library(name: "GameAPI", targets: ["GameAPI"]),
- ],
- dependencies: [
- .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.0.0"),
- ],
- targets: [
- .target(
- name: "GameAPI",
- dependencies: [
- .product(name: "ApolloAPI", package: "apollo-ios"),
- ],
- path: "./Sources"
- ),
- ]
-)
diff --git a/gameAPI/Sources/.DS_Store b/gameAPI/Sources/.DS_Store
deleted file mode 100644
index 8de1a40..0000000
Binary files a/gameAPI/Sources/.DS_Store and /dev/null differ
diff --git a/gameAPI/Sources/Operations/.DS_Store b/gameAPI/Sources/Operations/.DS_Store
deleted file mode 100644
index 4ffd9d2..0000000
Binary files a/gameAPI/Sources/Operations/.DS_Store and /dev/null differ
diff --git a/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift b/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift
deleted file mode 100644
index 33c8929..0000000
--- a/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift
+++ /dev/null
@@ -1,120 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-@_exported import ApolloAPI
-
-public class GamesQuery: GraphQLQuery {
- public static let operationName: String = "Games"
- public static let operationDocument: ApolloAPI.OperationDocument = .init(
- definition: .init(
- #"query Games { games { __typename id city date gender location opponentId result sport state time scoreBreakdown team { __typename id color image name } boxScore { __typename team period time description scorer assist scoreBy corScore oppScore } } }"#
- ))
-
- public init() {}
-
- public struct Data: GameAPI.SelectionSet {
- public let __data: DataDict
- public init(_dataDict: DataDict) { __data = _dataDict }
-
- public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.Query }
- public static var __selections: [ApolloAPI.Selection] { [
- .field("games", [Game?]?.self),
- ] }
-
- public var games: [Game?]? { __data["games"] }
-
- /// Game
- ///
- /// Parent Type: `GameType`
- public struct Game: GameAPI.SelectionSet {
- public let __data: DataDict
- public init(_dataDict: DataDict) { __data = _dataDict }
-
- public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.GameType }
- public static var __selections: [ApolloAPI.Selection] { [
- .field("__typename", String.self),
- .field("id", String?.self),
- .field("city", String.self),
- .field("date", String.self),
- .field("gender", String.self),
- .field("location", String?.self),
- .field("opponentId", String.self),
- .field("result", String?.self),
- .field("sport", String.self),
- .field("state", String.self),
- .field("time", String?.self),
- .field("scoreBreakdown", [[String?]?]?.self),
- .field("team", Team?.self),
- .field("boxScore", [BoxScore?]?.self),
- ] }
-
- public var id: String? { __data["id"] }
- public var city: String { __data["city"] }
- public var date: String { __data["date"] }
- public var gender: String { __data["gender"] }
- public var location: String? { __data["location"] }
- public var opponentId: String { __data["opponentId"] }
- public var result: String? { __data["result"] }
- public var sport: String { __data["sport"] }
- public var state: String { __data["state"] }
- public var time: String? { __data["time"] }
- public var scoreBreakdown: [[String?]?]? { __data["scoreBreakdown"] }
- public var team: Team? { __data["team"] }
- public var boxScore: [BoxScore?]? { __data["boxScore"] }
-
- /// Game.Team
- ///
- /// Parent Type: `TeamType`
- public struct Team: GameAPI.SelectionSet {
- public let __data: DataDict
- public init(_dataDict: DataDict) { __data = _dataDict }
-
- public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.TeamType }
- public static var __selections: [ApolloAPI.Selection] { [
- .field("__typename", String.self),
- .field("id", String?.self),
- .field("color", String.self),
- .field("image", String?.self),
- .field("name", String.self),
- ] }
-
- public var id: String? { __data["id"] }
- public var color: String { __data["color"] }
- public var image: String? { __data["image"] }
- public var name: String { __data["name"] }
- }
-
- /// Game.BoxScore
- ///
- /// Parent Type: `BoxScore`
- public struct BoxScore: GameAPI.SelectionSet {
- public let __data: DataDict
- public init(_dataDict: DataDict) { __data = _dataDict }
-
- public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.BoxScore }
- public static var __selections: [ApolloAPI.Selection] { [
- .field("__typename", String.self),
- .field("team", String?.self),
- .field("period", String?.self),
- .field("time", String?.self),
- .field("description", String?.self),
- .field("scorer", String?.self),
- .field("assist", String?.self),
- .field("scoreBy", String?.self),
- .field("corScore", Int?.self),
- .field("oppScore", Int?.self),
- ] }
-
- public var team: String? { __data["team"] }
- public var period: String? { __data["period"] }
- public var time: String? { __data["time"] }
- public var description: String? { __data["description"] }
- public var scorer: String? { __data["scorer"] }
- public var assist: String? { __data["assist"] }
- public var scoreBy: String? { __data["scoreBy"] }
- public var corScore: Int? { __data["corScore"] }
- public var oppScore: Int? { __data["oppScore"] }
- }
- }
- }
-}
diff --git a/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift b/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift
deleted file mode 100644
index b8b7fc2..0000000
--- a/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift
+++ /dev/null
@@ -1,54 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-@_exported import ApolloAPI
-
-public class GetTeamByIdQuery: GraphQLQuery {
- public static let operationName: String = "GetTeamById"
- public static let operationDocument: ApolloAPI.OperationDocument = .init(
- definition: .init(
- #"query GetTeamById($id: String!) { team(id: $id) { __typename id color image name } }"#
- ))
-
- public var id: String
-
- public init(id: String) {
- self.id = id
- }
-
- public var __variables: Variables? { ["id": id] }
-
- public struct Data: GameAPI.SelectionSet {
- public let __data: DataDict
- public init(_dataDict: DataDict) { __data = _dataDict }
-
- public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.Query }
- public static var __selections: [ApolloAPI.Selection] { [
- .field("team", Team?.self, arguments: ["id": .variable("id")]),
- ] }
-
- public var team: Team? { __data["team"] }
-
- /// Team
- ///
- /// Parent Type: `TeamType`
- public struct Team: GameAPI.SelectionSet {
- public let __data: DataDict
- public init(_dataDict: DataDict) { __data = _dataDict }
-
- public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.TeamType }
- public static var __selections: [ApolloAPI.Selection] { [
- .field("__typename", String.self),
- .field("id", String?.self),
- .field("color", String.self),
- .field("image", String?.self),
- .field("name", String.self),
- ] }
-
- public var id: String? { __data["id"] }
- public var color: String { __data["color"] }
- public var image: String? { __data["image"] }
- public var name: String { __data["name"] }
- }
- }
-}
diff --git a/gameAPI/Sources/Schema/.DS_Store b/gameAPI/Sources/Schema/.DS_Store
deleted file mode 100644
index 034f828..0000000
Binary files a/gameAPI/Sources/Schema/.DS_Store and /dev/null differ
diff --git a/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift b/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift
deleted file mode 100644
index b63d512..0000000
--- a/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-import ApolloAPI
-
-public extension Objects {
- /// Represents an individual entry in the box score of a game.
- ///
- /// Attributes:
- /// - `team`: The team involved in the scoring event.
- /// - `period`: The period or inning of the event.
- /// - `time`: The time of the scoring event.
- /// - `description`: A description of the play or scoring event.
- /// - `scorer`: The name of the scorer.
- /// - `assist`: The name of the assisting player.
- /// - `score_by`: Indicates which team scored.
- /// - `cor_score`: Cornell's score at the time of the event.
- /// - `opp_score`: Opponent's score at the time of the event.
- static let BoxScore = ApolloAPI.Object(
- typename: "BoxScore",
- implementedInterfaces: []
- )
-}
\ No newline at end of file
diff --git a/gameAPI/Sources/Schema/Objects/BoxScoreEntryType.graphql.swift b/gameAPI/Sources/Schema/Objects/BoxScoreEntryType.graphql.swift
deleted file mode 100644
index 2e05521..0000000
--- a/gameAPI/Sources/Schema/Objects/BoxScoreEntryType.graphql.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-import ApolloAPI
-
-public extension Objects {
- /// Represents an individual entry in the box score of a game.
- ///
- /// Attributes:
- /// - `team`: The team involved in the scoring event.
- /// - `period`: The period or inning of the event.
- /// - `time`: The time of the scoring event.
- /// - `description`: A description of the play or scoring event.
- /// - `scorer`: The name of the scorer.
- /// - `assist`: The name of the assisting player.
- /// - `score_by`: Indicates which team scored.
- /// - `cor_score`: Cornell's score at the time of the event.
- /// - `opp_score`: Opponent's score at the time of the event.
- static let BoxScoreEntryType = ApolloAPI.Object(
- typename: "BoxScoreEntryType",
- implementedInterfaces: []
- )
-}
\ No newline at end of file
diff --git a/gameAPI/Sources/Schema/Objects/GameType.graphql.swift b/gameAPI/Sources/Schema/Objects/GameType.graphql.swift
deleted file mode 100644
index 77c1659..0000000
--- a/gameAPI/Sources/Schema/Objects/GameType.graphql.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-import ApolloAPI
-
-public extension Objects {
- /// A GraphQL type representing a game.
- ///
- /// Attributes:
- /// - `id`: The ID of the game (optional).
- /// - `city`: The city of the game.
- /// - `date`: The date of the game.
- /// - `gender`: The gender of the game.
- /// - `location`: The location of the game. (optional)
- /// - `opponent_id`: The id of the opposing team.
- /// - `result`: The result of the game. (optional)
- /// - `sport`: The sport of the game.
- /// - `state`: The state of the game.
- /// - `time`: The time of the game. (optional)
- /// - `box_score`: The box score of the game.
- /// - `score_breakdown`: The score breakdown of the game.
- static let GameType = ApolloAPI.Object(
- typename: "GameType",
- implementedInterfaces: []
- )
-}
\ No newline at end of file
diff --git a/gameAPI/Sources/Schema/Objects/Query.graphql.swift b/gameAPI/Sources/Schema/Objects/Query.graphql.swift
deleted file mode 100644
index a4155e4..0000000
--- a/gameAPI/Sources/Schema/Objects/Query.graphql.swift
+++ /dev/null
@@ -1,11 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-import ApolloAPI
-
-public extension Objects {
- static let Query = ApolloAPI.Object(
- typename: "Query",
- implementedInterfaces: []
- )
-}
\ No newline at end of file
diff --git a/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift b/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift
deleted file mode 100644
index e714216..0000000
--- a/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-import ApolloAPI
-
-public extension Objects {
- /// A GraphQL type representing a team.
- ///
- /// Attributes:
- /// - `id`: The ID of the team (optional).
- /// - `color`: The color of the team.
- /// - `image`: The image of the team (optional).
- /// - `name`: The name of the team.
- static let TeamType = ApolloAPI.Object(
- typename: "TeamType",
- implementedInterfaces: []
- )
-}
\ No newline at end of file
diff --git a/gameAPI/Sources/Schema/SchemaConfiguration.swift b/gameAPI/Sources/Schema/SchemaConfiguration.swift
deleted file mode 100644
index 8723501..0000000
--- a/gameAPI/Sources/Schema/SchemaConfiguration.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-// @generated
-// This file was automatically generated and can be edited to
-// provide custom configuration for a generated GraphQL schema.
-//
-// Any changes to this file will not be overwritten by future
-// code generation execution.
-
-import ApolloAPI
-
-public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
- public static func cacheKeyInfo(for type: ApolloAPI.Object, object: ApolloAPI.ObjectData) -> CacheKeyInfo? {
- // Implement this function to configure cache key resolution for your schema types.
- return nil
- }
-}
diff --git a/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift b/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift
deleted file mode 100644
index 4805c9e..0000000
--- a/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift
+++ /dev/null
@@ -1,34 +0,0 @@
-// @generated
-// This file was automatically generated and should not be edited.
-
-import ApolloAPI
-
-public protocol SelectionSet: ApolloAPI.SelectionSet & ApolloAPI.RootSelectionSet
-where Schema == GameAPI.SchemaMetadata {}
-
-public protocol InlineFragment: ApolloAPI.SelectionSet & ApolloAPI.InlineFragment
-where Schema == GameAPI.SchemaMetadata {}
-
-public protocol MutableSelectionSet: ApolloAPI.MutableRootSelectionSet
-where Schema == GameAPI.SchemaMetadata {}
-
-public protocol MutableInlineFragment: ApolloAPI.MutableSelectionSet & ApolloAPI.InlineFragment
-where Schema == GameAPI.SchemaMetadata {}
-
-public enum SchemaMetadata: ApolloAPI.SchemaMetadata {
- public static let configuration: any ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self
-
- public static func objectType(forTypename typename: String) -> ApolloAPI.Object? {
- switch typename {
- case "BoxScore": return GameAPI.Objects.BoxScore
- case "GameType": return GameAPI.Objects.GameType
- case "Query": return GameAPI.Objects.Query
- case "TeamType": return GameAPI.Objects.TeamType
- default: return nil
- }
- }
-}
-
-public enum Objects {}
-public enum Interfaces {}
-public enum Unions {}
diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj
index 9003159..b90ba3d 100644
--- a/score-ios.xcodeproj/project.pbxproj
+++ b/score-ios.xcodeproj/project.pbxproj
@@ -9,6 +9,13 @@
/* Begin PBXBuildFile section */
1C87865D2D8CD76900EBDF74 /* TrailingFadeGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */; };
1C87865F2D8CDADC00EBDF74 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */; };
+ 2C1375BD2E722CB70089EBC7 /* GameAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375BC2E722CB70089EBC7 /* GameAPI */; };
+ 2C1375BF2E7230250089EBC7 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375BE2E7230250089EBC7 /* FirebaseAnalytics */; };
+ 2C1375C12E7230250089EBC7 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C02E7230250089EBC7 /* FirebaseCore */; };
+ 2C1375C32E7230250089EBC7 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C22E7230250089EBC7 /* FirebaseCrashlytics */; };
+ 2C1375C52E7230630089EBC7 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C42E7230630089EBC7 /* FirebaseAnalytics */; };
+ 2C1375C72E7230630089EBC7 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C62E7230630089EBC7 /* FirebaseCore */; };
+ 2C1375CB2E7233390089EBC7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2C1375CA2E7233390089EBC7 /* GoogleService-Info.plist */; };
CE335CD32C922E8D0037F572 /* PrimaryColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD22C922E8D0037F572 /* PrimaryColors.swift */; };
CE335CD52C922ECB0037F572 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD42C922ECB0037F572 /* Constants.swift */; };
CE335CD72C922F390037F572 /* Dates.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD62C922F390037F572 /* Dates.swift */; };
@@ -59,6 +66,9 @@
CE8ED5122D6C3FCB00A274DE /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8ED5112D6C3FCB00A274DE /* CarouselView.swift */; };
CE8ED5142D6C42D400A274DE /* GameSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8ED5132D6C42D400A274DE /* GameSectionHeaderView.swift */; };
CEA25A932D75279A00B9837A /* ScoreEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA25A922D75279A00B9837A /* ScoreEnvironment.swift */; };
+ D0A904F52E8DDD990008194B /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = D0A904F42E8DDD990008194B /* FirebaseAnalytics */; };
+ D0A904F72E8DDD990008194B /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = D0A904F62E8DDD990008194B /* FirebaseCore */; };
+ D0A904F92E8DDD990008194B /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = D0A904F82E8DDD990008194B /* FirebaseCrashlytics */; };
D836AD922CB62C8800BD1545 /* NoGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836AD912CB62C8800BD1545 /* NoGameView.swift */; };
D83EE8862CC9917C008B693C /* ScoreSummaryTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83EE8852CC9917C008B693C /* ScoreSummaryTile.swift */; };
D85802102D6C38700075B036 /* DynamicScoreBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D858020F2D6C38670075B036 /* DynamicScoreBox.swift */; };
@@ -79,6 +89,7 @@
D8B1C9D12CD2CE3D0095E563 /* PastGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1C9D02CD2CE3C0095E563 /* PastGamesView.swift */; };
D8B1C9D32CD2D20A0095E563 /* PastGameTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */; };
D8DD4E642CFD48ED00F2C46E /* Team.graphql in Resources */ = {isa = PBXBuildFile; fileRef = D8DD4E632CFD48E400F2C46E /* Team.graphql */; };
+ FD27F4232DC0A68900CC172E /* GamesCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD27F4222DC0A68900CC172E /* GamesCacheManager.swift */; };
FD5A38DB2D8F2BDD00CF5E30 /* GameLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5A38DA2D8F2BDD00CF5E30 /* GameLoadingView.swift */; };
FD5A38DD2D8F30CC00CF5E30 /* GameErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5A38DC2D8F30CC00CF5E30 /* GameErrorView.swift */; };
FD5A38DF2D8F3E1400CF5E30 /* ShimmerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5A38DE2D8F3E1400CF5E30 /* ShimmerModifier.swift */; };
@@ -104,6 +115,7 @@
/* Begin PBXFileReference section */
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 = ""; };
CE335CD22C922E8D0037F572 /* PrimaryColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryColors.swift; sourceTree = ""; };
CE335CD42C922ECB0037F572 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
CE335CD62C922F390037F572 /* Dates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dates.swift; sourceTree = ""; };
@@ -178,6 +190,7 @@
D8B1C9D02CD2CE3C0095E563 /* PastGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastGamesView.swift; sourceTree = ""; };
D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastGameTile.swift; sourceTree = ""; };
D8DD4E632CFD48E400F2C46E /* Team.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = Team.graphql; sourceTree = ""; };
+ FD27F4222DC0A68900CC172E /* GamesCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamesCacheManager.swift; sourceTree = ""; };
FD5A38DA2D8F2BDD00CF5E30 /* GameLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameLoadingView.swift; sourceTree = ""; };
FD5A38DC2D8F30CC00CF5E30 /* GameErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameErrorView.swift; sourceTree = ""; };
FD5A38DE2D8F3E1400CF5E30 /* ShimmerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerModifier.swift; sourceTree = ""; };
@@ -188,8 +201,17 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 2C1375C52E7230630089EBC7 /* FirebaseAnalytics in Frameworks */,
+ 2C1375BD2E722CB70089EBC7 /* GameAPI in Frameworks */,
+ 2C1375C12E7230250089EBC7 /* FirebaseCore in Frameworks */,
D89102042CED69EF004CE226 /* Apollo in Frameworks */,
+ 2C1375C32E7230250089EBC7 /* FirebaseCrashlytics in Frameworks */,
+ D0A904F92E8DDD990008194B /* FirebaseCrashlytics in Frameworks */,
+ 2C1375BF2E7230250089EBC7 /* FirebaseAnalytics in Frameworks */,
+ D0A904F72E8DDD990008194B /* FirebaseCore in Frameworks */,
+ 2C1375C72E7230630089EBC7 /* FirebaseCore in Frameworks */,
D89102182CF151DD004CE226 /* GameAPI in Frameworks */,
+ D0A904F52E8DDD990008194B /* FirebaseAnalytics in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -431,6 +453,7 @@
CEA25A972D752C6F00B9837A /* ScoreSecrets */ = {
isa = PBXGroup;
children = (
+ 2C1375CA2E7233390089EBC7 /* GoogleService-Info.plist */,
CEA25A962D75292900B9837A /* Keys.xcconfig */,
);
path = ScoreSecrets;
@@ -475,6 +498,7 @@
children = (
D89102122CF10CA5004CE226 /* NetworkManager.swift */,
D891020F2CF0EBA0004CE226 /* DataCoordinator.swift */,
+ FD27F4222DC0A68900CC172E /* GamesCacheManager.swift */,
);
path = Networking;
sourceTree = "";
@@ -570,7 +594,8 @@
mainGroup = CE725D2F2C89120100386943;
packageReferences = (
D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */,
- D89102162CF151DD004CE226 /* XCLocalSwiftPackageReference "gameAPI" */,
+ FD602E2D2DC11D4A002711BE /* XCLocalSwiftPackageReference "GameAPI" */,
+ D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
productRefGroup = CE725D392C89120200386943 /* Products */;
projectDirPath = "";
@@ -590,6 +615,7 @@
files = (
D8DD4E642CFD48ED00F2C46E /* Team.graphql in Resources */,
D891020B2CED6A8E004CE226 /* Game.graphql in Resources */,
+ 2C1375CB2E7233390089EBC7 /* GoogleService-Info.plist in Resources */,
CE528FE42C96A27500C238B5 /* Poppins-Light.ttf in Resources */,
CE528FEE2C96A27500C238B5 /* Poppins-Black.ttf in Resources */,
CE528FEC2C96A27500C238B5 /* Poppins-LightItalic.ttf in Resources */,
@@ -648,7 +674,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "";
+ shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -689,6 +715,7 @@
CE8ED4FA2D6BF46300A274DE /* Sex.swift in Sources */,
D8B1C9D12CD2CE3D0095E563 /* PastGamesView.swift in Sources */,
CE335CD32C922E8D0037F572 /* PrimaryColors.swift in Sources */,
+ FD27F4232DC0A68900CC172E /* GamesCacheManager.swift in Sources */,
CE725D3C2C89120200386943 /* Home.swift in Sources */,
FD5A38DF2D8F3E1400CF5E30 /* ShimmerModifier.swift in Sources */,
CE528FA02C96420700C238B5 /* PickerView.swift in Sources */,
@@ -865,9 +892,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\"";
- DEVELOPMENT_TEAM = ZGMCXU7X3U;
+ DEVELOPMENT_TEAM = W7U2WA4D54;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "score-ios/Info.plist";
@@ -881,7 +908,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "com.cornellappdev.score-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -899,9 +926,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 17;
+ CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\"";
- DEVELOPMENT_TEAM = ZGMCXU7X3U;
+ DEVELOPMENT_TEAM = W7U2WA4D54;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "score-ios/Info.plist";
@@ -915,7 +942,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = "com.cornellappdev.score-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1040,13 +1067,29 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- D89102162CF151DD004CE226 /* XCLocalSwiftPackageReference "gameAPI" */ = {
+ FD602E2D2DC11D4A002711BE /* XCLocalSwiftPackageReference "GameAPI" */ = {
isa = XCLocalSwiftPackageReference;
- relativePath = gameAPI;
+ relativePath = GameAPI;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */
+ 2CBC5B0A2E722C31006C1167 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 12.2.0;
+ };
+ };
+ D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 12.3.0;
+ };
+ };
D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apollographql/apollo-ios.git";
@@ -1058,6 +1101,50 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ 2C1375BC2E722CB70089EBC7 /* GameAPI */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = GameAPI;
+ };
+ 2C1375BE2E7230250089EBC7 /* FirebaseAnalytics */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2CBC5B0A2E722C31006C1167 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseAnalytics;
+ };
+ 2C1375C02E7230250089EBC7 /* FirebaseCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2CBC5B0A2E722C31006C1167 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCore;
+ };
+ 2C1375C22E7230250089EBC7 /* FirebaseCrashlytics */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2CBC5B0A2E722C31006C1167 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCrashlytics;
+ };
+ 2C1375C42E7230630089EBC7 /* FirebaseAnalytics */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2CBC5B0A2E722C31006C1167 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseAnalytics;
+ };
+ 2C1375C62E7230630089EBC7 /* FirebaseCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2CBC5B0A2E722C31006C1167 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCore;
+ };
+ D0A904F42E8DDD990008194B /* FirebaseAnalytics */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseAnalytics;
+ };
+ D0A904F62E8DDD990008194B /* FirebaseCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCore;
+ };
+ D0A904F82E8DDD990008194B /* FirebaseCrashlytics */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCrashlytics;
+ };
D89102032CED69EF004CE226 /* Apollo */ = {
isa = XCSwiftPackageProductDependency;
package = D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */;
diff --git a/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index f3f8c4a..d2b9013 100644
--- a/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,6 +1,15 @@
{
- "originHash" : "6b067d3fafa8b6816a1507517f5f83e403209fe5e4f2246c62127d371c045b62",
+ "originHash" : "2e424153447d52445d4234f070176ecbb15cb2d6c72add29422e89b4e4a643ba",
"pins" : [
+ {
+ "identity" : "abseil-cpp-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/abseil-cpp-binary.git",
+ "state" : {
+ "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+ "version" : "1.2024072200.0"
+ }
+ },
{
"identity" : "apollo-ios",
"kind" : "remoteSourceControl",
@@ -10,6 +19,114 @@
"version" : "1.15.3"
}
},
+ {
+ "identity" : "app-check",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/app-check.git",
+ "state" : {
+ "revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
+ "version" : "11.2.0"
+ }
+ },
+ {
+ "identity" : "firebase-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/firebase-ios-sdk",
+ "state" : {
+ "revision" : "1cce11cf94d27e2fc194112cc7ad51e8fb279230",
+ "version" : "12.3.0"
+ }
+ },
+ {
+ "identity" : "google-ads-on-device-conversion-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+ "state" : {
+ "revision" : "c7d04b7592d3a1d6f8b7ce4e103cfbcbd766f419",
+ "version" : "3.0.0"
+ }
+ },
+ {
+ "identity" : "googleappmeasurement",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleAppMeasurement.git",
+ "state" : {
+ "revision" : "34f5306ed8c9d9493f6390b4c2a18f19ecad1da4",
+ "version" : "12.3.0"
+ }
+ },
+ {
+ "identity" : "googledatatransport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleDataTransport.git",
+ "state" : {
+ "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+ "version" : "10.1.0"
+ }
+ },
+ {
+ "identity" : "googleutilities",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleUtilities.git",
+ "state" : {
+ "revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
+ "version" : "8.1.0"
+ }
+ },
+ {
+ "identity" : "grpc-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/grpc-binary.git",
+ "state" : {
+ "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+ "version" : "1.69.1"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "fb7f2740b1570d2f7599c6bb9531bf4fad6974b7",
+ "version" : "5.0.0"
+ }
+ },
+ {
+ "identity" : "interop-ios-for-google-sdks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+ "state" : {
+ "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+ "version" : "101.0.0"
+ }
+ },
+ {
+ "identity" : "leveldb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/leveldb.git",
+ "state" : {
+ "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+ "version" : "1.22.5"
+ }
+ },
+ {
+ "identity" : "nanopb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/nanopb.git",
+ "state" : {
+ "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
+ "version" : "2.30910.0"
+ }
+ },
+ {
+ "identity" : "promises",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/promises.git",
+ "state" : {
+ "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
+ "version" : "2.4.0"
+ }
+ },
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
@@ -18,6 +135,15 @@
"revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
"version" : "0.15.3"
}
+ },
+ {
+ "identity" : "swift-protobuf",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-protobuf.git",
+ "state" : {
+ "revision" : "2547102afd04fe49f1b286090f13ebce07284980",
+ "version" : "1.31.1"
+ }
}
],
"version" : 3
diff --git a/score-ios/.DS_Store b/score-ios/.DS_Store
index 1d33697..080e89c 100644
Binary files a/score-ios/.DS_Store and b/score-ios/.DS_Store differ
diff --git a/score-ios/Models/DummyData.swift b/score-ios/Models/DummyData.swift
index 1a575ba..9df92f7 100644
--- a/score-ios/Models/DummyData.swift
+++ b/score-ios/Models/DummyData.swift
@@ -16,7 +16,8 @@ extension Game {
// Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Princeton", state: "NJ", date: Date.dateComponents(year: 2024, month: 5, day: 20, hour: 10, minute: 0), sport: .Basketball, address: "2 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New Haven", state: "CT", date: Date.dateComponents(year: 2024, month: 5, day: 22, hour: 10, minute: 0), sport: .Soccer, address: "3 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
// Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Providence", state: "RI", date: Date.dateComponents(year: 2024, month: 5, day: 23, hour: 10, minute: 0), sport: .CrossCountry, address: "4 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
- Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .IceHockey, address: "5 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
+ Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .Baseball
+ , address: "5 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New York", state: "NY", date: Date.dateComponents(year: 2024, month: 5, day: 25, hour: 10, minute: 0), sport: .Lacrosse, address: "6 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
// Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Pennsylvania", state: "PA", date: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 10, minute: 0), sport: .Basketball, address: "0 Fake St", sex: .Men, timeUpdates: [], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
@@ -24,7 +25,7 @@ extension Game {
// Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Princeton", state: "NJ", date: Date.dateComponents(year: 2024, month: 5, day: 20, hour: 10, minute: 0), sport: .Basketball, address: "2 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New Haven", state: "CT", date: Date.dateComponents(year: 2024, month: 5, day: 22, hour: 10, minute: 0), sport: .Soccer, address: "3 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
// Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Providence", state: "RI", date: Date.dateComponents(year: 2024, month: 5, day: 23, hour: 10, minute: 0), sport: .CrossCountry, address: "4 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
- Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .IceHockey, address: "5 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
+ Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .Baseball, address: "5 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]),
Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New York", state: "NY", date: Date.dateComponents(year: 2024, month: 5, day: 25, hour: 10, minute: 0), sport: .Lacrosse, address: "6 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")])
]
// static let dummyData: [Game] = [
diff --git a/score-ios/Models/Game.swift b/score-ios/Models/Game.swift
index 9c5565e..9ed6f80 100644
--- a/score-ios/Models/Game.swift
+++ b/score-ios/Models/Game.swift
@@ -18,7 +18,7 @@ protocol GameType : Identifiable where ID == UUID {
var sport: Sport { get }
var sex: Sex { get }
- // TODO add more, maybe longitude and latitude for Transit integration? Idk
+ // TODO add more, maybe longitude and latitude for Transit integration
var address: String { get }
var timeUpdates: [TimeUpdate] { get }
@@ -27,6 +27,7 @@ protocol GameType : Identifiable where ID == UUID {
struct Game : GameType, Identifiable {
var id: UUID = UUID()
+ var serverId: String?
var opponent: Team
var city: String
var state: String
@@ -43,6 +44,7 @@ struct Game : GameType, Identifiable {
}
init(game: GamesQuery.Data.Game) {
+ self.serverId = game.id
self.city = game.city
self.state = game.state
self.date = Date.parseDate(dateString: game.date, timeString: game.time ?? "12:00 p.m.")
@@ -102,10 +104,11 @@ extension Game {
oppTotal += oppScore
updates.append(timeUpdate)
}
- if (index == corScores.count - 1) {
- let total = TimeUpdate(timestamp: index + 1, isTotal: true, cornellScore: corTotal, opponentScore: oppTotal)
- updates.append(total)
- }
+
+// if (index == corScores.count - 1) {
+// let total = TimeUpdate(timestamp: index + 1, isTotal: true, cornellScore: corTotal, opponentScore: oppTotal)
+// updates.append(total)
+// }
})
}
}
diff --git a/score-ios/Models/GraphQL/Game.graphql b/score-ios/Models/GraphQL/Game.graphql
index 251c9b5..03decfe 100644
--- a/score-ios/Models/GraphQL/Game.graphql
+++ b/score-ios/Models/GraphQL/Game.graphql
@@ -1,32 +1,45 @@
-query Games {
- games {
- id
- city
- date
- gender
- location
- opponentId
- result
- sport
- state
- time
- scoreBreakdown
- team {
- id
- color
- image
- name
- }
- boxScore {
- team
- period
- time
- description
- scorer
- assist
- scoreBy
- corScore
- oppScore
- }
- }
+fragment TeamFragment on TeamType {
+ id
+ color
+ image
+ name
+}
+
+fragment BoxScoreEntryFragment on BoxScoreEntryType {
+ team
+ period
+ time
+ description
+ scorer
+ assist
+ scoreBy
+ corScore
+ oppScore
+}
+
+fragment GameFragment on GameType {
+ id
+ city
+ date
+ gender
+ location
+ opponentId
+ result
+ sport
+ state
+ time
+ scoreBreakdown
+ utcDate
+ team {
+ ...TeamFragment
+ }
+ boxScore {
+ ...BoxScoreEntryFragment
+ }
+}
+
+query Games($limit: Int!, $offset: Int!) {
+ games(limit: $limit, offset: $offset) {
+ ...GameFragment
+ }
}
diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls
index bd9e8e4..76c410d 100644
--- a/score-ios/Models/GraphQL/schema.graphqls
+++ b/score-ios/Models/GraphQL/schema.graphqls
@@ -25,7 +25,13 @@ directive @defer(
type Query {
youtubeVideos: [YoutubeVideoType]
youtubeVideo(id: String!): YoutubeVideoType
- games: [GameType]
+ games(
+ """Number of games to return"""
+ limit: Int = 100
+
+ """Number of games to skip"""
+ offset: Int = 0
+ ): [GameType]
game(id: String!): GameType
gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType
gamesBySport(sport: String!): [GameType]
@@ -52,6 +58,7 @@ type YoutubeVideoType {
title: String!
description: String!
thumbnail: String!
+ b64Thumbnail: String!
url: String!
publishedAt: String!
}
@@ -84,9 +91,10 @@ type GameType {
sport: String!
state: String!
time: String
- boxScore: [BoxScore]
+ boxScore: [BoxScoreEntryType]
scoreBreakdown: [[String]]
team: TeamType
+ utcDate: String
}
"""
@@ -103,7 +111,7 @@ Attributes:
- `cor_score`: Cornell's score at the time of the event.
- `opp_score`: Opponent's score at the time of the event.
"""
-type BoxScore {
+type BoxScoreEntryType {
team: String
period: String
time: String
@@ -122,24 +130,26 @@ Attributes:
- `id`: The ID of the team (optional).
- `color`: The color of the team.
- `image`: The image of the team (optional).
+ - `b64_image`: The base64 encoded image of the team (optional).
- `name`: The name of the team.
"""
type TeamType {
id: String
color: String!
image: String
+ b64Image: String
name: String!
}
type Mutation {
"""Creates a new game."""
- createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!): CreateGame
+ createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!, utcDate: String): CreateGame
"""Creates a new team."""
- createTeam(color: String!, image: String, name: String!): CreateTeam
+ createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam
"""Creates a new youtube video."""
- createYoutubeVideo(description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo
+ createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo
}
type CreateGame {
@@ -152,4 +162,4 @@ type CreateTeam {
type CreateYoutubeVideo {
youtubeVideo: YoutubeVideoType
-}
+}
\ No newline at end of file
diff --git a/score-ios/Models/Sport.swift b/score-ios/Models/Sport.swift
index ebdf88f..837a2fa 100644
--- a/score-ios/Models/Sport.swift
+++ b/score-ios/Models/Sport.swift
@@ -16,7 +16,7 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible {
// Both
// case Basketball
// case CrossCountry
- case IceHockey
+// case IceHockey
case Lacrosse
case Soccer
// case Squash
@@ -64,8 +64,8 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible {
// return "Basketball"
// case .CrossCountry:
// return "Cross Country"
- case .IceHockey:
- return "Ice Hockey"
+// case .IceHockey:
+// return "Ice Hockey"
case .Lacrosse:
return "Lacrosse"
case .Soccer:
diff --git a/score-ios/Networking/GamesCacheManager.swift b/score-ios/Networking/GamesCacheManager.swift
new file mode 100644
index 0000000..e40537a
--- /dev/null
+++ b/score-ios/Networking/GamesCacheManager.swift
@@ -0,0 +1,232 @@
+//
+// GamesCacheManager.swift
+// score-ios
+//
+// Created by Jayson Hahn on 4/29/25.
+//
+
+import Foundation
+import GameAPI
+
+// Represents the type of games tab
+enum GameTimeframe {
+ case past
+ case upcoming
+}
+
+class GamesCacheManager {
+
+ static let shared = GamesCacheManager()
+
+ private var pastGames: [GamesQuery.Data.Game] = []
+ private var upcomingGames: [GamesQuery.Data.Game] = []
+
+ // Keep track of pagination state
+ private var currentOffset = 0
+ private var isLoading = false
+ private var hasMoreData = true
+
+ private var allGameIds: Set = []
+
+ // Page size for each tab
+ private let defaultPageSize = 10
+
+ // Initialize with empty cache
+ private init() {}
+
+ // Get cached games by type with specified limit
+ func getGames(type: GameTimeframe, limit: Int) -> [GamesQuery.Data.Game] {
+ switch type {
+ case .past:
+ return Array(pastGames.prefix(limit))
+ case .upcoming:
+ return Array(upcomingGames.prefix(limit))
+ }
+ }
+
+ // Check if we have enough games of the specified type
+ func hasEnoughGames(type: GameTimeframe, count: Int) -> Bool {
+ switch type {
+ case .past:
+ return pastGames.count >= count
+ case .upcoming:
+ return upcomingGames.count >= count
+ }
+ }
+
+ // TODO: Revise then when backend sorts games by date
+ // Load more games of the specified type
+ func loadMoreGames(type: GameTimeframe, pageSize: Int, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) {
+ // If already loading or no more data, return
+ guard !isLoading && hasMoreData else {
+ completion([], nil)
+ return
+ }
+
+ // If we already have enough games cached, just return from cache
+ if hasEnoughGames(type: type, count: getNextPageIndex(type: type) + pageSize) {
+ let startIndex = getNextPageIndex(type: type)
+ let endIndex = min(startIndex + pageSize, type == .past ? pastGames.count : upcomingGames.count)
+ let gamesPage = Array((type == .past ? pastGames : upcomingGames)[startIndex.. Int {
+ switch type {
+ case .past:
+ return pastGames.count
+ case .upcoming:
+ return upcomingGames.count
+ }
+ }
+
+ // TODO: Revise then when backend sorts games by date
+ // Load more games from the network with increasing batch size until we have enough
+ private func loadMoreFromNetwork(desiredType: GameTimeframe, desiredCount: Int, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) {
+ isLoading = true
+
+ // Calculate how many more games we need - use a multiplier since we don't know how many will be of each type
+ let requestLimit = max(desiredCount * 3, 20) // Request more than needed to account for filtering
+
+ NetworkManager.shared.fetchGames(limit: requestLimit, offset: currentOffset) { [weak self] games, error in
+ guard let self = self else { return }
+
+ self.isLoading = false
+
+ if let error = error {
+ completion(nil, error)
+ return
+ }
+
+ guard let games = games else {
+ completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No games returned"]))
+ return
+ }
+
+ // If no games returned, we've reached the end
+ if games.isEmpty {
+ self.hasMoreData = false
+ completion([], nil)
+ return
+ }
+
+ // Sort games into past and upcoming categories using updated logic
+ let now = Date()
+ let twoHours: TimeInterval = 2 * 60 * 60 // For determining live games
+ let calendar = Calendar.current
+ let startOfToday = calendar.startOfDay(for: now)
+
+ var newPastGames: [GamesQuery.Data.Game] = []
+ var newUpcomingGames: [GamesQuery.Data.Game] = []
+
+ for game in games {
+ if let gameDate = self.parseDate(from: game.date) {
+ // Check if game is live (started but within 2 hours)
+ let isLive = gameDate < now && now.timeIntervalSince(gameDate) <= twoHours
+
+ // Check if game is upcoming (hasn't started yet)
+ let isUpcoming = gameDate > now
+
+ // Check if game finished today
+ let isFinishedToday = gameDate < now && gameDate >= startOfToday
+
+ // Check if game finished before today
+ let isFinishedByToday = gameDate < startOfToday
+
+ // Apply sorting logic
+ if isLive || isUpcoming || isFinishedToday {
+ // Live games should be at the beginning of upcoming games list
+ if isLive {
+ // Insert at beginning of upcoming games
+ newUpcomingGames.insert(game, at: 0)
+ } else {
+ // Regular upcoming or finished today games
+ newUpcomingGames.append(game)
+ }
+ }
+
+ if isFinishedByToday {
+ newPastGames.append(game)
+ }
+ }
+ }
+
+ // Update the cache
+ self.pastGames.append(contentsOf: newPastGames)
+ self.upcomingGames.append(contentsOf: newUpcomingGames)
+
+ // Update the offset for the next request
+ self.currentOffset += games.count
+
+ // Determine which games to return based on the requested type
+ var gamesForType: [GamesQuery.Data.Game] = []
+ let startIndex = desiredType == .past ? self.pastGames.count - newPastGames.count : self.upcomingGames.count - newUpcomingGames.count
+ let allGamesOfType = desiredType == .past ? self.pastGames : self.upcomingGames
+
+ if startIndex < allGamesOfType.count {
+ let endIndex = min(startIndex + desiredCount, allGamesOfType.count)
+ gamesForType = Array(allGamesOfType[startIndex.. Date? {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXX"
+ dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
+ return dateFormatter.date(from: dateString)
+ }
+
+ // Refresh the cache (e.g., on pull to refresh)
+ func refreshCache(completion: @escaping (Error?) -> Void) {
+ pastGames = []
+ upcomingGames = []
+ currentOffset = 0
+ hasMoreData = true
+
+ // Load initial data
+ NetworkManager.shared.fetchGames(limit: defaultPageSize * 2, offset: 0) { [weak self] games, error in
+ guard let self = self else { return }
+
+ if let error = error {
+ completion(error)
+ return
+ }
+
+ guard let games = games else {
+ completion(nil)
+ return
+ }
+
+ // Sort games into past and upcoming
+ let now = Date()
+ for game in games {
+ if let gameDate = self.parseDate(from: game.date), gameDate < now {
+ self.pastGames.append(game)
+ } else {
+ self.upcomingGames.append(game)
+ }
+ }
+
+ self.currentOffset = games.count
+ completion(nil)
+ }
+ }
+
+}
diff --git a/score-ios/Networking/NetworkManager.swift b/score-ios/Networking/NetworkManager.swift
index b00c8fe..57bace5 100644
--- a/score-ios/Networking/NetworkManager.swift
+++ b/score-ios/Networking/NetworkManager.swift
@@ -10,11 +10,12 @@ import Apollo
import GameAPI
class NetworkManager {
+
static let shared = NetworkManager()
let apolloClient = ApolloClient(url: ScoreEnvironment.baseURL)
-
- func fetchGames(completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) {
- apolloClient.fetch(query: GamesQuery()) { result in
+
+ func fetchGames(limit: Int, offset: Int, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) {
+ apolloClient.fetch(query: GamesQuery(limit: limit, offset: offset)) { result in
switch result {
case .success(let graphQLResult):
if let gamesData = graphQLResult.data?.games?.compactMap({ $0 }) {
@@ -28,10 +29,10 @@ class NetworkManager {
}
}
}
-
+
func fetchTeamById(by id: String, completion: @escaping (GetTeamByIdQuery.Data.Team?, Error?) -> Void) {
let query = GetTeamByIdQuery(id: id)
-
+
apolloClient.fetch(query: query) { result in
switch result {
case .success(let graphQLResult):
@@ -45,5 +46,5 @@ class NetworkManager {
}
}
}
-
+
}
diff --git a/score-ios/ViewModels/GamesViewModel.swift b/score-ios/ViewModels/GamesViewModel.swift
index add5759..910b8e9 100644
--- a/score-ios/ViewModels/GamesViewModel.swift
+++ b/score-ios/ViewModels/GamesViewModel.swift
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
+import GameAPI
// State enum to track the loading state
enum DataState {
@@ -23,6 +24,11 @@ class GamesViewModel: ObservableObject
@Published var allUpcomingGames: [Game] = []
@Published var allPastGames: [Game] = []
+ // private games data
+ private var privateGames: [Game] = []
+ private var privateUpcomingGames: [Game] = []
+ private var privatePastGames: [Game] = []
+
// Carousel Logic
@Published var selectedCardIndex: Int = 0
@Published var topUpcomingGames: [Game] = [] // Displayed in Carousel
@@ -70,70 +76,226 @@ class GamesViewModel: ObservableObject
}
// Networking
+// func fetchGames() {
+// // Set loading state before fetch
+// dataState = .loading
+//
+// NetworkManager.shared.fetchGames { fetchedGames, error in
+// DispatchQueue.main.async {
+// self.games.removeAll()
+// self.allPastGames.removeAll()
+// self.allUpcomingGames.removeAll()
+//
+// if let error = error {
+// self.dataState = .error(error: .networkError)
+// print("Error in fetchGames: \(error.localizedDescription)")
+// return
+// }
+//
+// guard let fetchedGames = fetchedGames else {
+// self.dataState = .error(error: .emptyData)
+// return
+// }
+//
+// var updatedGames: [Game] = []
+// fetchedGames.indices.forEach { index in
+// let gameData = fetchedGames[index]
+// let game = Game(game: gameData)
+// if Sport.allCases.contains(game.sport) && game.sport != Sport.All {
+// // append the game only if it is upcoming/live
+// let now = Date()
+// let twoHours: TimeInterval = 2 * 60 * 60 // TODO: How to decide if a game is live
+// let calendar = Calendar.current
+// let startOfToday = calendar.startOfDay(for: now)
+//
+// let isLive = game.date < now && now.timeIntervalSince(game.date) <= twoHours
+// let isUpcoming = game.date > now
+// let isFinishedToday = game.date < now && game.date >= startOfToday
+// let isFinishedByToday = game.date < startOfToday
+//
+// updatedGames.append(game)
+// if isLive {
+// self.allUpcomingGames.insert(game, at: 0)
+// } else if isUpcoming {
+// self.allUpcomingGames.append(game)
+// } else if isFinishedToday {
+// self.allUpcomingGames.append(game)
+// }
+// if isFinishedByToday {
+// self.allPastGames.append(game)
+// }
+// }
+// }
+// // TODO: do this in a way that requires less copying by sorting at the top
+// self.allPastGames.sort(by: {$0.date > $1.date})
+// self.allUpcomingGames.sort(by: {$0.date < $1.date})
+// self.games = updatedGames.sorted(by: {$0.date < $1.date})
+// self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3))
+// self.topPastGames = Array(self.allPastGames.prefix(3))
+// self.filter()
+//
+// // Update state to success
+// self.dataState = .success
+// }
+// }
+// }
+
+ // TODO: Remove once backend is has implemented pagination with sorted dates and pages by game type
func fetchGames() {
// Set loading state before fetch
dataState = .loading
+ // Clear the current arrays
+ self.games.removeAll()
+ self.allPastGames.removeAll()
+ self.allUpcomingGames.removeAll()
- NetworkManager.shared.fetchGames { fetchedGames, error in
- DispatchQueue.main.async {
- self.games.removeAll()
- self.allPastGames.removeAll()
- self.allUpcomingGames.removeAll()
+ // Start fetching from the first page
+ fetchGamesRecursively(limit: 50, offset: 0, accumulatedGames: [])
+ }
- if let error = error {
+ private func fetchGamesRecursively(limit: Int, offset: Int, accumulatedGames: [GamesQuery.Data.Game], retryCount: Int = 0, maxRetries: Int = 3) {
+ // Create a timeout
+ let timeoutWorkItem = DispatchWorkItem {
+ print("Request timed out for offset: \(offset)")
+ // If we haven't exceeded max retries, try again
+ if retryCount < maxRetries {
+ print("Retrying request (\(retryCount + 1)/\(maxRetries))...")
+ DispatchQueue.main.async {
+ self.fetchGamesRecursively(limit: limit, offset: offset, accumulatedGames: accumulatedGames, retryCount: retryCount + 1, maxRetries: maxRetries)
+ }
+ } else {
+ print("Max retries exceeded. Giving up on request for offset: \(offset)")
+ DispatchQueue.main.async {
+// // If this was the first request and it failed after all retries, show error
+// if offset == 0 && accumulatedGames.isEmpty {
+// self.dataState = .error(error: .networkError)
+// } else {
+// // Otherwise process what we have so far
+// self.processGames(accumulatedGames)
+// }
self.dataState = .error(error: .networkError)
- print("Error in fetchGames: \(error.localizedDescription)")
- return
}
+ }
+ }
+
+ // Schedule timeout
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: timeoutWorkItem)
+
+ NetworkManager.shared.fetchGames(limit: limit, offset: offset) { [weak self] fetchedGames, error in
+ guard let self = self else { return }
+
+ // Cancel the timeout since we got a response
+ timeoutWorkItem.cancel()
+
+ DispatchQueue.main.async {
+ if let error = error {
+ print("Error in fetchGames for offset \(offset): \(error.localizedDescription)")
- guard let fetchedGames = fetchedGames else {
- self.dataState = .error(error: .emptyData)
+ // If we haven't exceeded max retries, try again
+ if retryCount < maxRetries {
+ print("Retrying request (\(retryCount + 1)/\(maxRetries))...")
+ self.fetchGamesRecursively(limit: limit, offset: offset, accumulatedGames: accumulatedGames, retryCount: retryCount + 1, maxRetries: maxRetries)
+ } else {
+ print("Max retries exceeded. Giving up on request for offset: \(offset)")
+ // If this was the first request and it failed after all retries, show error
+ if offset == 0 && accumulatedGames.isEmpty {
+ self.dataState = .error(error: .networkError)
+ } else {
+ // Otherwise process what we have so far
+ self.processGames(accumulatedGames)
+ }
+ }
return
}
- var updatedGames: [Game] = []
- fetchedGames.indices.forEach { index in
- let gameData = fetchedGames[index]
- let game = Game(game: gameData)
- if Sport.allCases.contains(game.sport) && game.sport != Sport.All {
- // append the game only if it is upcoming/live
- let now = Date()
- let twoHours: TimeInterval = 2 * 60 * 60 // TODO: How to decide if a game is live
- let calendar = Calendar.current
- let startOfToday = calendar.startOfDay(for: now)
-
- let isLive = game.date < now && now.timeIntervalSince(game.date) <= twoHours
- let isUpcoming = game.date > now
- let isFinishedToday = game.date < now && game.date >= startOfToday
- let isFinishedByToday = game.date < startOfToday
-
- updatedGames.append(game)
- if isLive {
- self.allUpcomingGames.insert(game, at: 0)
- } else if isUpcoming {
- self.allUpcomingGames.append(game)
- } else if isFinishedToday {
- self.allUpcomingGames.append(game)
- }
- if isFinishedByToday {
- self.allPastGames.append(game)
- }
+ guard let fetchedGames = fetchedGames, !fetchedGames.isEmpty else {
+ // If this is the first fetch and no games, show empty data error
+ if offset == 0 {
+ self.dataState = .error(error: .emptyData)
+ } else {
+// // Otherwise process all accumulated games
+// self.processGames(accumulatedGames)
+ self.dataState = .error(error: .networkError)
}
+ return
+ }
+
+ // Combine newly fetched games with previously accumulated games
+ let allGames = accumulatedGames + fetchedGames
+
+ // If we received a full page, there might be more games to fetch
+ if fetchedGames.count == limit {
+ // Continue fetching the next page (reset retry count for the next page)
+ self.fetchGamesRecursively(limit: limit, offset: offset + limit, accumulatedGames: allGames, retryCount: 0, maxRetries: maxRetries)
+ } else {
+ // We've fetched all games, process them
+ self.processGames(allGames)
}
- // TODO: do this in a way that requires less copying by sorting at the top
- self.allPastGames.sort(by: {$0.date > $1.date})
- self.allUpcomingGames.sort(by: {$0.date < $1.date})
- self.games = updatedGames.sorted(by: {$0.date < $1.date})
- self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3))
- self.topPastGames = Array(self.allPastGames.suffix(3))
- self.filter()
-
- // Update state to success
- self.dataState = .success
}
}
}
+ private func processGames(_ gameDataArray: [GamesQuery.Data.Game]) {
+ var updatedGames: [Game] = []
+ gameDataArray.indices.forEach { index in
+ let gameData = gameDataArray[index]
+ let game = Game(game: gameData)
+ if Sport.allCases.contains(game.sport) && game.sport != Sport.All {
+ // append the game only if it is upcoming/live
+ let now = Date()
+ let twoHours: TimeInterval = 2 * 60 * 60 // TODO: How to decide if a game is live
+ let calendar = Calendar.current
+ let startOfToday = calendar.startOfDay(for: now)
+ let isLive = game.date < now && now.timeIntervalSince(game.date) <= twoHours
+ let isUpcoming = game.date > now
+ let isFinishedToday = game.date < now && game.date >= startOfToday
+ let isFinishedByToday = game.date < startOfToday
+ updatedGames.append(game)
+ if isLive {
+ self.privateUpcomingGames.insert(game, at: 0)
+ } else if isUpcoming {
+ self.privateUpcomingGames.append(game)
+ } else if isFinishedToday {
+ self.privateUpcomingGames.append(game)
+ }
+ if isFinishedByToday {
+ self.privatePastGames.append(game)
+ }
+ }
+ }
+
+ // Filter out duplicates before sorting
+ self.allPastGames = uniqueGames(from: self.privatePastGames)
+ self.allUpcomingGames = uniqueGames(from: self.privateUpcomingGames)
+ self.games = uniqueGames(from: updatedGames)
+
+ // Sort all the collections
+ self.allPastGames.sort(by: {$0.date > $1.date})
+ self.allUpcomingGames.sort(by: {$0.date < $1.date})
+ self.games = updatedGames.sorted(by: {$0.date < $1.date})
+ self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3))
+ self.topPastGames = Array(self.allPastGames.prefix(3))
+ self.filter()
+
+ // Update state to success
+ self.dataState = .success
+ }
+
+ // Function to filter out duplicate games by ID
+ func uniqueGames(from games: [Game]) -> [Game] {
+ var uniqueGames: [Game] = []
+ var seenIDs: Set = []
+
+ for game in games {
+ if let id = game.serverId, !seenIDs.contains(id) {
+ uniqueGames.append(game)
+ seenIDs.insert(id)
+ }
+ }
+
+ return uniqueGames
+ }
+
// Method to retry after an error
func retryFetch() {
fetchGames()
diff --git a/score-ios/ViewModels/PastGameViewModel.swift b/score-ios/ViewModels/PastGameViewModel.swift
index 7532499..aeb8dcf 100644
--- a/score-ios/ViewModels/PastGameViewModel.swift
+++ b/score-ios/ViewModels/PastGameViewModel.swift
@@ -13,42 +13,56 @@ class PastGameViewModel: ObservableObject {
var numberOfRounds: Int {
switch game.sport {
- case .Baseball: return 9
+ case .Baseball: return game.timeUpdates.count > 3 ? game.timeUpdates.count - 3 : 9
+ // number of innings is not always 9
case .Soccer: return 2
- case .IceHockey: return 3
+// case .IceHockey: return 3
case .FieldHockey, .Football, .Lacrosse: return 4
default: return 1
}
}
- // TODO: will be discarded once backend is changed to include a total score in scoreBreakdown for all sports
- var cornellTotalScore: Int {
- if game.timeUpdates.count == numberOfRounds {
- return game.timeUpdates.reduce(0, { $0 + $1.cornellScore }) // sum up the score for each round
- } else if game.timeUpdates.count == numberOfRounds + 1 {
- // the last one is the sum
- return game.timeUpdates[game.timeUpdates.count-1].cornellScore
- } else if game.timeUpdates.count > numberOfRounds {
- let scores = game.timeUpdates[0.. 3 ? game.timeUpdates.count - 2 : 10
+ // the last three columns are total runs, hits, and errors
+ // if backend stores null for scoreBreakdown, display regular score box with 10 columns
+ case .Soccer: return game.timeUpdates.count >= 3 ? game.timeUpdates.count : 3
+// case .IceHockey: return game.timeUpdates.count
+ case .FieldHockey, .Football, .Lacrosse: return game.timeUpdates.count >= 5 ? game.timeUpdates.count : 5
+ default: return 1
+ }
+ }
+
+ var numberOfOvertimes: Int {
+ switch game.sport {
+ case .Baseball: return -1
+ case .Soccer: return game.timeUpdates.count - 3
+// case .IceHockey: return game.timeUpdates.count - 4
+ case .FieldHockey, .Football, .Lacrosse: return game.timeUpdates.count - 5
+ default: return -1
}
- else {
- return -1
+ }
+
+
+ var cornellTotalScore: Int {
+ // TODO: Get this back when backend fixes the boxScore (make sure the last entry reflects total score correctly)
+// return game.gameUpdates.count > 0 ? game.gameUpdates[game.gameUpdates.count - 1].cornellScore : -1
+ if game.sport == .Baseball {
+ return game.gameUpdates.count > 0 ? game.gameUpdates[game.gameUpdates.count - 1].cornellScore : -1
}
+
+ return game.timeUpdates.count > 0 ? game.timeUpdates[game.timeUpdates.count - 1].cornellScore : -1
}
var opponentTotalScore: Int {
- if game.timeUpdates.count == numberOfRounds {
- return game.timeUpdates.reduce(0, { $0 + $1.opponentScore })
- } else if game.timeUpdates.count == numberOfRounds + 1 {
- // the last one is the sum
- return game.timeUpdates[game.timeUpdates.count-1].opponentScore
- } else if game.timeUpdates.count > numberOfRounds {
- let scores = game.timeUpdates[0.. 0 ? game.gameUpdates[game.gameUpdates.count - 1].opponentScore : -1
+ if game.sport == .Baseball {
+ return game.gameUpdates.count > 0 ? game.gameUpdates[game.gameUpdates.count - 1].opponentScore : -1
}
+
+ return game.timeUpdates.count > 0 ? game.timeUpdates[game.timeUpdates.count - 1].opponentScore : -1
}
var corScore: String {
@@ -62,5 +76,4 @@ class PastGameViewModel: ObservableObject {
init(game: Game) {
self.game = game
}
-
}
diff --git a/score-ios/Views/DetailedViews/DynamicScoreBox.swift b/score-ios/Views/DetailedViews/DynamicScoreBox.swift
index a0b8e67..abfe92b 100644
--- a/score-ios/Views/DetailedViews/DynamicScoreBox.swift
+++ b/score-ios/Views/DetailedViews/DynamicScoreBox.swift
@@ -15,7 +15,7 @@ struct DynamicScoreBox: View {
GeometryReader { geometry in
let boxWidth = geometry.size.width
let firstColWidth = boxWidth / 5
- let columnWidth = (boxWidth - firstColWidth) / CGFloat((viewModel.numberOfRounds + 1))
+ let columnWidth = (boxWidth - firstColWidth) / CGFloat(viewModel.numberOfColumns)
VStack(spacing: 0) {
firstRow(firstColWidth: firstColWidth, columnWidth: columnWidth)
@@ -54,6 +54,14 @@ extension DynamicScoreBox {
}
.font(Constants.Fonts.gameText)
+ if (viewModel.numberOfOvertimes >= 1) {
+ ForEach(1...viewModel.numberOfOvertimes, id: \..self) { ot in
+ Text("OT \(ot)")
+ .frame(width: columnWidth)
+ }
+ .font(Constants.Fonts.gameText)
+ }
+
Text("Total")
.font(Constants.Fonts.gameText)
.padding(.trailing, 3)
@@ -71,7 +79,7 @@ extension DynamicScoreBox {
.padding(.leading, 5)
.frame(width: firstColWidth, alignment: .leading)
- ForEach(0..: View {
-
+
let games: [Game]
let tileView: (Game) -> TileView
-
+
+ @State private var displayedGamesCount = 10
+
var body: some View {
LazyVStack(spacing: 16) {
if games.isEmpty {
NoGameView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
- ForEach(games) { game in
+ // Calculate how many games to show (either displayedGamesCount or all games if less)
+ let gamesToShow = min(displayedGamesCount, games.count)
+
+ // Only display the subset of games
+ ForEach(games.prefix(gamesToShow)) { game in
GeometryReader { cellGeometry in
let isCellCovered = cellGeometry.frame(in: .global).minY < 100
if !isCellCovered {
@@ -31,8 +37,27 @@ struct GameListView: View {
.buttonStyle(PlainButtonStyle())
}
}
+ .frame(height: 96)
+ }
+
+ // Load More Button - only show if we haven't displayed all games yet
+ if displayedGamesCount < games.count {
+ Button(action: {
+ // Increase the displayed games count by 10 more (or remaining count)
+ displayedGamesCount = min(displayedGamesCount + 10, games.count)
+ }) {
+ Text("Load More")
+ .font(Constants.Fonts.buttonLabel)
+ .foregroundStyle(Constants.Colors.white)
+// .frame(maxWidth: .infinity)
+ .padding()
+ .background(Constants.Colors.primary_red)
+ .cornerRadius(8)
+ }
+ .clipShape(Capsule())
+ .padding(.top, 8)
+ .padding(.horizontal, 20)
}
- .frame(height: 96)
// Temp Fix So the last cell is not covered by the tab bar
Rectangle()
diff --git a/score-ios/Views/MainViews/Home.swift b/score-ios/Views/MainViews/Home.swift
index 3847653..90380d4 100644
--- a/score-ios/Views/MainViews/Home.swift
+++ b/score-ios/Views/MainViews/Home.swift
@@ -6,9 +6,20 @@
//
import SwiftUI
+import FirebaseCore
+
+class AppDelegate: NSObject, UIApplicationDelegate {
+ func application(_ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
+ FirebaseApp.configure()
+ return true
+ }
+}
@main
struct score_iosApp: App {
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
+
var body: some Scene {
WindowGroup {
ContentView()