Add iPhone target with shared data layer and persistent cache
Two-target restructure: shared sources (models, services, settings, extensions, team logos) move into Shared/, consumed by both the existing macOS menu bar app and a new iOS app. MainService no longer imports AppKit — platform code attaches via a MainServiceObserver protocol (MacObserverAdapter wires back to MenuManager / StatusItemManager / NotificationManager). iPhone app is a single SwiftUI page mirroring the macOS menu (playoff round + yesterday/today/tomorrow), with a gear-icon settings sheet (display option + IndieAbout for license/changelog). Persistent JSON snapshot in Application Support paints last-known data on cold launch; "Updated …" header escalates secondary → orange (>5min) → red (>30min) so staleness is visually unmistakable. Foreground polling, scenePhase refresh, and pull-to-refresh; no notifications on iOS in v1.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// NHLDataCache.swift
|
||||
// IceGlass
|
||||
//
|
||||
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Snapshot of API state persisted between app launches so cold-launch shows
|
||||
/// last-known data with an "as-of" timestamp instead of an empty page.
|
||||
struct CachedSnapshot: Codable, Sendable {
|
||||
let lastUpdated: Date
|
||||
let scoreboard: Scoreboard?
|
||||
let standings: Standings?
|
||||
let bracket: PlayoffBracket?
|
||||
}
|
||||
|
||||
/// Single-file Codable cache in Application Support. Atomic writes; fail-soft
|
||||
/// reads — corrupt or version-mismatched payloads return nil and the next
|
||||
/// fetch overwrites with fresh data.
|
||||
actor NHLDataCache {
|
||||
private let logger = IceGlassLogger(
|
||||
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass",
|
||||
category: "NHLDataCache"
|
||||
)
|
||||
|
||||
private let fileURL: URL
|
||||
|
||||
init(filename: String = "nhl-snapshot.json") {
|
||||
let fm = FileManager.default
|
||||
let supportDir = (try? fm.url(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true
|
||||
)) ?? fm.temporaryDirectory
|
||||
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass"
|
||||
let appDir = supportDir.appendingPathComponent(bundleId, isDirectory: true)
|
||||
try? fm.createDirectory(at: appDir, withIntermediateDirectories: true)
|
||||
self.fileURL = appDir.appendingPathComponent(filename)
|
||||
}
|
||||
|
||||
func load() -> CachedSnapshot? {
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
return try decoder.decode(CachedSnapshot.self, from: data)
|
||||
} catch {
|
||||
logger.warning("Cache load failed (will refetch): \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func save(_ snapshot: CachedSnapshot) {
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .iso8601
|
||||
let data = try encoder.encode(snapshot)
|
||||
try data.write(to: fileURL, options: .atomic)
|
||||
} catch {
|
||||
logger.error("Cache save failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user