Looped delegate method returning repeats (multithreading) - ios

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.

Related

Run API and update array before Navigation

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.

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
}

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.

Why can't I print my indexPath.row swift 3?

This is where I initialize my array of strings:
let databaseRef = FIRDatabase.database().reference().child("Butikker")
self.shopItems = [String]()
databaseRef.observe(FIRDataEventType.value, with: { (snapshot) in
for child in snapshot.children {
let snap = child as! FIRDataSnapshot
let dictionary = snap.value as! [String: AnyObject]
self.shopItems.append(dictionary["Name"] as! String)
}
})
This is my tableview:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as UITableViewCell
let when = DispatchTime.now() + 5
DispatchQueue.main.asyncAfter(deadline: when) {
print(self.shopItems[indexPath.row])
cell.textLabel?.text = self.shopItems[0]
}
return cell
}
My question is, why does my app crash when I try to print shopItem[index.row] and how would I solve this?
I'm not sure if this is going to solve your problems, but I have some basic improvements to your code that are probably going to prevent your app generally from crashing.
1.: after fetching a snapshot from Firebase I would always check if it actually exists; you can do that by calling if !snapshot.exists() { return } and in the for-in-loop if !child.exists() { return }. Before the return statement you can implement your error handling if you wish to. That means if for any reason there is no snapshot retrievable, it's going to stop the code from running.
2.: I would always get the snapshot values with an if-let-statement. This could be part of your problem as well. Simply call:
if let dictionary = snap.value as? [String:AnyObject] {
// do whatever you want to do if the dictionary can be created
} else { print("For some reason couldn't initialize the dictionary.")}
That also prevents your app from crashing (just in case) and can tell you if it can't find the value you need.
3.: Don't rely on executing the code in cellForRowAtIndexPath()-method asynchronously and with a delay. If you have a bad internet connection, it's not unlikely that the app is going to need more time to load and the data won't appear. Instead, call after the for-in-loop in your first part of code this method: tableView.reloadData() // replace tableView with whatever your tableView is called. That means that every time there's a new value, it's going to reload the data automatically so that you don't have to worry about losing the data on the way.
And to actually solve the problem you're asking about: obviously, the index of the indexPath.row is out of range and I think I know why: is it possible that in the numberOfRowsInSection method you're either not calling shopItems.count at all or maybe not asynchronously?
If not, you could test it by calling in the cellForRowAtIndexPath()-method
print("Number of shops items: \(shopItems.count); Current index path. \(indexPath.row)")
This is at least going to give you all the important values for solving your problem. Hope I could help.

How to add Error handling to a working tableviewcontroller with Json (Swift 2)

