Firebase child added in realtime not updating in collection view - ios

After reading through a bunch of other questions on SO about reloading Firebase observeEventType observers, I am pretty confused. I am adding my observer dictionaries to a variable accessible within the entire controller. I then assign that value to the data source dictionary and it loads all of the previously added children.
However, once I try to add new values from another simulator or manually inputting within the backend, my global dictionary updates with the new value, but my data source variable does not. Once I leave the controller it will eventually update, but it defeats the purpose of using Firebase.
I think an open observer should be in viewWillAppear, but a bunch of sources online seemed to have it in viewDidLoad.
I am using a segmented control to go through each custom class, which may be causing the issue. The setup is one collection view controller whose cells are custom collection views that are cells as well.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.loadFireData()
}
func loadFireData() {
if let locationId = location.locationId {
let postQuery = ref.child("Posts").child(selectedRegion).child(locationId).queryOrderedByChild("descTime")
postQuery.observeEventType(.ChildAdded, withBlock: { (snap) in
if snap.exists() {
let postQuery = snap.value! as! [String: AnyObject]
let feedPost = FeedModel()
feedPost.value = postQuery["value"] as? String
feedPost.key = postQuery["key"] as? Int
feedPost.balance = postQuery["balance"] as? Double
self.post.append(feedPost)
dispatch_async(dispatch_get_main_queue(), {
self.collectionView!.reloadData()
})
}
}
})
}
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if indexPath.section == 1 {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(feedCellId, forIndexPath: indexPath) as! LocationFeedCell
cell.location = location
//this doesn't seem to be updating here
cell.posts = posts
return cell
}
Any help will be greatly appreciated

you are only adding feetpost into post array. So after reload collectionview, you should add newest feetpost in to your custom cell's label according to indexpath.row
if indexPath.section == 1 {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(feedCellId, forIndexPath: indexPath) as! LocationFeedCell
cell.location = location
//Replace this below line:
cell.posts = post[indexpath.row] as! String
return cell
}

Related

UICollectionView nested in UITableViewCell does not update using DispatchQueue after receiving new data

I have a UICollectionView nested inside a UITableViewCell:
The number inside a collection view cell gets updated on a different view, so when I return back to this screen, I want to be able to refresh the view and the new numbers are reflected in their cells. I have a model called topUserModel in my collection view that I populate with data from my firebase database. When I pull down to refresh, the following function is run from inside my main table view:
#objc func refreshView(refreshControl: UIRefreshControl) {
DispatchQueue.main.async {
//this is the row that the collection view is in
if let index = IndexPath(row: 1, section: 0) as? IndexPath {
if let cell = self.homeTableView.cellForRow(at: index) as? TopUserContainerViewController {
cell.userCollectionView.reloadData()
}
}
}
refreshControl.endRefreshing()
}
Which then runs my awakeFromNib() in collection view triggering:
func fetchTopUsers() {
topUserModel.removeAll()
let queryRef = Database.database().reference().child("users").queryOrdered(byChild: "ranking").queryLimited(toLast: 10)
queryRef.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String : AnyObject] {
let topUser = TopUser(dictionary: dictionary)
self.topUserModel.append(topUser)
}
DispatchQueue.main.async {
self.userCollectionView.reloadData()
}
})
}
note that the first thing I do is remove all data from the topUserModel. After storing the new data and appending it (see above), I can print out the value of that integer in that block of code to screen and it displays as the updated value.
However in my collection view (see below), if I were to print out the integer value at any point here (it's called watchTime), it still displays the old value even though the topUserModel has been wiped clean and new data has been added?:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topUserCell", for: indexPath) as! TopUsersViewController
if topUserModel.count > indexPath.row {
//image stuff redacted
Nuke.loadImage(
with: ImageRequest(url: url).processed(with: _ProgressiveBlurImageProcessor()),
options: options,
into: cell.topUserImage
)
cell.topUserName.text = topUserModel[indexPath.row].username
cell.topUserMinutes.text = "\(String(describing: topUserModel[indexPath.row].watchTime!))"
}
return cell
}
You shouldn't call dequeueReusableCell anywhere but in cellForRowAt.
In order to get the currently displayed cell (if any) you use cellForRowAt:; this may return nil if the row isn't currently onscreen.
Once you have the cell you can reload it's data and refresh its collection view.
Your codes:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topUserCell", for: indexPath) as! TopUsersViewController
if topUserModel.count > indexPath.row {
//image stuff redacted
Nuke.loadImage(
with: ImageRequest(url: url).processed(with: _ProgressiveBlurImageProcessor()),
options: options,
into: cell.topUserImage
)
cell.topUserName.text = topUserModel[indexPath.row].username
cell.topUserMinutes.text = "\(String(describing: topUserModel[indexPath.row].watchTime!))"
}
return cell
}
For example, if current indexPath is (1, 0), but the cell is reused from an indexPath (100, 0). Because it is out of screen, and is reused to display new content.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topUserCell", for: indexPath) as! TopUsersViewController
if topUserModel.count > indexPath.row {
// ... Update to new content
}
// If topUserModel.count <= indexPath.row, then the cell content is undefined.
// Use the original content of reuse cell.
// Ex, it may be come from (100, 0), but current indexPath is (1, 0).
// ...
// You can add a property to the cell to observe the behavior.
if (cell.indexPath != indexPath) {
// Ops ....
}
// Update to current indexPath
cell.indexPath = indexPath
return cell

