Determine when urlsession.shared and Json parsing are finished - ios

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()
}

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
}
}

Parse image from web json

I have a json file that looks something like this:
{
"adTitle": "My Title",
"adURL": "https://mylink.com/",
"adImageURL": "http://mywebsite/bannerx#3x.png"
}
I get the JSON value from website: http://mywebsite.com/file.json
The problem is that the ad somehow doesn't load the adImageURL, so when I press the UIImageView, but when I press the area that then UIImageView should be, it open my adURL. This is the code I use for JSON:
var imageURL:String = "http://mywebsite/bannerx#3x.png"
var adURL:String = "https://mylink.com/"
func loadAdvertisement() {
// Set up the URL request
let todoEndpoint: String = "http://mywebsite.com/file.json"
guard let url = URL(string: todoEndpoint) 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) {
(data, response, error) in
// check for any errors
guard error == nil else {
// print("error calling GET on /todos/1")
// print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
guard (try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: AnyObject]) != nil else {
print("error trying to convert data to JSON")
return
}
let json = try JSONSerialization.jsonObject(with: responseData, options:.allowFragments) as! [String:AnyObject]
if (json != nil) {
self.imageURL = (json["adImageURL"] as? String)!
self.adURL = (json["adURL"] as? String)!
print(self.imageURL)
print(self.adURL)
DispatchQueue.main.async { () -> Void in
self.loadAdImage(self.imageURL)
}
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
// let jsonURL = URL(string: "http://mywebsite.com/file.json")
// self.getDataFromUrl(jsonURL!, completion: (data:Data?, response:URLResponse?, error:Error?)) -> Void
}
func loadAdImage(_ url:String) {
getDataFromUrl(URL(string: url)!) { (data, response, error) in
DispatchQueue.main.async { () -> Void in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? "")
print("Download Finished")
self.advertImageView.image = UIImage(data: data)
}
}
}
func getDataFromUrl(_ url:URL, completion: #escaping ((_ data: Data?, _ response: URLResponse?, _ error: NSError? ) -> Void)) {
URLSession.shared.dataTask(with: url) { (data:Data?, response:URLResponse?, error:Error?) in
completion(data, response, error as NSError?)
}.resume()
}
In the event LOG, is prints out both of the print("error trying to convert data to JSON") commands. I have used this code before in my project, and it worked just fine, but I have no idea why it wont work anymore.
Add the message to catch and check what actually error you are getting like this way:
do {
let json = try JSONSerialization.jsonObject(with: responseData, options:.allowFragments) as! [String:AnyObject]
} catch let message {
print("error trying to convert data to JSON" + "\(message)")
return
}

How parse JSON from 2 URLs properly?

I need to parse JSON from 2 different URL's
let jsonUrlStr1 = "https://123"
let jsonUrlStr2 = "https://325"
guard let url1 = URL(string: jsonUrlStr1) else { return }
guard let url2 = URL(string: jsonUrlStr2) else { return }
Here I'm running session for 1st url:
URLSession.shared.dataTask(with: url1) { (data, response, err) in
if err != nil {
print("Error:\(String(describing: err))")
}
guard let data = data else { return }
do {
let myData1 = try JSONDecoder().decode(SomeJsonModel1.self, from: data)
//Some code
} catch let jsonErr {
print("Error:\(jsonErr)")
}
}.resume()//URLSession
And then again, I'm running another session for 2nd url, using the same way:
URLSession.shared.dataTask(with: url2) { (data, response, err) in
if err != nil {
print("Error:\(String(describing: err))")
}
guard let data = data else { return }
do {
let myData2 = try JSONDecoder().decode(SomeJsonModel2.self, from: data)
//Some code
} catch let jsonErr {
print("Error:\(jsonErr)")
}
}.resume()//URLSession
This code works and I get the result.
But I think there should be a more correct way to parse 2 URLs.
Please advise how to do it correctly. Thanks.
You can try using completion block like this :
func getDataFromJson(url: String, completion: #escaping (_ success: [String : Any]) -> Void) {
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { Data, response, error in
guard let data = Data, error == nil else { // check for fundamental networking error
print("error=\(String(describing: error))")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print(response!)
return
}
let responseJSON = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String : Any]
completion(responseJSON)
}
task.resume()
}
and call method like this :
let jsonUrlStr1 = "https://123"
let jsonUrlStr2 = "https://325"
getDataFromJson(url: jsonUrlStr1, completion: { response in
print("JSON response for url1 :: ",response) // once jsonUrlStr1 get it's response call next API
getDataFromJson(url: jsonUrlStr2, completion: { response in
print("JSON response for url2 :: ",response)
})
})

URLSession Cancellation in swift while auto search

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()
}

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!

Resources