Cannot append data to array from GET request - ios

I am trying to load data from a GET request using Alamofire library in swift and cannot append data from the requests. I am trying to populate an array of orders to load into a UITableView.
I have tried a few various ways of solving this issue but nothing is working for me. I have commented out the method I tried because with 2 separate calls to fetchAll...Orders and the second call always overwrites the first and then the tableView is loaded with missing items.
class DrinkOrdersTableViewController: UITableViewController {
var orders: [Order] = []
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Current Orders"
}
override func viewWillAppear(_ animated: Bool) {
// fetchAllBeerOrders { orders in
// self.orders = orders!
// //print("Beer fetch: ", self.orders)
// self.tableView.reloadData()
// }
// fetchAllCocktailOrders { orders in
// self.orders = orders!
// //print("Cocktail fetch: ", self.orders)
// self.tableView.reloadData()
// }
fetchAllBeerOrders { orders in
self.orders.append(orders)
self.tableView.reloadData()
}
fetchAllCocktailOrders { orders in
self.orders.append(orders)
self.tableView.reloadData()
}
}
private func fetchAllCocktailOrders(completion: #escaping([Order]?) -> Void) {
Alamofire.request("http://127.0.0.1:4000/orders", method: .get)
.validate()
.responseJSON { response in
guard response.result.isSuccess else { return completion(nil) }
guard let rawInventory = response.result.value as? [[String: Any]?] else { return completion(nil) }
let currentOrders = rawInventory.compactMap { ordersDict -> Order? in
guard let orderId = ordersDict!["id"] as? String,
let orderStatus = ordersDict!["status"] as? String,
var pizza = ordersDict!["cocktail"] as? [String: Any] else { return nil }
pizza["image"] = UIImage(named: pizza["image"] as! String)
return Order(
id: orderId,
pizza: Pizza(data: pizza),
status: OrderStatus(rawValue: orderStatus)!
)
}
completion(currentOrders)
}
}
private func fetchAllBeerOrders(completion: #escaping([Order]?) -> Void) {
Alamofire.request("http://127.0.0.1:4000/orders", method: .get)
.validate()
.responseJSON { response in
guard response.result.isSuccess else { return completion(nil) }
guard let rawInventory = response.result.value as? [[String: Any]?] else { return completion(nil) }
let currentOrders = rawInventory.compactMap { ordersDict -> Order? in
guard let orderId = ordersDict!["id"] as? String,
let orderStatus = ordersDict!["status"] as? String,
var pizza = ordersDict!["pizza"] as? [String: Any] else { return nil }
pizza["image"] = UIImage(named: pizza["image"] as! String)
return Order(
id: orderId,
pizza: Pizza(data: pizza),
status: OrderStatus(rawValue: orderStatus)!
)
}
completion(currentOrders)
}
}
As of right now I am getting this error with code above: Cannot convert value of type '[Order]?' to expected argument type 'Order'. The ideal outcome of this code is to have the data that is gathered from each GET request to append to the array of Orders. I have verified that the GET requests are working and giving back the correct data. Please Help :]

You declared orders of type [Order] and your fetch methods compilation blocks return [Order]?. As you can see, you cannot convert value of type [Order]? to expected argument type Order when you wrote self.orders.append(orders).
To fix these, put a guard unwrap in fetch method invocations.
fetchAllBeerOrders { orders in
guard let _orders = orders else { return }
self.orders.append(_orders)
self.tableView.reloadData()
}
fetchAllCocktailOrders { orders in
guard let _orders = orders else { return }
self.orders.append(_orders)
self.tableView.reloadData()
}
Now, you have a potential memory leak in your code. fetchAllBeerOrders and fetchAllCocktailOrders are async methods with compilation blocks. You cannot use a strong reference to self here. Use weak to avoid a memory leak, like:
fetchAllBeerOrders { [weak self] orders in
guard let _orders = orders else { return }
self?.orders.append(_orders)
self?.tableView.reloadData()
}
fetchAllCocktailOrders { [weak self] orders in
guard let _orders = orders else { return }
self?.orders.append(_orders)
self?.tableView.reloadData()
}

Related

Swift: For loop to synchronous one by one until all function response