Adding sections to UITableView in 'Edit' Mode

I'm working on a basic list workout app right now that keeps track of workouts and then the exercises for each workout. I want to extend the current 'editing' mode of a TableViewController to allow for more advanced editing options. Here is what I have so far:
As you can see, I am inserting a section at the top of the table view so that the title of the workout can be edited. The problem I am facing is twofold:
There is no animation when the edit button is tapped anymore.
When you try to swipe right on one of the exercises (Squat or Bench press) the entire section containing exercises disappears.
I start by triggering one of two different functions on the setEditing function, to either switch to read mode or edit mode based on whether the boolean editing returns true or false.
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: true)
tableView.setEditing(editing, animated: true)
if editing {
switchToEditMode()
} else {
switchToReadMode()
}
}
Then I either insert the "addTitle" section (the text field seen in the second image) to an array called tableSectionsKey which I use to determine how to display the table (seen further below), and then reload the table data.
func switchToEditMode(){
tableSectionsKey.insert("addTitle", at:0)
self.tableView.reloadData()
}
func switchToReadMode(){
tableSectionsKey.remove(at: 0)
self.tableView.reloadData()
}
Here is my tableView data method. Basically the gist of it is that I have the array called tableSectionsKey I mentioned above, and I add strings that relate to sections based on what mode I'm in and what information should be displayed. Initially it just has "addExercise", which related to the "Add exercise to routine" cell
class WorkoutRoutineTableViewController: UITableViewController, UITextFieldDelegate, UINavigationControllerDelegate {
var tableSectionsKey = ["addExercise"]
}
Then in viewDidLoad I add the "exercise" section (for list of exercises) if the current workout routine has any, and I add the addTitle section if it's in new mode, which is used to determine if the view controller is being accessed from an add new workout button or a from a list of preexisting workouts (so to determine if the page is being used to create a workout or update an existing one)
override func viewDidLoad() {
super.viewDidLoad()
if workoutRoutine.exercises.count > 0 {
tableSectionsKey.insert("exercise", at:0)
}
if mode == "new" {
tableSectionsKey.insert("addTitle", at: 0)
}
}
Then in the cellForRowAt function I determine how to style the cell based on how the section of the table relates with an index in the tableSectionsKey array
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = indexPath.section
let sectionKey = tableSectionsKey[section]
let cellIdentifier = sectionKey + "TableViewCell"
switch sectionKey {
case "addTitle":
guard let addTitleCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? AddTitleTableViewCell else {
fatalError("Was expecting cell of type AddTitleTableViewCell.")
}
setUpAddTitleTableViewCell(for: addTitleCell)
return addTitleCell
case "exercise":
guard let exerciseCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? ExerciseTableViewCell else {
fatalError("Was expecting cell of type ExerciseTableViewCell.")
}
let exercise = workoutRoutine.exercises[indexPath.row]
setUpExerciseTableViewCell(for: exerciseCell, with: exercise)
return exerciseCell
case "addExercise":
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
return cell
default:
fatalError("Couldn't find section: \(section), in WorkoutRoutineTableView" )
}
}
private func setUpExerciseTableViewCell(for cell: ExerciseTableViewCell, with exercise: Exercise) {
let titleText = exercise.name
let detailsText = "\(exercise.sets)x\(exercise.reps) - \(exercise.weight)lbs"
cell.titleLabel.text = titleText
cell.detailsLabel.text = detailsText
}
private func setUpAddTitleTableViewCell(for cell: AddTitleTableViewCell) {
cell.titleTextField.delegate = self
if (workoutRoutine.title != nil) {
cell.titleTextField.text = workoutRoutine.title
}
// Set the WorkoutRoutineTableViewController property 'titleTextField' to the 'titleTextField' found in the addTitleTableViewCell
self.titleTextField = cell.titleTextField
}
This isn't all of my code but I believe it is all of the code that could be relevant to this problem.
Your animation issue is due to the use of reloadData. You need to replace the uses of reloadData with calls to insert or delete just the one section.
func switchToEditMode(){
tableSectionsKey.insert("addTitle", at:0)
let section = IndexSet(integer: 0)
self.tableView.insertSections(section, with: .left) // use whatever animation you want
}
func switchToReadMode(){
tableSectionsKey.remove(at: 0)
let section = IndexSet(integer: 0)
self.tableView.deleteSections(section, with: .left) // use whatever animation you want
}

