Getting data before navigating to a page - ios

I am trying to load some data from API and then navigate to some page.
The issue is that it navigates to the page before it finishes loading the data.
I need the data to be loaded and then move to the page
What I am doing is:
func getData(){
var serviceCenter : ServiceCenter?
var serviceCenterid : Int?
print("AM HERE")
let link: String = ""
guard let Requesturl = URL(string: link) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: Requesturl)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
print(responseData)
guard let aViewController = UIStoryboard(storyboard: .mainStoryboard).instantiateViewController(withIdentifier: String(describing: aViewController.self)) as? aViewController else {
return
}
aViewController.selectedServiceCenterID = serviceCenterID
let navController = UINavigationController(rootViewController: aViewController)
let controllerview = AppDelegate.topViewController()
controllerview?.present(navController, animated: true, completion: nil)
do {
guard let receivedData = try JSONSerialization.jsonObject(with: responseData,options: []) as? [String: Any] else {
print("Could not get JSON from responseData as dictionary")
return
}
print(responseData)
guard let data = receivedData["data"] as? [String: Any] else {
print("Could not get status from JSON")
return
}
guard let id = data["serviceCenterId"] as? Int else {
print(error)
return
}
serviceCenterid = id
print(serviceCenterid)
} catch {
print("error parsing response from POST on /public/api/login_customer")
return
}
}
task.resume()
}
What I want to print in my ViewController :--
override func viewDidLoad() {
super.viewDidLoad()
print("DEEPLINK")
print(selectedServiceCenter)
.....
}
what I am getting is:
> AM HERE
> 10041 bytes
> before
> nil
> 10041 bytes
> 26349 --> servicecenterid
my problem is that selectedservicecenter is empty because it navigates before data is loaded! How to make the data to be loaded first and then navigate after everything is completed above?

In your method, the data fetch is asynchronous . As you placed the code to navigate after the task.resume(), which means after the data fetch call is initiated, the next line that gets executed is your navigation code.
What you need to do is, you need to place the navigation code inside the response block, after you print(responseData) inside the do-catch block.
Note: Make sure you execute the navigation code on main thread.

Write function with callback handler and in call back navigate to desired viewController this will solve your problem.

Related

Json data not showing on tableView swift 5

I fetched the data and it is showing when printing but when i try to display it on tableview.Nothing is coming
Am i placing tableview.reloadData in wrong place ?
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
tableView.reloadData()
}
func fetchData()
{
if let url = URL(string: urlConstant) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, res, err) in
if err == nil
{
let decoder = JSONDecoder()
if let safeData = data
{
do{
let results = try decoder.decode(Results.self, from: safeData)
guard let array = results.Result as? [Products] else {return }
for product in array
{
self.productArray.append(product)
}
} catch {
print(error)
}
}
}
}
task.resume()
}
self.tableView.reloadData()
}
If you are getting correct response(check it once) from the server then next thing you need to reload tableView after getting the response from the server and populating the array.
func fetchData() {
if let url = URL(string: urlConstant) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, res, err) in
if err == nil
{
let decoder = JSONDecoder()
if let safeData = data
{
do{
let results = try decoder.decode(Results.self, from: safeData)
guard let array = results.Result as? [Products] else {return }
for product in array {
self.productArray.append(product)
}
// Reload table view here
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
Alternative, you can add completion handled in fetchData method.

Passing variable outside of a function for use in Swift 3

I'm new to Swift, and I want to 1) run a function that extracts a value from a JSON array (this part works) and 2) pass that variable into another function which will play that URL in my audio player.
My issue: I can't access that string stored in a variable outside the first function. Luckily, there's a bunch of questions on this (example), and they say to establish a global variable outside the function and update it. I have tried this like so:
var audio = ""
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
self.audio = (foo["audio"] as? String)!
} else {
// item could not be found
}
}).resume()
print(audio) // no errors but doesn't return anything
I have confirmed the JSON extraction is working -- if I move that print(audio) inside the function, it returns the value. I just can't use it elsewhere.
I originally tried it without the self. but returned an error.
Is there a better way to store this string in a variable so I can use it in another function?
EDIT: Trying new approach based on Oleg's first answer. This makes sense to me based on how I understand didSet to work, but it's still causing a thread error with the play button elsewhere.
var audiotest = ""{
didSet{
// use audio, start player
if let audioUrl = URL(string: audiotest) {
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationUrl = documentsDirectoryURL.appendingPathComponent(audioUrl.lastPathComponent)
//let url = Bundle.main.url(forResource: destinationUrl, withExtension: "mp3")!
do {
audioPlayer = try AVAudioPlayer(contentsOf: destinationUrl)
} catch let error {
print(error.localizedDescription)
}
} // end player
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.com/example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
if let foo = data_list.first(where: {$0["episode"] as? String == "Houston Preview"}) {
// do something with foo
self.audiotest = (foo["audio"] as? String)!
} else {
// item could not be found
}
print(self.audiotest)
}).resume()
The request for the data is asynchronous so the code that is inside the completionHandler block happens some time later (depending on the server or the timeout) , that’s why if you try to print outside the completionHandler actually the print func happens before you get the data.
There are couple of solution:
1. Add property observer to your audio property and start playing when it is set:
var audio = “”{
didSet{
// use audio, start player
}
}
2. Wrapping the request with a method that one of its parameters is a completion closure:
// the request
func fetchAudio(completion:(String)->()){
// make request and call completion with the string inside the completionHandler block i.e. completion(audio)
}
// Usage
fetchAudio{ audioString in
// dispatch to main queue and use audioString
}
Try this code. No need to take global variable if it is not being used in multiple function. you can return fetched URL in completion handler.
func getAudioUrl(completionHandler:#escaping ((_ url:String?) -> Void)) {
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
let audio = (foo["audio"] as? String)!
completionHandler(audio)
} else {
// item could not be found
completionHandler(nil)
}
}).resume()
}
func useAudioURL() {
self.getAudioUrl { (url) in
if let strUrl = url {
// perform your dependant operation
print(strUrl)
}else {
//url is nil
}
}
}

