Files
Sportstime/SportsTime/Features/Settings/SportsIconImageGenerator.swift
Trey t c94e373e33 fix: comprehensive codebase hardening — crashes, silent failures, performance, and security
Fixes ~95 issues from deep audit across 12 categories in 82 files:

- Crash prevention: double-resume in PhotoMetadataExtractor, force unwraps in
  DateRangePicker, array bounds checks in polls/achievements, ProGate hit-test
  bypass, Dictionary(uniqueKeysWithValues:) → uniquingKeysWith in 4 files
- Silent failure elimination: all 34 try? sites replaced with do/try/catch +
  logging (SavedTrip, TripDetailView, CanonicalSyncService, BootstrapService,
  CanonicalModels, CKModels, SportsTimeApp, and more)
- Performance: cached DateFormatters (7 files), O(1) team lookups via
  AppDataProvider, achievement definition dictionary, AnimatedBackground
  consolidated from 19 Tasks to 1, task cancellation in SharePreviewView
- Concurrency: UIKit drawing → MainActor.run, background fetch timeout guard,
  @MainActor on ThemeManager/AppearanceManager, SyncLogger read/write race fix
- Planning engine: game end time in travel feasibility, state-aware city
  normalization, exact city matching, DrivingConstraints parameter propagation
- IAP: unknown subscription states → expired, unverified transaction logging,
  entitlements updated before paywall dismiss, restore visible to all users
- Security: API key to Info.plist lookup, filename sanitization in PDF export,
  honest User-Agent, removed stale "Feels" analytics super properties
- Navigation: consolidated competing navigationDestination, boolean → value-based
- Testing: 8 sleep() → waitForExistence, duplicates extracted, Swift 6 compat
- Service bugs: infinite retry cap, duplicate achievement prevention, TOCTOU vote
  fix, PollVote.odg → voterId rename, deterministic placeholder IDs, parallel
  MKDirections, Sendable-safe POI struct

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:03:09 -06:00

285 lines
10 KiB
Swift

