Add team logos and monospaced tricodes to iPhone game/series rows
New TeamLogo view loads bundled TeamLogos/{abbrev}.png with a UIImage
cache so scrolling doesn't repeatedly re-decode. GameRow and SeriesRow
now render [logo] TRI @ [logo] TRI with tricodes in a monospaced font
so columns line up regardless of which letters are present. SF Symbol
fallback when an abbrev has no bundled logo (e.g. "TBD" for unfilled
playoff slots).
This commit is contained in:
@@ -12,7 +12,7 @@ struct GameRow: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: open) {
|
Button(action: open) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 10) {
|
||||||
if game.gameType == 2 {
|
if game.gameType == 2 {
|
||||||
Text("#\(game.seasonGameNumber)")
|
Text("#\(game.seasonGameNumber)")
|
||||||
.font(.caption2.monospacedDigit())
|
.font(.caption2.monospacedDigit())
|
||||||
@@ -20,12 +20,9 @@ struct GameRow: View {
|
|||||||
.frame(width: 44, alignment: .leading)
|
.frame(width: 44, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("\(game.awayTeam.abbrev) @ \(game.homeTeam.abbrev)")
|
matchupBlock
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
rightContent
|
rightContent
|
||||||
}
|
}
|
||||||
@@ -36,6 +33,24 @@ struct GameRow: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var matchupBlock: some View {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
TeamLogo(abbrev: game.awayTeam.abbrev)
|
||||||
|
Text(game.awayTeam.abbrev)
|
||||||
|
.font(.body.monospaced())
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Text("@")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
TeamLogo(abbrev: game.homeTeam.abbrev)
|
||||||
|
Text(game.homeTeam.abbrev)
|
||||||
|
.font(.body.monospaced())
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var rightContent: some View {
|
private var rightContent: some View {
|
||||||
let state = game.parsedGameState
|
let state = game.parsedGameState
|
||||||
|
|||||||
@@ -12,17 +12,9 @@ struct SeriesRow: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: open) {
|
Button(action: open) {
|
||||||
HStack(alignment: .center, spacing: 12) {
|
HStack(alignment: .center, spacing: 10) {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
matchupBlock
|
||||||
Text(matchupText)
|
Spacer(minLength: 8)
|
||||||
.font(.body)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
Text(scoreText)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
Text(statusText)
|
Text(statusText)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -42,10 +34,29 @@ struct SeriesRow: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var matchupText: String {
|
private var matchupBlock: some View {
|
||||||
let top = item.series.topSeedTeam?.abbrev ?? "TBD"
|
|
||||||
let bottom = item.series.bottomSeedTeam?.abbrev ?? "TBD"
|
let bottom = item.series.bottomSeedTeam?.abbrev ?? "TBD"
|
||||||
return "\(bottom) @ \(top)"
|
let top = item.series.topSeedTeam?.abbrev ?? "TBD"
|
||||||
|
return VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
TeamLogo(abbrev: bottom)
|
||||||
|
Text(bottom)
|
||||||
|
.font(.body.monospaced())
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Text("@")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
TeamLogo(abbrev: top)
|
||||||
|
Text(top)
|
||||||
|
.font(.body.monospaced())
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
}
|
||||||
|
Text(scoreText)
|
||||||
|
.font(.caption.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var scoreText: String {
|
private var scoreText: String {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// TeamLogo.swift
|
||||||
|
// IceGlass-iOS
|
||||||
|
//
|
||||||
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Loads a bundled team logo PNG from `TeamLogos/{abbrev}.png`. Falls back to
|
||||||
|
/// an SF Symbol if the file is missing (e.g. abbrev is "TBD" for unfilled
|
||||||
|
/// playoff matchups).
|
||||||
|
struct TeamLogo: View {
|
||||||
|
let abbrev: String
|
||||||
|
var size: CGFloat = 22
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let image = Self.loadImage(for: abbrev) {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.interpolation(.high)
|
||||||
|
.scaledToFit()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "shield")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle lookup is trivial but image decoding isn't free — cache by
|
||||||
|
/// abbrev so scrolling through 16+ rows doesn't repeatedly hit disk.
|
||||||
|
private static var cache: [String: UIImage] = [:]
|
||||||
|
|
||||||
|
private static func loadImage(for abbrev: String) -> UIImage? {
|
||||||
|
if let cached = cache[abbrev] { return cached }
|
||||||
|
guard let url = Bundle.main.url(
|
||||||
|
forResource: abbrev, withExtension: "png", subdirectory: "TeamLogos"
|
||||||
|
), let image = UIImage(contentsOfFile: url.path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cache[abbrev] = image
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user