URLSession Cancellation in swift while auto search - ios

My moto is Auto search, I have been trying with URLSession, When i am trying to search slowly the requests are handled as expected(when there is no text the response is empty i mean the placesarray) but when i am trying to clear the text or hit any searchtext speedily then the previous request response are being appended in the placesarray. I tried with cancelling the previous request yet i am not getting the result(i.e previous response not be appended)
func autoSearch(text:String){
let urlRequest = URLRequest(url: self.getQueryFormedBingURL()!)
let session = URLSession.shared
session.getTasksWithCompletionHandler
{
(dataTasks, uploadTasks, downloadTasks) -> Void in
// self.cancelTasksByUrl(tasks: dataTasks as [URLSessionTask])
self.cancelTasksByUrl(tasks: uploadTasks as [URLSessionTask])
self.cancelTasksByUrl(tasks: downloadTasks as [URLSessionTask])
}
let task = session.dataTask(with: urlRequest, completionHandler: { (data, response, error) -> Void in
print("response \(response)")
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: [])
if let response = response as? HTTPURLResponse , 200...299 ~= response.statusCode {
if let jsonDic = json as? NSDictionary {
let status = jsonDic.returnsObjectOrNone(forKey: "statusCode") as! Int
if status == 200 {
if let resourceSetsArr = jsonDic.returnsObjectOrNone(forKey: "resourceSets") as? NSArray {
if let placesDict = resourceSetsArr.object(at: 0) as? NSDictionary {
if let resourceArr = placesDict.object(forKey: "resources") as? NSArray, resourceArr.count > 0 {
if let _ = self.placesArray {
self.placesArray!.removeAll()
}
for loopCounter in 0...resourceArr.count - 1 {
let modalClass:BingAutoCompletePlace = BingAutoCompletePlace(responseDict: resourceArr[loopCounter] as! NSDictionary)
self.placesArray?.append(modalClass)
}
completion(self.placesArray!)
}
else { //URL Success, where there no places with the given search string
completion([])
}
}
}
}
}
}
else if let response = response as? HTTPURLResponse , 400...499 ~= response.statusCode {// When url fails
if let _ = error {
print("error=\(error!.localizedDescription)")
}
completion([])
}
else {
if let _ = error {
print("error=\(error!.localizedDescription)")
}
completion([])
}
}
})
task.resume()
}
//Request cancellation
private func cancelTasksByUrl(tasks: [URLSessionTask]) {
for task in tasks
{
task.cancel()
}
}

Unfortunately, the framework does not guarantee any order in which tasks finish -- because this depends on the running time. It could also be that you're in a completion handler of a currently cancelled task.
To circumvent this, you could do the following:
Create a private instance variable to store the most-recent task
Cancel everything else as before
In the completion handler
check if the task is still the most recent task (like if (task !== self.currentTask) {return})
create a local Array to store the data
Update the view controllers array in the main thread (DispatchQueue.main.async(...))
I cleaned up you code a litte (using guard statments to minimize the nesting). Maybe you should also
Empty the array in all the error / empty cases (instead of simple return from the guard statement)
hand-in the task to the completion call and check there again if the task is still the currentTask. This would also be a good way to reset currentTask to nil.
Just adopt it to your needs :-)
var currentTask:URLSessionDataTask?
func autoSearch(text:String){
let completion:(_ x:[AnyObject])->() = {_ in }
let urlRequest = URLRequest(url: self.getQueryFormedBingURL()!)
let session = URLSession.shared
session.getTasksWithCompletionHandler
{
(dataTasks, uploadTasks, downloadTasks) -> Void in
// self.cancelTasksByUrl(tasks: dataTasks as [URLSessionTask])
self.cancelTasksByUrl(tasks: uploadTasks as [URLSessionTask])
self.cancelTasksByUrl(tasks: downloadTasks as [URLSessionTask])
}
var task:URLSessionDataTask!
task = session.dataTask(with: urlRequest, completionHandler: { (data, response, error) -> Void in
print("response \(response)")
if (task !== self.currentTask) {
print("Ignore this task")
return
}
if let error = error {
print("response error \(error)")
}
guard let data = data else { return }
let json = try? JSONSerialization.jsonObject(with: data, options: [])
var newPlacesArray = [AnyObject]() // Empty array of whichever type you want
if let response = response as? HTTPURLResponse , 200...299 ~= response.statusCode {
guard let jsonDic = json as? NSDictionary else { return }
let status = jsonDic.returnsObjectOrNone(forKey: "statusCode") as! Int
if status == 200 {
guard let resourceSetsArr = jsonDic.returnsObjectOrNone(forKey: "resourceSets") as? NSArray else { return }
guard let placesDict = resourceSetsArr.object(at: 0) as? NSDictionary else { return }
guard let resourceArr = placesDict.object(forKey: "resources") as? NSArray, resourceArr.count > 0 else {
//URL Success, where there no places with the given search string
DispatchQueue.main.async {completion(newPlacesArray)}
return
}
for loopCounter in 0...resourceArr.count - 1 {
let modalClass:BingAutoCompletePlace = BingAutoCompletePlace(responseDict: resourceArr[loopCounter] as! NSDictionary)
newPlacesArray.append(modalClass)
}
DispatchQueue.main.async {completion(newPlacesArray)}
}
}
else if let response = response as? HTTPURLResponse , 400...499 ~= response.statusCode {// When url fails
if let _ = error {
print("error=\(error!.localizedDescription)")
}
DispatchQueue.main.async {completion(newPlacesArray)}
}
else {
if let _ = error {
print("error=\(error!.localizedDescription)")
}
DispatchQueue.main.async {completion(newPlacesArray)}
}
})
self.currentTask = task
task.resume()
}