I'm new to swift and practicing my best.
I have main function appointmentCall() when it executes and in response I may get multiple appointments. Then I pass appointmentId to appointmentDetail function for more details.
All I want to is how can I set For loop to synchronous process. Means it will not execute next appointment until first is finished. At the moment it executes all appointments.
I need appointment one by one executes all function once finished executes next appointment.
AppointmentCall
-> AppoinmentDetail -> processDetail -> Completed.
Code:
func appointmentCall(_ selectedDate:Date) {
DataProvider.main.serviceGetAppointment(date: selectedDate, callback: {success, result in
do{
if(success){
print(result as! Data)
let decoder = JSONDecoder()
let response = try decoder.decode(ResponseData.self, from: result! as! Data)
if let appointments = response.appointments {
self.appData = appointments.map { AppointmentDownloadModel(appointmentModel: $0)}
}
for eachApp in self.appData {
self.appointmentDetail(AppId: appId)
}
self.tableView.reloadData()
return true
}else{
return false
}
}catch let error {
DataProvider.main.token = nil
print(error as Any)
return false
}
})
}
func appointmentDetail(AppId: Int){
DataProvider.main.serviceGetAppointmentDetail(Id: AppId , callback: {success, result in
do{
if(success){
let decoder = JSONDecoder()
let resp = try decoder.decode(AppointmentDetail.self, from: result! as! Data)
self.AppDetailData = resp
self.processDetail(appId: AppId)
return true
}else{
return false
}
}catch let error {
print(error as Any)
return false
}
})
}
func processDetail(appId: Int) {
guard let detail = AppDetailData, AppDetailData?.appointmentId == appId else {
return
}
for firmParam in (detail.sectionList ?? []) {
for firmItem in firmParam.items! {
if firmItem.actionParamData != nil {
let str = firmItem.actionParamData
let param = str?.components(separatedBy: ":")
let final = param![1].replacingOccurrences(of: "}", with: "")
let fmId = final.components(separatedBy: ",")
let frmId = fmId[0]
self.firmDetails(actionParamData: Int(frmId) ?? 0)
}
//pdf download
if firmItem.actionType == 2 {
if firmItem.actionUrl != nil {
self.contentLength(link: firmItem.actionUrl!)
let fileURL = URL(fileURLWithPath: firmItem.actionUrl ?? "")
let fileTitle = firmItem.textField ?? ""
self.downloadPDFTask(pdfURL: firmItem.actionUrl ?? "")
}
}
}
}
}
If you use a recursive function with completion handler it would ensure you could essentially loop over an array and call a function to do something and know that it has completed before you do the same to your next value. See below to high level generic example
func doSomething()
{
// Call this function with the first index in the array
firstFunction(index: 0)
// Now were here and we know that every value in the member variable array has been processed by "secondFunction"
}
func firstFunction(index: Int)
{
let value = memberVariableArray[index]
secondFunction(String: value) { (returnValueFromSecondFunction) in
// do something with the return value from the second function
// Recall this function with the next index (if we have another one)
let newIndex = index + 1
if newIndex < memberVariableArray.count
{
self.firstFunction(index: newIndex)
}
}
}
func secondFunction(valueToDoSomethingWith: String, completion: #escaping (String) -> Void)
{
// Do something here
// Complete function
completion("The Value you want back")
}

How to get an array from URLSession

Trying to make a program for a news site. I take information from the site through the api, everything works fine.
The only question is, how do I get this array out of the loop?
Here is my code:
import UIKit
class ViewController: UIViewController {
var news:[News] = []
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
func getUsers() {
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
news = try JSONDecoder().decode([News].self, from: data)
// print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
}
struct News:Codable, CustomStringConvertible{
let href:String?
let site:String?
let title:String?
let time:String?
var description: String {
return "(href:- \(href), site:- \(site), title:- \(title), time:- \(time))"
}
}
Declare news array in your class and assign the response to this array in getUsers method
var news:[News] = []
func getUsers(){
guard let url = URL(string: "https") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
self.news = try JSONDecoder().decode([News].self, from: data)
print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
The fundamental problem is you are retrieving data asynchronously (e.g. getUsers will initiate a relatively slow request from the network using URLSession, but returns immediately). Thus this won’t work:
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
You are returning from getUsers before the news has been retrieved. So news will still be [].
The solution is to give getUsers a “completion handler”, a parameter where you can specify what code should be performed when the asynchronous request is done:
enum NewsError: Error {
case invalidURL
case invalidResponse(URLResponse?)
}
func getUsers(completion: #escaping (Result<[News], Error>) -> Void) {
let queue = DispatchQueue.main
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {
queue.async { completion(.failure(NewsError.invalidURL)) }
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
queue.async { completion(.failure(error)) }
return
}
guard
let data = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
queue.async { completion(.failure(NewsError.invalidResponse(response))) }
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let news = try decoder.decode([News].self, from: data)
queue.async { completion(.success(news)) }
} catch let parseError {
queue.async { completion(.failure(parseError)) }
}
}.resume()
}
Then your view controller can fetch the news, passing a “closure”, i.e. code that says what to do when the asynchronous call is complete. In this case, it will set self.news and trigger the necessary UI update (e.g. maybe refresh tableview):
class ViewController: UIViewController {
var news: [News] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchNews()
}
func fetchNews() {
getUsers() { result in
switch result {
case .failure(let error):
print(error)
case .success(let news):
self.news = news
print(news)
}
// trigger whatever UI update you want here, e.g., if using a table view:
//
// self.tableView.reloadData()
}
// but don't try to print the news here, as it hasn't been retrieved yet
// print(news)
}

How to save data from Json with Alamofire and SwiftyJSON?

I am trying to save "author" data to global variable named "authors" from json(Link:"https://learnappmaking.com/ex/books.json") with these two libraries. But it only works at the trailing closure of func Alamofire.request(url).responseJSON. When I access the global variable named "authors" from somewhere except the trailing closure, what I get is an empty array of string.
Can someone explain the reason behind this werid situation?
Thanks a lot.
class ViewController: UIViewController {
var authors = [String]()
let url = "https://learnappmaking.com/ex/books.json"
func getAuthorsCount() {
print("the number of authors : \(authors.count)") // I hope that here, the number of authors should be 3 too! actually, it is 0. Why?
// this for loop doesn't get excuted
for author in authors {
print(author)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
print("authors.count: \(self.authors.count)")
}
}
}
}
getAuthorsCount()
print("-------------")
}
}
the actual output is:
Update:
I adjusted my code:
class ViewController: UIViewController {
var authors = [String]()
let url = "https://learnappmaking.com/ex/books.json"
func getAuthorsCount() {
print("the number of authors : \(authors.count)")
// this for loop doesn't get excuted
for author in authors {
print(author)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
//print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
//print("authors.count: \(self.authors.count)")
}
self.getAuthorsCount() // I added this line of code.
}
}
}
getAuthorsCount()
print("-------------")
}
}
But why does the func getAuthorsCount() (not self. version) still print an empty array of strings ? I think the result should be the same as the result which
func self.getAuthorsCount() printed.
I am so confused now...
Again, I want to use the data kept in the variable named "authors", but what I only got is an empty array of strings.
I'll try to answer all your questions :
The data is persistant
You are doing the following : Alamo.request (Network call) -> getAuthors(print result - empty) ->
response (receive response) -> self.authors.append(save response) -> self.authors (print result)
You need to do : Alamo.request (Network call) -> response (receive response) -> self.authors.append(save response) -> self.getAuthors or getAuthors(same) (inside the response {})
You need to call getAuthors once you have your result, inside the response callback :
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
print("authors.count: \(self.authors.count)")
}
self.getAuthorsCount()
print("-------------")
//Do whatever you want from here : present/push
}
}
}
Then you can use the saved data :
To send the data to another ViewController you can use various methods (present/push, closure/callback, ...)
Usually you will have a loading spinner to wait for the network to
answer then you will show your next controller
As requested via direct message: a Swift-only approach. Just paste this in a blank Playground:
import Foundation
final class NetworkService {
enum ServiceError: LocalizedError {
case invalidUrl
case networkingError(error: Error)
case parsingError
var localizedDescription: String? { return String(describing: self) }
}
func request(completion: #escaping (Result<[UserObject], Error>) -> Void ) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
completion(.failure(ServiceError.invalidUrl))
return
}
let dataTask = URLSession.shared.dataTask(with: url) { (jsonData, response, error) in
if let jsonData = jsonData {
let jsonDecoder = JSONDecoder()
do {
let users = try jsonDecoder.decode([UserObject].self, from: jsonData)
completion(.success(users))
} catch {
completion(.failure(ServiceError.parsingError))
}
} else if let error = error {
completion(.failure(ServiceError.networkingError(error: error)))
}
}
dataTask.resume()
}
}
struct UserObject: Codable {
let id: Int
let name: String
let username: String
let email: String?
let website: String?
}
let networkService = NetworkService()
networkService.request { result in
switch result {
case .success(let users):
debugPrint("Received \(users.count) users from REST API")
debugPrint(users)
case .failure(let error):
debugPrint(error.localizedDescription)
}
}

