Alamofire http json request block ui - ios

I've been creating a function which retrieve objects from a JSON script. I've chosen for this to use alamofire for async request and swiftyJSON for easy parsing. However i seem to have a problem with it blocking the UI? How come it does that when it is async request? Do i need to run it on a separate thread or what could the explanation be?
Basically what i mean by blocking UI is that it does not react on other buttons before the below function is finished executing.
func getRecent() {
var url = "http://URL/recent.php?lastid=\(lastObjectIndex)&limit=\(numberOfRecordsPerAPICall)"
isApiCalling = true
request(.GET, url, parameters: nil)
.response { (request, response, data, error) in
if error == nil {
let data: AnyObject = data!
let jsonArray = JSON(data: data as! NSData)
if jsonArray.count < self.numberOfRecordsPerAPICall {
self.recentCount = 0
self.tableVIew.tableFooterView = nil
} else {
self.recentCount = jsonArray.count
self.tableVIew.tableFooterView = self.footerView
}
for (key: String, subJson: JSON) in jsonArray {
// Create an object and parse your JSON one by one to append it to your array
var httpUrl = subJson["image_url"].stringValue
let url = NSURL(string: httpUrl)
let data = NSData(contentsOfURL: url!)
if UIImage(data: data!) != nil {
// Create an object and parse your JSON one by one to append it to your array
var newNewsObject = News(id: subJson["id"].intValue, title: subJson["title"].stringValue, link: subJson["url"].stringValue, imageLink: UIImage(data: data!)!, summary: subJson["news_text"].stringValue, date: self.getDate(subJson["date"].stringValue))
self.recentArray.append(newNewsObject)
}
}
self.lastObjectIndex = self.lastObjectIndex + self.numberOfRecordsPerAPICall
self.isApiCalling = false
self.tableVIew.reloadData()
self.refreshControl?.endRefreshing()
}
}
}

The response closure is executed on the main thread. If you are doing your JSON parsing there (and you have a large amount of data) it will block the main thread for a while.
In that case, you should use dispatch_async for the JSON parsing and only when you are completed update the main thread.
Just do your parsing like this
func getRecent() {
var url = "http://URL/recent.php?lastid=\(lastObjectIndex)&limit=\(numberOfRecordsPerAPICall)"
isApiCalling = true
request(.GET, url, parameters: nil)
.response { (request, response, data, error) in
if error == nil {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// Parse stuff here
let data: AnyObject = data!
let jsonArray = JSON(data: data as! NSData)
if jsonArray.count < self.numberOfRecordsPerAPICall {
self.recentCount = 0
self.tableVIew.tableFooterView = nil
} else {
self.recentCount = jsonArray.count
self.tableVIew.tableFooterView = self.footerView
}
for (key: String, subJson: JSON) in jsonArray {
// Create an object and parse your JSON one by one to append it to your array
var httpUrl = subJson["image_url"].stringValue
let url = NSURL(string: httpUrl)
let data = NSData(contentsOfURL: url!)
if UIImage(data: data!) != nil {
// Create an object and parse your JSON one by one to append it to your array
var newNewsObject = News(id: subJson["id"].intValue, title: subJson["title"].stringValue, link: subJson["url"].stringValue, imageLink: UIImage(data: data!)!, summary: subJson["news_text"].stringValue, date: self.getDate(subJson["date"].stringValue))
self.recentArray.append(newNewsObject)
}
}
dispatch_async(dispatch_get_main_queue()) {
// Update your UI here
self.lastObjectIndex = self.lastObjectIndex + self.numberOfRecordsPerAPICall
self.isApiCalling = false
self.tableVIew.reloadData()
self.refreshControl?.endRefreshing()
}
}
}
}
}

Swift5
An update to Stefan Salatic's answer, if you are parsing a large amount of json data from the Alamofire response it is better you use a global dispatch Queue and if for any reason you need to update the UI in the main thread switch to DispatchQueue.main.async.
So a sample code will look like this.
AF.request(UrlGetLayers, method: .post, parameters: parameters, headers: headers)
.responseJSON { response in
DispatchQueue.global(qos: .background).async {
//parse your json response here
//oops... we need to update the main thread again after parsing json
DispatchQueue.main.async {
}
}
}

