How to Return HTTP Request Data in a Function Call in Swift? - swift

I want to make a generic HTTP request function. The code I saw does not return data to the caller. Instead it prints out the error code or the parsed JSON object within the function. In my case I would like to return (data, response, error) to the caller.
func performHTTPRequest(urlString: String) -> (Data, URLResponse, Error) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) {(data, response, error) in
// some logic
}
task.resume()
}
}
The problem is the three variables (data, response, error) are not available outside the closure. If I assign them to global variables within the closure, compiler complains the global variables are not in scope.
Also, where would I put the return (data, response, error) statement? Before or after task.resume()? Thanks

The short answer is, you can't. You can us the new async/await syntax in Swift 5.5 to simulate a synchronous network call. (I haven't had a chance to use async/await in my own projects yet, so I'd have to look that up in order to guide you.)
Without async/await, you will need to refactor your function to take a completion handler. You'd then call the completion handler with the results once the data task completes.
This question comes up all the time on SO. You should be able to find dozens of examples of writing a completion handler-based function for async networking.

For example you can do that
struct Message: Decodable {
var username: String
var message: String
}
enum RequestError: Error {
case invalidURL
case missingData
}
func performHTTPRequest(urlString: String) async throws -> Message{
guard let url = URL(string: urlString) else {throw RequestError.invalidURL}
guard let (data, response) = try? await URLSession.shared.data(from: url) else{throw RequestError.invalidURL}
guard (response as? HTTPURLResponse)?.statusCode == 200 else {throw RequestError.invalidURL}
let decoder = JSONDecoder()
guard let jsonResponse = try? decoder.decode(Message.self, from: data) else {throw RequestError.missingData}
return jsonResponse
}
and call the fonction
do {
try await performHTTPRequest(urlString: "wwww.url.com")
} catch RequestError.invalidURL{
print("invalid URL")
} catch RequestError.missingData{
print("missing data")
}

Related

Swift Combine to map URLSession.shared.dataTaskPublisher HTTP response errors

