// // FetchService.swift // Loader // // Created by samara on 14.03.2025. // import Foundation // MARK: - Class public class NBFetchService { public enum NBFetchServiceError: Error, LocalizedError { case invalidURL case networkError(Error) case noData case parsingError(Error) public var errorDescription: String? { switch self { case .invalidURL: "The URL is invalid." case .networkError(let error): "Network error: \(error.localizedDescription)" case .noData: "No data received." case .parsingError(let error): "Failed to parse data: \(error.localizedDescription)" } } } public init() {} /// Identifies the app to its repo backend. The server gates its feed on this, so it /// must be deterministic and start with `FTStore`/`FTStore-Manager`. Derived from /// the running app's bundle so the same code yields the right value in both apps. public static let userAgent: String = { let info = Bundle.main.infoDictionary let version = (info?["CFBundleShortVersionString"] as? String) ?? "1.0" let bundleId = Bundle.main.bundleIdentifier ?? "app.ftstore" let product = bundleId == "app.ftstore.manager" ? "FTStore-Manager" : "FTStore" return "\(product)/\(version) (\(bundleId))" }() } // MARK: - Class extension: fetch extension NBFetchService { public func fetch( from urlString: String, completion: @escaping (Result) -> Void ) { guard let url = URL(string: urlString) else { completion(.failure(NBFetchServiceError.invalidURL)) return } fetch(from: url, completion: completion) } public func fetch( from url: URL, completion: @escaping (Result) -> Void ) { DispatchQueue.global(qos: .userInitiated).async { // Always pull a fresh copy: source feeds change frequently, and the // default protocol cache policy would otherwise serve a stale JSON // (showing versions that no longer exist in the live manifest). var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringLocalCacheData request.setValue(Self.userAgent, forHTTPHeaderField: "User-Agent") let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(NBFetchServiceError.networkError(error))) return } guard let data = data else { completion(.failure(NBFetchServiceError.noData)) return } do { let decoder = JSONDecoder() let decodedData = try decoder.decode(T.self, from: data) completion(.success(decodedData)) } catch { completion(.failure(NBFetchServiceError.parsingError(error))) } } task.resume() } } }