I am implementing a Networking Layer in Swift. Here is one of the functions. The function works as expected but I want to improve upon it. I am using DispatchQueue to make sure that the callback from the network client is always on the main thread. This ends up repeating the DispatchQueue.main.async in 3 different places.
Also, when I encounter some error when performing the request I still send back nil but as a success.
func getAllStocks(url: String, completion: #escaping (Result<[Stock]?,NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completion(.failure(.invalidURL)) // wrap in DispatchQueue also
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(.success(nil)) // should I send nil or some sort of failure
}
return
}
let stocks = try? JSONDecoder().decode([Stock].self, from: data)
DispatchQueue.main.async {
completion(.success(stocks))
}
}
}
How can I minimize the code or is it fine?
The goal of the Result type is that you return a non-optional type on success and an error on failure.
I recommend to call completion on the current thread and dispatch the result on the caller side.
And handle also the DecodingError
func getAllStocks(url: String, completion: #escaping (Result<[Stock],Error>) -> Void) {
guard let url = URL(string: url) else {
completion(.failure(NetworkError.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { completion(.failure(error)); return }
// if error is nil then data has a value
do {
let stocks = try JSONDecoder().decode([Stock].self, from: data!)
completion(.success(stocks))
} catch {
completion(.failure(error))
}
}.resume()
}
getAllStocks(url: someURL) { result in
DispatchQueue.main.async {
switch result {
case .success(let stocks): print(stocks)
case .failure(let networkError as NetworkError): handleNetworkError(networkError)
case .failure(let decodingError as DecodingError): handleDecodingError(decodingError)
case .failure(let error): print(error)
}
}
}
Lean into the build-in constructs and standard types.
func getAllStocks(url: String, completion: #escaping (Result<[Stock], Error>) -> Void) {
func completeOnMain(_ result: Result<[Stock], Error>) { // <-- Nested function
DispatchQueue.main.async { completion(result) } // <-- Handle repeated work
}
guard let url = URL(string: url) else {
completeOnMain(.failure(URLError(.badURL))) // <-- Standard Error
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
do {
if let error = error { throw error }
guard let data = data else { throw URLError(.badServerResponse) }
let stocks = try JSONDecoder().decode([Stock].self, from: data)
completeOnMain(.success(stocks))
} catch {
completeOnMain(.failure(error)) // <-- Unified error handling
}
}
}
A nested function is used to do the repeated work of dispatching to the main thread.
Standard error are used instead of defining custom errors.
A do/catch and throws are used to handle all the errors at once.
I have one final note: Async functions should always be async. The bad URL error should not call completion(_:) directly; use DispatchQueue.main.async to make sure the call happens in a later run loop.
Related
First of all, sorry for my english, I hope it won't be a pain to read me :D
I'm currently building an iOS application for a project in my school and I'm facing a problem.
I make an API call and I test it by using the URLProtocol method.
The API call :
task?.cancel()
task = exchangeSession.dataTask(with: request) { data, response, error in
// The dataTask method will execute in a separate queue, so we
// get back into the main one because
// we will modify the user interface with our exchange result
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil, error)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil, nil)
return
}
guard let responseJSON = try? JSONDecoder().decode(ConvertResponse.self, from: data), let result = responseJSON.result else {
callback(false, nil, nil)
return
}
callback(true, result, nil)
}
}
task?.resume()
The MockURLProtocol :
final class MockURLProtocol: URLProtocol {
// We return true in order to allow URLSession to use this protocol for any URL Request
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
static var loadingHandler: ((URLRequest) -> (Data?, HTTPURLResponse?, Error?))?
override func startLoading() {
guard let handler = MockURLProtocol.loadingHandler else {
print("Loading handler is not set.")
return
}
let (data, response, error) = handler(request)
guard error == nil else {
client?.urlProtocol(self, didFailWithError: error!)
return
}
if let data = data {
client?.urlProtocol(self, didLoad: data)
}
if let response = response {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
}
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
As you can see, it contains a handler which will have the data / response and error.
In one of my tests I want to check the case where I don't have any data, so I put nil for it.
The test :
func testConvertShouldPostFailedCallbackIfNoData() {
MockURLProtocol.loadingHandler = { request in
return (nil, nil, nil)
}
let expectation = XCTestExpectation(description: "Wait for queue change.")
client.convert(from: from, to: to, amount: amount) { success, result, error in
XCTAssertFalse(success)
XCTAssertNil(result)
XCTAssertNil(error)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.01)
}
Here is my problem: nil doesn't work for my data parameter, it shows me "0 Bytes" instead.
It works for error and response, which is strange to me.
The result : The result
I wanted to ask you why the data parameter isn't nil?
With 0 bytes it's not considered as nil and I'm not going in the right loop in my code.
I tried a lot of breakpoints, but I still can't figure it out.
But one thing is really strange. If I put and error in the handler with nil data, the data will be nil.
Maybe it has something to do with the didFailWithError function?
This function forces data to be nil somehow?
startLoading is not the right place to check for the response or data or error. Instead, use the URLSessionDataDelegate's delegate methods to check the response/data/error received for that request.
After trying to fetch all pending favors, I want to save the data in a variable and somehow print it in the table view. Still not sure how to do it, but currently I cant "save" MyResults in a variable to return? Can someone please help me? and also, Ive tried searching but cant find what type of data structure is (Result<T, Error>).
extension URLSession {
func fetchData<T: Decodable>(for url: URL, completion: #escaping (Result<T, Error>) -> Void) {
self.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch let decoderError {
completion(.failure(decoderError))
}
}
}.resume()
}
}
func fetchAllFavor()->MyResults
{
let url = URL(string: get_all_pending_favors_url)!
URLSession.shared.fetchData(for: url) { (result: Result<[MyResults], Error>) in
switch result {
case .success(let MyResults):
break
// A list of todos!
case .failure(let error):
break
// A failure, please handle
default:
print("unknown")
}
}
}
This sort of question comes up all the time. You can't return a result from an async function. An async function like dataTask(with:) or your fetchData() function takes a completion handler, which is a closure that it calls once the results are available.
Your should rewrite your fetchAllFavor() function following a similar pattern:
func fetchAllFavor(completion: #escaping (Result<MyResults,Error>) -> Void) {
let url = URL(string: get_all_pending_favors_url)!
URLSession.shared.fetchData(for: url) { (result) in
completion(result)
}
}
So this code works for me in playground but for some reason URLSession.shared.dataTask(... doesnt call my flask api that im currently locally running. Any idea on what's wrong? So far I'm only concerned on why it does not enter the do{in my project but it works properly in playground.
func getWords() -> [Word]{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!)
let group = DispatchGroup()
print("XD")
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
print("A")
if let data = data{
print("B")
if let decodedResponse = try? JSONDecoder().decode([Word].self, from: data){
group.enter()
DispatchQueue.main.async(){
words = decodedResponse
print("C")
print(words)
group.leave()
}
}
}
print("DD")
} catch {
print("Words.swift Error in try catch")
}
group.enter()
}).resume()
group.leave()
group.notify(queue: DispatchQueue.main, execute: {
print(words)
})
print("ASDASD WORDS: \(words)")
for _ in 1 ... 4 {
// - to make sure there aren't duplicates -
var wordId:Int = Int.random(in: 0..<words.count)
while randomIds.contains(wordId){
wordId = Int.random(in: 0..<words.count)
}
randomIds.append(wordId)
}
//returns 4 words
return words
}
You aren't using DispatchGroup correctly; You should call enter before you start the asynchronous work and leave once it is complete. You can then use notify to perform some operation.
However, you don't really need a DispatchGroup in this situation; You have that because you are trying to turn an asynchronous operation into a synchronous one;
The correct approach is to accept that the operation is asynchronous and it isn't possible for this function to return [Word]. You will need to refactor the function to accept a completion handler closure and invoke that with the result.
Something like this:
func getWords(completionHandler:#escaping (Result<[Word], Error>) -> Void) -> Void{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!) // Note you should use a guard and call the completion handler with an error if url is `nil`
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
} else {
do {
if let data = data {
let words = try JSONDecoder().decode([Word].self, from: data)
completionHandler(.success(words))
} else {
// TODO call completionHander with a .failure(SomeError)
}
} catch {
completionHandler(.failure(error))
}
}
}).resume()
}
Then you can call it:
getWords() { result in
switch result {
case .success(let words):
print(words)
case .failure(let error):
print(error.localizedDescription)
}
}
I've been using DispatchWorkItem and DispatchQueue to make async requests in my app.
However, I've ran into trouble when trying to abort one of the requests.
I successfully use the workItem.cancel() to change the flag and I then check it where I want to abort. Like this:
for stop in self.userSettingsController.history {
stop.PassingInfo?.removeAll()
if workItem?.isCancelled ?? false {
print("CANCELED")
workItem = nil
break
}
...
However there's one case where I have no loop in which I can keep checking if the cancelled flag changes, so I cannot abort the request using the process above. Here's the code:
let tripQueue = DispatchQueue(label: "tripQueue")
var tripWorkItem: DispatchWorkItem? = nil
tripWorkItem = DispatchWorkItem {
self.soapServices.GetPathsByLineAndDirection(lineCode: self.lineCode!, direction: passingInfo.Direction!) { response in
DispatchQueue.main.async {
self.linePaths = response?.filter({$0.Places.contains(where: {$0.Code == self.singleStopSelected?.Code})})
if realTime {
//Get estimated trip
self.showingRealTime = true
if self.linePaths?.count ?? 0 > 0 {
self.getEstimatedTrip(lineCode: self.lineCode ?? "", direction: passingInfo.Direction ?? 0, stopCode: self.singleStopSelected?.Code ?? "", path: (self.linePaths?.first)!) { updateTripTimes in
//Does not work, as is expected. Just showing what I would like to achieve
if tripWorkItem?.isCancelled ?? false {
tripWorkItem = nil
return
}
if updateTripTimes {
DispatchQueue.main.async {
self.updateTripTimes = true
}
}
}
}
} else {
//Get trip
self.showingRealTime = false
self.getTrip(tripId: passingInfo.Id!)
}
}
}
tripWorkItem = nil
}
self.currentTripWorkItem = tripWorkItem
tripQueue.async(execute: tripWorkItem ?? DispatchWorkItem {})
Is there any way to do this?
Thanks in advance.
p.s: I'm sorry if this is duplicated, but I searched before and I couldn't find the question. I might be using the wrong terms.
Rather than putting your code in a DispatchWorkItem, consider wrapping it in a Operation subclass. You get the same isCancelled Boolean pattern:
class ComputeOperation: Operation {
override func main() {
while ... {
if isCancelled { break }
// do iteration of the calculation
}
// all done
}
}
For your network request, wrap it in an custom AsynchronousOperation subclass (e.g. this implementation), and implement cancel which will cancel the network request. For example:
enum NetworkOperationError: Error {
case unknownError(Data?, URLResponse?)
}
class NetworkOperation: AsynchronousOperation {
var task: URLSessionTask!
init(url: URL, completion: #escaping (Result<Data, Error>) -> Void) {
super.init()
self.task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
error == nil
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkOperationError.unknownError(data, response)))
self.finish()
}
return
}
DispatchQueue.main.async {
completion(.success(responseData))
self.finish()
}
}
}
override func main() {
task.resume()
}
override func cancel() {
super.cancel()
task.cancel()
}
}
Don't get lost in the details of the above example. Just note that
It subclassed AsynchronousOperation;
In the completion handler, it calls finish after calling the completion handler; and
The cancel implementation cancels the asynchronous task.
Then you can define your queue:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4 // use whatever you think is reasonable
And add your operations to it:
let operation = NetworkOperation(url: url) { response in
switch response {
case .failure(let error):
// do something with `error`
case .success(let data):
// do something with `data`
}
}
queue.addOperation(operation)
Now, the issue in your GetPathsByLineAndDirection and getEstimatedTrip is that you're not following “cancelable” patterns, namely you don't appear to be returning anything that could be used to cancel the request.
So, let's look at an example. Imagine you had some trivial method like:
func startNetworkRequest(with url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
task.resume()
}
What you'd do is change it to return something that can be canceled, the URLSessionTask in this example:
#discardableResult
func startNetworkRequest(with url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
task.resume()
return task
}
Now that’s an asynchronous task that is cancelable (and you can wrap it with the above AsynchronousOperation pattern).
I'm looking for a good pattern with which I can chain multiple HTTP requests. I want to use Swift, and preferrably Alamofire.
Say, for example, I want to do the following:
Make a PUT request
Make a GET request
Reload table with data
It seems that the concept of promises may be a good fit for this. PromiseKit could be a good option if I could do something like this:
NSURLConnection.promise(
Alamofire.request(
Router.Put(url: "http://httbin.org/put")
)
).then { (request, response, data, error) in
Alamofire.request(
Router.Get(url: "http://httbin.org/get")
)
}.then { (request, response, data, error) in
// Process data
}.then { () -> () in
// Reload table
}
but that's not possible or at least I'm not aware of it.
How can I achieve this functionality without nesting multiple methods?
I'm new to iOS so maybe there's something more fundamental that I'm missing. What I've done in other frameworks such as Android is to perform these operations in a background process and make the requests synchronous. But Alamofire is inherently asynchronous, so that pattern is not an option.
Wrapping other asynchronous stuff in promises works like this:
func myThingy() -> Promise<AnyObject> {
return Promise{ fulfill, reject in
Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo": "bar"]).response { (_, _, data, error) in
if error == nil {
fulfill(data)
} else {
reject(error)
}
}
}
}
Edit: Nowadays, use: https://github.com/PromiseKit/Alamofire-
I wrote a class which handles a chain of request one by one.
I created a class RequestChain wich takes Alamofire.Request as parameter
class RequestChain {
typealias CompletionHandler = (success:Bool, errorResult:ErrorResult?) -> Void
struct ErrorResult {
let request:Request?
let error:ErrorType?
}
private var requests:[Request] = []
init(requests:[Request]) {
self.requests = requests
}
func start(completionHandler:CompletionHandler) {
if let request = requests.first {
request.response(completionHandler: { (_, _, _, error) in
if error != nil {
completionHandler(success: false, errorResult: ErrorResult(request: request, error: error))
return
}
self.requests.removeFirst()
self.start(completionHandler)
})
request.resume()
}else {
completionHandler(success: true, errorResult: nil)
return
}
}
}
And I use it like this
let r1 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("1")
}
let r2 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("2")
}
let r3 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("3")
}
let chain = RequestChain(requests: [r1,r2,r3])
chain.start { (success, errorResult) in
if success {
print("all have been success")
}else {
print("failed with error \(errorResult?.error) for request \(errorResult?.request)")
}
}
Importent is that you are telling the Manager to not execute the request immediately
let manager = Manager.sharedInstance
manager.startRequestsImmediately = false
Hope it will help someone else
Swift 3.0 Update
class RequestChain {
typealias CompletionHandler = (_ success:Bool, _ errorResult:ErrorResult?) -> Void
struct ErrorResult {
let request:DataRequest?
let error:Error?
}
fileprivate var requests:[DataRequest] = []
init(requests:[DataRequest]) {
self.requests = requests
}
func start(_ completionHandler:#escaping CompletionHandler) {
if let request = requests.first {
request.response(completionHandler: { (response:DefaultDataResponse) in
if let error = response.error {
completionHandler(false, ErrorResult(request: request, error: error))
return
}
self.requests.removeFirst()
self.start(completionHandler)
})
request.resume()
}else {
completionHandler(true, nil)
return
}
}
}
Usage Example Swift 3
/// set Alamofire default manager to start request immediatly to false
SessionManager.default.startRequestsImmediately = false
let firstRequest = Alamofire.request("https://httpbin.org/get")
let secondRequest = Alamofire.request("https://httpbin.org/get")
let chain = RequestChain(requests: [firstRequest, secondRequest])
chain.start { (done, error) in
}
You have multiple options.
Option 1 - Nesting Calls
func runTieredRequests() {
let putRequest = Alamofire.request(.PUT, "http://httpbin.org/put")
putRequest.response { putRequest, putResponse, putData, putError in
let getRequest = Alamofire.request(.GET, "http://httpbin.org/get")
getRequest.response { getRequest, getResponse, getData, getError in
// Process data
// Reload table
}
}
}
This is definitely the approach I would recommend. Nesting one call into another is very simple and is pretty easy to follow. It also keeps things simple.
Option 2 - Splitting into Multiple Methods
func runPutRequest() {
let putRequest = Alamofire.request(.PUT, "http://httpbin.org/put")
putRequest.response { [weak self] putRequest, putResponse, putData, putError in
if let strongSelf = self {
// Probably store some data
strongSelf.runGetRequest()
}
}
}
func runGetRequest() {
let getRequest = Alamofire.request(.GET, "http://httpbin.org/get")
getRequest.response { [weak self] getRequest, getResponse, getData, getError in
if let strongSelf = self {
// Probably store more data
strongSelf.processResponse()
}
}
}
func processResponse() {
// Process that data
}
func reloadData() {
// Reload that data
}
This option is less dense and splits things up into smaller chunks. Depending on your needs and the complexity of your response parsing, this may be a more readable approach.
Option 3 - PromiseKit and Alamofire
Alamofire can handle this pretty easily without having to pull in PromiseKit. If you really want to go this route, you can use the approach provided by #mxcl.
Here is another way to do this (Swift 3, Alamofire 4.x) using a DispatchGroup
import Alamofire
struct SequentialRequest {
static func fetchData() {
let authRequestGroup = DispatchGroup()
let requestGroup = DispatchGroup()
var results = [String: String]()
//First request - this would be the authentication request
authRequestGroup.enter()
Alamofire.request("http://httpbin.org/get").responseData { response in
print("DEBUG: FIRST Request")
results["FIRST"] = response.result.description
if response.result.isSuccess { //Authentication successful, you may use your own tests to confirm that authentication was successful
authRequestGroup.enter() //request for data behind authentication
Alamofire.request("http://httpbin.org/get").responseData { response in
print("DEBUG: SECOND Request")
results["SECOND"] = response.result.description
authRequestGroup.leave()
}
authRequestGroup.enter() //request for data behind authentication
Alamofire.request("http://httpbin.org/get").responseData { response in
print("DEBUG: THIRD Request")
results["THIRD"] = response.result.description
authRequestGroup.leave()
}
}
authRequestGroup.leave()
}
//This only gets executed once all the requests in the authRequestGroup are done (i.e. FIRST, SECOND AND THIRD requests)
authRequestGroup.notify(queue: DispatchQueue.main, execute: {
// Here you can perform additional request that depends on data fetched from the FIRST, SECOND or THIRD requests
requestGroup.enter()
Alamofire.request("http://httpbin.org/get").responseData { response in
print("DEBUG: FOURTH Request")
results["FOURTH"] = response.result.description
requestGroup.leave()
}
//Note: Any code placed here will be executed before the FORTH request completes! To execute code after the FOURTH request, we need the request requestGroup.notify like below
print("This gets executed before the FOURTH request completes")
//This only gets executed once all the requests in the requestGroup are done (i.e. FORTH request)
requestGroup.notify(queue: DispatchQueue.main, execute: {
//Here, you can update the UI, HUD and turn off the network activity indicator
for (request, result) in results {
print("\(request): \(result)")
}
print("DEBUG: all Done")
})
})
}
}
Details
Alamofire 4.7.2
PromiseKit 6.3.4
Xcode 9.4.1
Swift 4.1
Full Sample
NetworkService
import Foundation
import Alamofire
import PromiseKit
class NetworkService {
static fileprivate let queue = DispatchQueue(label: "requests.queue", qos: .utility)
fileprivate class func make(request: DataRequest) -> Promise <(json: [String: Any]?, error: Error?)> {
return Promise <(json: [String: Any]?, error: Error?)> { seal in
request.responseJSON(queue: queue) { response in
// print(response.request ?? "nil") // original URL request
// print(response.response ?? "nil") // HTTP URL response
// print(response.data ?? "nil") // server data
//print(response.result ?? "nil") // result of response serialization
switch response.result {
case .failure(let error):
DispatchQueue.main.async {
seal.fulfill((nil, error))
}
case .success(let data):
DispatchQueue.main.async {
seal.fulfill(((data as? [String: Any]) ?? [:], nil))
}
}
}
}
}
class func searchRequest(term: String) -> Promise<(json: [String: Any]?, error: Error?)>{
let request = Alamofire.request("https://itunes.apple.com/search?term=\(term.replacingOccurrences(of: " ", with: "+"))")
return make(request: request)
}
}
Main func
func run() {
_ = firstly {
return Promise<Void> { seal in
DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
print("1 task finished")
DispatchQueue.main.async {
seal.fulfill(Void())
}
}
}
}.then {
return NetworkService.searchRequest(term: "John").then { json, error -> Promise<Void> in
print("2 task finished")
//print(error ?? "nil")
//print(json ?? "nil")
return Promise { $0.fulfill(Void())}
}
}.then {_ -> Promise<Bool> in
print("Update UI")
return Promise { $0.fulfill(true)}
}.then { previousResult -> Promise<Void> in
print("previous result: \(previousResult)")
return Promise { $0.fulfill(Void())}
}
}
Result
You can use the when method in PromiseKit to attach/append as many calls you want.
Here's an example from PromiseKit docs:
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//…
}
It worked perfectly for me and it's a much cleaner solution.
Call itself infinitely and DEFINE END CONDITION.
urlring for API link and Dictionary for json
WE may construct the queue model or delegate
func getData(urlring : String , para : Dictionary<String, String>) {
if intCount > 0 {
Alamofire.request( urlring,method: .post, parameters: para , encoding: JSONEncoding.default, headers: nil) .validate()
.downloadProgress {_ in
}
.responseSwiftyJSON {
dataResponse in
switch dataResponse.result {
case .success(let json):
print(json)
let loginStatus : String = json["login_status"].stringValue
print(loginStatus)
if loginStatus == "Y" {
print("go this")
print("login success : int \(self.intCount)")
self.intCount-=1
self.getData(urlring: urlring , para : para)
}
case .failure(let err) :
print(err.localizedDescription)
}
}
}else{
//end condition workout
}
}