var arrayLength:Int = 0 // is equal to 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let urlPath = "http://example.com/json"
let url = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
if (error? != nil) {
println(error)
} else {
var jsonResponse: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonResponse!)
self.arrayLength = json["dump"].array?.count as Int!
println("Second: \(self.arrayLength)") // is equal to 3
for postIndex in 0...self.arrayLength-1 {
println(json["dump"][postIndex]["title"])
}
}
})
task.resume()
println(arrayLength) // is equal to 0 again
I set the
var arrayLength:Int = 0
in the beginning of code. Later inside of the viewDidLoad() I changed it to 3. And when I call it, outside of the task block it again equal to 0. What is the problem here and what I do wrong?
Yes, you're changing your arrayLengthvar inside your closure. The problem you're facing here it's just normal way of how threads work.
When viewDidLoadstarts you're on the Main Thread
the line let task = session.dataTaskWithURL(url, completionHandler: { ... defines a new task, that's not yet executing
task.resume() launches this task on another thread. This is going to take some time to finish. When it's finished your arrayLength will be changed
immediately you're asking on the main thread for your arrayLength value
Here:
println(arrayLength) // is equal to 0 again
it's not that arrayLength is equal to 0 again, you haven't given enough time to your task to execute and change that value.
EDIT: if you need to check arrayLength after the closure finishes...
Just create a function and call it last thing inside your closure. Like this:
override func viewDidLoad() {
...
let task = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
if (error? != nil) {
println(error)
} else {
var jsonResponse: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonResponse!)
self.arrayLength = json["dump"].array?.count as Int!
println("Second: \(self.arrayLength)") // is equal to 3
for postIndex in 0...self.arrayLength-1 {
println(json["dump"][postIndex]["title"])
}
functionToDoSomethingAtTheEndOfTheClosure();
}
})
task.resume()
}
func functionToDoSomethingAtTheEndOfTheClosure() {
// this is going to be executed at the end of the closure's code
}
Related
func handleGetAllPhotoURLs is called from the line below and I have confirmed that the line of code only executes once with breakpoints.
_ = FlickrClient.getAllPhotoURLs(currentPin: self.currentPin, fetchCount: fetchCount, completion: self.handleGetAllPhotoURLs(pin:urls:error:))
According to output from my print statements, the function runs twice because it prints two lines of output if urls.count is non-zero. However, if urls.count is zero then I only get one print statement that states "urls.count ---> 0"
handleGetAllPhotoURLs ---> urls.count ---> 0 //this line is always printed
handleGetAllPhotoURLs ---> urls.count ---> 21 //this line is only printed if the urls parameter is not empty
func handleGetAllPhotoURLs(pin: Pin, urls: [URL], error: Error?){
print("handleGetAllPhotoURLs ---> urls.count ---> \(urls.count)")
let backgroundContext: NSManagedObjectContext! = dataController.backGroundContext
if let error = error {
print("func mapView(_ mapView: MKMapView, didSelect... \n\(error)")
return
}
let pinId = pin.objectID
backgroundContext.perform {
let backgroundPin = backgroundContext.object(with: pinId) as! Pin
backgroundPin.urlCount = Int32(urls.count)
try? backgroundContext.save()
}
for (index, currentURL) in urls.enumerated() {
URLSession.shared.dataTask(with: currentURL, completionHandler: { (imageData, response, error) in
guard let imageData = imageData else {return}
connectPhotoAndPin(dataController: self.dataController, currentPin: pin , data: imageData, urlString: currentURL.absoluteString, index: index)
}).resume()
}
}
In addition, I have a UILabel that only reveals itself when urls.count is zero and I only want to reveal it when urls is empty.
Right now, if urls is not empty, the app is very quickly flashing the empty message UILabel. Which now makes sense to me because print statement shows that urls array is temporarily empty.
Is there a way for me to determine to avoid flashing the empty message UILabel to user when urls.count is non-zero?
edit: Added code below based on request. The function below is called to obtain [URL] in completion handler. Then the completion handler is fed into:
func handleGetAllPhotoURLs(pin: Pin, urls: [URL], error: Error?)
class func getAllPhotoURLs(currentPin: Pin, fetchCount count: Int, completion: #escaping (Pin, [URL], Error?)->Void)-> URLSessionTask?{
let latitude = currentPin.latitude
let longitude = currentPin.longitude
let pageNumber = currentPin.pageNumber
let url = Endpoints.photosSearch(latitude, longitude, count, pageNumber).url
var array_photo_URLs = [URL]()
var array_photoID_secret = [[String: String]]()
var array_URLString = [String]()
var array_URLString2 = [String]()
var count = 0
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let dataObject = data, error == nil else {
DispatchQueue.main.async {
completion(currentPin, [], error)
}
return
}
do {
let temp = try JSONDecoder().decode(PhotosSearch.self, from: dataObject)
temp.photos.photo.forEach{
let tempDict = [$0.id : $0.secret]
array_photoID_secret.append(tempDict)
let photoURL = FlickrClient.Endpoints.getOnePicture($0.id, $0.secret)
let photoURLString = photoURL.toString
array_URLString.append(photoURLString)
getPhotoURL(photoID: $0.id, secret: $0.secret, completion: { (urlString, error) in
guard let urlString = urlString else {return}
array_URLString2.append(urlString)
array_photo_URLs.append(URL(string: urlString)!)
count = count + 1
if count == temp.photos.photo.count {
completion(currentPin, array_photo_URLs, nil)
}
})
}
completion(currentPin, [], nil)
return
} catch let conversionErr {
DispatchQueue.main.async {
completion(currentPin, [], conversionErr)
}
return
}
}
task.resume()
return task
}
In the do block, you are calling completion twice. Please see the correction,
do {
let temp = try JSONDecoder().decode(PhotosSearch.self, from: dataObject)
if temp.photos.photo.isEmpty == false {
temp.photos.photo.forEach{
let tempDict = [$0.id : $0.secret]
array_photoID_secret.append(tempDict)
let photoURL = FlickrClient.Endpoints.getOnePicture($0.id, $0.secret)
let photoURLString = photoURL.toString
array_URLString.append(photoURLString)
getPhotoURL(photoID: $0.id, secret: $0.secret, completion: { (urlString, error) in
guard let urlString = urlString else {return}
array_URLString2.append(urlString)
array_photo_URLs.append(URL(string: urlString)!)
count = count + 1
if count == temp.photos.photo.count {
completion(currentPin, array_photo_URLs, nil)
}
})
}
} else {
completion(currentPin, [], nil)
}
return
}
I've been trying to understand this process, I've done a lot of reading and it's just not clicking so I would be grateful if anyone can break this down for me.
I have a method to retrieve JSON from a URL, parse it, and return the data via a completion handler. I could post code but it's all working and I (mostly) understand it.
In my completion handler I can print the data in the console so I know it's there and everything good so far.
The next bit is what's tripping me up. While I can use the data in the completion handler I can't access it from the view controller that contains the handler.
I want to be able to pass tableData.count to numberOfRows and get "Use of unresolved identifier 'tableData'"
I'd really appreciate it if anyone can lay out what I need to do next. Thanks!
Edit: adding code as requested
Here is my completion handler, defined in the ViewController class:
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
print("Here are the \(patients)")
}
in viewDidLoad:
let url = URL(string: "http://***.***.***.***/backend/returnA")
let returnA = URLRequest(url: url!)
retrieveJSON(with: returnA, completionHandler: completionHandler)
Defined in Networking.swift file:
func retrieveJSON(with request: URLRequest, completionHandler: #escaping ([Patient]) -> Void) {
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: request as URLRequest) {
// completion handler argument
(data, response, error) in
// completion handler
guard let data = data else {
print("Did not recieve data")
completionHandler([])
return
}
do {
let decoder = JSONDecoder()
let Patient = try decoder.decode(Array<Patient>.self, from: data)
// print(Patient)
completionHandler(Patient)
}
catch let err {
print("Err", err)
completionHandler([])
}
}
task.resume()
}
I also have a struct defined called Patient but I won't post that as it's very long and just a simple struct matching the JSON received.
First of all, when you use closure, you should consider strong reference cycle.
let completionHandler: ([Patient]) -> Void = { [weak self] patients in
guard let strongSelf = self else { return }
strongSelf.tableData = patients // update tableData that must be used with UITableViewDataSource functions.
strongSelf.tableView.reloadData() // notify tableView for updated data.
}
You are not populating the array(tableData) in the closure:
var completionHandler: ([Patient]) -> Void = {[weak self] (patients) in
print("Here are the \(patients)")
self?.tableData = patients
}
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
self.tableData = patients
self.tableView.reloadData()
//make sure your tableview datasource has tableData property used
}
I have an issue with my code and I think it could be related to the order in which code is called.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
private var tasks = [Task]()
override func willActivate() {
let taskUrl = "http://myjsonurl.com"
downloadJsonTask(url: taskUrl)
print(tasks.count) // EMPTY
super.willActivate()
}
func downloadJsonTask(url: String) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
do
{
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("somehting went wrong after downloading")
}
}.resume()
}
}
I'm defining the private var tasks and fill it with the downloadJsonTask function but after the function ran the print(tasks.count) gives 0.
When I call print(downloadedTasks.tasks.count) it gives 4.
I think that in sequence of time the tasks variable is empty when I print it and it is filled later on.
When you are trying to print number of tasks in willActivate(), function downloadJsonTask(url: String) hasn't been completed yet, so you have empty array because tasks haven't been set yet.
You should add completion handler to downloadJsonTask just like this:
(don't forget to pass completion as parameter of function)
func downloadJsonTask(url: String, completion: #escaping () -> Void) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
completion()
return
}
do {
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("something went wrong after downloading")
}
completion() // This is moment when code which you write inside closure get executed
}.resume()
}
In your willActivate() use this function like this:
downloadJsonTask(url: taskUrl) {
print(tasks.count)
}
So that means when you get your data, your code inside curly braces will get executed.
You’re correct in your assumption that tasks has not yet been assigned a value when it’s first printed.
The thing is network requests are performed asynchronously. It means that iOS does not wait until downloadJsonTask(url:) is finished but continues executing the code right away (i.e. it calls print(tasks.count) immediately after the network request started, without waiting for it to produce any results).
The piece of code inside brackets after URLSession.shared.dataTask(with:) is called a completion handler. This code gets executed once the network request is competed (hence the name). The tasks variable is assigned a value only when the request is finished. You can make sure it works by adding print(self.tasks.count) after self.tasks = downloadedTasks.tasks:
self.tasks = downloadedTasks.tasks
print(self.tasks)
print(downloadedTasks.tasks.count)
I'm having difficulty assigning a value after execution of a closure.
The problem is in //step4. It prints the contents and it captures no nil,
but at //step5 it prints Nil.
I tried to creating a class and assigning the values within the HTTP Request, but no change.
My code:
var x: String!override func viewDidLoad() {
super.viewDidLoad()
//1
let urlAsString = "http://date.jsontest.com/"
let url = NSURL(string: urlAsString)!let urlSession = NSURLSession.sharedSession()
//2
let jsonQuery = urlSession.dataTaskWithURL(url, completionHandler: {
data, response, error - > Void in
if error != nil {
println(error.localizedDescription)
}
var err: NSError ?
//3
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: & err) as!NSDictionary
if err != nil {
println("JSON Error \(err!.localizedDescription)")
}
//4
let jsonDate = jsonResult["date"] as!String
let jsonTime = jsonResult["time"] as!String
//Step 4
//let's Assume
x = jsonDate
println("Inside \(x)") // Prints Date :)
dispatch_async(dispatch_get_main_queue(), {
self.dateLabel.text = jsonDate
self.timeLabel.text = jsonTime
})
}) //end of JsonQuery
jsonQuery.resume()
//Step 5
println("Outside \(x)") // Prints nil
}
Your web request is an asynchronous request. That means it will finish, and the completion block will be called, at some point in the future. Some time long after you call jsonQuery.resume(). Some time long after your whole function returns.
Your variable doesn't go back to nil after the call - you check it long before it is ever set to nil. It is the job of your completion block to do everything that is necessary to process the data and store the results.
Your "step 5" will likely execute before steps 3 or 4. Calling resume does not block and wait for the response to compete so you execute step 5 before the completion block is called.
I have this same question as was asked and answered here: How to get data to return from NSURLSessionDataTask
The difference: How can I do this in Swift? I do not know Objective C at all so trying to interpret that answer is proving a bit futile for me..
So given the code below I have a FirstViewController which will call my HomeModel class to go and get the data using a NSURLSession call. Once the call is complete I want return the data to the FirstViewController so that I may go and set it in the view.
FirstViewController class looks like:
import UIKit
class FirstViewController: UIViewController
{
let homeModel = HomeModel()
override func viewDidLoad()
{
super.viewDidLoad()
// Go load the home page content
SetHomeContent()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
/*Call to go get home page content*/
func SetHomeContent()
{
homeModel.GetHomeData();
//TODO find a way to get result here... and set it to the textView
}
}
HomeModel class looks like:
import Foundation
class HomeModel
{
private let siteContent:String = "http://www.imoc.co.nz/MobileApp/HomeContent"
func GetHomeData()
{
var url : NSURL! = NSURL(string:siteContent)
var request: NSURLRequest = NSURLRequest(URL:url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
var error: AutoreleasingUnsafeMutablePointer<NSError?> = nil
let jsonResult: NSDictionary! = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: error) as? NSDictionary!
// At this stage I want the jsonResult to be returned to the FirstViewController
});
// do whatever you need with the task
task.resume()
}
I guess I want either a way to pass in a completion handler from the original all or something similar to C# 5 using async tasks to await the result..
Any help appreciated..
With the help and suggestion taken from Abizern I finally managed how to write up this block or closure if you want.
So what worked for me in the end:
The GetHomeData function I changed as follows:
private let siteContent:String = "http://www.imoc.co.nz/MobileApp/HomeContent"
// I changed the signiture from my original question
func GetHomeData(completionHandler: ((NSDictionary!) -> Void)?)
{
var url : NSURL! = NSURL(string:siteContent)
var request: NSURLRequest = NSURLRequest(URL:url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
var error: AutoreleasingUnsafeMutablePointer<NSError?> = nil
let jsonResult: NSDictionary! = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: error) as? NSDictionary!
// then on complete I call the completionHandler...
completionHandler?(jsonResult?);
});
task.resume()
}
Then I call the function like this:
/*Call to go get home page content*/
func SetHomeContent()
{
homeModel.GetHomeData(self.resultHandler)
}
func resultHandler(jsonResult:NSDictionary!)
{
let siteContent = jsonResult.objectForKey("SiteContent") as NSDictionary
let paraOne = siteContent.objectForKey("HomePageParagraphOne") as String
}
Change your GetHomeData() method so that it takes a block. When you call the method pass it the block that does what you want to with the data. In the completion block of the data task, call this passed in block.
You can use this framework for Swift coroutines - https://github.com/belozierov/SwiftCoroutine
DispatchQueue.main.startCoroutine {
let dataFuture = URLSession.shared.dataTaskFuture(for: url)
let data = try dataFuture.await().data
. . . parse data ...
}