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.
Related
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.
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.
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.
I'm working on a Core Data project and I got a complier error for deleting item from a TableViewController:
Cannot convert value of type String to expected argument type NSManagedObject
Here's the code:
var listArr:[String] = []
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName:"Entity")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request)
if results.count > 0 {
for result in results as! [NSManagedObject] {
if let listName = result.value(forKey: "listName") {
listArr.append(listName as! String)
}
}
}
} catch {
// Handle error
print(error)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "cellID"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
// Configure the cell...
cell.textLabel?.text = listArr[indexPath.row]
return cell
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
context.delete(listArr[indexPath.row]) //The error is here
listArr.remove(at: indexPath.row)
}
self.tableView.reloadData()
}
What do I need to change?
The delete() method that you're calling takes an NSManagedObject as its sole argument. You're passing in an element from your listArr array, which is an array of strings.
It seems obvious to say it, but if you want to delete a managed object, you need to tell Core Data which one to delete. It doesn't know what to do with a string.
I'm guessing (since you didn't say) that your listArr is made up of strings which are stored in Core Data as a property of an entity. You're fetching data somewhere and saving only the strings. That's fine as long as your data store is read-only, at least in this part of the app. You can display the strings but you can't go back and update Core Data, because you don't have the necessary information.
If that's true, what you should do is use an array of managed objects instead of an array of strings. When you need the string value, get it from one of the managed objects. When you need to delete an entry, delete that managed object.
There are two ways that you can populate a table from core-data. Either you can use a NSFetchedResultsController that tracks changes from core data and keeps your data in sync. Or you can do a single fetch and store the values you want to display in our own datasource (such as an array).
With a NSFetchedResultsController you will get updates as your data changes. With this model the correct way to delete a row is to delete it from core-data and wait for a callback from the NSFetchedResultsController to update your view.
With a single fetch (as you are doing) you do not get updates as core-data changes. In this model you can simply remove the object from your array and update the view. Updating core-data is another task unrelated to your view or datasource. In this case you should use persistentContainer performBackgroundTask to fetch the object you want to delete and then delete it in the background. For this to work you need to have a way to fetch the managedObject that you which to delete. I don't know what kind of strings you are storing - but if they are unique you could use them for your fetch. Otherwise you would also have to store some unique ID of the object such as it's objectID.
I am able to retrieve results from a Firebase query but I am having trouble retrieving them as a dictionary to populate a tableView. Here's how I'm storing the query results:
var invites: Array<FIRDataSnapshot> = []
func getAlerts(){
let invitesRef = self.rootRef.child("invites")
let query = invitesRef.queryOrderedByChild("invitee").queryEqualToValue(currentUser?.uid)
query.observeEventType(.Value, withBlock: { snapshot in
self.invites.append(snapshot)
print(self.invites)
self.tableView.reloadData()
})
}
Printing self.invites returns the following:
[Snap (invites) {
"-KKQWErkyuehmbxom8NO" = {
invitedBy = T2k7Bm9G9RNLcHLvLlKApRbnas23;
invitee = dRJ1FqctSfTlLF8iO2ddlc9BANJ3;
role = guardian;
};
}]
I'm having trouble populating the tableView. The cell labels don't show anything:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let inviteDict = invites[indexPath.row].value as! [String : AnyObject]
let role = inviteDict["role"] as? String
cell.textLabel!.text = role
return cell
}
Any ideas?
Thanks!
EDIT: Printed the dictionary to console after my function is run, and it's printing []. Why is it losing it's values?
override func viewWillAppear(animated: Bool) {
getAlerts() // inside of function prints values
print(self.invites) //prints []
}
EDIT 2: Paul's solution worked!! I added the following function to have Firebase "listen" for results! I may have to edit this to only show alerts for the logged in user, but this has at least pointed me in the right direction:
override func viewWillAppear(animated: Bool) {
getAlerts()
configureDatabase()
}
func configureDatabase() {
// Listen for new messages in the Firebase database
let ref = self.rootRef.child("invites").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
self.invites.append(snapshot)
self.tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: self.invites.count-1, inSection: 0)], withRowAnimation: .Automatic)
})
}
Check out the friendlychat Firebase codelab for an example of populating a table view from an asynchronous call. Specifically, see viewDidLoad and configureDatabase in FCViewController.swift.
Your array will always return [ ] because Firebase is async. If you add var invites: Array<FIRDataSnapshot> = [] inside of the Firebase closure you will print the data you are looking for, but im assumming you would like to use the variable outside of the closure. in order to complete that you must create a completion within getAlerts() function. If you are not sure how to complete that do a google search and that will solve your question.