diff --git a/.DS_Store b/.DS_Store index 5d6bdfb..02ed4fa 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 52fe2f7..96f28be 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output +.DS_Store diff --git a/MacTorn-v1.2.1.zip b/MacTorn-v1.2.1.zip new file mode 100644 index 0000000..810cd41 Binary files /dev/null and b/MacTorn-v1.2.1.zip differ diff --git a/MacTorn/MacTorn/Info.plist b/MacTorn/MacTorn/Info.plist index 7e734d3..58a8280 100644 --- a/MacTorn/MacTorn/Info.plist +++ b/MacTorn/MacTorn/Info.plist @@ -17,8 +17,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2 + 1.2.1 CFBundleVersion - 1.2 + 1.2.1 diff --git a/MacTorn/MacTorn/ViewModels/AppState.swift b/MacTorn/MacTorn/ViewModels/AppState.swift index 0e1ee54..bb111a0 100644 --- a/MacTorn/MacTorn/ViewModels/AppState.swift +++ b/MacTorn/MacTorn/ViewModels/AppState.swift @@ -1,6 +1,9 @@ import Foundation import Combine import SwiftUI +import os.log + +private let logger = Logger(subsystem: "com.mactorn", category: "AppState") @MainActor class AppState: ObservableObject { @@ -234,53 +237,96 @@ class AppState: ObservableObject { func fetchData() { guard !apiKey.isEmpty else { errorMsg = "API Key required" + logger.warning("Fetch aborted: API Key required") return } - + guard let url = TornAPI.url(for: apiKey) else { errorMsg = "Invalid URL" + logger.error("Fetch aborted: Invalid URL") return } - + isLoading = true errorMsg = nil - + + logger.info("Starting data fetch...") + Task { + let startTime = Date() + + // Ensure minimum loading time for UX, then set isLoading = false + defer { + Task { @MainActor in + let elapsed = Date().timeIntervalSince(startTime) + if elapsed < 0.5 { + try? await Task.sleep(nanoseconds: UInt64((0.5 - elapsed) * 1_000_000_000)) + } + self.isLoading = false + } + } + do { let (data, response) = try await URLSession.shared.data(from: url) - + guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse } - + + logger.info("HTTP response: \(httpResponse.statusCode)") + switch httpResponse.statusCode { case 200: + // Check for Torn API error in response (API returns 200 even on errors) + if let tornError = checkForTornAPIError(data: data) { + await MainActor.run { + self.errorMsg = tornError + } + logger.error("Torn API error: \(tornError)") + return + } + // Parse on background thread try await parseDataInBackground(data: data) - + // Fetch faction data separately await fetchFactionData() - + + logger.info("Data fetch completed successfully") + case 403, 404: await MainActor.run { self.errorMsg = "Invalid API Key" self.data = nil - self.isLoading = false } + logger.error("HTTP \(httpResponse.statusCode): Invalid API Key") default: await MainActor.run { self.errorMsg = "HTTP Error: \(httpResponse.statusCode)" - self.isLoading = false } + logger.error("HTTP Error: \(httpResponse.statusCode)") } } catch { await MainActor.run { - self.errorMsg = error.localizedDescription - self.isLoading = false + self.errorMsg = "Network error: \(error.localizedDescription)" } + logger.error("Network error: \(error.localizedDescription)") } } } + + /// Check if Torn API returned an error (API returns HTTP 200 even on errors like rate limiting) + private func checkForTornAPIError(data: Data) -> String? { + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let error = json["error"] as? [String: Any], + let errorMessage = error["error"] as? String else { + return nil + } + + let errorCode = error["code"] as? Int ?? 0 + logger.warning("Torn API error code \(errorCode): \(errorMessage)") + return "API Error: \(errorMessage)" + } // Move parsing logic here and mark as non-isolated or detached private func parseDataInBackground(data: Data) async throws {