Related

Swift - View loads before http request is finished in viewDidLoad()

I am trying to load a values from a database and put them into a UITableView in the viewDidLoad function in one of my Swift files. When debugging, at the time of the view rendering, the list of values is empty, but after the view loads, the list gets populated by the view loads. I don't have much experience with threads in Swift, so I am not exactly sure why this is happening, any ideas? I have tried to run DispatchQueue.main.async, but that did not work My code is below:
override func viewDidLoad() {
super.viewDidLoad()
// Load any saved meals, otherwise load sample data.
loadDbMeals()
}
private func loadDbMeals() {
var dbMeals = [Meal]()
let requestURL = NSURL(string: self.URL_GET)
let request = NSMutableURLRequest(url: requestURL! as URL)
request.httpMethod = "GET"
//creating a task to send the post request
let task = URLSession.shared.dataTask(with: request as URLRequest){
data, response, error in
if error != nil{
print("error is \(String(describing: error))")
return;
}
//parsing the response
do {
//converting response to NSDictionary
let myJSON = try JSONSerialization.jsonObject(with: data!, options: [.mutableContainers]) as? NSDictionary
//parsing the json
if let parseJSON = myJSON {
if let nestedDictionary = parseJSON["message"] as? NSArray {
for meal in nestedDictionary {
if let nestedMeal = meal as? NSDictionary {
let mealName = nestedMeal["name"]
let rating = nestedMeal["rating"]
dbMeals.append(Meal(name: mealName as! String, photo: UIImage(named: "defaultPhoto"), rating: rating as! Int, ingredientList: [])!)
}
}
}
}
} catch {
print(error)
}
}
meals += dbMeals
//executing the task
task.resume()
}
So, the current order of breakpoints, is the call to loadDbMeals() in the viewDidLoad() function, then it tries to add the dbMeals variables to the global meals variable, and then the http request gets executed, after the empty list has already been added. I appreciate any help!
Reload your table after loading data
if let parseJSON = myJSON {
if let nestedDictionary = parseJSON["message"] as? NSArray {
for meal in nestedDictionary {
if let nestedMeal = meal as? NSDictionary {
let mealName = nestedMeal["name"]
let rating = nestedMeal["rating"]
dbMeals.append(Meal(name: mealName as! String, photo: UIImage(named: "defaultPhoto"), rating: rating as! Int, ingredientList: [])!)
}
}
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
}
the request happens asynchronously. so the view is loaded while the request may still be in progress.
move the meals += dbMeals line into the request's completion handler (after the for loop), add a self. to the meals var since you are referencing it from within a closure and reload the tableview from the main thread afterwards:
DispatchQueue.main.async{
self.tableView.reloadData()
}
Because dataTask is not a synchronised call, we need to use lock to wait until all fetch is finished.
Code will look something like this:
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async {
let lock = DispatchSemaphore(value: 0)
// Load any saved meals, otherwise load sample data.
self.loadDbMeals(completion: {
lock.signal()
})
lock.wait()
// finished fetching data
}
}
private func loadDbMeals(completion: (() -> Void)?) {
var dbMeals = [Meal]()
let requestURL = NSURL(string: self.URL_GET)
let request = NSMutableURLRequest(url: requestURL! as URL)
request.httpMethod = "GET"
//creating a task to send the post request
let task = URLSession.shared.dataTask(with: request as URLRequest){
data, response, error in
if error != nil{
print("error is \(String(describing: error))")
return;
}
//parsing the response
do {
//converting response to NSDictionary
let myJSON = try JSONSerialization.jsonObject(with: data!, options: [.mutableContainers]) as? NSDictionary
//parsing the json
if let parseJSON = myJSON {
if let nestedDictionary = parseJSON["message"] as? NSArray {
for meal in nestedDictionary {
if let nestedMeal = meal as? NSDictionary {
let mealName = nestedMeal["name"]
let rating = nestedMeal["rating"]
dbMeals.append(Meal(name: mealName as! String, photo: UIImage(named: "defaultPhoto"), rating: rating as! Int, ingredientList: [])!)
}
}
}
}
} catch {
print(error)
}
// call completion
completion()
}
meals += dbMeals
//executing the task
task.resume()
}
So execute loadDbMeals with completion block which will be called when fetching is finished and lock will wait until completion block is called.

