Getting data out of a completion handler and into a tableView - swift

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
}

Related

Asynchronous thread in Swift - How to handle?

I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)

swift - order of functions - which code runs when?

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)

Can't access data outside of closure in swift

I am trying to extract an array from closure in swift 3 and its not working for me. I have my JSON parser in the class WeatherGetter and I am calling it in the view did load file in the viewcontroller.swift how to assign the weather_data array to some outside variable?
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "Insert API address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
//print(weather)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
//temps.updateValue(tempF, forKey: "\(date)//\(j)")
temps.append(tempF)
winds.append(windspeed)
}
}
//print(temps)
//print(winds)
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
}
//Do your stuff with isResponse variable.
}
You can assign it to a class property like this:
var weatherData: [[Double]]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
self.weatherData = weather_data
// reload or display data
}
}
You need to remember that the network request takes some time, so this is why you would call something like reloadData once you know you have received the response.
Say for example, the network response takes 100 milliseconds to respond. By the time the data has responded, all of the code in viewDidLoad will very likely be completely finished. So you need to respond to the data being received, when you receive it. If you have a bad mobile signal, it may take longer.
This is why you use callbacks/closures. They are called when the operation completes
UPDATE:
The code inside getWeather shows multiple errors for me and won't let me run it as is.
I managed to get a response from the weather API by modifying the code slightly and commenting alot out. Your main issue here is that you are not casting your JSON data to specific types.
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
guard error == nil, let data = data else {
print("ERROR")
return
}
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
if let weather = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:AnyObject] {
if let conditions1 = weather["data"] as? [String:AnyObject] {
print(conditions1)
}
}
} catch let jsonError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError)")
}
}
dataTask.resume()
See in the code above how I am optionally unwrapping the values whilst casting their types. This is what you need to do throughout your code and check you get the right data at each step along the way. Unfortunately the API response is too large for me to do it here.
Unwrapping JSON Swift
Swift Closures
iOS Networking with Swift - This is a free course which I highly recommend. This is how I learnt iOS networking.
As mentioned by #Scriptable, it takes a while for the response to be processed since it's asynchronous. What you can do is to add the OperationQueue.main.addOperation to assign the current process to the main queue. This will prioritize the processing of your network response. You can also put your reloadData in this part.
var weatherData: [Double]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
OperationQueue.main.addOperation {
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
// reloadData()
}
}
}

Return values from completion handler

I want to return the values from an api call.
The call to my api class (I want to get the values in res):
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let t = Api_test();
let res = t.getSomething();
print(res)
}
}
The api class:
import Foundation
class Api_test {
func getAllStations(completionHandler: (response : XMLIndexer) -> ()) {
getRequest { result in
completionHandler(response: SWXMLHash.parse(result))
};
}
func getRequest(completionHandler: (result: NSData) -> ()) {
let baseUrl = "http://api.test.com"
let request = NSMutableURLRequest(URL: NSURL(string: baseUrl)!)
let session = NSURLSession.sharedSession()
request.HTTPMethod = "GET"
let task = session.dataTaskWithRequest(request) {
(data, response, error) in
if data == nil {
print("dataTaskWithRequest error: \(error)")
return
} else {
completionHandler(result: data!)
}
}
task.resume()
}
}
Everything works as thought, but I'm stuck at the point to return the values back to the getSomething function. The data is in xml format. How can I get the result set as returned values in the res (viewDidLoad)?
NSURLSession is a fully asynchronous networking API so ideally your view controller should operate correctly and not wait for the data to be returned from the network.
You have three options here:
You can pass a completion block to getSomething and have it pass the result to the block:
func getSomething(completionHandler: (result: XMLIndexer) -> ()) {
getRequest { result in
completionHandler(result: SWXMLHash.parse(result))
}
}
override func viewDidLoad() {
...
t.getSomething { res in
print(res)
}
}
If you desperately need the XML data in hand before view is displayed onto screen, you can make the main thread wait till network operation finishes executing. You can use dispatch_semaphore_t:
func getSomething() -> XMLIndexer? {
var xml: XMLIndexer? = nil
let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
getRequest { result in
xml = SWXMLHash.parse(result)
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
return xml
}
Last option is, you can use another 3rd party that does the parsing synchronously. There is a great one named Ono:
var error: NSError?
let xml = ONOXMLDocument(data: result, error: &error)

How to get data to return from NSURLSessionDataTask in Swift

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