CompletionHandler with Async call in Swift 3 - arrays

I would like to return an array (arr) from method (with async call). I implement the completionHandler in the method, but I can't use my method to get my array : Cast from '(#escaping ((Array<Any>) -> Void)) -> ()' to unrelated type '[[String : Any]]' always fails
How can I fix this ?
Here is my code :
func dataWithURL(completion: #escaping ((_ result:Array<Any>) -> Void)) {
let urlString = "https://api.unsplash.com/photos/?client_id=71ad401443f49f22556bb6a31c09d62429323491356d2e829b23f8958fd108c4"
let url = URL(string: urlString)!
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
var arr = [[String:String]]()
let task = session.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// do stuff with response, data & error here
if let statusesArray = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [[String: Any]] {
for item in statusesArray! {
let photos = item["urls"] as? [String: Any]
let photo = photos?["small"] as? String
let myDictionary = [
"name": "test",
"imageURL": photo]
arr.append(myDictionary as! [String : String])
}
print(arr)
completion(arr)
}
})
task.resume()
}
And when I want to get my array :
lazy var photos: [Photo] = {
var photos = [Photo]()
// HERE THE ERROR APPEARS
guard let data = self.dataWithURL as? [[String: Any]] else { return photos }
for info in data {
let photo = Photo(info: info)
photos.append(photo)
}
return photos
}()

dataWithURL takes in a callback (completion handler), therefore you can only access the results in the callback.
self.dataWithURL { result in
//do stuff with the result
}
However the problem with the code above is that you are expecting dataWithURL to return the results which it doesn't. It returns void.
Another problem is that you are trying to use the results of dataWithURL for a property. The call to access the lazy var photos would yield no result (at least on first invocation) because the call dataWithURL is async (returns immediately).

You seem to be also xcode_Dev having asked this question yesterday.
I wrote a comment to that question:
You cannot return something from a function (or computed variable) which contains an asynchronous task
This is still true.
dataWithURL is an asynchronous function, it does not return anything but you have to pass a closure which is called on return.
First of all, the array is clearly [[String:String]] (array of dictionaries with string keys and string values) so it's pretty silly to use the much more unspecified type [Any]
func dataWithURL(completion: #escaping ([[String:String]]) -> Void) {
In Swift 3 specify only the type in the declaration without underscores and parameter labels.
You have to call the function this way:
dataWithURL { result in
for item in result { // the compiler knows the type
print(item["name"], item["imageURL"])
}
}
Once again: There is no return value of dataWithURL. The closure is called later.

Related

Using array of a struct outside or a function and do/catch loop [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I have been trying for hours to get the array results values outside of the function it's created in - having defined the var as global outside and such bit it always sets to nil outside the do braces
I also tried to create a return value function but I still cannot pass the variable back as I need to return outside of the do-braces
First question here - please be gentle - I'm a newbie and completing a college project!
func searchAPI(country: String, date: String) {
let headers = [
"x-rapidapi-host": "covid-19-data.p.rapidapi.com",
"x-rapidapi-key": "9d0431109emsh8f01caba0edb8d7p16cebdjsnc69ffe8f2569"
]
var request = URLRequest(url: URL(string: "https://covid-19-data.p.rapidapi.com/report/country/name?date-format=YYYY-MM-DD&format=json&date=\(date)&name=\(country)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, error) in
guard let data = data else {
return
}
do {
let httpResponse = response as? HTTPURLResponse
//print(httpResponse!)
let decoder = JSONDecoder()
let results = try decoder.decode([Covid].self, from: data)
print(results[0].country)
//print(results[0].provinces.count)
//print(results[0].provinces[0])
//print(results[0].provinces[0].active)
}
catch let error {
print("\(error)")
}
}
dataTask.resume()
return
}
func formatDate(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-dd"
let newDate = formatter.string(from: date)
return newDate
}
Here you need to use closure to get the results outside of the asynchronous call.
You need to modify the function definition and use the completion closure block to send the decoded data through it like this:
func searchAPI(country: String, date: String, completion: #escaping ([Covid]) -> Void) {
//...
let results = try decoder.decode([Covid].self, from: data)
completion(results)
}
Note: It would be better design to add the and Error property too since if the decoding or searchAPI fails to fetch data the error info can be used to show a message for the user. In this case you can modify the definition and completion handler like this:
func searchAPI(country: String, date: String, completion: #escaping ([Covid], Error?) -> Void) {
//...
if error != nil {
completion([], error)
}
//...
let results = try decoder.decode([Covid].self, from: data)
completion(results, nil)
//...
} catch {
completion([], error)
}
}
Better approach: A modern approach is to use the Result type and use the success and failure block to handle both scenarios as #Vadian has just posted.

How to take a function result and turn it into a key value pair array

I am fairly new to Swift but I have a function that returns a key value pair result of numbers
func dataPostRequest(_ url:String,_ paramString:String)
{
let url:NSURL = NSURL(string: url)!
let session = URLSession.shared
let request = NSMutableURLRequest(url: url as URL)
request.httpMethod = "POST"
request.httpBody = paramString.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request as URLRequest) {
(
data, response, error) in
guard let _:NSData = data as NSData?, let _:URLResponse = response, error == nil else {
print("error")
return
}
if let dataString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
{
print(dataString)
}
}
task.resume()
}
I am able to call the function like this:
dataPostRequest("https://myserver.com/","user_email=emailtest.com")
This works correctly but I want to now use the data that I've pulled and display it in a Table View. I have my tableView set up correctly but I am wondering how I can take my function and turn it into a key value pair array or a dictionary that I can use. I have tried creating an empty dictionary and set my function call to it but I get an error:
var emptyDictionary = [Int: Int]()
emptyDictionary = dataPostRequest("https://myserver.com/","user_email=emailtest.com")
And no matter what variation I try I keep getting:
Cannot assign value of type '()' to type '[Int : Int]'
Any guidance would be greatly appreciated.
dataPostRequest has no return value (aka ()). You can decode the received data in the completion handler and assign it to the dictionary. See the comment line below.
If you need to proceed in another function you have to add a completion handler described here.
Basically don't use NS... classes in Swift at all if there are native counterparts. And don't annotate types the compiler can infer.
The native Swift version is
func dataPostRequest(with url:String, paramString : String)
{
let url = URL(string: url)!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = paramString.data(using: .utf8)
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
print(error)
return
}
let dataString = String(data: data!, encoding: .utf8)!
print(dataString)
// here decode the data to the desired type and assign it to emptyDictionary
}
task.resume()
}

