AlamoFire no thread warning - ios

while using AlamoFire to make a request and access UI element on the completion block I don't get any warnings, however, if I tried doing the same thing without AlamoFire I get "UILabel.text must be used from main thread only", I wonder what's happening here?
AlamoFire Example
makeRequest(parameters:parameters,URL:.request){
validatedResponse in
label.text = "anything"
}
private func makeRequest(parameters:[String:Any]?,URL:URLs,requestType:requestType,method:HTTPMethod , completion: #escaping (_ response:APIResponse) -> ())
{
APIinterface.afManager.request(
baseUrl + URL.rawValue,
method: method,
parameters: parameters
)
.validate()
.validate(contentType: ["application/json"])
.responseJSON
{
response in
debugPrint(response)
guard let validatedResponse = self.validateResponseForAPI(response: response) else { return }
//let validatedResponse = (requestType == .api) ?
// : self.validateExternalResponse(response: response)
completion(validatedResponse)
}
}
Native Example
makeRequest(parameters:parameters,URL:.request){
validatedResponse in
label.text = "anything"
}
private func makeRequest(parameters:[String:Any]?,url:URLs,requestType:requestType,method:HTTPMethod , completion: #escaping (_ response:ResponseObject?) -> ())
{
guard let url = URL(string:baseUrl + url.rawValue) else
{
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest, completionHandler:
{
(data, response, error) in
// do stuff with response, data & error here
print(response)
print(error)
completion(self.validateResponseForAPISwift4(response: data))
})
task.resume()
}

Unless you specify otherwise, Alamofire callbacks are always executed on the main thread. That is why you are not getting the warning when using Alamofire.

Related

Swift combine recursive retry

I would like to perform a recursive once retry with Swift Combine when the server responds with a certain message (in the example a 401 error). The data in that response alters a model, which allows for a single retry.
I wrote a small extension for the result type that was used pre iOS 13
extension URLSession {
typealias HTTPResponse = (response: HTTPURLResponse, data: Data)
typealias DataTaskResult = ((Result<HTTPResponse, Error>) -> Void)
func dataTask(with request: URLRequest, completionHandler: #escaping DataTaskResult) -> URLSessionDataTask {
self.dataTask(with: request) { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
}
completionHandler(.success((response as! HTTPURLResponse, data!)))
}
}
}
I used this extension to do the following
class Account {
enum CommunicationError: Swift.Error {
case counterOutOfSync
}
var counter: Int = 0
func send(isRetry: Bool = false, completionBlock: #escaping URLSession.DataTaskResult) {
var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { [weak self] taskResult in
do {
let taskResponse = try taskResult.get()
if taskResponse.response.statusCode == 401 {
if isRetry { throw CommunicationError.counterOutOfSync }
// Counter is resynced based on taskResponse.data
self?.send(isRetry: true, completionBlock: completionBlock)
} else {
completionBlock(.success(taskResponse))
}
} catch {
completionBlock(.failure(error))
}
}.resume()
}
}
You can see the recursive call in the function. I would like to do the same with Combine, but I don't know how to. This is as far as I get
func combine(isRetry: Bool = false) -> AnyPublisher<Data, Error> {
var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
request.httpMethod = "POST"
return URLSession.shared.dataTaskPublisher(for: request).tryMap {
let response = $0.response as! HTTPURLResponse
if response.statusCode == 401 {
if isRetry { throw CommunicationError.counterOutOfSync }
// Counter is resynced based on $0.data
return self.combine(isRetry: true)
} else {
return $0.data
}
}.eraseToAnyPublisher()
}
Any help is appreciated
If you have the original send(isRetry:completionBlock:), you can use Future to convert it to a publisher:
func send() -> AnyPublisher<URLSession.HTTPResponse, Error> {
Future { [weak self] promise in
self?.send(isRetry: false) { result in promise(result) }
}
.eraseToAnyPublisher()
}
Alternatively, Combine already has a .retry operator, so the entire thing could be made purely in Combine:
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response in
let response = response as! HTTPURLResponse
if response.statusCode == 401 {
throw CommunicationError.counterOutOfSync
} else {
return (response: response, data: data)
}
}
.retry(1)
.eraseToAnyPublisher()
This will retry once whenever there's any error (not just 401) from upstream. You can play around more to only retry under some conditions (e.g. see this answer)

Execute GET request synchronously