How to return a value within an if let statement in Swift 4? [duplicate]

This question already has an answer here:
Return a string from a web scraping function in swift
(1 answer)
Closed 4 years ago.
How can I return a value within an if let statement to be further returned within a function? Here is the code:
func loadUrl(url:String) -> String {
DispatchQueue.global().async {
do {
let appUrl = URL(string:url)!
let data = try Data(contentsOf:appUrl)
let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
print("Test from do")
if let results = json["results"] as? [[String:Any]] {
print("Test from if let 1")
if let first = results[0] as? [String:Any] {
print("Test from if let 2")
var cityStateLocation = first["formatted_address"]!
return cityStateLocation
//What needs to be returned
}
}
DispatchQueue.main.async {
print("No Error")
}
} catch {
DispatchQueue.main.async {
print("Cannot connect to the server.")
}
}
}
}
What I would like to be able to do is take cityStateLocation and return it in the func, but because it is a part of an if let statement within an .async method I don't know how to do that. Could someone please explain?
EDIT: I need the return value of cityStateLocation to equal a variable in a separate function. Here is the separate function:
#IBAction func continueButton(_ sender: Any) {
var cityState:String
if locationSwitch.isOn == true {
print(location.latitude)
print(location.longitude)
let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(location.latitude),\(location.longitude)&result_type=locality&key=AIzaSyDI-ZacHyPbLchRhkoaUTDokwj--z_a_jk"
loadUrl(url: url)
cityState = loadUrl(url: url)
} else {
cityState = ""
}
CoreDataHandler.saveObject(locationLocality: cityState)
}
Edit 2: The main reason why the "duplicate answer" is not a duplicate is that my code needs to call the return of this function within a separate function then save it to Core Data. Also, my code is not using an array.
You could modify your function to include a closure. For instance:
func loadUrl(url: String, completionHandler: #escaping (_ location: String?) -> (Void)) {
And then, where you want to return it, you'd pass it in as such.
completionHandler(cityStateLocation)
I made it an optional so that, in your fail paths, you could return nil.
Then, where you call the function would change. Using trailing closure syntax, it could look like this:
loadUrl(url: "someurl.com/filepath.txt") { optionalLocation in
guard let nonOptionalLocation = optionalLocation else {
// Location was nil; Handle error case here
return
}
// Do something with your location here, like setting UI or something
}
This is a fairly common pattern when dealing with asynchronous activity, such as working with network calls.
The simplest (perhaps no the prettiest), way of doing this would simply be to declare and instantiate a variable above the dispatch queue. Then you can set the variable equal to whatever you want, within the dispatch queue, and return it afterwards. You can change the type of ret, so that it suits your needs more directly.
func loadUrl(url:String) -> String {
var ret = NSObject()
DispatchQueue.global().async {
do {
let appUrl = URL(string:url)!
let data = try Data(contentsOf:appUrl)
let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
print("Test from do")
if let results = json["results"] as? [[String:Any]] {
print("Test from if let 1")
if let first = results[0] as? [String:Any] {
print("Test from if let 2")
var cityStateLocation = first["formatted_address"]!
ret = cityStateLocation
//What needs to be returned
}
}
DispatchQueue.main.async {
print("No Error")
}
} catch {
DispatchQueue.main.async {
print("Cannot connect to the server.")
}
}
}
return ret
}
DispatchQueue.global().async will cause the coded included in the closure to be executed at some point the future, meaning you loadUrl function will return (almost) immediately.
What you need is some kind of callback which can be called when you have a result (AKA closure)
This is just another way to approach the problem, the difference between this and Josh's example is simply, I provide an additional closure to handle the errors
func loadUrl(url:String, complition: #escaping (String?) -> Void, fail: #escaping (Error) -> Void) {
DispatchQueue.global().async {
do {
let appUrl = URL(string:url)!
let data = try Data(contentsOf:appUrl)
let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
print("Test from do")
if let results = json["results"] as? [[String:Any]], !results.isEmpty {
print("Test from if let 1")
let first = results[0]
print("Test from if let 2")
if let cityStateLocation = first["formatted_address"] as? String {
complition(cityStateLocation)
} else {
complition(nil)
}
} else {
complition(nil)
}
} catch let error {
fail(error)
}
}
}
Which you might call using something like...
loadUrl(url: "your awesome url", complition: { (value) in
guard let value = value else {
// No value
return
}
// process value
}) { (error) in
// Handle error
}

iOS swift how can I await an async task inside a function that needs a return value

I am using swift 3.0 and have created a function that returns an Array of Integers. The arrays of Integers are very specific and they are gotten from a database therefore the HTTP call is asynchronous . This is a function because I use it in 3 different controllers so it makes sense to write it once . My problem is that the Async code is returned after the return statement at the bottom therefore it is returning nil . I have tried the example here Waiting until the task finishes however it is not working mainly because I need to return the value . This is my code
func ColorSwitch(label: [UILabel]) -> [Int] {
for (index, _) in label.enumerated() {
label[index].isHidden = true
}
// I need the value of this variable in the return
// statement after the async is done
var placeArea_id = [Int]()
let urll:URL = URL(string:ConnectionString+"url")!
let sessionn = URLSession.shared
var requestt = URLRequest(url: urll)
requestt.httpMethod = "POST"
let group = DispatchGroup()
group.enter()
let parameterr = "http parameters"
requestt.httpBody = parameterr.data(using: String.Encoding.utf8)
let task = sessionn.dataTask(with:requestt, completionHandler: {(data, response, error) in
if error != nil {
print("check check error")
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:Any]
DispatchQueue.main.async {
if let Profiles = parsedData?["Results"] as? [AnyObject] {
if placeArea_id.count >= 0 {
placeArea_id = [Int]()
}
for Profiles in Profiles {
if let pictureS = Profiles["id"] as? Int {
placeArea_id.append(pictureS)
}
}
}
group.leave()
}
} catch let error as NSError {
print(error)
}
}
})
task.resume()
group.notify(queue: .main) {
// This is getting the value however can't return it here since it
// expects type Void
print(placeArea_id)
}
// this is nil
return placeArea_id
}
I already checked and the values are returning inside the async code now just need to return it any suggestions would be great .
You will want to use closures for this, or change your function to be synchronous.
func ColorSwitch(label: [UILabel], completion:#escaping ([Int])->Void) {
completion([1,2,3,4]) // when you want to return
}
ColorSwitch(label: [UILabel()]) { (output) in
// output is the array of ints
print("output: \(output)")
}
Here's a pretty good blog about closures http://goshdarnclosuresyntax.com/
You can't really have your function return a value from an asynchronous operation within that function. That would defeat the purpose of asynchronicity. In order to pass that data back outside of your ColorSwitch(label:) function, you'll need to also have it accept a closure that will be called on completion, which accepts an [Int] as a parameter. Your method declaration will need to look something like this:
func ColorSwitch(label: [UILabel], completion: #escaping ([Int]) -> Void) -> Void {
for (index, _) in label.enumerated() {
label[index].isHidden = true
}
var placeArea_id = [Int]()
let urll:URL = URL(string:ConnectionString+"url")!
let sessionn = URLSession.shared
var requestt = URLRequest(url: urll)
requestt.httpMethod = "POST"
let group = DispatchGroup()
group.enter()
let parameterr = "http parameters"
requestt.httpBody = parameterr.data(using: String.Encoding.utf8)
let task = sessionn.dataTask(with:requestt, completionHandler: {(data, response, error) in
if error != nil {
print("check check error")
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:Any]
DispatchQueue.main.async {
if let Profiles = parsedData?["Results"] as? [AnyObject] {
if placeArea_id.count >= 0 {
placeArea_id = [Int]()
}
for Profiles in Profiles {
if let pictureS = Profiles["id"] as? Int {
placeArea_id.append(pictureS)
}
}
}
group.leave()
completion(placeArea_id) // This is effectively your "return"
}
} catch let error as NSError {
print(error)
}
}
})
task.resume()
}
Later on, you can call it like this:
ColorSwitch(label: []) { (ids: [Int]) in
print(ids)
}

Resources