Error: Cannot call value of non-function type '[JSONObject]' - json

I'm trying to update an object with JSON data. That data is returned in a completion block after it has been parsed. When I try and return it I get the error:
Cannot call value of non-function type '[JSONObject]'
Here is my function:
static func updateResultsDictionary(urlExtension: String, completion: [JSONObject]){
var jsonDataArray = [JSONObject]()
let nm = NetworkManager.sharedManager
_ = nm.getJSONData(urlExtension: urlExtension, completion: {data in
if let jsonDictionary = nm.parseJSONFromData(data) {
let resultDictionaries = jsonDictionary["result"] as! [[String : Any]]
for resultsDictionary in resultDictionaries {// enumerate through dictionary
let jsonInfo = JSONObject(resultsDictionary: resultsDictionary)
jsonDataArray.append(jsonInfo)
}
}
})
completion(jsonDataArray) //The error happens here
}
EDIT: After changing
completion: [JSONObject]
to
completion: #escaping ([JSONObject]) -> Void
it no longer throws that previous error but now when I use the completion block it throws a new error:
Cannot assign value of type '()' to type '[JSONObject]'
override func viewDidLoad() {
super.viewDidLoad()
self.jsonObjectsArray = JSONObject.updateResultsDictionary(urlExtension: "/cities/basel-switzerland", completion: {JSONObject in
self.practiceTableView.reloadData()
})
}

You've declared the completion parameter to be an array of JSONObject. It seems you mean to declare it as a closure that passes one parameter whose type would be an array of JSONObject.
static func updateResultsDictionary(urlExtension: String, completion: #escaping (_: [JSONObject]) -> Void) {
You also need to change how you call this method:
override func viewDidLoad() {
super.viewDidLoad()
JSONObject.updateResultsDictionary(urlExtension: "/cities/basel-switzerland", completion: { (results) in
self.jsonObjectsArray = results
self.practiceTableView.reloadData()
})
}
And your call to:
completion(jsonDataArray)
needs to be moved to where jsonDataArray is actually being set. It needs to be called at the end of the completion block to the call to getJSONData.

Related

How to return JSON data from Swift URLSession [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
I'm trying to return json data from an api call. I'm able to access the json data successfully but am struggling to find a way / the best way to return it for access in my app. Thanks for any ideas!
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// make the api call and obtain data
let data = self.loadData()
print("inside viewDidLoad", data) // prints 'inside viewDidLoad emptyString'
}
func loadData() -> String {
var circData = "emptyString"
let session = URLSession.shared
let url = URL(string: "https://us-east-1.aws.webhooks.mongodb-realm.com/api/client/v2.0/app/cirdata-khyvx/service/cirData/incoming_webhook/cirlAPI")!
let task = session.dataTask(with: url, completionHandler: { data, response, error in
if let json = try? JSONSerialization.jsonObject(with: data!, options: []) {
// print("json: ", json) // prints the whole json file, verifying the connection works. Some 300kb of data.
// print("json file type: ", type(of: json)) // prints '__NSArrayI'
let jsonString = "\(json)"
circData = jsonString
// print("circData", circData) // prints the whole json file, verifying that the json string has been assigned to 'circData'
}
})
task.resume()
// print("after: ", circData) // prints 'after: emptyString'. It's as if the reassignment didn't take place.
return circData
}
}
You can't return a value synchronously becuase the api call that is fetching json data is asynchronous. You need to use a completion handler instead.
You can put breakpoints in different places inside the code to understand how the flow executes.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.loadData(completion: { [weak self] (result, error) in
if let error = error {
print(error.localizedDescription)
}
if let result = result {
print(result)
}
})
}
func loadData(completion: #escaping (_ data: Any?, _ error: Error?) -> Void) {
let url = URL(string: "https://us-east-1.aws.webhooks.mongodb-realm.com/api/client/v2.0/app/cirdata-khyvx/service/cirData/incoming_webhook/cirlAPI")!
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
completion(nil, error)
return
}
do {
if let data = data {
let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments])
completion(json, nil)
} else {
completion(nil, nil)
}
} catch {
completion(nil, error)
}
})
task.resume()
}
}

Error at generic helper function for reading JSON from an URL

