Run API and update array before Navigation - ios

So I have an app that lets you search for words and gives you the definition and other details about it. I use 2 different apis so if you use a wildcard search (e.g. ?OUSE) you get all the possibilities and if you put in the whole word you just get the one result.
To cut down on API usage, when run for a single word I collect all the data in a WordDetail object. Similary, when run for a wildcard search, each word is created as a WordDetail object but with a lot of missing data.
My plan is, for wildcard search, when you select a specific word, it'll then go use the API again and retrieve the data, update the array, and then navigate you to the DetailViewController.
My problem is (I think), it's navigating to the DetailViewController before it's updated the array. Is there a way to make things wait before it has all the information before navigating?
The did select function looks like this...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var word: WordDetails?
word = results[indexPath.row]
if word?.results.count == 0 {
fetchWords(word: word!.word, updating: true)
}
print(word?.word)
print(word?.pronunciation)
let detailVC = TabBarController()
detailVC.selectedWord = word
detailVC.navigationItem.title = word?.word.capitalizingFirstLetter()
tableView.deselectRow(at: indexPath, animated: true)
navigationController?.pushViewController(detailVC, animated: true)
}
and the update to the array happens here...
func parseJSON(resultData: Data, update: Bool){
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WordDetails.self, from: resultData)
if update {
if let row = self.results.firstIndex(where: {$0.word == decodedData.word}) {
self.results[row] = decodedData
}
} else {
results.append(decodedData)
DispatchQueue.main.async {
self.resultLabel.text = "\(self.results.count) results found"
self.resultsView.reloadData()
}
}
} catch {
print(error)
}
}
could be a timing problem or might be a stupid approach. My alternative is to just run the api call again in the DetailViewController for words that are missing data as they came from a wildcard search.
UPDATE
Found a solution using DispatchSemaphore
Define it globally
let semaphore = DispatchSemaphore(value: 0)
Ask the code to wait for a signal before proceding, which I added to the didSelectRowAt function
_ = semaphore.wait(timeout: .distantFuture)
And then send a signal when the code has done what it needed to do e.g.
if update {
if let row = self.results.firstIndex(where: {$0.word == decodedData.word}) {
self.results[row] = decodedData
DispatchQueue.main.async {
self.resultsView.reloadData()
}
semaphore.signal()
}
}
Which then allows the rest of the code to carry on. Has worked perfectly in the few test cases I've tried.

Related

why data in tableview is always randomly sorted?