Given an API that for invalid requests, along with 400-range HTTP status code the server returns a JSON payload that includes a readable message. As an example, the server could return { "message": "Not Found" } with a 404 status code for deleted or non-existent content.
Without using publishers, the code would read,
struct APIErrorResponse: Decodable, Error {
let message: String
}
func request(request: URLRequest) async throws -> Post {
let (data, response) = try await URLSession.shared.data(for: request)
let statusCode = (response as! HTTPURLResponse).statusCode
if 400..<500 ~= statusCode {
throw try JSONDecoder().decode(APIErrorResponse.self, from: data)
}
return try JSONDecoder().decode(Post.self, from: data)
}
Can this be expressed succinctly using only functional code?
In other words, how can the following pattern be adapted to decode a different type based on the HTTPURLResponse.statusCode property, to return as an error, or more generally, how can the response property be handled separately from data attribute?
URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Post.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
you could try something like this approach:
func request(request: URLRequest) -> AnyPublisher<Post, any Error> {
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { (output) -> Data in
let statusCode = (output.response as! HTTPURLResponse).statusCode
if 400..<500 ~= statusCode {
throw try JSONDecoder().decode(APIErrorResponse.self, from: output.data)
}
return output.data
}
.decode(type: Post.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
I use a helper method for this:
extension Publisher where Output == (data: Data, response: HTTPURLResponse) {
func decode<Success, Failure>(
success: Success.Type,
failure: Failure.Type,
decoder: JSONDecoder
) -> AnyPublisher<Success, Error> where Success: Decodable, Failure: DecodableError {
tryMap { data, httpResponse -> Success in
guard httpResponse.statusCode < 500 else {
throw MyCustomError.serverUnavailable(status: httpResponse.statusCode)
}
guard httpResponse.statusCode < 400 else {
let error = try decoder.decode(failure, from: data)
throw error
}
let success = try decoder.decode(success, from: data)
return success
}
.eraseToAnyPublisher()
}
}
typealias DecodableError = Decodable & Error
which allows me to simplify the call sites like so:
URLSession.shared.dataTaskPublisher(for: request)
.decode(success: Post.self, failure: MyCustomError.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
workingdogsupport has provided a good literal translation (+1). And LuLuGaGa has illustrated a nice compositional style (+1).
I might expand upon the latter, though, and recommend pattern matching on the various status codes, e.g. 2xx codes for decoding success, 4xx for graceful web service errors, and a more general .badServerResponse (and includes the diagnostic information so that the developer working on the call point has a chance to figure out what went wrong) for anything else.
E.g., you might have an general extension (which doesn’t use any types particular to the app):
extension Publisher where Output == (data: Data, response: URLResponse) {
func decode<Success: Decodable, Failure: Decodable & Error>(
success: Success.Type = Success.self,
failure: Failure.Type = Failure.self,
decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<Success, Error> {
tryMap { data, response -> Success in
switch (response as! HTTPURLResponse).statusCode {
case 200..<300: return try decoder.decode(Success.self, from: data)
case 400..<500: throw try decoder.decode(Failure.self, from: data)
default: throw URLError(.badServerResponse, userInfo: ["data": data, "response": response])
}
}
.eraseToAnyPublisher()
}
}
Or, because I hate force-unwrapping:
extension Publisher where Output == (data: Data, response: URLResponse) {
func decode<Success: Decodable, Failure: Decodable & Error>(
success: Success.Type = Success.self,
failure: Failure.Type = Failure.self,
decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<Success, Error> {
tryMap { data, response -> Success in
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse, userInfo: ["data": data, "response": response])
}
switch response.statusCode {
case 200..<300: return try decoder.decode(Success.self, from: data)
case 400..<500: throw try decoder.decode(Failure.self, from: data)
default: throw URLError(.badServerResponse, userInfo: ["data": data, "response": response])
}
}
.eraseToAnyPublisher()
}
}
Regardless, I might then have an extension for this app that decodes your particular web service’s specific error struct:
extension Publisher where Output == (data: Data, response: URLResponse) {
func decode<Success: Decodable>(
success: Success.Type = Success.self,
decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<Success, Error> {
decode(success: success, failure: APIErrorResponse.self, decoder: decoder)
}
}
Then the app code can avail itself of the above (and infer the success type):
func postsPublisher(for request: URLRequest) -> AnyPublisher<Post, Error> {
URLSession.shared.dataTaskPublisher(for: request)
.decode()
.eraseToAnyPublisher()
}
Anyway, that results in a succinct call-point, with a reusable extension.

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:))

I am building an app with Swift and SwiftUI. In MainViewModel I have a function who call Api for fetching JSON from url and deserialize it. this is made under async/await protocol.
the problem is the next, I have received from xcode the next comment : "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates." in this part of de code :
func getCountries() async throws{
countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}
who calls this one:
func fetchCountries() async throws -> [Country]? {
guard let url = URL(string: CountryUrl.countriesJSON.rawValue ) else {
print("Invalid URL")
return nil
}
let urlRequest = URLRequest(url: url)
do {
let (json, _) = try await URLSession.shared.data(for: urlRequest)
if let decodedResponse = try? JSONDecoder().decode([Country].self, from: json) {
debugPrint("return decodeResponse")
return decodedResponse
}
} catch {
debugPrint("error data")
}
return nil
}
I would like to know if somebody knows how I can fix it
First fetch the data asynchronously and then assign the result to the property on the main thread
func getCountries() async throws{
let fetchedData = try await MainViewModel.countriesApi.fetchCountries()
await MainActor.run {
countries = fetchedData ?? []
}
}
Off topic perhaps but I would change fetchCountries() to return an empty array rather than nil on an error or even better to actually throw the errors since it is declared as throwing.
Something like
func fetchCountries() async throws -> [Country] {
guard let url = URL(string: CountryUrl.countriesJSON.rawValue ) else {
return [] // or throw custom error
}
let urlRequest = URLRequest(url: url)
let (json, _) = try await URLSession.shared.data(for: urlRequest)
return try JSONDecoder().decode([Country].self, from: json)
}
There are two ways to fix this. One, you can add the #MainActor attribute to your functions - this ensures they will run on the main thread. Docs: https://developer.apple.com/documentation/swift/mainactor. However, this could cause delays and freezing as the entire block will run on the main thread. You could also set the variables using DispatchQueue.main.async{} - see this article from Hacking With Swift. Examples here:
#MainActor func getCountries() async throws{
///Set above - this will prevent the error
///This can also cause a lag
countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}
Second option:
func getCountries() async throws{
DispatchQueue.main.async{
countries = try await MainViewModel.countriesApi.fetchCountries() ?? []
}
}

How to refactor Swift API call to use Swift 5.5 async/await?

I want to refactor some API calls to use Swift 5.5's new async/await in my SwiftUI project. However, it's unclear to me how to replace or accomodate the completions.
Here's an example function which I want to refactor:
static func getBooks(completion: #escaping ([Book]?) -> Void) {
let request = getRequest(suffix: "books")
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
fatalError("Error: \(error)")
}
if let data = data {
if let books = try? JSONDecoder().decode([Book].self, from: data) {
DispatchQueue.main.async {
print("books.count: \(books.count)")
completion(books)
}
return
} else {
fatalError("Unable to decode JSON")
}
} else {
fatalError("Data is nil")
}
}.resume()
}
I beleve the new function signature would look something like this:
static func getBooks() async throws -> ([Book]?) {
// ...
}
However, I have no idea what to do with the URLSession.shared.dataTask, DispatchQueue.main.async and completion, etc.
Anyone know what the new function body should look like?
Thanks
func getBooks() async throws -> [Book] {
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode([Book].self, from: data)
}
This will throw if the request fails, and if the response cannot be decoded. Since the function is marked as throwing, then the calling function has to handle the raised errors.
You don't need to declare the returned [Book] to be optional, because it will either return an honest array, or throw an error.
In your additional code, you had to call your completion handler on the main queue, because you were calling it from within the completion block of the request. You don't need to do that here.