In one of my projects, I want to read JSON from several URLs in several Views into several structs so I decided to write a small, but generic helper function.
This function should be called e.g.
View 1:
let call1 = Bundle.main.decode(iobrokerSection.self, from: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")
View 2:
let call2 = Bundle.main.decodeURL(iobrokerweather.self, from: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Weather")
and so on.
For the first example the struct iobrokerSection is
struct iobrokerNumberDataPoint: Codable {
var val: Int
var ack: Bool
var ts: Int
var q: Int
var from: String
var user: String
var lc: Int
var _id: String
var type: String
}
And here is my helper function
extension Bundle {
func decodeURL<T: Decodable>(_ type: T.Type, from urlString: String) -> T {
guard let url = URL(string: urlString) else {
fatalError("Placeholder for a good error message")
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let loaded = try? JSONDecoder().decode(T.self, from: data!) else {
fatalError("Placeholder for a good error message")
}
}.resume()
return loaded
}
}
I think that I understand why I'm getting the compiler message "Cannot convert return expression of type Bool to return Type T" at "return loaded".
But I don't have any idea how to fix this.
May anyone give me a hint?
First it is Swift naming convention to name all your structures, classes and protocols starting with an uppercase letter. Second you can't wait for an asynchronous method to finish to return a value. You need to add a completion handler to your method. Third you don't need to use a URLRequest if your intent is only to get some data. You can just use an URL and pass a URL to your method instead of a string. Fourth don't force unwrap the returned data it might be nil. You need to safely unwrap your optional data and in case of error pass it to the completion handler. Your decode method should look like something like this:
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from url: URL, completion: #escaping (T?, Error?) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
completion(nil, error)
return
}
do {
try completion(JSONDecoder().decode(T.self, from: data), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
When calling this method you need to get the asynchronous result inside the resulting closure:
struct IOBrokerNumberDataPoint: Codable {
var val: Int
var ack: Bool
var ts: Int
var q: Int
var from: String
var user: String
var lc: Int
var id: String
var type: String
enum CodingKeys: String, CodingKey {
case val, ack, ts, q, from, user, lc, id = "_id", type
}
}
let url = URL(string: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")!
Bundle.main.decode(IOBrokerNumberDataPoint.self, from: url) { brokerNumberDataPoint, error in
guard let brokerNumberDataPoint = brokerNumberDataPoint else {
print("error", error ?? "")
return
}
print("brokerNumberDataPoint", brokerNumberDataPoint)
// use brokerNumberDataPoint here
}
Another option is to use Swift 5 Result generic enumeration.
extension Bundle {
func decode<T: Decodable>(from url: URL, completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
if let error = error { completion(.failure(error)) }
return
}
do {
try completion(.success(JSONDecoder().decode(T.self, from: data)))
} catch {
completion(.failure(error))
}
}.resume()
}
}
Usage:
let url = URL(string: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")!
Bundle.main.decode(from: url) { (result: Result<IOBrokerNumberDataPoint, Error>) in
switch result {
case let .success(brokerNumberDataPoint):
print("brokerNumberDataPoint", brokerNumberDataPoint)
// use brokerNumberDataPoint here
case let .failure(error):
print("error:", error)
}
}

swift pass json decode type to http request

I want to make a static func that could return a stuct defined like this:
struct Category: Codable {
public let data: Array<CateItem>
public let status: Int
public let msg: String
}
And I have write a static func like this:
static func Get(codePoint: String, responseType: Codable){
let urlString = UrlUtils.GetUrl(codePoint: codePoint)
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!){
(data,response,error) in
if error != nil{
print(error!)
}else{
if let data = data{
JSONDecoder().decode(responseType, from: data)
}
}
}
task.resume()
}
and invoke the method like this:
HttpRequests.Get(codePoint: "getCategoryList", responseType: Category)
but here responseType will not work.
How to fix this?
You want to pass type of struct, not protocol.
First, make generic constraint for your method which says that T has to conform to Decodable (since you need it just for decoding, you don’t need conforming to Encodable)
Then say that parameter should be of type T.Type - this allows compiler to infer type of T, you can avoid using this parameter, see at the end of the answer
static func Get<T: Decodable>(codePoint: String, responseType: T.Type) { ... }
... so T will be type which you'll pass to method.
Then for JSONDecoder's decode method use type of T
JSONDecoder().decode(T.self, from: data)
and then when you want to call your method, pass type of your struct like you did it within decoding
HttpRequests.Get(codePoint: "getCategoryList", responseType: Category.self)
Also note that your call is async so for returning data you'll need completion handler defined as parameter of your method
completion: #escaping (T?) -> Void
note that names of methods should start with small capital letters
static func get<T: Decodable>(codePoint: String, responseType: T.Type, completion: #escaping (T?) -> Void) {
let urlString = UrlUtils.GetUrl(codePoint: codePoint)
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) { data, response, error in
guard let data = data else {
print(error!)
return completion(nil)
}
do {
let decoded = try JSONDecoder().decode(T.self, from: data)
completion(decoded)
} catch {
print(error)
completion(nil)
}
}.resume()
}
HttpRequests.get(codePoint: "getCategoryList", responseType: Category.self) { response in
if let category = response {
...
}
}
You can also avoid using responseType parameter since type of T can be inferred from the type of parameter of completion closure
static func get<T: Codable>(codePoint: String, completion: #escaping (T?) -> Void) { ... }
HttpRequests.get(codePoint: "getCategoryList") { (category: Category?) -> Void in ... }

-- swift: async + JSON + completion + DispatchGroup

The code in my viewcontroller-class is executed before the JSON-download-process is ready even though there is a completion handler in the func for downloading JSON an a DispatchGroup(). I store the JSON-data in an array called "fetchedModules" and this is filled with 11 items in this case. Why does this happen?
result in console:
---> in Class PostCell - func numberOfSections: 0
JSON call finished
// ViewController
override func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup()
group.enter()
self.fetchJSON()
// here calling downloadJSONasync
group.leave()
group.notify(queue: .main) {
print("JSON call finished")
}
...
// networkService with completion
func downloadJSONasync(searchItem: String, completion: #escaping ([NSDictionary]) -> Void) {
//request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
request.httpMethod = "GET"
let configuration = URLSessionConfiguration.default
//let session = URLSession(configuration: configuration, delegate: nil)
let session = URLSession(configuration: configuration)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
if (error != nil) {
print("error!")
}
else{
do {
let fetchedData = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! [NSDictionary]
completion(fetchedData)
}
catch {
print("error")
}
}
})
task.resume()
}
// call in viewController
override func numberOfSections(in tableView: UITableView) -> Int {
print("---> in Class PostCell - func numberOfSections: \(String(describing: fetchedModules.count))")
return fetchedModules.count
// code of fetchJSON
func fetchJSON()
{
let baseurl = AppConstants.Domains.baseurl // https://m.myapp2go.de
let compositURL = baseurl + "getmodules_noItems.php?id=\(AppConstants.appString.startString)"
let encodedUrl : String! = compositURL.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) // remove the spaces in the url string for safty reason
let JSONurl = URL(string: encodedUrl)! // convert the string into url
var JSONrequest = URLRequest(url: JSONurl) // make request
JSONrequest.httpMethod = "GET"
//JSONrequest.cachePolicy = .reloadIgnoringCacheData
let networkService = NetworkService(request: JSONrequest)
networkService.downloadJSONasync(searchItem: AppConstants.appString.startString, completion: { (fetchedData) in
fetchedModules.removeAll()
DispatchQueue.main.async {
for eachFetchedModul in fetchedData {
let eachModul = eachFetchedModul
if
let custid = eachModul["custid"] as? String,
let modulcat = eachModul["modulcat"] as? String,
let modulname = eachModul["modulname"] as? String,
let comment = eachModul["comment"] as? String
{
fetchedModules.append(CModules(custid: custid, modulcat: modulcat, modulname: modulname, comment: comment))
print(custid)
print(modulcat)
print(modulname)
print(comment)
print("---------------------")
}
}// for end
// ... somehow set data source array for your table view
self.tableView.reloadData()
}// dispatch
}
)} // func end
Because fetchJSON returns immediately, before the JSON is downloaded. The effect is that the DispatchGroup is entereed and left right away, without waiting for the JSON:
group.enter()
self.fetchJSON() // returns immediately
group.leave() // the JSON has yet to be downloaded
To wait until the JSON has arrived, add a completion handler to fetchJSON:
override func viewDidLoad() {
group.enter()
self.fetchJSON {
group.notify(queue: .main) {
print("JSON call finished")
}
group.leave()
}
}
// Change the data type of the completion handler accordingly
func fetchJSON(completionHandler: #escaping (Data?) -> Void) {
// ...
networkService.downloadJSONasync(searchItem: AppConstants.appString.startString) { fetchedData in
defer { completionHandler(fetchedData) }
// ...
}
)
Using defer ensures that the completion handler will always be called, no matter how the outer closure returns. I'm not clear why you use a DispatchGroup here as there is no waiting, but I kept it in place to answer your question.
Your table view doesn't have any data from the beginning because any data hasn't been fetched yet. So it's ok, that table view has no cells. You just need to reloadData of your table view since now you appended elements to table view's data source array and now you should show this data.
Please, don't use DispatchGroup for this, just use your completion parameter of your method and inside of the completion closure after data are received set data source array for table view and then ... reload data of table view
downloadJSONasync(searchItem: "someString") { dictionary in
DispatchQueue.main.async { // don't forget that your code doesn't run on the main thread
// ... somehow set data source array for your table view
self.tableView.reloadData()
}
}
Note that you should avoid using NSDictonary and you should rather use Dictionary. Also from Swift 4+ you can use Codable instead of JSONSerialization.

