Implemented complete TCA architecture for iOS and watchOS targets: - Authentication flow (Sign in with Apple + Yahoo OAuth) - OAuth token management with iCloud Key-Value Storage - Yahoo Fantasy Sports API client with async/await - Watch Connectivity for iPhone ↔ Watch data sync - Complete UI for both iPhone and Watch platforms Core features: - Matchup score display - Category breakdown with win/loss/tie indicators - Roster status tracking - Manual refresh functionality - Persistent data caching on Watch Technical stack: - The Composable Architecture for state management - Swift Concurrency (async/await, actors) - WatchConnectivity framework - Sign in with Apple - OAuth 2.0 authentication flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
117 lines
3.8 KiB
Swift
117 lines
3.8 KiB
Swift
//
|
|
// WatchConnectivityClient.swift
|
|
// FantasyWatch Watch App
|
|
//
|
|
// Created by Claude Code
|
|
//
|
|
|
|
import ComposableArchitecture
|
|
import Foundation
|
|
import WatchConnectivity
|
|
|
|
@DependencyClient
|
|
struct WatchConnectivityClient {
|
|
var matchupUpdates: @Sendable () -> AsyncStream<(Matchup, RosterStatus, Date)>
|
|
var requestRefresh: @Sendable () async throws -> Void
|
|
}
|
|
|
|
extension WatchConnectivityClient: DependencyKey {
|
|
static let liveValue: WatchConnectivityClient = {
|
|
let manager = WatchConnectivityManager()
|
|
|
|
return Self(
|
|
matchupUpdates: {
|
|
manager.matchupUpdatesStream()
|
|
},
|
|
requestRefresh: {
|
|
try await manager.requestRefresh()
|
|
}
|
|
)
|
|
}()
|
|
|
|
static let testValue = Self(
|
|
matchupUpdates: {
|
|
AsyncStream { continuation in
|
|
continuation.yield((
|
|
Matchup(
|
|
week: 1,
|
|
status: "midevent",
|
|
userTeam: TeamScore(teamKey: "test.l.123.t.1", teamName: "Test Team", wins: 5, losses: 3, ties: 1),
|
|
opponentTeam: TeamScore(teamKey: "test.l.123.t.2", teamName: "Opponent", wins: 4, losses: 4, ties: 1),
|
|
categories: []
|
|
),
|
|
RosterStatus(activeCount: 10, benchedCount: 5, injuredReserve: 2),
|
|
Date()
|
|
))
|
|
}
|
|
},
|
|
requestRefresh: {}
|
|
)
|
|
}
|
|
|
|
extension DependencyValues {
|
|
var watchConnectivityClient: WatchConnectivityClient {
|
|
get { self[WatchConnectivityClient.self] }
|
|
set { self[WatchConnectivityClient.self] = newValue }
|
|
}
|
|
}
|
|
|
|
// MARK: - Watch Connectivity Manager
|
|
|
|
final class WatchConnectivityManager: NSObject, WCSessionDelegate {
|
|
private var session: WCSession?
|
|
private var updatesContinuation: AsyncStream<(Matchup, RosterStatus, Date)>.Continuation?
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
guard WCSession.isSupported() else { return }
|
|
|
|
session = WCSession.default
|
|
session?.delegate = self
|
|
session?.activate()
|
|
}
|
|
|
|
func matchupUpdatesStream() -> AsyncStream<(Matchup, RosterStatus, Date)> {
|
|
AsyncStream { continuation in
|
|
self.updatesContinuation = continuation
|
|
|
|
if let context = session?.receivedApplicationContext,
|
|
let data = context["payload"] as? Data,
|
|
let payload = try? JSONDecoder().decode(WatchPayload.self, from: data) {
|
|
continuation.yield((payload.matchup, payload.roster, payload.timestamp))
|
|
}
|
|
}
|
|
}
|
|
|
|
func requestRefresh() async throws {
|
|
guard let session = session, session.activationState == .activated else {
|
|
throw NetworkError.networkFailure(
|
|
NSError(domain: "WatchConnectivity", code: -1, userInfo: [NSLocalizedDescriptionKey: "Watch session not active"])
|
|
)
|
|
}
|
|
|
|
let message = ["type": WatchMessage.refreshRequest.rawValue]
|
|
session.sendMessage(message, replyHandler: nil) { error in
|
|
print("Failed to send refresh request: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// MARK: - WCSessionDelegate
|
|
|
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
|
if let error = error {
|
|
print("Watch session activation failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
|
guard let data = applicationContext["payload"] as? Data,
|
|
let payload = try? JSONDecoder().decode(WatchPayload.self, from: data) else {
|
|
return
|
|
}
|
|
|
|
updatesContinuation?.yield((payload.matchup, payload.roster, payload.timestamp))
|
|
}
|
|
}
|