//
// SportsIconImageGenerator.swift
// SportsTime
//
// Generates a 1024x1024 PNG with randomly scattered sports icons.
//
import SwiftUI
import UIKit
struct SportsIconImageGenerator {
/// All sports icons used in the animated background
private static let sportsIcons: [String] = [
// Sports balls
"football.fill",
"basketball.fill",
"baseball.fill",
"hockey.puck.fill",
"soccerball",
"tennisball.fill",
"volleyball.fill",
// Sports figures
"figure.run",
"figure.baseball",
"figure.basketball",
"figure.hockey",
"figure.soccer",
// Venues/events
"sportscourt.fill",
"trophy.fill",
"ticket.fill",
// Travel/navigation
"mappin.circle.fill",
"car.fill",
"map.fill",
"flag.checkered"
]
/// Generates a 1024x1024 PNG image with randomly scattered sports icons
/// - Returns: A UIImage with transparent background containing the icons (exactly 1024x1024 pixels)
static func generateImage() -> UIImage {
let size = CGSize(width: 1024, height: 1024)
let maxIconSize: CGFloat = 150
let minIconSize: CGFloat = 40
let minSpacing: CGFloat = 80 // Minimum distance between icon centers
// Use scale 1.0 to ensure exact pixel dimensions (not retina scaled)
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
let renderer = UIGraphicsImageRenderer(size: size, format: format)
// Generate well-distributed positions using Poisson disk sampling
let positions = generateScatteredPositions(
in: size,
minSpacing: minSpacing,
targetCount: 45
)
let image = renderer.image { context in
// Clear background (transparent)
UIColor.clear.setFill()
context.fill(CGRect(origin: .zero, size: size))
// Shuffle icons to randomize which icon appears where
let shuffledIcons = sportsIcons.shuffled()
// Draw icons at scattered positions
for (index, position) in positions.enumerated() {
let iconName = shuffledIcons[index % shuffledIcons.count]
let targetSize = CGFloat.random(in: minIconSize...maxIconSize)
let rotation = CGFloat.random(in: -30...30) * .pi / 180
// Create SF Symbol image at a large point size for quality
let config = UIImage.SymbolConfiguration(pointSize: 100, weight: .regular)
if let symbolImage = UIImage(systemName: iconName, withConfiguration: config) {
// Tint with warm orange
let tintedImage = symbolImage.withTintColor(
UIColor(Theme.warmOrange),
renderingMode: .alwaysOriginal
)
// Calculate proportional size maintaining aspect ratio
let originalSize = tintedImage.size
let aspectRatio = originalSize.width / originalSize.height
let drawSize: CGSize
if aspectRatio > 1 {
// Wider than tall - constrain by width
drawSize = CGSize(width: targetSize, height: targetSize / aspectRatio)
} else {
// Taller than wide - constrain by height
drawSize = CGSize(width: targetSize * aspectRatio, height: targetSize)
}
// Save graphics state for rotation
context.cgContext.saveGState()
// Position at the scattered point
context.cgContext.translateBy(x: position.x, y: position.y)
context.cgContext.rotate(by: rotation)
// Draw the icon centered at origin with correct aspect ratio
let drawRect = CGRect(
x: -drawSize.width / 2,
y: -drawSize.height / 2,
width: drawSize.width,
height: drawSize.height
)
tintedImage.draw(in: drawRect)
context.cgContext.restoreGState()
}
}
}
return image
}
/// Generates well-distributed points using a grid-based approach with jitter
/// This ensures icons are scattered across the entire image, not clumped together
private static func generateScatteredPositions(
in size: CGSize,
minSpacing: CGFloat,
targetCount: Int
) -> [CGPoint] {
var positions: [CGPoint] = []
// Calculate grid dimensions to achieve roughly targetCount points
let gridSize = Int(ceil(sqrt(Double(targetCount))))
let cellWidth = size.width / CGFloat(gridSize)
let cellHeight = size.height / CGFloat(gridSize)
// Margin from edges
let margin: CGFloat = 40
for row in 0..<gridSize {
for col in 0..<gridSize {
// Base position at center of grid cell
let baseCenterX = (CGFloat(col) + 0.5) * cellWidth
let baseCenterY = (CGFloat(row) + 0.5) * cellHeight
// Add random jitter within the cell (up to 40% of cell size)
let jitterX = CGFloat.random(in: -cellWidth * 0.4...cellWidth * 0.4)
let jitterY = CGFloat.random(in: -cellHeight * 0.4...cellHeight * 0.4)
var x = baseCenterX + jitterX
var y = baseCenterY + jitterY
// Clamp to stay within margins
x = max(margin, min(size.width - margin, x))
y = max(margin, min(size.height - margin, y))
// Randomly skip some cells for more organic feel (keep ~80%)
if Double.random(in: 0...1) < 0.85 {
positions.append(CGPoint(x: x, y: y))
}
}
}
// Shuffle to randomize draw order (affects overlap)
return positions.shuffled()
}
/// Generates PNG data from the image
static func generatePNGData() -> Data? {
let image = generateImage()
return image.pngData()
}
}
// MARK: - SwiftUI View for generating and sharing
struct SportsIconImageGeneratorView: View {
@State private var generatedImage: UIImage?
@State private var isGenerating = false
@State private var showShareSheet = false
var body: some View {
VStack(spacing: 16) {
// Preview of generated image
if let image = generatedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 300, maxHeight: 300)
.background(
// Checkerboard pattern to show transparency
CheckerboardPattern()
)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
}
// Generate button
Button {
generateNewImage()
} label: {
HStack {
if isGenerating {
ProgressView()
.tint(.white)
} else {
Image(systemName: "dice.fill")
}
Text(generatedImage == nil ? "Generate Image" : "Regenerate")
}
.frame(maxWidth: .infinity)
.padding()
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.disabled(isGenerating)
// Share button (only visible when image exists)
if let generatedImage {
ShareLink(
item: Image(uiImage: generatedImage),
preview: SharePreview("Sports Icons", image: Image(uiImage: generatedImage))
) {
HStack {
Image(systemName: "square.and.arrow.up")
Text("Share via AirDrop")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
.padding()
}
private func generateNewImage() {
isGenerating = true
// Generate image (UIKit drawing requires main thread)
Task {
let image = SportsIconImageGenerator.generateImage()
withAnimation {
generatedImage = image
isGenerating = false
}
}
}
}
// MARK: - Checkerboard Pattern for transparency preview
private struct CheckerboardPattern: View {
var body: some View {
Canvas { context, size in
let tileSize: CGFloat = 10
let rows = Int(ceil(size.height / tileSize))
let cols = Int(ceil(size.width / tileSize))
for row in 0..<rows {
for col in 0..<cols {
let isLight = (row + col) % 2 == 0
let rect = CGRect(
x: CGFloat(col) * tileSize,
y: CGFloat(row) * tileSize,
width: tileSize,
height: tileSize
)
context.fill(
Path(rect),
with: .color(isLight ? Color.gray.opacity(0.2) : Color.gray.opacity(0.3))
)
}
}
}
}
}
#Preview {
SportsIconImageGeneratorView()
}