Swift 3 - Pass Dictionary inside Array from JSON to Another Method

I have the following JSON:
{
"stores":2,
"store_data":[
{
"store_id":1,
"store_name":"Target"
"store_color":"000000"
},
{
"store_id":2,
"store_name":"Walmart"
"store_color":"FFFFFF"
}
]
}
And I am collecting it (within a function) the following way (safeguards removed for simplicity):
let task = URLSession.shared.dataTask(with: baseURL) { (data, response, error) in
if let tmpRawData: NSDictionary = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary {
process(rawData: tmpRawData)
}
}
And sending it to the helper process function defined as:
func process(rawData: NSDictionary) -> Bool {
if let storeDataArray = rawData["store_data"] {
// Here I want to loop through the store_data array
}
}
And I am having some trouble looping through the array in the function above. Initially, I had tried:
for store: Dictionary<String, String> in storeDataArray {
// Do things with store["store_id"] here
}
But I am new to swift and am having trouble deciphering between NSArray, Array, Dictionary, NSDictionary, etc. I'm working in Swift 3. Any help is much appreciated!
First of all, don't annotate types that much. The compiler will tell you if it needs an explicit annotation
Second of all it's convenient to declare a type alias for a JSON dictionary
typealias JSONObject = [String:Any]
This is the task, tmpRawData is a dictionary – represented by {} in the JSON.
let task = URLSession.shared.dataTask(with: baseURL) { (data, response, error) in
if let tmpRawData = try JSONSerialization.jsonObject(with: data, options: []) as! JSONObject {
process(rawData: tmpRawData)
}
}
and the process function, the type alias makes everything more readable.
The value of rawData is an array of dictionaries – represented by [] in the JSON.
func process(rawData: JSONObject) -> Bool {
if let storeDataArray = rawData["store_data"] as? [JSONObject] {
for store in storeDataArray {
let storeID = store["store_id"] as! Int
let storeName = store["store_name"] as! String
let storeColor = store["store_color"] as! String
print(storeID, storeName, storeColor)
}
}
}
I have no idea why all tutorials suggests the mutableContainers option. You don't need it at all in Swift when using native collection types.

Swift Array: [AnyObject] vs. [xxxxxxxxClass] and the method "append"