Preserving Failure Type with Combine's tryMap

I'm using Combine to write a simple web scraper. I'm trying to map the returned data to a string of HTML, throwing ScraperErrors at each possible failure point. At the end, I want to pass this string to my htmlSubject, which is a PassthroughSubject<String, ScraperError>, for further processing.
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.tryMap { (data, response) -> String in
guard let html = String(data: data, encoding: .utf8) else {
throw ScraperError.readFailed
}
return html
}
.subscribe(htmlSubject) // <-- Not allowed because failure type is now Error
However, I'm finding that .tryMap is erasing my ScraperError to a regular Error, preventing me from chaining my htmlSubject to the end:
Instance method 'subscribe' requires the types 'Error' and
'ScraperError' be equivalent.
Is there an obvious way around this that I'm missing, or am I getting tripped up conceptually? I'm thinking of this chain as building blocks in a large function that maps <(Data, URLResponse), URLError> to <String, ScraperError>.
Any help is appreciated.
Use mapError to convert back to ScraperError after the tryMap:
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.tryMap { (data, response) -> String in
guard let html = String(data: data, encoding: .utf8) else {
throw ScraperError.readFailed
}
return html
}
.mapError { $0 as! ScraperError }
.subscribe(htmlSubject)
If you don't want to use as!, you'll have to pick some other case to map to:
.mapError { $0 as? ScraperError ?? ScraperError.unknown }
If you don't like that either, you can use flatMap over Result<String, ScraperError>.Publisher:
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.flatMap { (data, response) -> Result<String, ScraperError>.Publisher in
guard let html = String(data: data, encoding: .utf8) else {
return .init(.readFailed)
}
return .init(html)
}
.subscribe(htmlSubject)
I find the resulting code to be a bit more readable when wrapping Rob's flatMap approach into an extension:
extension Publisher {
func flatMapResult<T>(_ transform: #escaping (Self.Output) -> Result<T, Self.Failure>) -> Publishers.FlatMap<Result<T, Self.Failure>.Publisher, Self> {
self.flatMap { .init(transform($0)) }
}
}
The code example above would then become:
urlSubscription = URLSession.shared
.dataTaskPublisher(for: url)
.mapError { _ -> ScraperError in // Explicitly stating my failure type is ScraperError
ScraperError.unreachableSite
}
.flatMapResult { (data, response) -> Result<String, ScraperError> in
guard let html = String(data: data, encoding: .utf8) else {
return .failure(.readFailed)
}
return .success(html)
}
.subscribe(htmlSubject)