I have a function that sends a GET request to an API and I need to wait for the response before I can continue with the rest of my code.
I have tried various different threads but haven't got it to work. I've been trying to solve this by myself but I am quite confused by threads and so fourth.
These are my functions:
func checkDomains() {
let endings = [".com"]
for ending in endings {
let domainName = names[randomNameIndx] + ending
let urlString = "https://domainr.p.mashape.com/v2/status?mashape-key={my-key}" + domainName
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.setValue("{my-key}", forHTTPHeaderField: "X-Mashape-Key")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "GET"
var ourBool = false
DispatchQueue.main.sync {
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, er) in
do {
print("hey")
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
ourBool = String(describing: json).contains("inactive")
if ourBool {
self.domainStatuses.append("available")
} else {
self.domainStatuses.append("taken")
}
}
} catch {
}
})
task.resume()
}
}
}
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains() // This needs to finish before calling the next functions
self.setupForNewName()
self.animateForNewName()
}
My suggestion is in adding callback param into your async function. Example below:
func checkDomains(_ onResult: #escaping (Error?) -> Void)
Then you can call onResult inside your function in places where server return success result or error.
func checkDomains(_ onResult: #escaping (Error?) -> Void) {
...
let task = URLSession.shared.dataTask ... {
do {
//Parse response
DispatchQueue.main.async { onResult(nil) }
} catch {
DispatchQueue.main.async { onResult(error) }
}
...
}
}
The last thing you need pass callback param in place where you calling checkDomains function
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains { [unowned self] error in
if let error = error {
// handle error
return
}
self.setupForNewName()
self.animateForNewName()
}
}
Thank you for your answers. I just came home and realized there is a simpler way of doing this.
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
...
if response != nil {
DispatchQueue.main.async {
self.setupForNewName()
self.animateForNewName()
}
}
}
} catch {
print(error)
}
})
task.resume()
Of course this is dependent on the response of the API but I am confident enough the API will always respond.
Lately it is a good practice to do network call asynchronously. There some technics to make code simpler, like https://cocoapods.org/pods/ResultPromises
TBC...

How can I stop URLSessionTask when the Internet is disconnected?

I am using URLSessionTask to get the source code of url. When the internet is connected, it works well.
However, when the Internet is disconnected, I try building. And in simulator it is blank and the cpu is 0%. What affects is that My Tab Bar Controller is also missing and blank (It is my initial view controller). It seems that this task is under connecting?
I want the data received from dataTask, so I use semaphore to make it synchronous. Otherwise, as dataTask is an asynchronous action, what I
get is an empty string.
How can I fix this problem?
Thanks!
let urlString:String="http://www.career.fudan.edu.cn/jsp/career_talk_list.jsp?count=50&list=true"
let url = URL(string:urlString)
let request = URLRequest(url: url!)
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
let dataTask = session.dataTask(with: request,
completionHandler: {(data, response, error) -> Void in
if error != nil{
errorString = "Error!"
}else{
htmlStr = String(data: data!, encoding: String.Encoding.utf8)!
//print(htmlStr)
}
semaphore.signal()
}) as URLSessionTask
//start task
dataTask.resume()
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
Update: As #Moritz mentioned, I finally use completion handler (callback).
func getforData(completion: #escaping (String) -> ()) {
if let url = URL(string: "http://XXXXX") {
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {
data, response, error in
if let data = data, let getString = String(data: data, encoding: String.Encoding.utf8), error == nil {
completion(getString)
} else {
print("error=\(error!.localizedDescription)")
}
}
task.resume()
}
}
And in viewdidload
override func viewDidLoad() {
super.viewDidLoad()
getforData { getString in
// and here we get the "returned" value from the asynchronous task
print(getString) //works well
//tableview should work in main thread
DispatchQueue.main.async {
self.newsTableView.dataSource = self
self.newsTableView.delegate = self
self.newsTableView.reloadData()
}
}

DispatchGroup not working in Swift 3

I am trying to make dispatchgroup work in my code
let dispatchQueue:DispatchQueue = DispatchQueue(label: "com.dispatchgroup", attributes: .concurrent, target: .main)
var dispatchGroup:DispatchGroup = DispatchGroup()
func renewLoginIfRequired()->String{
self.dispatchGroup.enter()
dispatchQueue.async(group:dispatchGroup){
self.authorizeApplication(completionHandler: {
self.dispatchGroup.leave()
})
}
}
self.dispatchGroup.wait() // Stops execution here
return "Login Success"
}
Above code stops execution at self.dispatchGroup.wait().
I have tried the same code without dispatchQueue.async(group:dispatchGroup) around self.authorizeApplication as well with no luck
I am not sure what am i doing wrong. One thing to mention here is that self.authorizeApplication will make an async web service request within that function
Edit 1:
To elaborate the problem even more. I will be returning a session token (String) from this method.
Here is the function which is calling this
func processPOSTRequest(_ urlString : String, isAnAuthReq:Bool = false, requestObject: Data?, onSuccess: #escaping (NSDictionary?, NSError?) -> Void, onFailure: #escaping (Data?, NSError?) -> Void){
let manager = AFHTTPSessionManager()
let url = URL(string: urlString);
var request = URLRequest(url: url!);
if !isAnAuthReq{
let token = self.renewLoginIfRequired() //-- Get new token before processing a webservice request
request.setValue(token, forHTTPHeaderField: "Authorization")
}
print("processing URl : "+urlString)
request.httpMethod="POST";
request.httpBody = requestObject
request.setValue("application/json; charset=utf-8", forHTTPHeaderField:"Content-Type")
request.setValue("application/json; charset=utf-8", forHTTPHeaderField:"Accept")
let task = manager.dataTask(with: request, uploadProgress: nil, downloadProgress: nil, completionHandler:{ data, response, error in
if(error == nil){
if let responseCode = (data as? HTTPURLResponse)?.statusCode{
if responseCode != 200 || !((response as? [String: Any])?["success"] as? Bool)!{
let errorResponse = NSError()
print("Response received for URL: "+urlString)
onFailure(nil, errorResponse.addItemsToUserInfo(newUserInfo: ["errorCode":String(responseCode)]))
}
else{
onSuccess(response as! NSDictionary?, nil)
}
}
}
else{
onFailure(nil, error as NSError?)
}
})
task.resume()
}
If i use Notify or closure. How can i do that? I have tried both of them
I don't know what is under the hood of the authorizeApplication request, but it's likely that its callback performs in the main thread, that is, in the main queue. So you enter, leave and wait in the same queue, therefore you can't reach the self.dispatchGroup.leave() after you've invoked self.dispatchGroup.wait().
To handle that you need to redesign the async request and call self.dispatchGroup.leave() in a background queue.

