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 {