Related

How can I unit test a network request using a local json file?

I'm trying to figure out the best way to unit test a network request. My initial thought was to create a local file with the JSON response for testing purposes but that doesn't seem to be working. See my code below.
I wanna test that I can get a non-nil array back from the completion handler in the function below.
class APIClient {
let downloader = JSONDownloader() // just a class that creates a new data task
// what I want to test
func getArticles(from url: URL?, completion: #escaping([Article]?, Error?) -> ()) {
guard let url = url else { return }
let request = URLRequest(url: url)
let task = downloader.createTask(with: request) { json, error in
DispatchQueue.main.async {
// parse JSON
...
completion(articles, nil)
}
}
task.resume()
}
}
I tried testing as shown below to no avail.
func testArticleResponseIsNotNil() {
let bundle = Bundle(for: APIClientTests.self)
guard let path = Bundle.path(forResource: "response-articles", ofType: "json", inDirectory: bundle.bundlePath) else {
XCTFail("Missing file: response-articles.json")
return
}
let url = URL(fileURLWithPath: path)
var articles: [Article]?
let expectation = self.expectation(description: "Articles")
let client = APIClient()
client.getArticles(from: url) { response, error in
articles = response
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
XCTAssertNotNil(articles)
}
Any ideas on how exactly I should test this function?
Edit: This is the JSONDownloader class.
class JSONDownloader {
let session: URLSession
init(configuration: URLSessionConfiguration) {
self.session = URLSession(configuration: configuration)
}
convenience init() {
self.init(configuration: .default)
}
typealias JSON = [String: AnyObject]
func createTask(with request: URLRequest, completion: #escaping(JSON?, Error?) -> ()) -> URLSessionDataTask {
let task = session.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else { return }
if httpResponse.statusCode == 200 {
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? JSON
completion(json, nil)
} catch { completion(nil, error) }
} else { completion(nil, error) }
} else { completion(nil, error) }
}
return task
}
}

Determine when urlsession.shared and Json parsing are finished