Generic function with Alamofire

I work with iOS app that use Alamofire, I want to write a generic function(s) which used to send and retrieve data from server to a decodable objects, my function was as below :
func pop <T : Codable> (_ Url: inout String, _ popedList: inout [T]) {
let url = URL(string:Url)
Alamofire.request(url!, method: .post).responseJSON { response in
let result = response.data
do {
let data = try JSONDecoder().decode(popedList, from: result!)// get error here
print(data[0])
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(data[0])
let jsonString = String(data: jsonData, encoding: .utf8)
print("jsonString: \(String(describing: jsonString))")
} catch let e as NSError {
print("error : \(e)")
}
}
}
and a function to send an object to server as below:
func push <T : Codable> (_ Url: inout String, _ pushObject: inout T) {
let jsonData = try! JSONEncoder().encode(pushObject)
let jsonString = String(data: jsonData, encoding: .utf8)
print("jsonString: \(String(describing: jsonString))")
let url = URL(string:Url)
Alamofire.request(url!,
method: .post,
parameters:jsonString)//it's need to creat a Dictionary instate of String
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.response { response in
// response handling code
let result = response.data
print(response.data)
}
}
I get an error in first function,
"Cannot invoke 'decode' with an argument list of type '([T], from: Data)'"
and
"Escaping closures can only capture inout parameters explicitly by value"
What is the best way to write these to function as generic?
After a few searches and trying to edit my functions I capable to rewrite my two functions in such away that I get what I need:
func pop<T: Decodable>(from: URL, decodable: T.Type, completion:#escaping (_ details: [T]) -> Void)
{
Alamofire.request(from, method: .post).responseJSON { response in
let result_ = response.data
do {
let data = try JSONDecoder().decode([T].self, from: result_!)
//let data = try JSONDecoder().decode(decodable, from: result_!)// get error here
//print(data[0])
print("data[0] : \(data[0])")
completion(data)
} catch let e as NSError {
print("error : \(e)")
}
}
}
func push <T : Codable> (_ Url: String, _ pushObject: T)
{
let jsonData = try! JSONEncoder().encode(pushObject)
let jsonString = String(data: jsonData, encoding: .utf8)
print("jsonString: \(String(describing: jsonString))")
let url = URL(string:Url)
Alamofire.request(url!,
method: .post,
parameters:convertToDictionary(text: jsonString!))//it's need to creat a Dictionary instate of String
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.response { response in
// response handling code
print(response.data!)
if let jsonData = response.data {
let jsonString = String(data: jsonData, encoding: .utf8)
print("response.data: \(String(describing: jsonString))")
}
}
}
func convertToDictionary(text: String) -> [String: Any]? {
if let data = text.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
} catch {
print(error.localizedDescription)
}
}
return nil
}
For the first function, JSONDecoder.decode() wants 2 parameters:
Type to decode to: the class/struct you want it to decode to. This is not an instantiated object, just the type.
Data to decode from: the generic Data object that will be converted to the type you specified.
So, in order to be able to write your function so that it has a generic URL and result object, you would need to pass it the object type and a callback to pass the result to, since network operations are asynchronous.
func dec<T: Decodable>(from: URL, decodable: T.Type, result: (T) -> Void) {
// your Alamofire logic
let data = try JSONDecoder().decode(popedList, from: result!)
result(data)
}
You can apply the same logic to the second function.
Note that this is not the best way to take care of eventual errors, just an example of how you can handle encoding with a generic function.
JSONDecoder().decode method takes type and data parameter. Pass type not popedList.
let data = try JSONDecoder().decode([T].self, from: result!)
Inout Paramaters
Function parameters are constants by default. Trying to change the value of a function parameter from within the body of that function results in a compile-time error. This means that you can’t change the value of a parameter by mistake. If you want a function to modify a parameter’s value, and you want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.
You are not changing value of popedList in both functions, so using inout is meaningless.