Here's my code. You don't need to look at all of it. I added comments where I'm confused:
class ProductData: NSObject {
var title = ""
var icon = ""
private init(dict: NSDictionary){
title = dict["title"] as! String
icon = dict["icon"] as! String
super.init()
}
class func getTheData(fromJSONPath JSONPath: String) -> [ProductData] {
let JSONData = NSData(contentsOfFile: JSONPath)!
var JSONArray = [[String : AnyObject]]()
do {
JSONArray = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions.MutableContainers) as! [Dictionary]
} catch { print("error")}
-----------------------------------------------------------------------------------------
//↓↓↓↓↓↓↓↓↓ different: data = "[AnyObject]()" or "[ProductData]()" ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
var data = [AnyObject]()
// var data = [ProductData]()
for d in JSONArray {
data.append(ProductData(dict: d))
}
return data as! [ProductData]
// return data
//↑↑↑↑↑↑↑↑↑ and here: return "data as! [ProductData]" or "data" ↑↑↑↑↑↑↑↑↑↑↑↑↑↑
}
}
I use "var data = [ProductData](), retun data" first. There's no error or warning, but when I run my app, and run to the code data.append(ProductData(dict: d)), it crashes with the error: thread 1:exc_bad_access(code=1,address=0x10). What?!
I found a way to fix it: if I use var datas = [AnyObject]() and return datas as! [ProductData], it works very well.
I am so confused:
Why does [AnyObject] make the code OK?
When I use [ProductData], why does the code: data.append(ProductData(dict: d)) crash?
What is the different between [AnyObject] and [ProductData]?
Your original version works for me (screenshot) (only slightly modified for testing with my data). You shouldn't have to do this dance, something else is causing trouble.
I suggest cleaning up your class a bit and take advantage of Swift 2 using guard, map and error. It will be easier to debug and will work more efficiently anyway.
Here's an example. The only difference is that I'm using NSURL to access the data in my case and I've removed the icon value, but it's easy to change it back to your case.
class ProductData: NSObject {
var title = ""
private init(dict: [String : AnyObject]){
if let t = dict["title"] as? String { self.title = t }
super.init()
}
class func getTheData(fromJSONPath JSONPath: String) -> [ProductData] {
do {
// safely unwrap and typecast the values else return empty array
guard let url = NSURL(string: JSONPath),
let JSONData = NSData(contentsOfURL: url),
let JSONArray = try NSJSONSerialization.JSONObjectWithData(JSONData, options: [])
as? [[String : AnyObject]] else { return [] }
return JSONArray.map() { ProductData(dict: $0) }
} catch {
// this `error` variable is created by the `catch` mechanism
print(error)
// return empty array if unkown failure
return []
}
}
}
let test = ProductData.getTheData(fromJSONPath: "http://localhost:5678/file/test.json")
Note: I'm sure you know it but just in case for the readers, NSData(contentsOf... is a synchronous function, so it will block the main thread (unless executed from a background thread). It's better practice to use asynchronous functions when possible.

Swift: Looping through a Dictionary Array

I'm struggling to loop through an array of dictionary values returned from a web service call.
I've implemented the following code and I seem to be encountering a crash on running.
I'd also like to store the results into a custom Struct. Really having difficulty achieving this and the answers on here so far haven't worked. Would be grateful if someone is able to help.
let nudgesURLString = "http://www.whatthefoot.co.uk/NUDGE/nudges.php"
let nudgesURL = NSURL(string: nudgesURLString)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(nudgesURL!, completionHandler: {data, response, error -> Void in
if error != nil {
println(error)
} else {
let nudgesJSONResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
let nudges: NSDictionary = nudgesJSONResult["nudges"] as NSDictionary
if let list = nudgesJSONResult["nudges"] as? [[String:String]] {
for nudgeDict in list {
let location = nudgeDict["location"]
println(location)
}
}
}
})
task.resume()
}
NOTICE
This answer was written using Swift 1.2 and as such, there may be some slight stylistic and syntax changes required for the answer to work depending on your current Swift system.
Answer -- Swift 1.2
This line is crashing your code:
let nudges: NSDictionary = nudgesJSONResult["nudges"] as NSDictionary
You're forcing a cast that Swift can't handle. You never make it to your for-loop.
Try changing your code to look more like this:
let nudgesURLString = "http://www.whatthefoot.co.uk/NUDGE/nudges.php"
let nudgesURL = NSURL(string: nudgesURLString)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(nudgesURL!, completionHandler: {data, response, error -> Void in
if error != nil {
println(error)
} else {
let nudgesJSONResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as [String : AnyObject]
if let nudges = nudgesJSONResult["nudges"] as? [[String : String]] {
for nudge in nudges {
let location = nudge["location"]
println("Got location: \(location)")
println("Got full nudge: \(nudge)")
}
}
}
})
task.resume()
Thanks,
I created the following Struct which stored the data, and also lets me create dictionaries in the view controller for a particular index.
struct NudgesLibrary {
var location: NSArray?
var message: NSArray?
var priority: NSArray?
var date: NSArray?
var nudges: NSArray?
init(nudgesObject: AnyObject) {
nudges = (nudgesObject["nudges"] as NSArray)
if let nudges = nudgesObject["nudges"] as? NSArray {
location = (nudges.valueForKey("location") as NSArray)
message = (nudges.valueForKey("message") as NSArray)
priority = (nudges.valueForKey("priority") as NSArray)
date = (nudges.valueForKey("date") as NSArray)
}
}
}

Resources