I am downloading and then reading a json file. this json contains a list of files and their address on the server.
Everything works fine but I want to get the size of all files to download.
but I have some trouble to set up a completionblock that would indicate that everything is finished.
here is the code.
jsonAnalysis {
self.sum = self.sizeArray.reduce(0, +)
print(self.sum)
} here
func jsonAnalysis(completion: #escaping () -> ()) {
let urlString = "xxxxxxxxxxxxxxxxxxxxx"
let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print("error")
} else {
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
self.i = -1
guard let array = json?["Document"] as? [Any] else { return }
for documents in array {
self.i = self.i + 1
guard let VersionDictionary = documents as? [String: Any] else { return }
guard let DocumentName = VersionDictionary["documentname"] as? String else { return }
guard let AddressServer = VersionDictionary["addressserver"] as? String else { return }
self.resultAddressServer.append(AddressServer)
self.addressServer = self.resultAddressServer[self.i]
self.resultDocumentName.append(DocumentName)
self.documentName = self.resultDocumentName[self.i]
let url1 = NSURL(string: AddressServer)
self.getDownloadSize(url: url1! as URL, completion: { (size, error) in
if error != nil {
print("An error occurred when retrieving the download size: \(String(describing: error?.localizedDescription))")
} else {
self.sizeArray.append(size)
print(DocumentName)
print("The download size is \(size).")
}
})
}
} catch {
print("error")
}
}
completion()
} .resume()
}
func getDownloadSize(url: URL, completion: #escaping (Int64, Error?) -> Void) {
let timeoutInterval = 5.0
var request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: timeoutInterval)
request.httpMethod = "HEAD"
URLSession.shared.dataTask(with: request) { (data, response, error) in
let contentLength = response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
completion(contentLength, error)
}.resume()
}
I would like to get the sum of the array at the end when everything is done, right now print(self.sum) is running before and shows 0.
I am not familiar with the completion and I am sure I am doing everything wrong.
You need DispatchGroup.
Before calling the inner asynchronous task enter, in the completion block of the inner asynchronous task leave the group.
Finally when the group notifies, call completion
let group = DispatchGroup()
for documents in array {
...
let url1 = URL(string: AddressServer) // no NSURL !!!
group.enter()
self.getDownloadSize(url: url1!, completion: { (size, error) in
if error != nil {
print("An error occurred when retrieving the download size: \(String(describing: error?.localizedDescription))")
} else {
self.sizeArray.append(size)
print(DocumentName)
print("The download size is \(size).")
}
group.leave()
})
}
group.notify(queue: DispatchQueue.main) {
completion()
}

Swift Dispatch Groups with Network calls and Completion

I have a a function where I need to get the difference of 2 arrays and use a completion to return the difference in dictionary form. It requires a network call to create an object from the TVDB api. I tried to implement dispatchGroups with .enter() and .leave() and everything seems to work in the right order (checking with breakpoints) until the last iteration it crashes on the line with "groupDispatch.leave()" with no error message in the console.
Here is the function in question:
func showsToWatch(idArray: [Int], completion:#escaping(_ dict: [Int:[Int]])->Void){
var toWatch:[Int:[Int]] = [:]
let groupDispatch = DispatchGroup()
for id in idArray {
groupDispatch.enter()
guard let watchedId = SeriesController.sharedController.watchedDict[id] else {return}
NetworkController.getEpisodes(id) { (episodes, error) in
if let episodes = episodes {
let episodesId = episodes.map({$0.id})
let difference = episodesId.filter { !watchedId.contains($0) }
toWatch[id] = difference
}
groupDispatch.leave()
}
}
groupDispatch.notify(queue: DispatchQueue.main, execute: { () -> Void in
completion(toWatch)
})
}
This is the NetworkController.getEpisodes function:
static func getEpisodes(_ id: Int, completion:#escaping (_ episode: [Episode]?, _ error: NSError?)->Void) {
NetworkController.getPageCount(id) { (pageCount) in
var allEpisodes = [Episode]()
for i in 1...pageCount {
let idString = String(id)
let searchUrl = baseUrl + "series/\(idString)/episodes?page=\(i)"
let searchParam = searchUrl
let myUrl = URL(string: searchParam)
var request = URLRequest(url:myUrl!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Accept")
let headerString = "Bearer " + myToken
request.addValue(headerString, forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if error != nil {
print("error=\(error)")
completion(nil, error as NSError?)
return
}
do {
if let convertedJsonIntoDict = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary {
if let dataDict = convertedJsonIntoDict as? [String:AnyObject] {
if let dataArray = dataDict["data"] as? [[String:AnyObject]]{
for episodeDict in dataArray {
let episode = Episode(dict: episodeDict)
//print("S\(episode.airedSeason)E\(episode.airedEpisodeNumber)")
if episode.absoluteNumber < 0 {
if episode.airedSeason > 0 && episode.airedEpisodeNumber > 0 {
allEpisodes.append(episode)
}
} else {
allEpisodes.append(episode)
}
}
}
}
}
completion(allEpisodes, nil)
} catch let error as NSError {
print(error.localizedDescription)
completion(nil, error as NSError?)
}
}); task.resume()
}
}
}
I tried putting the groupDispatch.leave() in different places within the function/for loop but then it doesn't complete in the correct order (edit: it hits the completion before it sets the variable "difference" array to the correct key in "toWatch" dictionary variable). I am a bit confused as the how the .leave() works it seems.
Thanks!