Parse JSON response with SwiftyJSON without crash

My iOS app is getting JSON response from server
let myURL = NSURL(string: SERVER_URL);
let request = NSMutableURLRequest(URL:myURL!);
request.HTTPMethod = "POST";
let postString = ""
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding);
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{
data, response, error in
if error != nil {
print("error=\(error)")
return
}
dispatch_async(dispatch_get_main_queue(),{
var json = JSON(data: data!)
let someInt = json["someInt"].int
let message = json["message"].stringValue
Sometimes server is down or there may be errors in JSON so there will be no such values (message, someInt) and I want to handle it without app crash - what can I do?
With SwiftyJSON, non-optional getters end with Value, and optional getters don't.
So to test if the value is here you can use optional binding with if let:
if let someInt = json["someInt"].int,
message = json["message"].string {
// someInt and message are available here
} else {
// someInt and message are not available
}
Or with guard:
guard let someInt = json["someInt"].int,
message = json["message"].string else {
// error, someInt and message are not available
return
}
// someInt and message are available here
Very simple, probably you already know it, you could protect your code with:
if let someInt = json["someInt"].int {
// do whatever you want with someInt
}
if let message = json["message"].string {
// do whatever you want with message
}
Try this approach:
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if let data = data,
jsonString = NSString(data: data, encoding: NSUTF8StringEncoding)
where error == nil {
var json = JSON(data: data!)
// use some protection as explained before..
} else {
print("error=\(error!.localizedDescription)")
}
}
task.resume()
Let me post my answer too =)
first of all you can implement small extension for failure JSON initializer:
extension JSON {
init?(_ data: NSData?) {
if let data = data {
self.init(data: data)
} else {
return nil
}
}
}
You may put it in global scope with SwiftyJSON imported and forget about forcing unwrap your data before use it in JSON. Same fail initializers can be written for other data types if you use them. Its only for a bit shorter and readable code in future. With many routes or in some cases, for example when you wait from json some single fields, this extension can make your code looks extremely easy and readable:
guard let singleMessage = JSON(data: data)?["message"].string else {return}
Then you need to check for nil in way that you need (in fact explained in previous answers). Probably you searching for fully valid data, so use if-let chain:
let myURL = NSURL(string: SERVER_URL);
let request = NSMutableURLRequest(URL:myURL!);
request.HTTPMethod = "POST";
let postString = ""
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding);
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{
data, response, error in
if error != nil {
print("error=\(error)")
return
}
dispatch_async(dispatch_get_main_queue()) {
if let json = JSON(data: data),
someInt = json["someInt"].int,
message = json["message"].string,
// ...
{
// all data here, do what you want
} else {
print("error=\(error)")
return
}
}
}
The best would be to handle using try catch
request.HTTPBody = postdata.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{
(data, response, error) in
dispatch_async(dispatch_get_main_queue(), {
var jsondata: AnyObject?
do
{
let jsondata = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(jsondata)
// your code here
}
catch
{
print("Some Error Found")
}
})
}
task.resume()
If you encounter any error, you will receive a message in the console, thus preventing the application from crashing

Why NSOperation starts before completion previous operation?