Swift - Why fetching data from JSON have a blank TableView delay before data get displayed?

Edit: Please ignore the old answers. I can't create a new question my accunt was blocked. So I edited this one with a new question in the title.
How can i have my data result as soon as i viewDidLoad my tableView ?
Here is a snippet of my code. Can anyone help me to understand the asynchronous part that make my code fetch data and bring me back the result late.
`
let apiUrl = "https://shopicruit.myshopify.com/admin/products.json?
page=1&access_token=c32313df0d0ef512ca64d5b336a0d7c6"
var myRowProduct : String = ""
var productArrayName : [String] = []
var productsPassedOver : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
getFullProductsJson(url: apiUrl, tag1: myRowProduct){array, err in
if err != nil {
print("ERROR 2")
}else{
self.productsPassedOver = array
self.tableView.reloadData()
print("productsPassedOver \(self.productsPassedOver)")
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return productsPassedOver.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
cell.selectionStyle = .none
cell.textLabel?.text = "\(indexPath.row + 1). " + productsPassedOver[indexPath.row] + "\n" //label are in every single cell. The current text that is displayed by the label
cell.textLabel?.numberOfLines = 0; //removes any limitations on the number of lines displayed
return cell
}
//Networking JSON
func getFullProductsJson(url: String, tag1: String, completion: #escaping ([String], Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
//response.result.value comes back as an optional so use ! to use it. Also value have datatype of Any? thats why we JSON casting
if response.result.isSuccess{
let fullJSON : JSON = JSON(response.result.value!)
let finalArrayOfProductS = self.updateTagProductData(json: fullJSON, tag2: tag1)
completion(finalArrayOfProductS, nil)
}
else{
print("Error JSON")
}
}
}
//JSON parsing, deal with formatting here
func updateTagProductData(json : JSON, tag2: String) -> [String]{
let tagClicked = tag2
var nbOfProducts : Int = 0
nbOfProducts = json["products"].count
var inventoryAvailableTotal : Int = 0
var nbOfVariants : Int = 0
for i in 0..<nbOfProducts {
let tagsGroup : String = json["products"][i]["tags"].stringValue
let productName : String = json["products"][i]["product_type"].stringValue
nbOfVariants = json["products"][i]["variants"].count
if tagsGroup.contains(tagClicked){
for j in 0..<nbOfVariants{
inventoryAvailableTotal += json["products"][i]["variants"][j]["inventory_quantity"].int!
}
print("\(tagClicked) tag exist in the product \(i): \(productName)")
print("inventorytotal \(inventoryAvailableTotal)")
productArrayName.append("\(productName): \(inventoryAvailableTotal) available")
}
}
return productArrayName
}
}
`
This is a function that does something in the background and return immediately, and when it finishes, it calls your completion handler.
So it is natural for this function to go directly to the return finaleNbOfRowsInProductsListPage.
Your solution is that the function shouldn't return the value, but it should
accept a completion handler, and call it when ended. This is the continuation passing style.
func getFullProductsJson(url: String, tag1: String, completion: #escaping (Int?, Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
...
// When you are done
completion(finaleNbOfRowsInProductsListPage, nil)
}
}
Also please note to try avoiding setting a lot of variables, try to make everything a parameter or a return value from a function, this is easier to debug. For example, try to make the list as a parameter that is passed to the completion handler, not as a member variable of your view controller, make this member variable only for the list that is ready to be displayed.
func getFullProductsJson(url: String, tag1: String, completion: #escaping (Products?, Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
...
// When you are done
completion(products, nil)
}
}
var products: [Product] = []
func refreshData() {
getFullProductsJson(url: "YOUR_URL_HERE", tag1: "TAG") {
// Try to use [weak self], also, read about Automatic Reference Counting
productsNullable, error in
if let products = productsNullable {
// Alamofire calls your callback on a background thread
// So you must return to the main thread first when you want
// to pass the result to any UI component.
DispatchQueue.main.async {
self?.products = products
self?.tableView.reloadData()
}
}
}
}