Im a just starting with programming apps in Xcode 7 / Swift 2.0
Im am pretty far with developing my ideas, but I can't seem to get the error handling to work.
The viewcontroller it concerns is presenting dates where and when our band plays.
In this Viewcontroller I call Json data from our online server and parse it into a tableview. It all works. But i want the following things to happen too.
If there is no connection whatsoever (wifi/4G/3G) perform a segue (No Connection)
If the server or the php script is unreachable, perform a segue (server error
)
If there is no data available (as in Empty Array) Just give a message "There are no dates set."
The Json I get from my PHP script:
(
{
date = "some date";
description = "Some description";
location = "Some location";
others = "some details";
showtime = "some time";
},
{
date = "some date";
description = "Some description";
location = "Some location";
others = "some details";
showtime = "some time";
}
)
This is the ViewController
import UIKit
class GigsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var gigsdata: NSArray = []
override func viewDidLoad() {
super.viewDidLoad()
let logo = UIImage(named: "where_is_header_navigationController.jpg")
let imageView = UIImageView(image:logo)
self.navigationItem.titleView = imageView
func dataOfJson(url: String) -> NSArray {
let gigsdata = NSData(contentsOfURL: NSURL(string: url)!)
let jsonArray: NSArray = try! NSJSONSerialization.JSONObjectWithData(gigsdata!, options: .MutableContainers) as! NSArray
return jsonArray
}
gigsdata = dataOfJson("http://www.mydomain.eu/app/myscript.php")
}// end of viewDidLoad
// MARK: Table View Delegate Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if gigsdata.count != 0 {
return gigsdata.count
} else {
return 1
}
}
func allowMultipleLines(tableViewCell:UITableViewCell) {
tableViewCell.textLabel?.numberOfLines = 0
tableViewCell.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("GigsCell")! as UITableViewCell
// setting the text color
cell.textLabel?.textColor = UIColor.whiteColor()
//Getting the JSON data and turn it into objects
let maingigsdata = (gigsdata[indexPath.row] as! NSDictionary)
//setting constants for the detailview
let gigsDate = maingigsdata["date"] as! String
let gigsLocation = maingigsdata["location"] as! String
// Setting the number of lines per row
cell.textLabel!.numberOfLines = 2
// Filling the cell with data
cell.textLabel!.text = ("\(gigsDate) \n\(gigsLocation)")
// setting the beackground color when selected
let backgroundView = UIView()
backgroundView.backgroundColor = UIColor.darkGrayColor()
cell.selectedBackgroundView = backgroundView
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Like i said, im fairly new to this, so please dont go around and name all kinds of proceduresm, functions or things like that.
Please don't think that I don't try myself, but 'm stuck now for two weeks.
The thing I saw a lot on videos and tutorials was the DO TRY CATCH thing.
But implementing that as good as I can gave me just all kinds of errors, so I must be doing something wrong there.
I hope that there is someone out there who can help me out and make me a lot wiser as I am today!
You should use NSURLRequest or some lib like Alamofire to fetch the data. NSData contentsOfURL is not asynchronous and in your case blocks the main thread. I wonder if you can compile your code as NSData contentOfURL throws and exception, which must be catched. Check the domain of NSError object, which is thrown. It can be e.g. NSURLErrorDNSLookupFailed, NSURLErrorDomain, NSURLErrorNotConnectedToInternet, NSURLErrorInternationalRoamingOff.
https://github.com/Alamofire/Alamofire
To detech the connection type:
Detect carrier connection type (3G / EDGE / GPRS)
Example of the error handling based on your code:
do {
// Blocks your UI if you do this in the main thread!
let gigsdata = NSData(contentsOfURL: NSURL(string: url)!)
}
catch let error as! NSError {
if error.domain == NSURLErrorNotConnectedToInternet {
// If you do the data fetch using the background queue then this must be dispatched to the main queue:
performSegueWithIdentifier("noInternet", sender: self)
}
}
For the 1st and 2nd issue I came up with the following working code:
// performing a first check if there is an connection based on the HTTPStatusCode 200
Alamofire.request(.GET, "http://www.google.com")
.responseJSON { response in
if response.response?.statusCode == 200 {
print("Code is 200 and good")
}else{
print("Code is not 200 and therefor bad")
self.performSegueWithIdentifier("NoConnectionSegue", sender: self)
}
}
I implemented the code in the first view controller of my app and the 'NoConnectionSegue' is a view controller that "blocks" the whole screen with a message that the app is not usable without an internet connection and a friendly request to close the app and try later.
Of course the url "google.com" can be replaced with your own domain.
For the 3rd issue I had the following solution.
The viewcontroller is a TableView with cells populated by a json file on a remote server. At the end of the viewDidLoad I check if the array.count is less then 1.
If so, then perform a segue to a new viewcontroller of the kind: "Present Modally" and the presentation: "Over current context".
This way I don't get the navigationbar that I used in the previous Tableview.
Leaving the original Tableview controller with one empty cell invisable in the background.
It might not be the cleanest and the best way. But at least I got what I wanted.
the code:
// check if there are any gigs, otherwise perform segue to NoGigs
if self.arrRes.count <= 1 {
self.performSegueWithIdentifier("NoGigsSegue", sender: self)
}

Resources