I'm trying the next:
I get response from Alamofire, fill an array
print this array
for this I did:
var queue = NSOperationQueue()
let firstOperation = NSBlockOperation(block: {
let getMyProfileURL = "\(self.property.host)\(self.property.getMyProfile)"
Alamofire.request(.POST, getMyProfileURL, parameters: self.userParameters.profileParameteres, encoding: .JSON).responseJSON { response in
do {
let json = JSON(data: response.data!)
print(json)
if json["user"].count > 0 {
self.profileDetails.append(ProfileDetailsModel(json: json["user"]))
}
}
}
})
firstOperation.completionBlock = {
print("firstOperation completed")
}
queue.addOperation(firstOperation)
let secondOperation = NSBlockOperation(block: {
print(self.profileDetails)
})
secondOperation.addDependency(firstOperation.completionBlock)
secondOperation.completionBlock = {
print(self.profileDetails)
}
queue.addOperation(secondOperation)
So, in the theory, at first it needs to fill my array, complete this task(block) and just later print those array. But I get:
firstOperation completed
[] -> self.profileDetails from the secondOperation
[] -> self.profileDetails from the secondOperation completion block
and just here I get my JSON from the Alamofire 'do' block
So, what I did wrong? And how can I fix it that it will work as I want?
Don't add the second operation until after the first operation completes (e.g. at the end of the first operations block).
First, you have to understand that Alamofire request is always performed in a separate thread.
So your firstOperation is useless. You do not need it because Alamofire is already asynchronous.
var queue = NSOperationQueue()
let secondOperation = NSBlockOperation(block: {
print(self.profileDetails)
})
secondOperation.completionBlock = {
print(self.profileDetails)
}
let getMyProfileURL = "\(self.property.host)\(self.property.getMyProfile)"
Alamofire.request(.POST, getMyProfileURL, parameters: self.userParameters.profileParameteres, encoding: .JSON).responseJSON { response in
do {
let json = JSON(data: response.data!)
print(json)
if json["user"].count > 0 {
self.profileDetails.append(ProfileDetailsModel(json: json["user"]))
}
}
print("Alamofire.request completed") // instead of: print("firstOperation completed")
queue.addOperation(secondOperation)
}

Making a re-useable function of JSON URL fetching function in SWIFT 2.0

I am stuck in a problem. I think it is all due to my weak basics. I am sure someone can help me easily and put me in the right direction.
I have different segues and all get the data from JSON via remote URL.
So in-short all segues need to open URL and parse JSON and make them into an ARRAY
I have made the first segue and it is working fine.
Now i plan to use the functions where it download JSON and turns it into ARRAY as a common function
I read in another page on stackoverflow that I can declare all common functions outside the class in ViewController
I hope everyone is with me this far.
now in ViewController i declare a function
getDataFromJson(url: String)
This function code looks like following
func getJsonFromURL(url: String)
{
// some class specific tasks
// call the common function with URL
// get an array
let arrJSON = getJsonArrFromURL(url)
for element in arrJSON
{
// assign each element in json to ur table
print("Element: \(element)")
}
// some class specific tasks
}
and this will call the common function declared outside the score of class
getArrFromJson(url: String) -> NSArray
This common function is just very generic.
Take a URL, call it, open it, parse its data into ARRAY and return it back.
The problem i am stuck is where to put the return
It returns empty array as the task is not finished and i am clueless
func getJsonArrFromURL(var url: String) -> NSArray
{
var parseJSON : NSArray?
if ( url == "" )
{
url = self.baseURLHomepage
}
print("Opening a JSON URL \(url)")
let myUrl = NSURL(string: url);
let request = NSMutableURLRequest(URL:myUrl!);
request.HTTPMethod = "GET";
let postString = "";
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding);
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{
data, response, error in
if ( error != nil )
{
print("Error open JSON url \n\(error)")
return
}
do
{
parseJSON = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSArray
}
catch
{
self.showAlert("Error", msg: "Error occurred while trying to process the product information data")
print("Error occured in JSON = \(error)")
}
}
task.resume()
return parseJSON!
}
You can probably add a method like below in any of your class
func post(url: String, info: String, completionHandler: (NSString?, NSError?) -> ()) -> NSURLSessionTask {
let URL = NSURL(string: url)!
let request = NSMutableURLRequest(URL:URL)
request.HTTPMethod = "GET"
let bodyData = info
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
dispatch_async(dispatch_get_main_queue()) {
guard data != nil else {
print("response String is nil")
completionHandler(nil, error)
return
}
if let dataNew = data {
completionHandler(NSString(data: (NSData(base64EncodedData: dataNew, options: NSDataBase64DecodingOptions([])))!, encoding: NSASCIIStringEncoding), nil)
}
}
}
task.resume()
return task
}
and access it anywhere like
let url = "your URL String"
let info = "The data you would like to pass"
yourClassName.post(url, info: info) { responseString, error in
guard responseString != nil else {
print("response String is nil")
print(error)
return
}
do {
if !(responseString as? String)!.isEmpty {
let json = try NSJSONSerialization.JSONObjectWithData((responseString as! String).data, options: NSJSONReadingOptions.init(rawValue: 0))
//process your json here
}
} catch {
print("Error\n \(error)")
return
}
}
Extend your string like follows
extension String {
var data:NSData! {
return dataUsingEncoding(NSUTF8StringEncoding)
}
}

