I have a number of requests witch I would like to call one after another without having nested spaghetti code.
I tried it already with a serial dispatch queue
let queue = dispatch_queue_create("label", DISPATCH_QUEUE_SERIAL)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(1)
}
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(2)
}
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(3)
}
But unfortunately that does not work. The output of this can be 1,3,2 or 3,1,2 or any other combination.
What would be the best approach to get the output 1,2,3 so one after the other.
Ok I ended up writing my own implementation.
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
I'm using Artman's Signals to notify my app once a result is returned, after which a queue elsewhere can call it's next request:
Alamofire.request( httpMethod, url, parameters: params ).responseJSON
{
( response: Response< AnyObject, NSError > ) in
self._signals.dispatchSignalFor( Key: url, data: response.result )
}
More details here.
One solution is to call your second request in the first one's callback :
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(1)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(2)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(3)
}
}
}
Related
Im trying to implement a stock API but get an error with ".append":
No exact matches in call to instance method 'append'.
I'm not familiar with it and need help to solve this problem
final class StockQuoteManager: QuoteManagerProtocol, ObservableObject {
#Published var quotes: [Quote] = []
func download(stocks: [String], completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.dwonloadQueue")
let downloadGroup = DispatchGroup()
stocks.forEach { (stock) in
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { (result) in
switch result {
case .failure(let err):
print(err)
downloadQueue.async {
downloadGroup.leave()
}
case .success(let resp):
downloadQueue.async {
internalQuotes.append(resp.quote) // <-- ERROR
downloadGroup.leave()
}
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
}
Using DispatchGroup I am trying to run 2 network requests against my client, returning the results when both have completed.
I am having an issue in that sometimes the completion handler for one of DispatchGroup requests is called twice and the other is not called at all.
An example would be -
func fetchProfileWithRelatedArticle(onSuccess: #escaping (User, [RelatedArticle]) -> Void, onError: #escaping (Error) -> Void) {
let dispatchGroup = DispatchGroup()
var user: User?
var articles: [RelatedArticle] = []
var errors: [Error] = []
dispatchGroup.enter()
fetchProfileForUser(onSuccess: {
user = $0
print("fetchProfile:",$0)
print("123")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.enter()
getArticlesForUser(onSuccess: {
articles = $0
print("getArticlesForUser:",$0)
print("456")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main) {
guard let user = user, errors.isEmpty else { return }
onSuccess(user, articles)
}
}
Here I fetch a user profile and also fetch a list of articles they have written. These are returned via a completion handler and presented elsewhere.
Most of the time this works, however it appears on occasion either one of those requests will call its own completion handler twice and the other request wont.
I suspect this may be down to when my access token expires as it occurs if I leave the app for a short time. My access token has a life of 2 minutes.
Should a request receive a 401 response, I have the following method in my network client that requests a new token, then invokes the call again. I believe this may not be working as I'd like.
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
I suspect calling the method again after the update is doing something to the requests my dispatch group is returning.
Is it possible to chain requests in this fashion?
struct NoContent: Codable { }
typealias RefreshHandler = (#escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]
protocol ClientType: class {
associatedtype Route: RouterType
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void)
}
class Client<Route: RouterType>: ClientType {
enum APIError: Error {
case unknown, badResponse, jsonDecoder, other
}
enum TokenError: String, Error {
case expired = "Access Token Expired"
case refused = "Refresh Token Failed"
}
private(set) var session: SessionType
private(set) var tokenGetter: TokenGetter
private(set) var refreshHandler: RefreshHandler
private lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
return decoder
}()
init(session: SessionType, tokenGetter: #escaping TokenGetter, refreshHandler: #escaping RefreshHandler) {
self.session = session
self.tokenGetter = tokenGetter
self.refreshHandler = refreshHandler
}
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void) {
let request = URLRequest(
resource: resource,
headers: tokenGetter()
)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard error == nil else { completion(.failure(APIError.unknown)); return }
guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
if response.statusIsSuccess() {
guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
do {
let value = try self.decoder.decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(value))
}
} catch let error {
print(error)
}
return
}
completion(.failure(APIError.other))
}.resume()
}
// some calls return a 200/201 with no data
private func deserializeNoContentResponse(data: Data?) -> Data? {
if data?.count == 0 {
return "{ }".data(using: .utf8)
}
return data
}
}
Sounds like you would need to something like the following in your networking client:
func makeTheRequest(_ completion: CompletionHandler) {
URLSession.shared.dataTask(with: someURL) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
return
}
if httpResponse.statusCode == 401 {
self.refreshToken { success in
if success { self.makeTheRequest(completion) }
}
}
// handle response
completion(whateverDataYouNeedToPass)
}
}
That would make the call, check the response code, refresh the token is needed and if that succeeds it calls the method that should make the response again with the completion handler that was passed to in the first place without calling it first. So the completion handler wouldn't be called until after the API call is made the second time.
Of course, adopt this for your own code, shouldn't be too hard to do
Basically I have some JSON data that I wish to retrieve from a bunch of URL's (all from the same host), however I can only request this data roughly every 2 seconds at minimum and only one at a time or I'll be "time banned" from the server. As you'll see below; while URLSession is very quick it also gets me time banned almost instantly when I have around 700 urls to get through.
How would I go about creating a queue in URLSession (if its functionality supports it) and while having it work asynchronously to my main thread; have it work serially on its own thread and only attempt each item in the queue after 2 seconds have past since it finished the previous request?
for url in urls {
get(url: url)
}
func get(url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
print(error.localizedDescription)
}
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
DispatchQueue.main.async {
print("Server Error")
}
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
DispatchQueue.main.async {
self.itemsCount.append(count)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
Recursion solves this best
import Foundation
import PlaygroundSupport
// Let asynchronous code run
PlaygroundPage.current.needsIndefiniteExecution = true
func fetch(urls: [URL]) {
guard urls.count > 0 else {
print("Queue finished")
return
}
var pendingURLs = urls
let currentUrl = pendingURLs.removeFirst()
print("\(pendingURLs.count)")
let session = URLSession.shared
let task = session.dataTask(with: currentUrl, completionHandler: { (data, response, error) in
print("task completed")
if let _ = error {
print("error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("server error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
if response.mimeType == "application/json" {
print("json data parsed")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}else {
print("unknown data")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}
})
//start execution after two seconds
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
print("resume called")
task.resume()
}
}
var urls = [URL]()
for _ in 0..<100 {
if let url = URL(string: "https://google.com") {
urls.append(url)
}
}
fetch(urls:urls)
The easiest way is to perform recursive call:
Imagine you have array with your urls.
In place where you initially perform for loop with, replace it with single call get(url:).
self.get(urls[0])
Then add this line at the and of response closure right after self.itemsCount.append(count):
self.urls.removeFirst()
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
self.get(url: urls[0])
}
Make DispatchQueue to run your code on threads. You don't need to do this work on Main Thread. So,
// make serial queue
let queue = DispatchQueue(label: "getData")
// for delay
func wait(seconds: Double, completion: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + seconds) { completion() }
}
// usage
for url in urls {
wait(seconds: 2.0) {
self.get(url: url) { (itemCount) in
// update UI related to itemCount
}
}
}
By the way, Your get(url: url) function is not that great.
func get(url: URL, completionHandler: #escaping ([Int]) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print(error.localizedDescription)
/* Don't need to use main thread
DispatchQueue.main.async {
print(error.localizedDescription)
}
*/
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("Server Error")
/* Don't need to use main thread
DispatchQueue.main.async {
print("Server Error")
}
*/
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
self.itemsCount.append(count)
// append all data that you need and pass it to completion closure
DispatchQueue.main.async {
completionHandler(self.itemsCount)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
I would recommend you to learn concept of GCD(for thread) and escaping closure(for completion handler).
GCD: https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
Escaping Closure: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID546
I have an NetworkRequest class, where all my alamofire requests made:
class NetworkRequest {
static let request = NetworkRequest()
var currentRequest: Alamofire.Request?
let dataManager = DataManager()
let networkManager = NetworkReachabilityManager()
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
func downloadData<T: Film>(slug: String, provider: String, section: String, dynamic: String, anyClass: T, completion: ([T]?) -> Void ) {
var token: String = ""
if LOGGED_IN == true {
token = "\(NSUserDefaults.standardUserDefaults().valueForKey(TOKEN)!)"
}
let headers = [
"Access": "application/json",
"Authorization": "Bearer \(token)"
]
let dataUrl = "\(BASE_URL)\(slug)\(provider)\(section)\(dynamic)"
print(headers)
print(dataUrl)
if networkManager!.isReachable {
currentRequest?.cancel()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
if let url = NSURL(string: dataUrl) {
let request = Alamofire.request(.GET, url, headers: headers)
request.validate().responseJSON { response in
switch response.result {
case .Success:
if let data = response.result.value as! [String: AnyObject]! {
let receivedData = self.dataManager.parseDataToFilms(data, someClass: anyClass)
completion(receivedData)
}
case .Failure(let error):
print("Alamofire error: \(error)")
if error.code == 1001 {
self.goToNoConnectionVC()
}
print("canceled")
}
}
}
}
} else {
goToNoConnectionVC()
}
}
}
And I need to cancel previous downloadData request, when the new one starts, tried to cancel using currentRequest?.cancel(), but it doesn't help.
Already tried to cancelOperations using NSOperationsBlock, but it doesn't cancels current operation.
I block UI now, so that user can't send another request. But this is not correct, causes some errors later...
Pls, help
Now on Alamofire 4 the Alamofire.Manager.sharedInstance.session is not available you should use this solution:
let sessionManager = Alamofire.SessionManager.default
sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
}
and if you want to cancel (suspend, resume) a particular request you can check the request url in your .forEach block like this:
dataTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == url) {
$0.cancel()
}
}
Found needed solution:
func stopAllSessions() {
Alamofire.Manager.sharedInstance.session.getAllTasksWithCompletionHandler { tasks in
tasks.forEach { $0.cancel() }
}
}
Update for Alamofire 5
func stopAllSessions() {
AF.session.getTasksWithCompletionHandler { (sessionDataTask, uploadData, downloadData) in
sessionDataTask.forEach { $0.cancel() }
uploadData.forEach { $0.cancel() }
downloadData.forEach { $0.cancel() }
}
}
If you want to cancel the request, you need to trace the requests made and try to cancel it. You can store it in an array and cancel every previous request stored.
In your code you create a request:
let request = Alamofire.request(.GET, url, headers: headers)
but you try to cancel the currentRequest?.cancel() that is never valued.
Swift 5
To cancel all requests use
Alamofire.Session.default.session.getTasksWithCompletionHandler({ dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
})
To cancel a request with a particular url use
Alamofire.Session.default.session.getTasksWithCompletionHandler({ dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == "www.google.com") {
$0.cancel()
}
}
uploadTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == "www.google.com") {
$0.cancel()
}
}
downloadTasks.forEach {
if ($0.originalRequest?.url?.absoluteString == "www.google.com") {
$0.cancel()
}
}
})
I'm trying to implement the following RxSwift example:
Login in with facebook in my application -> retrieve the user information -> retrieve user's profile photo.
I have these three functions and they must be executed in this order: requestAccess() -> fetchUserInformation() -> fetchUserPhoto()
func requestAccess() -> Observable<(ACAccount)> {
return create { observer -> Disposable in
let accountStore = ACAccountStore()
let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierFacebook)
let dictionary: [NSObject : AnyObject] = [ACFacebookAppIdKey:"***APPID***", ACFacebookPermissionsKey:["public_profile", "email", "user_friends"]]
accountStore.requestAccessToAccountsWithType(accountType, options: dictionary) { granted, error in
if granted == false || error != nil {
sendError(observer, error ?? UnknownError)
} else {
let accounts = accountStore.accountsWithAccountType(accountType)
let account = accounts.last as! ACAccount
sendNext(observer, account)
sendCompleted(observer)
}
}
return AnonymousDisposable({})
}
}
func fetchUserInformation(account: ACAccount) -> Observable<User> {
return create { observer -> Disposable in
let url = NSURL(string: "https://graph.facebook.com/me")
let request = SLRequest(forServiceType: SLServiceTypeFacebook, requestMethod: .GET, URL: url, parameters: nil)
request.account = account
request.performRequestWithHandler { (data, response, error) -> Void in
if data == nil || response == nil {
sendError(observer, error ?? UnknownError)
} else {
let result: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil)
let user = User()
user.updateInformationWithJSON(result! as! JSONObject)
sendNext(observer, user)
sendCompleted(observer)
}
}
return AnonymousDisposable({})
}
}
func fetchUserPhoto(user: User) -> Observable<AnyObject> {
return create { observer -> Disposable in
let url = NSURL(string: "https://graph.facebook.com/***myid***/picture")
let params = ["redirect":"false", "height":"200", "width":"200"]
let request = SLRequest(forServiceType: SLServiceTypeFacebook, requestMethod: .GET, URL: url, parameters: params)
request.account = SocialController.account
request.performRequestWithHandler { (data, response, error) -> Void in
if data == nil || response == nil {
sendError(observer, error ?? UnknownError)
} else {
let result: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil)
sendNext(observer, result!)
sendCompleted(observer)
}
}
return AnonymousDisposable({})
}
}
I already tried to implement this flow but it doesn't feel right. What is the best way to solve this problem?
requestAccess()
>- subscribeNext { account in
fetchUserInformation(account)
>- map { user in return UserViewModel(model: user) }
>- subscribeNext { viewModel in self.viewModel = viewModel }
}
Have you tried using flatMap?
It's an equivalent to then in the JavaScript bluebird or Q world. The difference between map and flatMap is that flatMap must return an Observable<T> which will then be unwrapped in the following block/closure.
requestAccess()
>- flatMap{ account in
return fetchUserInformation(account)
}
>- map { user in
return UserViewModel(model:user)
}
>- subscribeNext { viewModel in
self.viewModel = viewModel
}
Tidbit #1: Consider using unowned self when referencing self to avoid a retain cycle.
Tidbit #2: These two are pretty much the same thing.
flatMap { return just("hello") }
>- subscribeNext{ greeting in println(greeting) }
map { return "hello" }
>- subscribeNext{ greeting in println(greeting) }
private
func requestFacebookAccess(_ viewController: UIViewController) -> Observable<LoginManagerLoginResult?> {
return Observable.create { observer -> Disposable in
let loginManager = LoginManager()
loginManager.logIn(permissions: ["public_profile", "email"], from: viewController, handler: { result, error in
observer.onNext(result)
observer.onCompleted()
})
return Disposables.create()
}
}