spb/vrai-prix-ios Public
App iOS native de Vrai-Prix — estimation immobilière transparente pour le Québec (SwiftUI, MapKit, Swift Charts)
Swift 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Client de l'API de production Vrai-Prix (https://www.vrai-prix.com).3import Foundation45enum APIError: LocalizedError {6 case server(String)7 case http(Int)89 var errorDescription: String? {10 switch self {11 case .server(let message): return message12 case .http(let code): return "Erreur réseau (\(code)). Réessayez."13 }14 }15}1617struct VraiPrixAPI {18 static let shared = VraiPrixAPI()19 let base = URL(string: "https://www.vrai-prix.com")!2021 private func decode<T: Decodable>(_ type: T.Type, from data: Data, status: Int) throws -> T {22 guard (200...299).contains(status) else {23 if let payload = try? JSONDecoder().decode(APIErrorPayload.self, from: data) {24 throw APIError.server(payload.error)25 }26 throw APIError.http(status)27 }28 return try JSONDecoder().decode(type, from: data)29 }3031 private func get<T: Decodable>(_ type: T.Type, path: String, query: [URLQueryItem]) async throws -> T {32 var comps = URLComponents(url: base.appending(path: path), resolvingAgainstBaseURL: false)!33 comps.queryItems = query34 let (data, response) = try await URLSession.shared.data(from: comps.url!)35 let status = (response as? HTTPURLResponse)?.statusCode ?? 036 return try decode(type, from: data, status: status)37 }3839 // MARK: Endpoints4041 func search(_ q: String) async throws -> [SearchResult] {42 try await get(SearchResponse.self, path: "/api/search", query: [.init(name: "q", value: q)]).results43 }4445 func nearby(lat: Double, lng: Double, halfLat: Double, halfLng: Double, limit: Int = 250) async throws -> [NearbyUnit] {46 try await get(NearbyResponse.self, path: "/api/nearby", query: [47 .init(name: "lat", value: String(lat)),48 .init(name: "lng", value: String(lng)),49 .init(name: "halfLat", value: String(halfLat)),50 .init(name: "halfLng", value: String(halfLng)),51 .init(name: "limit", value: String(limit)),52 ]).results53 }5455 func estimate(id: String) async throws -> UnitEstimate {56 try await get(UnitEstimate.self, path: "/api/estimate", query: [.init(name: "id", value: id)])57 }5859 func estimateManual(60 municipality: String,61 typeProp: String,62 floorArea: Double?,63 yearBuilt: Int?,64 landArea: Double?65 ) async throws -> UnitEstimate {66 var body: [String: Any] = ["municipality": municipality, "typeProp": typeProp]67 if let floorArea { body["floorArea"] = floorArea }68 if let yearBuilt { body["yearBuilt"] = yearBuilt }69 if let landArea { body["landArea"] = landArea }7071 var request = URLRequest(url: base.appending(path: "/api/estimate"))72 request.httpMethod = "POST"73 request.setValue("application/json", forHTTPHeaderField: "Content-Type")74 request.httpBody = try JSONSerialization.data(withJSONObject: body)75 let (data, response) = try await URLSession.shared.data(for: request)76 let status = (response as? HTTPURLResponse)?.statusCode ?? 077 return try decode(UnitEstimate.self, from: data, status: status)78 }7980 /// Télécharge un rapport PDF dans un fichier temporaire et retourne son URL locale.81 func downloadReport(path: String, query: [URLQueryItem], fileName: String) async throws -> URL {82 var comps = URLComponents(url: base.appending(path: path), resolvingAgainstBaseURL: false)!83 comps.queryItems = query84 let (data, response) = try await URLSession.shared.data(from: comps.url!)85 let status = (response as? HTTPURLResponse)?.statusCode ?? 086 guard (200...299).contains(status) else {87 if let payload = try? JSONDecoder().decode(APIErrorPayload.self, from: data) {88 throw APIError.server(payload.error)89 }90 throw APIError.http(status)91 }92 let url = FileManager.default.temporaryDirectory.appending(path: fileName)93 try data.write(to: url)94 return url95 }96}97