Notify when a background request has finished swift

I'm sending a request to my backend server and I need to know the right way to notify my main thread the response. I'm using the NSNotificationCenter for this task but this not work sometimes and has some delay when it works because when I'm debugging I can see when the console prints the result but then a few secs later the corresponding alert to appear.
Here is my request:
let request1 = NSMutableURLRequest(URL: NSURL(string: serverID)!)
request1.HTTPMethod = "GET"
for key in parameters.keys{
request1.addValue(parameters[key] as String!, forHTTPHeaderField: key)
}
let task = NSURLSession.sharedSession().dataTaskWithRequest(request1) {
data, response1, error in
if error != nil {
print("error=\(error)")
NSNotificationCenter.defaultCenter().postNotificationName("request1Error", object: self)
return
}
else {
let httpResponse = response1 as! NSHTTPURLResponse
let headers = httpResponse.allHeaderFields as NSDictionary
let sucess: AnyObject? = headers.objectForKey("UserId")
if (sucess != nil){
let value = headers.valueForKey("info") as! String
print("info: \(value)")
NSNotificationCenter.defaultCenter().postNotificationName("sucessRequest", object: self)
}
else{
NSNotificationCenter.defaultCenter().postNotificationName("requestError2", object: self)
}
}
}
task.resume()
How should I notify my main thread the result of the request?
I don't know what exactly do you mean with main thread and if that is just a main thread (and not another controller or something similar) I think you should use closures that will get executed on main thread by using dispatch async.A function that described what I wrote would look similar like this :
func request(onSuccess : (value:String) -> Void, onError: ()->Void){
let request1 = NSMutableURLRequest(URL: NSURL(string: serverID)!)
request1.HTTPMethod = "GET"
for key in parameters.keys{
request1.addValue(parameters[key] as String!, forHTTPHeaderField: key)
}
let task = NSURLSession.sharedSession().dataTaskWithRequest(request1) {
data, response1, error in
if error != nil {
print("error=\(error)")
NSNotificationCenter.defaultCenter().postNotificationName("request1Error", object: self)
return
}
else {
let httpResponse = response1 as! NSHTTPURLResponse
let headers = httpResponse.allHeaderFields as NSDictionary
let sucess: AnyObject? = headers.objectForKey("UserId")
if (sucess != nil){
let value = headers.valueForKey("info") as! String
print("info: \(value)")
dispatch_async(dispatch_get_main_queue(),{
onSuccess(value)
})
}
else{
dispatch_async(dispatch_get_main_queue(),{
onError()
})
}
}
}
task.resume()
}
The delay is due to the fact that you're trying use the NSNotificationCenter on the same thread that you're using NSURLSession on. Try updating your calls to the NSNotificationCenter with something along the lines of:
dispatch_async(dispatch_get_main_queue()) {
NSNotificationCenter.defaultCenter().postNotificationName("sucessRequest", object: self)
}

Resources