Last active
February 17, 2024 02:07
-
-
Save stinger/e8b706ab846a098783d68e5c3a4f0ea5 to your computer and use it in GitHub Desktop.
Combine - fetching and decoding JSON data
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Combine | |
enum APIError: Error, LocalizedError { | |
case unknown, apiError(reason: String), parserError(reason: String) | |
var errorDescription: String? { | |
switch self { | |
case .unknown: | |
return "Unknown error" | |
case .apiError(let reason), .parserError(let reason): | |
return reason | |
} | |
} | |
} | |
struct Fact: Decodable { | |
var text: String | |
} | |
func fetch(url: URL) -> AnyPublisher<Data, APIError> { | |
let request = URLRequest(url: url) | |
return URLSession.DataTaskPublisher(request: request, session: .shared) | |
.tryMap { data, response in | |
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { | |
throw APIError.unknown | |
} | |
return data | |
} | |
.mapError { error in | |
if let error = error as? APIError { | |
return error | |
} else { | |
return APIError.apiError(reason: error.localizedDescription) | |
} | |
} | |
.eraseToAnyPublisher() | |
} | |
func fetch<T: Decodable>(url: URL) -> AnyPublisher<T, APIError> { | |
fetch(url: url) | |
.decode(type: T.self, decoder: JSONDecoder()) | |
.mapError { error in | |
if let error = error as? DecodingError { | |
var errorToReport = error.localizedDescription | |
switch error { | |
case .dataCorrupted(let context): | |
let details = context.underlyingError?.localizedDescription ?? context.codingPath.map { $0.stringValue }.joined(separator: ".") | |
errorToReport = "\(context.debugDescription) - (\(details))" | |
case .keyNotFound(let key, let context): | |
let details = context.underlyingError?.localizedDescription ?? context.codingPath.map { $0.stringValue }.joined(separator: ".") | |
errorToReport = "\(context.debugDescription) (key: \(key), \(details))" | |
case .typeMismatch(let type, let context), .valueNotFound(let type, let context): | |
let details = context.underlyingError?.localizedDescription ?? context.codingPath.map { $0.stringValue }.joined(separator: ".") | |
errorToReport = "\(context.debugDescription) (type: \(type), \(details))" | |
@unknown default: | |
break | |
} | |
return APIError.parserError(reason: errorToReport) | |
} else { | |
return APIError.apiError(reason: error.localizedDescription) | |
} | |
} | |
.eraseToAnyPublisher() | |
} | |
// Usage | |
if let url = URL(string: "https://cat-fact.herokuapp.com/facts/random") { | |
fetch(url: url) | |
.sink(receiveCompletion: { completion in | |
switch completion { | |
case .finished: | |
break | |
case .failure(let error): | |
print("Error: \(error.localizedDescription)") | |
} | |
}, receiveValue: { (fact: Fact) in | |
print(fact.text) | |
}) | |
} |
This is a great example of how to fetch and decode JSON using combine. Thanks!
Could this repurposed/reused for loading local JSON files ?
Good example snippet! Thanks for sharing.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nicely coded example to implement separation of concern principles; keeps decoding and fetching logic modular/customisable.