Why NSOperation starts before completion previous operation? - ios

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

Related

How to call Main Thread after an asynchronous request

When I make the following network request the print statement appears to fire before the async. request has completed. I'm trying to call the main thread only after the async request is complete.
func getRating (articleID: String) {
let endPointURL = "http://www.smarttapp.com/DesktopModules/DnnSharp/DnnApiEndpoint/Api.ashx?method=GetRating"
let encodedarticleIDURL = endPointURL.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!
let myURL = URL(string: encodedarticleIDURL)
Alamofire.request(
myURL!,
parameters: ["articleID": articleID]
)
.responseData { response in
guard response.result.isSuccess else {
return
}
if let data = response.data, let myDataString = String(data: data, encoding: .utf8) {
print("MyDataString is: \(myDataString)")
let newRating = Double(myDataString)!
self.post.rating = newRating
print(newRating)
}
}
DispatchQueue.main.async {
print("Final value \(self.post.rating)")
self.networkingState = .finishedSearching
self.tableView.reloadData()
}
}
You need to do the main thread inside your Alamofire response completion block and it should work
Alamofire.request(
myURL!,
parameters: ["articleID": articleID]
)
.responseData { response in
guard response.result.isSuccess else {
return
}
if let data = response.data, let myDataString = String(data: data, encoding: .utf8) {
print("MyDataString is: \(myDataString)")
let newRating = Double(myDataString)!
self.post.rating = newRating
print(newRating)
}
DispatchQueue.main.async {
print("Final value \(self.post.rating)")
self.networkingState = .finishedSearching
self.tableView.reloadData()
}
}

Alamofire Appending Data

I have a simple Alamofire request and i'm parsing it with SwiftyJSON. Here is code:
var fetchedData: [TestDB]? = []
func fetchData() {
Alamofire.request(url!, method: .get).validate().responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
self.fetchedData = [TestDB]()
for dict in json["articles"].array! {
let data = TestDB()
if let image = dict["urlToImage"].string {
data.imageUrls = image
}
if let titlee = dict["title"].string {
data.titlee = titlee
}
if let desc = dict["description"].string {
data.desc = desc
}
self.fetchedData?.append(data)
// If i print fetchedData here, i can see it has right values.
// print(fetchedData)
}
case .failure(let error):
print(error)
}
}
// If i try to print fetchedData here, it's empty.
}
As i said in code, i can't append and use my datas. I think that's something with Alamofire being asynchronous. But couldn't figure out why. Any help is appreciated. Thanks!
When you use asynchronous method you have some way to proceed, for example:
use callback
use RAC/observation
Callback:
func fetchData(completion: (result: [TestDB]) -> ) {
Alamofire.request(url!, method: .get).validate().responseJSON { response in
self.fetchedData = [TestDB]()
// fill self.fetchedData then:
completion(self.fetchedData)
}
})
Where you call fetchData:
self.fetchData(completion: {
// update collectionview?
self.collectionView.reloadData()
})
RAC:
You can find all documentation here.
Plus:
Some suggestion:
self.fetchedData = TestDB is not necessary, probably is sufficient self.fetchData.removeAll()
use ObjectMappar to map any json response to object

Validate JSON response with Alamofire

Is it possible to have a Alamofire validator that gets the parsed JSON response, check a property and return true / false depending on that value?
I have an API that always returns 200 response codes, but the response has a success property.
I would like to check this property before the responseJSON callback is fired and only call responseJSON if success == true.
Is this possible with custom validators?
Found a solution I feel ok with. First I created extension methods that check for errors and extract the data I'm interested in. I have one success callback and one error callback.
import Foundation
import Alamofire
extension Request {
public func apiSuccess(
queue queue: dispatch_queue_t? = nil,
options: NSJSONReadingOptions = .AllowFragments,
completionHandler: [String:AnyObject] -> Void)
-> Self
{
return response(
queue: queue,
responseSerializer: Request.JSONResponseSerializer(options: options),
completionHandler: { response in
if let jsonValue = response.result.value as? [String:AnyObject] {
let success = jsonValue["success"] as! Bool
if (success) {
completionHandler(jsonValue["object"] as! [String:AnyObject])
}
}
}
)
}
public func apiError(
queue queue: dispatch_queue_t? = nil,
options: NSJSONReadingOptions = .AllowFragments,
completionHandler: [String] -> Void)
-> Self
{
return response(
queue: queue,
responseSerializer: Request.JSONResponseSerializer(options: options),
completionHandler: { response in
if let jsonValue = response.result.value as? [String:AnyObject] {
let success = jsonValue["success"] as! Bool
if (!success) {
let errorDict = jsonValue["errors"] as! [String:[String]]
var errors : [String] = []
errorDict.keys.forEach { key in
errors += errorDict[key] as [String]!
}
completionHandler(errors)
}
}
}
)
}
}
Then I can use it like this:
Alamofire.request(.POST, url,
parameters: parameters,
encoding: .JSON)
.apiSuccess { response in
print("Success Callback", response)
}
.apiError { errors in
print("Errors ", errors)
}
I don't think it is. Validator blocks don't receive the response data as arguments, only headers and such.

Async issue on getting values from the server

In my viewDidLoad I do:
// Get Profile Info
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
let getMyProfileURL = "\(self.property.host)\(self.property.getMyProfile)"
print(getMyProfileURL)
Alamofire.request(.POST, getMyProfileURL, parameters: self.property.profileParameteres, encoding: .JSON).responseJSON { response in
do {
let json = JSON(data: response.data!)
if json["user"].count > 0 {
self.profileDetails.append(ProfileDetailsModel(json: json["user"]))
}
}
}
print(self.profileDetails[0].firstName)
}
so, when I want to get the firstName from the array:
print(self.profileDetails[0].firstName)
my app crashes with the error:
fatal error: Array index out of range
My array is empty. I've used NSOperationQueue for it, but I still get empty array and crash. How can I fix this problem and access my filled array from anywhere in my controller, not only inside the Alamofire block?
The Request is Asynchronous, it will only fill the self.profileDetails array when it receive an answer, and execute the completionHandler block.
In your code, you call to execute the Asynchronous task, it will run in another thread, and the app goes back to the next operation and immediately try to access the self.profileDetails, with at the time may still be nil.
You must wait the completion handler block finish to fill the array, and then access the data.
The code of #Md.Muzahidul Islam , will work as you want.
Now it will work fine
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
let getMyProfileURL = "\(self.property.host)\(self.property.getMyProfile)"
print(getMyProfileURL)
Alamofire.request(.POST, getMyProfileURL, parameters: self.property.profileParameteres, encoding: .JSON).responseJSON { response in
do {
let json = JSON(data: response.data!)
if json["user"].count > 0 {
self.profileDetails.append(ProfileDetailsModel(json: json["user"]))
}
}
print(self.profileDetails[0].firstName)
}
}
I'd suggest refactoring your code to move this to a function outside of your viewDidLoad and then adding your own completion handler. Something like:
func getData(completion: () -> Void) {
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
let getMyProfileURL = "\(self.property.host)\(self.property.getMyProfile)"
print(getMyProfileURL)
Alamofire.request(.POST, getMyProfileURL, parameters: self.property.profileParameteres, encoding: .JSON).responseJSON { response in
do {
let json = JSON(data: response.data!)
if json["user"].count > 0 {
self.profileDetails.append(ProfileDetailsModel(json: json["user"]))
}
}
completion()
}
}
}
Then in viewDidLoad you can call it with its completion handler to access the data outside of the Alamofire block after it's finished, e.g.
getData() {
print(self.profileDetails[0].firstName)
}

Alamofire http json request block ui

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

Resources