Files
Sportstime/SportsTime/Export/Services/RemoteImageService.swift
Trey t 045fcd9c07 Remove debug prints and fix build warnings
- Remove all print statements from planning engine, data providers, and PDF generation
- Fix deprecated CLGeocoder usage with MKLocalSearch for iOS 26
- Fix Swift 6 actor isolation by converting PDFGenerator/ExportService to @MainActor
- Add @retroactive to CLLocationCoordinate2D protocol conformances
- Fix unused variable warnings in GameDAGRouter and scenario planners
- Remove unreachable catch blocks in SettingsViewModel

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:25:27 -06:00

179 lines
5.1 KiB
Swift

//
// RemoteImageService.swift
// SportsTime
//
// Downloads and caches remote images for PDF export (team logos, stadium photos).
//
import Foundation
import UIKit
actor RemoteImageService {
// MARK: - Errors
enum ImageFetchError: Error, LocalizedError {
case invalidURL
case downloadFailed(String)
case invalidImageData
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid image URL"
case .downloadFailed(let reason):
return "Image download failed: \(reason)"
case .invalidImageData:
return "Downloaded data is not a valid image"
}
}
}
// MARK: - Properties
private let urlSession: URLSession
private var imageCache: [URL: UIImage] = [:]
// MARK: - Init
init() {
// Configure URLSession with caching
let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
config.urlCache = URLCache(
memoryCapacity: 50 * 1024 * 1024, // 50 MB memory
diskCapacity: 100 * 1024 * 1024, // 100 MB disk
diskPath: "ImageCache"
)
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 30
self.urlSession = URLSession(configuration: config)
}
// MARK: - Public Methods
/// Fetch a single image from URL
func fetchImage(from url: URL) async throws -> UIImage {
// Check cache first
if let cached = imageCache[url] {
return cached
}
// Download image
let (data, response) = try await urlSession.data(from: url)
// Validate response
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
throw ImageFetchError.downloadFailed("HTTP \(statusCode)")
}
// Create image
guard let image = UIImage(data: data) else {
throw ImageFetchError.invalidImageData
}
// Scale image for PDF (max 400pt width to keep file size reasonable)
let scaledImage = scaleImage(image, maxWidth: 400)
// Cache it
imageCache[url] = scaledImage
return scaledImage
}
/// Batch fetch multiple images in parallel
func fetchImages(from urls: [URL]) async -> [URL: UIImage] {
var results: [URL: UIImage] = [:]
await withTaskGroup(of: (URL, UIImage?).self) { group in
for url in urls {
group.addTask {
do {
let image = try await self.fetchImage(from: url)
return (url, image)
} catch {
return (url, nil)
}
}
}
for await (url, image) in group {
if let image = image {
results[url] = image
}
}
}
return results
}
/// Fetch team logos by team ID
func fetchTeamLogos(teams: [Team]) async -> [UUID: UIImage] {
let urlToTeam: [URL: UUID] = Dictionary(
uniqueKeysWithValues: teams.compactMap { team in
guard let logoURL = team.logoURL else { return nil }
return (logoURL, team.id)
}
)
let images = await fetchImages(from: Array(urlToTeam.keys))
var result: [UUID: UIImage] = [:]
for (url, image) in images {
if let teamId = urlToTeam[url] {
result[teamId] = image
}
}
return result
}
/// Fetch stadium photos by stadium ID
func fetchStadiumPhotos(stadiums: [Stadium]) async -> [UUID: UIImage] {
let urlToStadium: [URL: UUID] = Dictionary(
uniqueKeysWithValues: stadiums.compactMap { stadium in
guard let imageURL = stadium.imageURL else { return nil }
return (imageURL, stadium.id)
}
)
let images = await fetchImages(from: Array(urlToStadium.keys))
var result: [UUID: UIImage] = [:]
for (url, image) in images {
if let stadiumId = urlToStadium[url] {
result[stadiumId] = image
}
}
return result
}
/// Clear the in-memory cache
func clearCache() {
imageCache.removeAll()
}
// MARK: - Private Methods
/// Scale image to max width while maintaining aspect ratio
private func scaleImage(_ image: UIImage, maxWidth: CGFloat) -> UIImage {
let currentWidth = image.size.width
guard currentWidth > maxWidth else { return image }
let scale = maxWidth / currentWidth
let newSize = CGSize(
width: image.size.width * scale,
height: image.size.height * scale
)
let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
}
}