Table view cell doesn't show updated data

I reached a correct value and printed it during the debug sessions. However, when i run the application, the calculated value (newcalory) doesn't show up the specific table cell text field. (aka. cell.itemTotalCalory.text) Do you have any ideas for the solution?
*I attached the related code blocks below.
Thanks a lot,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! IngredientTableViewCell
cell.ingredientNameTextField.text = ingredients [indexPath.row].ingredientName
cell.numberofItem.text = "1"
let cellcalory = ingredients [indexPath.row].ingredientCalory
cell.itemTotalCalory.text = cellcalory
cell.plusButton.tag = Int(cell.itemTotalCalory.text!)! //indexPath.row
cell.plusButton.addTarget(self, action:#selector(plusAction), for: .touchUpInside)
cell.minusButton.tag = Int(cell.itemTotalCalory.text!)!
cell.minusButton.addTarget(self, action:#selector(minusAction), for: .touchUpInside)
return cell
}
#IBAction func plusAction(sender: UIButton)
{
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell") as! IngredientTableViewCell
let buttonRow = sender.tag
if cell.numberofItem.text == "1" || cell.numberofItem.text != "1"
{
cell.numberofItem.text = "1"
let textValue1 = cell.numberofItem.text
var textValue = Int(textValue1!)
textValue = textValue! + 1
cell.numberofItem.text = String(describing: textValue)
let oldcalory = buttonRow
cell.itemTotalCalory.text = String (((textValue! * Int(oldcalory)) + Int(oldcalory)))
let newcalory = cell.itemTotalCalory.text
refresh(newcalory: newcalory!);
}
}
func refresh(newcalory :String)
{
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell") as! IngredientTableViewCell
cell.itemTotalCalory.text = newcalory
DispatchQueue.main.async {
self.ingredientTableView.reloadData()
}
}
What you should do is to update the value in ingredients array and then call ingredientTableView.reloadData() to reflect this to the UI.
Calling dequeueReusableCell(withIdentifier:) in refresh method will not work as expected for what are you trying to do:
For performance reasons, a table view’s data source should generally
reuse UITableViewCell objects when it assigns cells to rows in its
tableView(_:cellForRowAt:) method. A table view maintains a queue or
list of UITableViewCell objects that the data source has marked for
reuse. Call this method from your data source object when asked to
provide a new cell for the table view. This method dequeues an
existing cell if one is available or creates a new one using the class
or nib file you previously registered. If no cell is available for
reuse and you did not register a class or nib file, this method
returns nil.
So, refresh method should be similar to:
func refresh() {
// updating ingredients array upon reqs satisfaction...
// and then:
ingredientTableView.reloadData()
// nameOfYourRefreshControl.endRefreshing()
}
Also, if you are pretty sure that you want to get a specific cell from the tableView, you might want to use cellForRow(at:) instance method:
Returns the table cell at the specified index path.
func refresh() {
let cell = ingredientTableView?.cellForRow(at: YOUR_INDEX_PATH)
//...
}
Hope this helped.
I found the solution, the lines that are listed below are useless.
let cell = ingredientTableView.dequeueReusableCell(withIdentifier: "cell") as! IngredientTableViewCell
cell.itemTotalCalory.text = newcalory
I updated the ingredient array with the new value inside the plusAction function and my problem solved. Thanks for all postings.

Swift Button inside CustomCell (TableView) passing arguments to targetMethod

My TableView features custom Cells which have a button to display corresponding detailed info in another view.
This thread here got me started and I tried to implement the approach with the delegate inside the customCell:
How to access the content of a custom cell in swift using button tag?
What I want to achieve is that when I click on the button it reads the name of the cell and passes it on to the next controller. However it seems that I cannot pass the name with the delegate method and its field is nil.
How can I get the specific content of a cell when clicking on its button?
This is what I did so far:
In the class creating my own cell I set delegate:
protocol CustomCellDelegate {
func cellButtonTapped(cell: DemoCell)
}
(........)
var delegate: CustomCellDelegate?
#IBAction func buttonTapped(sender: AnyObject) {
delegate?.cellButtonTapped(self)
}
In the TableViewController I have the following:
override func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier("FoldingCell",
forIndexPath: indexPath) as! DemoCell
cell.delegate = self
//TODO: set all custom cell properties here (retrieve JSON and set in cell), use indexPath.row as arraypointer
let resultList = self.items["result"] as! [[String: AnyObject]]
let itemForThisRow = resultList[indexPath.row]
cell.schoolNameClosedCell.text = itemForThisRow["name"] as! String
cell.schoolNameOpenedCell.text = itemForThisRow["name"] as! String
self.schoolIdHelperField = itemForThisRow["name"] as! String
cell.schoolIntroText.text = itemForThisRow["name"] as! String
//call method when button inside cell is tapped
cell.innerCellButton.addTarget(self, action: #selector(MainTableViewController.cellButtonTapped(_:)), forControlEvents: .TouchUpInside)
cell.school_id = itemForThisRow["name"] as! String
// cell.schoolIntroText.text = "We from xx University..."
return cell
}
And finally the target method when the button inside the cell is clicked
func cellButtonTapped(cell: DemoCell) {
print("the school id: ")
print(cell.schoolNameOpenedCell) //this line throws an error EXC_BAD_ACCESS 0x0
}
Firstly, the object innerCellButton is not a Cell, it's a button. The simple way to solve your problem is, just refer the index of the button. Please find the below method.
func cellButtonTapped(AnyObject: sender) {
let resultList = self.items["result"] as! [[String: AnyObject]]
//Get the tag value of the selected button.
//Button tag should be matching with the corresponding cell's indexpath.row
let selectedIndex = sender.tag
let itemForThisRow = resultList[selectedIndex]
print("the school id: \(itemForThisRow[\"name\"])")
}
* And set each button's tag as indexPath.row *
E.g.,
override func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
// Dequeue your cell and other code goes here.
// set the button's tag like below.
cell.innerCellButton.tag = indexPath.row
return cell
}
Close. I wouldn't use Suresh's method since it does not help find the IndexPath, which includes section and row.
First, I would recommend a model object for your table view data source. Learn more about the MVC pattern as well as parsing a JSON response to an object with mapping. However, this would give you the data you want.
func cellButtonTapped(cell: UITableViewCell) {
let indexPath = tableView.indexPathForCell(cell)
let resultList = self.items["result"] as! [[String: AnyObject]]
let itemForThisRow = resultList[indexPath.row]
let name = itemForThisRow["name"] as! String
}

