Fix game times with UTC data, restructure schedule by date
- Update games_canonical.json to use ISO 8601 UTC timestamps (game_datetime_utc) - Fix BootstrapService timezone-aware parsing for venue-local fallback - Fix thread-unsafe shared DateFormatter in RichGame local time display - Bump SchemaVersion to 4 to force re-bootstrap with correct UTC data - Restructure schedule view: group by date instead of sport, with sport icons on each row and date section headers showing game counts - Fix schedule row backgrounds using Theme.cardBackground instead of black - Sort games by UTC time with local-time tiebreaker for same-instant games Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ struct ScheduleListView: View {
|
||||
@State private var showDiagnostics = false
|
||||
|
||||
private var hasGames: Bool {
|
||||
!viewModel.gamesBySport.isEmpty
|
||||
!viewModel.gamesByDate.isEmpty
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -109,16 +109,31 @@ struct ScheduleListView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
ForEach(viewModel.gamesBySport, id: \.sport) { sportGroup in
|
||||
ForEach(viewModel.gamesByDate, id: \.date) { dateGroup in
|
||||
Section {
|
||||
ForEach(sportGroup.games) { richGame in
|
||||
GameRowView(game: richGame, showDate: true, showLocation: true)
|
||||
ForEach(dateGroup.games) { richGame in
|
||||
GameRowView(game: richGame, showSport: true, showLocation: true)
|
||||
}
|
||||
} header: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: sportGroup.sport.iconName)
|
||||
.accessibilityHidden(true)
|
||||
Text(sportGroup.sport.rawValue)
|
||||
HStack {
|
||||
Text(formatSectionDate(dateGroup.date))
|
||||
|
||||
if Calendar.current.isDateInToday(dateGroup.date) {
|
||||
Text("TODAY")
|
||||
.font(.caption2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(dateGroup.games.count) games")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.headline)
|
||||
}
|
||||
@@ -233,98 +248,70 @@ struct SportFilterChip: View {
|
||||
// MARK: - Game Row View
|
||||
|
||||
struct GameRowView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
let game: RichGame
|
||||
var showDate: Bool = false
|
||||
var showSport: Bool = false
|
||||
var showLocation: Bool = false
|
||||
|
||||
// Static formatter to avoid allocation per row (significant performance improvement)
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEE, MMM d"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
// Cache isToday check to avoid repeated Calendar calls
|
||||
private var isToday: Bool {
|
||||
Calendar.current.isDateInToday(game.game.dateTime)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Date (when grouped by sport)
|
||||
if showDate {
|
||||
HStack(spacing: 6) {
|
||||
Text(Self.dateFormatter.string(from: game.game.dateTime))
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if isToday {
|
||||
Text("TODAY")
|
||||
.font(.caption2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
// Sport icon
|
||||
if showSport {
|
||||
Image(systemName: game.game.sport.iconName)
|
||||
.font(.title3)
|
||||
.foregroundStyle(game.game.sport.themeColor)
|
||||
.frame(width: 28)
|
||||
.accessibilityLabel(game.game.sport.rawValue)
|
||||
}
|
||||
|
||||
// Teams
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
TeamBadge(team: game.awayTeam, isHome: false)
|
||||
Text("@")
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Teams
|
||||
HStack(spacing: 8) {
|
||||
TeamBadge(team: game.awayTeam, isHome: false)
|
||||
Text("@")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
TeamBadge(team: game.homeTeam, isHome: true)
|
||||
}
|
||||
|
||||
// Game info
|
||||
HStack(spacing: 12) {
|
||||
Label(game.localGameTimeShort, systemImage: "clock")
|
||||
Label(game.stadium.name, systemImage: "building.2")
|
||||
.lineLimit(1)
|
||||
|
||||
if let broadcast = game.game.broadcastInfo {
|
||||
Label(broadcast, systemImage: "tv")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Location
|
||||
if showLocation {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.accessibilityHidden(true)
|
||||
Text(game.stadium.city)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
TeamBadge(team: game.homeTeam, isHome: true)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Game info
|
||||
HStack(spacing: 12) {
|
||||
Label(game.localGameTimeShort, systemImage: "clock")
|
||||
Label(game.stadium.name, systemImage: "building.2")
|
||||
|
||||
if let broadcast = game.game.broadcastInfo {
|
||||
Label(broadcast, systemImage: "tv")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Location info (when grouped by game)
|
||||
if showLocation {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.accessibilityHidden(true)
|
||||
Text(game.stadium.city)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.listRowBackground(isToday ? Color.orange.opacity(0.1) : nil)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(gameAccessibilityLabel)
|
||||
}
|
||||
|
||||
private var gameAccessibilityLabel: String {
|
||||
var parts = ["\(game.awayTeam.name) at \(game.homeTeam.name)"]
|
||||
parts.append(game.stadium.name)
|
||||
var parts: [String] = []
|
||||
if showSport { parts.append(game.game.sport.rawValue) }
|
||||
parts.append("\(game.awayTeam.name) at \(game.homeTeam.name)")
|
||||
parts.append(game.localGameTimeShort)
|
||||
if showDate {
|
||||
parts.append(Self.dateFormatter.string(from: game.game.dateTime))
|
||||
}
|
||||
parts.append(game.stadium.name)
|
||||
return parts.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user