How to show all data in table view during pagination in swift 3?

Here i had implemented pagination for the table view and items are loaded by using model class but here the loaded items are replacing with the new items and whenever it calls api it returns the new data and old data is overriding on it and displaying only 10 items at a time i am implementing it for first time can anyone help me how to resolve the issue ?
func listCategoryDownloadJsonWithURL(listUrl: String) {
let url = URL(string: listUrl)!
print(listUrl)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil { print(error!); return }
do {
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel = ModelClass(dict: jsonObj as [String : AnyObject])
DispatchQueue.main.async {
guard let obj = self.listClassModel else { return }
let itemsCount = obj.items.count
print(itemsCount)
for i in 0..<itemsCount {
let customAttribute = obj.items[i].customAttribute
for j in 0..<customAttribute.count {
if customAttribute[j].attributeCode == "image" {
let baseUrl = "http://192.168.1.11/magento2/pub/media/catalog/product"
self.listCategoryImageArray.append(baseUrl + customAttribute[j].value)
print(self.listCategoryImageArray)
}
}
}
self.activityIndicator.stopAnimating()
self.activityIndicator.hidesWhenStopped = true
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
self.collectionView.isHidden = false
self.tableView.reloadData()
}
}
} catch {
print(error)
}
}
task.resume()
}
You are assigning your result data to model array, each time you call your API. This is the reason that your old data is getting replaced with new one. Rather than assigning, you should append the new data to your datasource array.
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel.append(contentsOf: ModelClass(dict: jsonObj as [String : AnyObject]))
Also make sure you initialize your array as an empty array first. (maybe in declaration or viewDidLoad) before calling API.

How to insert a value into a URL to make a request to YQL