TableView showing wrong data after segue

This is a project is a simple car's dictionary, I am using core data, from a .csv file uploaded from a server.
When I select the word in the first tableview trigger a second page to read the definition in another tableview, there is the problem is always showing incorrect word and definition.
You are ignoring the section number in the index path you get from tableView.indexPathForSelectedRow. For a sectioned table, you need to translate a section/row combination into a data reference.
A standard way of doing that is with an array of arrays (e.g. dictionaryItems:[[Dictionary]]). That way, you can get an array of items by using the index path section on the outer array and the specific item by using the index path row on the array the section reference returns.
--- UPDATE with methods that need code changes in DictionaryTableViewController
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Assume a single section after a search
return (searchController.active) ? 1 : sectionTitles.count
}
// Create a standard way to get a Dictionary from an index path
func itemForIndexPath(indexPath: NSIndexPath) -> Dictionary? {
var result: Dictionary? = nil
if searchController.active {
result = searchResults[indexPath.row]
} else {
let wordKey = sectionTitles[indexPath.section]
if let items = cockpitDict[wordKey] {
result = items[indexPath.row]
}
}
return result
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! DictionaryTableViewCell
if let dictionary = itemForIndexPath(indexPath) {
cell.wordLabel.text = dictionary.word
cell.definitionSmallLabel.text = dictionary.definition
} else {
print("Cell error with path \(indexPath)")
}
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDictionaryDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let destinationController = segue.destinationViewController as! DictionaryDetailViewController
if let dictionary = itemForIndexPath(indexPath) {
destinationController.dictionary = dictionary
} else {
print("Segue error with path \(indexPath)")
}
searchController.active = false
}
}
}
I checked your code and think the trouble is with destinationController.dictionary = (searchController.active) ? searchResults[indexPath.row] : dictionaryItems[indexPath.row]
you should get dictionary like this (as you did in cellForRowAtIndexPath):
let dictionary = (searchController.active) ? searchResults[indexPath.row]: dictionaryItems[indexPath.row]
let wordKey = sectionTitles[indexPath.section]
let items = cockpitDict[wordKey]
Now item will be the dictionary to pass to detail view.
I got this idea, when I saw you are populating your data in table view very efficiently.

Resources