here is relevant for my question part of my code and it works almost fine, except one thing. The data is always sorted randomly. About code: i should receive data (name of CoffeeShop and .jpg) from firebase and display the foto in tableView
var coffeeShopLists: [CoffeeShopList] = []
struct CoffeeShopList {
let name: String
let shopImage: UIImage
}
func loadShops () {
coffeeShopLists = []
db.collection("coffeeShop").getDocuments { (querySnapshot, error) in
if let e = error {
print("Error\(e)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
print(doc.data())
if let shop = data["name"] as? String {
print(shop)
self.getFoto(named: shop)
// self.coffeeShopLists.append(shopList) //retrieving Data from firebase
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
}
func getFoto (named: String) {
let storage = Storage.storage().reference().child("CoffeeShops/\(named).jpg")
storage.getData(maxSize: 1 * 1024 * 1024) {data, error in
if let error = error {
return print(error)
} else {
if let image = UIImage(data: data!) {
let newList = CoffeeShopList(name: named, shopImage: image)
self.coffeeShopLists.append(newList)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CoffeeShopCell", bundle: nil), forCellReuseIdentifier: "ReusableCell")
loadShops()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! CoffeeShopCell
cell.shopImage.image = coffeeShopLists[indexPath.row].shopImage
return cell
}
How i said: it works. but it should be sorted how it sorted in firebase collection.But its not. And sorry for my english.(
after editing code like in comments bellow ordering by Position.Here is the result of print statements:
["position": 1, "name": soren]
soren
["position": 2, "name": ramozoti]
ramozoti
["position": 3, "name": coffeesoul]
coffeesoul //until this part the result is always the same(correct) and it should be by this order. But the results bellow is always in random order. That part belong to function getFoto()
coffeesoul
[CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: <UIImage:0x600000e17600 anonymous {1080, 1080}>)]
ramozoti
[CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: <UIImage:0x600000e17600 anonymous {1080, 1080}>), CoffeeShop.CoffeeShopList(name: "ramozoti", shopImage: <UIImage:0x600000e17840 anonymous {621, 621}>)]
soren
[CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: <UIImage:0x600000e17600 anonymous {1080, 1080}>), CoffeeShop.CoffeeShopList(name: "ramozoti", shopImage: <UIImage:0x600000e17840 anonymous {621, 621}>), CoffeeShop.CoffeeShopList(name: "soren", shopImage: <UIImage:0x600000e10000 anonymous {1080, 1080}>)]
the struct feels each time by the random order
So, im on the right way. The last question to finish it:How i can sort this struct by the third value
[CoffeeShop.CoffeeShopList(name: "ramozoti", shopImage: Optional(<UIImage:0x600003b27de0 anonymous {621, 621}>), position: 2), CoffeeShop.CoffeeShopList(name: "soren", shopImage: Optional(<UIImage:0x600003b34480 anonymous {1080, 1080}>), position: 1), CoffeeShop.CoffeeShopList(name: "coffeesoul", shopImage: Optional(<UIImage:0x600003b31b00 anonymous {1080, 1080}>), position: 3)]
This is it, its working now. i made extra function
func sort () {
sortedShopLists = coffeeShopLists.sorted {$0.position < $1.position}
}
and passt it into getFoto() function after self.coffeeShopLists.append(newList)
maybe it was somewhere more elegant way to solve my problem but i found just this.
The Firestore console orders the documents by their key. If you want the same in your code, you can
db.collection("coffeeShop")order(by: FieldPath.documentID()).getDocuments(...
Also see: Order by document id firestore?
Your update and comments point towards this being about the loading of the photo's related to those documents.
The loading of the photos starts in the correct order. But since each photo takes a different amount of time to load, it may complete in another order.
If you want the photos to appear in the order that you had the documents, you should make sure you work from a list that maintains the document order.
For example, you could add each document to coffeeShopLists before you start loading its photo, and then update the item in coffeeShopLists with the photo.
So in your loadFotos you'd do sometihng like this:
for doc in snapshotDocuments {
let data = doc.data()
if let shop = data["name"] as? String {
self.coffeeShopLists.append(shopList)
self.getFoto(named: shop, self.coffeeShopLists.length - 1)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
The change here is that we're passing the index of the document into the getFoto function, so that it knows which element in the array to update.
Then in getFoto you'd fine the array item and update that in stead of adding a new item.
If you don't specify an order of documents in the query, Firestore doesn't guarantee one. Documents are not fundamentally ordered except by what you specify in the query. So, if ordering is important to you, you'll have to arrange for that yourself.
I am a learner on Swift and was facing the same problem, i figured out the way by using .order
db.collection(Constants.FStore.collectionName).order(by: Constants.FStore.dateField).addSnapshotListener { (querySnapShot, error) in
while persisting, i persisted a date field also and I am ordering by that. You can order by a different field.
your code could be
db.collection("coffeeShop").order(by: <<your field>>).getDocuments { (querySnapshot, error)
As I said, I am learner of Swift but I hope this would help.

Use a function that needs UITableView in another view?

In my WalletTableViewController I have two functions, used to calculate the Wallet Value:
A. updateCellValue() Is called by reloadData() with the tableView and uses indexPath.row to fetch a value (price) and an amount (number of coins) corresponding to the cell and make a calculation to get the total value of that coin (amountValue = value * amount). That is then saved with Core Data.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.delegate = self
cell.amountTextField.delegate = self
updateCellValue(cell, atRow: indexPath.row)
return cell
}
func updateCellValue(_ walletTableViewCell: WalletTableViewCell, atRow row: Int) {
var newCryptos : [CryptosMO] = []
var doubleAmount = 0.0
if CoreDataHandler.fetchObject() != nil {
newCryptos = CoreDataHandler.fetchObject()!
}
cryptoPrice = cryptos[row].code!
guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else { return }
let selectedAmount = newCryptos[row]
guard let amount = selectedAmount.amount else { return }
var currentAmountValue = selectedAmount.amountValue
doubleAmount = Double(amount)!
let calculation = cryptoDoublePrice * doubleAmount
currentAmountValue = String(calculation)
CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)
updateWalletValue()
}
B. updateWalletValue() Is a function that fetches all the amountValue objects in Core Data and adds them together to calculate the Wallet Value.
func updateWalletValue() {
var items : [CryptosMO] = []
if CoreDataHandler.fetchObject() != nil {
items = CoreDataHandler.fetchObject()!
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
In my MainViewController, the Wallet Value is displayed too, but how can I refresh it's value?
func updateMainVCWalletLabel() {
//... what can I do here??
}
This works great for the WalletViewController of course with the TableView and indexPath, but how can I call updateCellValue from the MainViewController to keep the value updated?
The WalletViewController is instantiated and pushed from the MainViewController :
#IBAction func walletButtonTapped(_ sender: Any) {
let walletViewController = self.storyboard?.instantiateViewController(withIdentifier: "walletTableViewController")
self.present(walletViewController!, animated: true)
}
If you want to use a single method in multiple view controllers you should implement that method where you can call that method from anywhere. For example you can use singleton class here.
Create a swift file and name it as your wish (like WalletHelper or WalletManager)
Then you will get a file with the following format
class WalletHelper: NSObject
{
}
Create a shared instance for that class
static let shared = WalletHelper()
Implement the method you want
func getWalletValue() -> Float {
// write your code to get wallet value`
// and return the calculated value
}
Finally call that method like
let walletValue = WalletHelper.shared. getWalletValue()
WalletHelper.swift looks like
import UIKit
class WalletHelper: NSObject
{
static let shared = WalletHelper()
func getWalletValue() -> Float {
// write your code to get wallet value
// and return the calculated value
}
}
Update (old answer below)
To me it is absolutly unclear what you want to achieve: Which value do you want to be updated? The staticTotal?
Seems a litte like an XYProblem to me. As #vadian commented yesterday, please clearly describe where the data is stored, how the controllers are connected, what you want to update when in order to achieve what. You could also provide a MCVE which makes clear what you are asking, instead of adding more and more code snippets.
And, even more interesting: Why do you modify CoreData entries (CoreDataHandler.editObject) when you are in the call stack of tableView(_: cellForRowAt:)? Never ever ever do so! You are in a reading case - reloadData is intended to update the table view to reflect the data changes after the data has been changed. It is not intended to update the data itself. tableView(_: cellForRowAt:) is called many many times, especially when the user scrolls up and down, so you are causing large write impacts (and therefore: performance losses) when you write into the database.
Old Post
You could just call reloadData on the table view, which then will update it's cells.
There are also a few issues with your code:
Why do you call updateWalletValue() that frequently? Every time a cell is being displayed, it will be called, run through the whole database and do some reduce work. You should cache the value and only update it if the data itself is modified
Why do you call fetchObject() twice (in updateWalletValue())?
You should do this:
func updateWalletValue() {
guard let items:[CryptosMO] = CoreDataHandler.fetchObject() else {
WalletTableViewController.staticTotal = 0.0
return
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}

Looped delegate method returning repeats (multithreading)

So I'm trying to make an app that lists the times the ISS is scheduled to Pass over the user's location. It works more or less smoothly, with one hiccup.
What I expect: Around 5 different times listed in a UITableView
What I get: 5 times listed in a UITableView with randomly repeated values. Sometimes all 1 value, sometimes the last one is also the second and/or 3rd to last, sometimes 2 values repeat themselves, any number of incorrect combinations. A small portion of tests return correctly.
What bugs me most is that the wrong results are inconsistent, so I can't see a way to brute force a crude solution.
Relevant code:
First the network manager class/delegate
import Foundation
import CoreLocation
//Delegate for thread communication
protocol NetworkManagerDelegate: class {
func didGetPass(lastPass: Bool, pass: Pass)
//Flag last model object to limit tableview reloads
}
//Using struct as manager class is not going to change state
struct NetworkManager {
weak var delegate: NetworkManagerDelegate?
//Set a base URL as far along as possible, will finish with data passed from function
let baseURL = "http://api.open-notify.org/iss-pass.json?"
func getPasses(coordinate: CLLocationCoordinate2D) {
//complete URL
let lat = coordinate.latitude
let lon = coordinate.longitude
let requestURL = URL(string: "\(baseURL)lat=\(lat)&lon=\(lon)")
//begin task
let task = URLSession.shared.dataTask(with: requestURL!) { (data, response, error) in
if error != nil {
print(error as Any)
//Generic report on failure to connect
print("Could not reach API")
} else {
do {
//Get JSON from data to parse
if let resultJSON = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any] {
//Get list of passes from JSON
let passes = resultJSON["response"] as! [[String: Int]]
//Set default parameters for delegate method
var testPass: Pass
var lastPass = false
//loop through passes
for pass in passes {
//determine if last pass
if pass == passes.last! {
lastPass = true
}
testPass = Pass()/*This seems terribly inefficient to me
However, attempting to create the object outside the loop and simply modify it
leads to the same exact object being passed multiple times to the main thread, so
the tableview's cells are all identical.*/
testPass.durationInSeconds = pass["duration"] ?? 0
//Convert date to Date object and set to testPass
testPass.riseTime = Date(timeIntervalSince1970: (Double(pass["risetime"] ?? 0)))
//Inform main thread via delegate
DispatchQueue.main.async {
self.delegate?.didGetPass(lastPass: lastPass, pass: testPass)
}
}
}
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
And on the main thread:
extension TrackingController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return passes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Get cell, pass default if nil
guard let cell = tableView.dequeueReusableCell(withIdentifier: "passCell") as? PassCell else {
return PassCell()
}
//Retrieve data from passes, assign to labels
let pass = passes[indexPath.row]
cell.timeLabel.text = "\(dateFormatter.string(from: pass.riseTime))"
cell.durationLabel.text = "Duration: \(pass.durationInSeconds) Seconds"
return cell
}
}
extension TrackingController: NetworkManagerDelegate {
func didGetPass(lastPass: Bool, pass: Pass) {
passes.append(pass)
if lastPass { //reload only once api calls are done
passList.reloadData()
}
}
}
That should be all code that triggers from the moment the Network Manager's one function is called. Can anyone explain this to me? I have manually checked the API, I should not be receiving duplicate outputs here.
Edit: I just tried changing DispatchQueue.main.async to DispatchQueue.main.sync, and while it seems to fix the issue my understanding of multithreading leads me to believe this defies the point of running the call on another thread. That said, my understanding of multithreading isn't very practical, so I'm open to correction on that or to better solutions to my problem.
Ideally, on the main thread, you don't do anything with the data until the last object has been received. So, passing the whole array in one go is a better approach.
You can do this instead:
func didGetPasses(_ passes: Passes) {
passes.append(passes);
passList.reloadData();
}
And call
self.delegate?.didGetPasses(passes)
once the for loop is completed.
PS: You should also consider using closures. It helps in handling logic and makes code more 'in-place' at the call site.
Is
guard let cell = tableView.dequeueReusableCell(withIdentifier: "passCell") as? PassCell else {
return PassCell()
}
correct? seems you are returning an uninitialised PassCell there
I decided on an answer inspired by advice from Kunal Shah. I stopped looping the delegate call and instead put together a collection of Pass objects to send to the main thread.
I didn't want to do this as my previous experience with this led to empty collections being sent for some reason, but it seems to work here. This way I still only reload the tableview once, but also only call the delegate method once.

Why does an instance variable have a different value inside of a closure?

func loadYelpComments(){
guard let business = business else {return}
guard let _ = tableView else {return}
guard let businessID = business.id else {return}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
HTTPHelper.getYelpComments(for: businessID, completionHandler: { data in
let json = JSON(data)
let reviews = json["reviews"].arrayValue.map({return $0.dictionaryValue})
var commentDate: Date?
for (index, review) in reviews.enumerated(){
let userDictionary = review["user"]?.dictionary
if let dateString = review["time_created"]?.stringValue{
commentDate = dateFormatter.date(from: dateString)
}
let yelpComment = YelpComment(rating: review["rating"]?.intValue, userImageURL: userDictionary?["image_url"]?.stringValue, userName: userDictionary?["name"]?.stringValue, comment: review["text"]?.stringValue, commentDate: commentDate, commentPageURL: review["url"]?.stringValue)
self.comments.append(yelpComment)
}
print("Number of comments: \(self.comments.count)") //Prints: Number of comments: 3"
self.tableView.reloadData()
})
print("Number of comments: \(self.comments.count)") //This prints firt "Number of comments: 0"
}
The getYelpComments(for:completionHandler:) class method is responsible for fetching JSON data from the Yelp API. To my surprise even though the comments instance array is being updated by appending new YelpComment objects to it, its count has different values inside and outside of the closure.
These comments are supposed to be displayed in a table view. What further confuses me is the fact that when I add tableView.reloadData() within the closure I get an index out of bounds for an empty array error but when I add the same line of code: tableView.reloadData() out side of the closure I get no error and comments.count equates zero. Any help would be much appreciated!
P.S. I don't know if mentioning this is going to make any difference but data is #escaping. I am also using SwiftyJSON and Alamofire.
UPDATE:
My table view data source methods are:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return super.tableView(tableView, numberOfRowsInSection: section)
} else {
return comments.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
return super.tableView(tableView, cellForRowAt: indexPath)
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomCellTypeIdentifiers.YelpCommentCell, for: indexPath) as! YelpCommentCell
cell.configureYelpCell(with: comments[indexPath.row])
return cell
}
}
Because the job of the completion block is to report to you when the network call is done. HTTPHelper is making a call to a server, which can take some considerable amount of time. It does not stop your program from executing, and just wait at that line: it goes to the next line, immediately, and calls print("Number of comments: \(self.comments.count)"), and you get 0, because the network call isn't done yet. Then, some time later, that whole completion block happens.
This is called an asynchronous function, and it commonly trips up people who haven't seen it before, but you see lots of it eventually, so it's worth reading up on.
The fact that you say "when I add tableView.reloadData() within the closure I get an index out of bounds for an empty array error", it sounds like one of your UITableViewDataSource functions that configures the table (cellForRowAt, numberOfRowsInSection), has an error in it somewhere.
Closures are not necessarily executed immediately at the point where you see them.
In your code, the print outside the closure will be executed before the print inside the closure. This is because getYelpComments is what is called an "asynchronous method". While you are waiting for the Yelp servers to respond, the code doesn't just sit there doing nothing. It continues to execute the code after the closure, printing that the count is 0.
After Yelp responds, the closure gets executed. And count, being 3, is printed.
As for why putting tableView.reloadData() causes it to crash, there is probably something wrong in your table view datasource methods. It is most likely an off-by-1.
Edit:
I think the fact that you write
if section == 0 {
return super.tableView(tableView, numberOfRowsInSection: section)
is weird. I don't know why you want to return the super implementation if the section is 0. Does your table view have multiple sections? If not, you should probably remove the check. If yes, then properly return a value for the first section.

Load data again after segue is done

I would like to know how should I behave if I got several articles in table view controller, which are loaded and parsed from some API. Then I would like to click on some article to view his detail.
Should I load all articles on start up with all data and then pass concrete whole article to next detail table controller or just send article id and in new table controller load again whole one article with passed id?
I think that is much better second method, but have no idea how to do it. Any help? Thanks a lot.
EDIT: added code
MainTableViewController:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let articleId = articles[indexPath.row].Id
let destination = DetailTableViewController()
destination.articleId = articleId
destination.performSegue(withIdentifier: "Main", sender: self)
}
DetailTableViewController:
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140
// Vyzkoušíme jestli jsme připojeni do internetu
if Helper.hasConnectivity() {
// Zapneme načítací obrazovku
setLoadingScreen()
// Načteme jídla
loadMainArticles()
} else {
promptAlert(title: "Upozornění", message: "Ujistěte se, že Vaše zařízení je připojené k internetu")
}
}
private func loadMainArticles() {
ApiService.sharedInstance.fetchArticle(part: "GetArticle", articleId: "brananske-posviceni--spravna-lidova-zabava-s-pecenou-husou") { (articles: [Article]) in
self.removeLoadingScreen()
self.tableView.separatorStyle = .singleLine
if articles.count == 0 {
self.promptAlert(title: "Upozornění", message: "Žádná data")
}
self.selectedArticle = articles[0]
self.tableView.reloadData()
}
}
ApiService:
func fetchArticle(part: String, articleId: String, completion: #escaping ([Article]) -> ()) {
var Id = ""
var Title = ""
var Content = ""
var Picture = ""
var PublishDate = ""
let url = NSURL(string: "http://xxx")
URLSession.shared.dataTask(with: url! as URL) { (data, response, error) in
if error != nil {
print(error as Any)
return
}
do {
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments)
var articles = [Article]()
for dictionary in json as! [[String: AnyObject]] {
.
.
.
let article = Article(Id: Id, Title: Title, Content: Content, Picture: Picture, PublishDate: PublishDate, Categories: Categories)
articles.append(article!)
}
DispatchQueue.main.async(execute: {
completion(articles)
})
}
}.resume()
}
The problem is that when I do the segue after click on row, then should loadMainArticles. The method is triggered, but always stops on URLSession row and immediately jump to resume() and the completion block is never triggered. Is there any reason why?
This is a choice that should be done according to article data model size.
You should pay attention to which data you need to show on TableView single cell and which on Article detail page.
I suppose you to show:
Title and posted hour in TableView
Title, hour, long description, maybe some image in Detail page
In this case i would reccommend you to download only title and hour for each article item, and then, in detail page, download single item details (avoid to download unused content, since user could not open every item).
Otherwise, if amounts of data are similar, download all informations on first connection and open detail page without making any other network request.
I think that is much better second method, but have no idea how to do it.
You need to retrieve article id from TableView selected cell and pass it to DetailPageViewController using prepareForSegue. In DetailPageViewController ViewDidLoad send article id to your backend api and retrieve article details to show.

Resources