I'm running into a problem when I try to make a request to YQL for stock data, when the symbol (newCompanyStockSymbol) to look up is user-entered. I fetch the stocks in this function:
func handleSave() {
// Fetch stock price from symbol provided by user for new company
guard let newCompanyStockSymbol = stockTextField.text else {
print("error getting text from field")
return
}
var newCompanyStockPrice = ""
let url = URL(string: "https://query.yahooapis.com/v1/public/yql?q=select%20symbol%2C%20Ask%2C%20YearHigh%2C%20YearLow%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22\(newCompanyStockSymbol)%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
let json = JSON(data: data!)
if let quotes = json["query"]["results"]["quote"].array {
for quote in quotes {
let ask = quote["Ask"].stringValue
newCompanyStockPrice = ask
}
}
print("new company json: \(json)")
}
guard let newCompanyName = self.nameTextField.text else {
print("error getting text from field")
return
}
guard let newCompanyLogo = self.logoTextField.text else {
print("error getting text from field")
return
}
print("2: The new commpany stock price is: \(newCompanyStockPrice)")
// Call save function in view controller to save new company to core data
self.viewController?.save(name: newCompanyName, logo: newCompanyLogo, stockPrice: newCompanyStockPrice)
self.viewController?.tableView.reloadData()
}
task.resume()
// Present reloaded view controller with new company added
let cc = UINavigationController()
let companyController = CompanyController()
viewController = companyController
cc.viewControllers = [companyController]
present(cc, animated: true, completion: nil)
}
And I use string interpolation to insert \(newCompanyStockSymbol) into the request URL at the appropriate place. However I get a crash and error on that line because it's returning nil, I expect because it's using the URL with \(newCompanyStockSymbol) in there verbatim, instead of actually inserting the value.
Is there another way to do this?
EDIT
And the save function in view controller that's called from handleSave() above if it's helpful:
func save(name: String, logo: String, stockPrice: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let entity =
NSEntityDescription.entity(forEntityName: "Company",
in: managedContext)!
let company = NSManagedObject(entity: entity,
insertInto: managedContext)
company.setValue(stockPrice, forKey: "stockPrice")
company.setValue(name, forKey: "name")
company.setValue(logo, forKey: "logo")
do {
try managedContext.save()
companies.append(company)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
tableView.reloadData()
}
Supposing you entered AAPL in your stockTextField, using simply:
let newCompanyStockSymbol = stockTextField.text
results in newCompanyStockSymbol being:
Optional("AAPL")
which is not what you want in your URL string. The critical section ends up like this:
(%22Optional("AAPL")%22)
Instead, use guard to get the value from the text field:
guard let newCompanyStockSymbol = stockTextField.text else {
// handle the error how you see fit
print("error getting text from field")
return
}
Now your URL should be parsed correctly.
--- Additional info ---
I'm not entirely sure of the rules on 'continued conversation' around here, but hopefully editing this will be acceptable... anyway...
Make sure you are following this flow:
func handleSave() {
let newCompanyName = nameTextField.text
let newCompanyStockSymbol = stockTextField.text
let newCompanyLogo = logoTextField.text
var newCompanyStockPrice = ""
// Fetch stock price from symbol provided by user for new company
let url = URL(string: "https://query.yahooapis.com/v1/public/yql?q=select%20symbol%2C%20Ask%2C%20YearHigh%2C%20YearLow%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22\(newCompanyStockSymbol)%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
let json = JSON(data: data!)
if let quotes = json["query"]["results"]["quote"].array {
for quote in quotes {
let ask = quote["Ask"].stringValue
newCompanyStockPrice = ask
// task completed, we've parsed the return data,
// so NOW we can finish the save process and
// update the UI
viewController?.save(name: newCompanyName!, logo: newCompanyLogo!, stockPrice: newCompanyStockPrice)
}
}
}
}
task.resume()
}
I'm not testing this, so it might need a tweak, and your .save() function may need to be forced onto the main thread (since it's doing UI updates). But maybe that's a little more clear.

issue passing data to segue.destination within function

I'm trying to make 2 API calls on Segue invoke and ultimately pass Array of Data from Second Call to CollectionView. With first call I'm getting one value catID, which I need in order to make the other call:
let searchEndpoint: String = MY_ENDPOINT
// Add auth key
let serviceCallWithParams = searchEndpoint + "?PARAMETER"
guard let url = URL(string: serviceCallWithParams) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// setting up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// making the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// error check
guard error == nil else {
print("error")
print(error)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse JSON
do {
guard let catData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let data = catData["data"] as? [String: Any] {
if let array = data["categories"] as? [Any] {
if let firstObject = array.first as? [String: Any] {
if let catId = firstObject["catId"] as? Int {
getTitles(catId: catId)
}
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
And then getTitles function looks like this:
func getTitles(catId: Int) {
let catIdString = String(catId)
let titlesEndPoint: String = MY_ENDPOINT + catIdString
// Add auth key
let titlesEndPointWithParams = titlesEndPoint + "?PARAMETER"
guard let titlesUrl = URL(string: titlesEndPointWithParams) else {
print("Error: cannot create URL")
return
}
let titlesUrlRequest = URLRequest(url: titlesUrl)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: titlesUrlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on listCategoryTitles")
print(error)
return
}
// make sure we got data
guard let titlesData = data else {
print("Error: did not receive data")
return
}
// parse the JSON
do {
guard let allTitles = try JSONSerialization.jsonObject(with: titlesData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let titlesJson = allTitles["data"] as? [String: Any] {
if let titlesArray = titlesJson["titles"] as? Array<AnyObject> {
self.books = []
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
}
Now when I put:
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
outside function, in target controller I'm getting an empty array as output on first click, but on every next one I'm getting proper data.
When I try putting this inside function "getTitles" the output in CollectionViewController is "nil" every time.
Worth mentioning could be that I have "books" variable defined like so:
Main Controller:
var books: [Book]? = []
Collection Controller:
var books: [Book]?
and I have created type [Book] which is basically object with 3 string variables in separate struct.
All of the code above is encapsulated in
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
Any help/guideline would be much appreciated!
When you make api call it will execute in background means asynchronously where as prepare(for:sender:) will call synchronously.
Now from your question it is looks like that you have create segue in storyboard from Button to ViewController, so before you get response from your api you are moved to your destination controller, to solved your issue you need to create segue from your Source ViewController to Destination ViewController and set its identifier. After that inside getTitles(catId: Int) method after your for loop perform segue on the main thread.
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ShowResults", sender: nil)
}
After that inside your prepare(for:sender:) make changes like below.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
}
}

Resources