How can I slow down a program to make the request to the server?

I need to know that the server is returned to display in a cell right inoformatsiya after editing or return the previous line, if an error occurs. Or I can get data asynchronously, and later to update the results of a cell?
The first press of the button allows the editing, the second store information on a server and object-question
var tempText:String!
#IBAction func editButtonTapped(_ sender:UIButton) {
print("editButtonTapped")
textIsEditable = !textIsEditable
if textIsEditable == true {
tempText = questionTextView.text
questionTextView.isEditable=true
questionTextView.backgroundColor = UIColor.white
} else {
questionTextView.isEditable=false
questionTextView.backgroundColor = UIColor.clear
question.questionText=questionTextView.text
//Edit question on the server
if question.editQuestion() == true {
print("return true")
if delegate != nil {
//delegate.editQuestionAction(question: question)
delegate.editQuestionAction(cell: self)
}
} else {
questionTextView.text = tempText
question.questionText = tempText
}
}
}
Method in Question class for server request:
func editQuestion() -> Bool {
var edited=false
//Prepare image for put
let stringImage:String
if questionImage == nil {
stringImage=""
} else {
stringImage=imageName
}
let editDict:[String:String] = ["category" : category,
"text" : questionText,
"image": stringImage,
"id" : questionId]
Connection.fetchData(feed: "quests", token: nil, parameters: editDict as [String : AnyObject]?, method: "PUT") { (result, responseDict) in
if let success = responseDict?["success"] as? String {
if success == "1" {
edited = true
} else {
edited = false
}
}
}
return edited
}
Method for request to the server:
static func fetchData(feed:String,token:String? = nil,parameters:[String:AnyObject]? = nil,method:String? = nil, onCompletion:#escaping (_ success:Bool,_ data:NSDictionary?)->Void){
DispatchQueue.main.async() {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
//let url = NSURL(string: feed)
if let unwrapped_url = NSURL(string: serverString+feed){
let request = NSMutableURLRequest(url: unwrapped_url as URL)
if let tk = token {
let authValue = "Token \(tk)"
request.setValue(authValue, forHTTPHeaderField: "Authorization")
}
if let parm = parameters{
do {
if let data = try JSONSerialization.data(withJSONObject: parm, options:[]) as NSData? {
//println(NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: nil))
request.httpBody = data as Data
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("\(data.length)", forHTTPHeaderField: "Content-Length")
}
} catch let error as NSError {
print(error)
}
}
if let unwrapped_method = method {
request.httpMethod = unwrapped_method
}
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.timeoutIntervalForRequest = 15.0
let session = URLSession(configuration: sessionConfiguration)
let taskGetCategories = session.dataTask(with: request as URLRequest){ (responseData, response, error) -> Void in
let statusCode = (response as! HTTPURLResponse?)?.statusCode
print("Status Code: \(statusCode), error: \(error)")
if error != nil || (statusCode != 200 && statusCode != 201 && statusCode != 202){
onCompletion(false, nil)
}
else {
do {
if let dictionary = try JSONSerialization.jsonObject(with: responseData!, options: [.mutableContainers, .allowFragments]) as? NSDictionary{
onCompletion(true,dictionary)
} else{
onCompletion(false, nil)
}
} catch let error as NSError {
print(error)
}
}
}
UIApplication.shared.isNetworkActivityIndicatorVisible = false
taskGetCategories.resume()
}
}
}
UPDATE(import SwiftHTTP, need ios8):
func editQuestion(completion:#escaping (Bool)->()) {
var edited=false
//Prepare image for put
let stringImage:String
if questionImage == nil {
stringImage=""
} else {
stringImage=imageName
}
let editDict:[String:String] = ["category" : category,
"text" : questionText,
"image": stringImage,
"id" : questionId]
do {
let opt = try HTTP.PUT(serverString+"quests", parameters: editDict)
opt.start { response in
//do things...
if let err = response.error {
print("error: \(err.localizedDescription)")
DispatchQueue.main.async {
completion(edited)
}
return //also notify app of failure as needed
}
let responseDict=convertStringToDictionary(text: response.text!)
if let success = responseDict?["success"] as? String {
if success == "1" {
edited = true
completion(edited)
} else {
edited = false
completion(edited)
}
}
}
} catch let error {
print("got an error creating the request: \(error)")
}
}
now good?
You should never make a remote request on the main thread. That is a rule for any mobile platform, the app should always remain responsible to user actions, even when downloading or uploading data.
What you want to do is make the request asynchronously using whatever library you use (I recommend you have a look at Alamofire), and pass a callback that should receive the response. There you can use GCD (dispatch_async) to update the UI from the main thread (you can't change the UI from any other thread, on iOS at least).
Also note that Apple already deprecated methods to make synchronous requests on iOS (although they can still be done using semaphore or other forms of synchronization).

Swift do catch inside function doesn't work

I have a function which parses JSON, but I get a nil error dealing with the URL strings:
var jsonResponse: NSMutableDictionary?
do{
jsonResponse = try NSJSONSerialization.JSONObjectWithData(data!,
options: NSJSONReadingOptions.AllowFragments) as? NSMutableDictionary;
let info : NSArray = jsonResponse!.valueForKey("latest_receipt_info") as! NSArray
let transaction_id: String? = info[0].valueForKey("transaction_id") as? String
let purchase_date: String? = info[0].valueForKey("purchase_date") as? String
let product_id: String? = info[0].valueForKey("product_id") as? String
let web_order_line_item_id: String? = info[0].valueForKey("web_order_line_item_id") as? String
print("test")
// Send Values
let addIAPUrl:NSString = "http://bla.com/application/addIAP.php?transaction_id=\(transaction_id!)&purchase_date=\(purchase_date)&product_id=\(product_id)&web_order_line_item_id=\(web_order_line_item_id)&userID=\(prefs.valueForKey("userID") as! String!)"
self.apiRequests(addIAPUrl as String, completionHandler: { (success, message) -> Void in
print("success \(addIAPUrl)")
if(success == 1){
dispatch_async(dispatch_get_main_queue()){
// ADDED
print("success \(addIAPUrl)")
}
}else{
// DONT ADDED
}
})
The output doesn't return any error but the function fails after print("test"). The apiRequests function works in other cases, but doesn't seem to work in this context.
I would appreciate any help finding the problem.
Here is the code for the apiRequest function:
func apiRequests(url : String, completionHandler : ((success : Int, message : String) -> Void)) {
guard let url = NSURL(string: url as String) else {
return
}
let urlRequest = NSURLRequest(URL: url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) in
guard let responseData = data else {
return
}
guard error == nil else {
print(error)
return
}
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
return
}
let numberFromString = Int((post["success"] as? String)!)
completionHandler(success: (numberFromString)!, message: (post["message"] as? String)!)
})
task.resume()
}
It seems to me that the problem is most likely that your apiRequests: function is erroring at one of many places, and is returning instead of calling your callback with an error state.
func apiRequests(url : String, completionHandler : ((success : Int, message : String) -> Void)) {
guard let url = NSURL(string: url as String) else {
completionHandler(0, "Couldn't get URL")
return
}
let urlRequest = NSURLRequest(URL: url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) in
guard let responseData = data else {
completionHandler(0, "Data was nil")
return
}
guard error == nil else {
print(error)
completionHandler(0, "Error wasn't nil")
return
}
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
completionHandler(0, "Error with NSJSONSerialization")
return
}
let numberFromString = Int((post["success"] as? String)!)
completionHandler(success: (numberFromString)!, message: (post["message"] as? String)!)
})
task.resume()
}
Side note, but not related to the fix,
let addIAPUrl:NSString = "http://bla.com/application/addIAP.php?transaction_id=\(transaction_id!)&purchase_date=\(purchase_date)&product_id=\(product_id)&web_order_line_item_id=\(web_order_line_item_id)&userID=\(prefs.valueForKey("userID") as! String!)"
self.apiRequests(addIAPUrl as String, completionHandler: { (success, message) -> Void in
Can easily be replaced with
let addIAPUrl = "http://bla.com/application/addIAP.php?transaction_id=\(transaction_id!)&purchase_date=\(purchase_date)&product_id=\(product_id)&web_order_line_item_id=\(web_order_line_item_id)&userID=\(prefs.valueForKey("userID") as! String!)"
self.apiRequests(addIAPUrl, completionHandler: { (success, message) -> Void in
Because you are converting a String to an NSString then back to a String

Resources