How to use httpMethod = POST on Swift 3 , URLRequest?

I have working json fetching codes with GET methods but when i convert it to POST with adding;
request.httpMethod = "POST"
gives me ;
Cannot assign to property: 'httpMethod' is a get-only property Error
My codes under below how can fix it ? How can i convert it to POST
#discardableResult
open func getLoginMethod(_ method: String, parameters: String, completionHandler: #escaping (_ login: [Login]?, _ error: Error?) -> Void) -> URLSessionTask! {
let session = URLSession.shared
guard let url = NSURL(string: parameters) else {
completionHandler(nil, myErrors.InvalidUrlError)
return nil
}
let request = NSURLRequest(url: url as URL)
let task = session.dataTask(with: request as URLRequest) { data, response, error in
if error != nil {
completionHandler(nil, myErrors.getError)
} else {
do {
let login = try self.getLogin(jsonData: data! as NSData)
completionHandler(login, nil)
} catch {
completionHandler(nil, error)
}
}
}
task.resume()
return task
}
Also my calling codes under below for get method my variables inside parameters , for POST method which changes i need to do ?
AWLoader.show(blurStyle: .dark, shape: .Circle)
DispatchQueue.global(qos: .background).async {
myApp.sharedInstance().getLoginMethod("", parameters: "http://bla.com/Post?Phone=\(self.phone.text)") {login, error in
if error != nil {
DispatchQueue.main.async {
// self.showAlert()
print(error!)
AWLoader.hide()
}
} else {
DispatchQueue.main.async {
self.getlogin = login!
// self.collectionView.reloadData()
// AWLoader.hide()
// self.loginButton.alpha = 0
print(self.getlogin)
AWLoader.hide()
}
}
}
}
Thanks
You need to use NSMutableURLRequest to be able to modify httpMethod. Using NSURLRequest won't do.
Example:
let request = NSMutableURLRequest(url: url)
request.httpMethod = "POST"
But even better, you should be using URLRequest struct:
var request = URLRequest(url: url)
request.httpMethod = "POST"
Your refactored code with additional improvements:
#discardableResult
open func getLoginMethod(_ method: String, parameters: String,
session: URLSession = .shared,
completionHandler: #escaping (_ login: [Login]?, _ error: Error?) -> Void) -> URLSessionTask! {
guard let url = URL(string: parameters) else {
completionHandler(nil, myErrors.InvalidUrlError)
return nil
}
var request = URLRequest(url: url)
request.httpMethod = method
let task = session.dataTask(with: request) { data, _, _ in
guard let responseData = data else {
completionHandler(nil, myErrors.getError)
return
}
do {
let login = try self.getLogin(jsonData: data as NSData)
completionHandler(login, nil)
} catch {
completionHandler(nil, error)
}
}
task.resume()
return task
}
EDIT:
To answer your second part of the question. It depends on what kind of data format your API is expecting - whether it should be multipart/form-data or json format, it all depends.
But in general, assuming that you are using URLRequest struct that I've mentioned, you should just convert it to Data using your serializer and use it like this:
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = <your parameters converted to Data>
Usually, your API will expect proper Content-Type header added to your URLRequest:
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

Resources