I have a table view where I am getting data from Firebase. The table is in a view controller that is part of a navigation controller. When coming to the view from it's parent view, the table data always displays correctly. When I go to the detail view when I click a row and come back, the changes I have made to my data are not showing even though the data values are the new values.
I am loading the table in viewWillAppear so that I can reload the data when coming back from the detail view controller. I am using DispatchQueue.main.async since the UI code is in a completion handler from the Firebase Call. I can set breakpoints and see that the data is being updated from FB and the table is reloading all the rows.
The specific issue I am having is an image, which is hidden by default, should no longer be hidden based on a data value from FB. I can step in the debugger and see that the data says not to hide the image, the image.isHidden = false is called, yet the image doesn't appear. All of my UI calls are wrapped in DispatchQueue.main.async if they are called from a closure. The image will show if I come from the parent view controller to the VC with the tableview, but when going back from the detail VC it doesn't show.
According to the code it should be working when stepping through in the debugger, the data values are correct and the image is being set to hidden/not hidden correctly.
Here is the ViewController Class that has the TableView
override func viewDidLoad() {
super.viewDidLoad()
}
// We want the table to reload every time the page appears
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
RouteService.instance.getRouteByIdFromFirebase(id: route.id) { [weak self] (route) in
guard let self = self else {return}
self.route = route
// Initialize the ViewModel as the table view delegate
self.routeDetailsVM.initialize(route : route,routeDetailsVC : self)
self.segmentsTableView.dataSource = self.routeDetailsVM
self.segmentsTableView.delegate = self.routeDetailsVM
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.segmentsTableView.reloadData()
}
}
}
The UITableViewCell Class
class TravelSegmentTableViewCell: UITableViewCell {
static let cellHeight : CGFloat = 100
static let identifier = "TravelSegmentTableViewCell"
#IBOutlet weak var completedImage: UIImageView!
func initToModel(segment : Segment, routeDetailsVM : RouteDetailsVM) {
setCompletedImageVisible(segment: segment,completedImage : completedImage)
}
func setCompletedImageVisible(segment : Segment,completedImage : UIImageView) {
if (segment.status == .finished) {
// This is getting called correctly but when coming
// back from the details VC it's not showing the image
// When coming from parent VC it works
completedImage.isHidden = false
}
}
UITableViewDelegate Class
// Cell for Row at Index Path Method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let segment = route.segments[indexPath.row]
let type = segment.type
switch (type) {
case .pickup :
if let cell = tableView.dequeueReusableCell(withIdentifier: PickupSegmentTableViewCell.identifier, for: indexPath) as? PickupSegmentTableViewCell {
cell.initToModel(segment: segment,routeDetailsVM: self)
return cell
} else {
let cell = PickupSegmentTableViewCell()
cell.initToModel(segment: segment, routeDetailsVM: self)
return cell
}
case .delivery :
if let cell = tableView.dequeueReusableCell(withIdentifier: DeliverySegmentTableViewCell.identifier, for: indexPath) as? DeliverySegmentTableViewCell {
cell.initToModel(segment: segment,routeDetailsVM: self)
return cell
} else {
let cell = DeliverySegmentTableViewCell()
cell.initToModel(segment: segment, routeDetailsVM: self)
return cell
}
// This is the cell type in question
case .travel :
if let cell = tableView.dequeueReusableCell(withIdentifier: TravelSegmentTableViewCell.identifier, for: indexPath) as? TravelSegmentTableViewCell {
cell.initToModel(segment: segment, routeDetailsVM: self)
} else {
let cell = TravelSegmentTableViewCell()
cell.initToModel(segment: segment, routeDetailsVM: self)
return cell
}
default :
return UITableViewCell()
}
print("RouteDetailsVM:cellForRowAt indexPath ERROR - Returning blank UITableViewCell for type = \(type)")
return UITableViewCell()
}
The reason for the behavior is if you look inside cell for row at index path and at the .travel switch, I never return the tableViewCell that I dequeued. The code would fall through to the bottom and return a default UITableViewCell instead of the TravelSegmentTableViewCell
Here is what the code should look like
....
case .travel :
if let cell = tableView.dequeueReusableCell(withIdentifier: TravelSegmentTableViewCell.identifier, for: indexPath) as? TravelSegmentTableViewCell {
cell.initToModel(segment: segment, routeDetailsVM: self)
// THIS IS WHAT WAS MISSING
return cell
} else {
let cell = TravelSegmentTableViewCell()
cell.initToModel(segment: segment, routeDetailsVM: self)
return cell
}
default :
return UITableViewCell()
}
return UITableViewCell()
}
Related
I have one collection view, where in each cell i have one background view. So whenever user select any cell that particular cell view background color will change.
But now the problem is its background color is changing...but if i select another cell the previous selected cell view background color should be change to normal color.That is not happening.
the previous cell view background color also still as selected state
here is my vc didselectmethod :
let cell = chartCollectionView.cellForItem(at: indexPath) as? UserDataVC
var data = [String: Any]()
data["selectedCell"] = true
cell?.set(dataSource: data)
my collectionview cell :
class userCell: CollectionViewCell {
override func set(data: [String : AnyObject]) {
if let selectedCell = data["selectedCell"] as? Bool {
if selectedCell {
mainView.backgroundColor = UIColor.red
}
}
}
}
Any solution would be helpful
What's mainView?
You should generally change either the cell's contentView or assign a backgroundView and selectedBackgroundView to a cell.
Additionally and a bit unrelated, I would use a different method for changing content in response to events:
I would never change a cell directly by down casting it.
Instead, I would change the data I use to initialize the cells, and then reload the cells I want to change, and then re-create (actually, it would probably be re-used) the cells as needed using collectionView(_:cellForItemAt:).
This way, you never need to down cast.
Your current implementation is very poor.
I'd suggest saving the highlighted cell's index as a property, and accessing it to highlight or unhighlight when it dequeues. For example:
// Your CollectionView Delegate class
var currentHighlightedCellIndex: Int?
Then in your collectionView(_:cellForItemAt:):
// Dequeue the cell
...
if let selectedIndex = self.currentHighlightedCellIndex, selectedIndex == indexPath.row {
cell.selectedCell = true
} else {
cell.selectedCell = false
}
// Return the cell
...
In your collectionView(_:didSelectItemAt:):
guard let cell = collectionView.cellForItem(at: indexPath) as? YourCustomCellClass else { fatalError() }
cell.selectedCell = true
if let previouslySelectedIndex = self.currentHighlightedCellIndex {
// Here we get the index paths for each visible cell and check wether it's selected.
let indexPaths = collectionView.visibleCells.compactMap { collectionView.indexPath(for: $0) }
for index in (indexPaths.map { $0.item }) {
if index == previouslySelectedIndex {
// We found a cell that is highlighted and visible, get it in deselect it.
guard let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { fatalError() }
cell.selectedCell = false
}
}
}
self.currentHighlightedCellIndex = indexPath.item
You can set an observed property in your custom collection view cell class that will control the background color, like so:
// Custom cell class
var selectedCell: Bool = false {
didSet {
self.mainView.backgroundColor = selectedCell ? .red : .white
}
}
I have a collection view with cells that are populated based on an array. Each cell has a UIImageView and an UILabel. I want to be able to update the text within the UILabel each time the Cell View itself is tapped. I have the tapping gesture working fine and i can print out the sender information and access the sender view and even get the restoration identifier but I can't seem to access the 'myImage' or 'myLabel' sub views within that cell which is being tapped.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! CardCell
cell.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
let railroadName = railroadNames[indexPath.item]
cell.myImage.image = #imageLiteral(resourceName:railroadName)
cell.myImage.clipsToBounds = true
cell.myImage.layer.cornerRadius = 8
cell.restorationIdentifier = railroadName
cell.myLabel.isHidden = true
cell.myLabel.text = "2"
cell.myLabel.layer.backgroundColor = UIColor(red:0.25, green:0.52, blue:0.96, alpha:1.0).cgColor
cell.myLabel.layer.cornerRadius = 18
cell.myLabel.textColor = UIColor.white
return cell
}
#objc func tap(_ sender: UITapGestureRecognizer) {
//print(sender)
//print(sender.view?.restorationIdentifier)
}
Can't access subviews. Want to take the current text from 'myLabel' and increase the count by 1 each tap.
You can use below code to get label text of cell. but i insist you to manage this in railroadNames array. so, you can save label text in array object's field and can get that field value using indexpath using below code and perform operation on based of your requirement. and then you can reload that particular cell to show effect on tableview.
Also, you should use didSelectItemAt method to get select cell event. if not necessary reason to use UITapGestureRecognizer.
#objc func tap(_ sender: UITapGestureRecognizer) {
//Get Collection view cell indexpath on based of location of tap gesture
let cellPosition = sender.location(in: self.collectionView)
guard let indexPath = self.collectionView.indexPathForItem(at: cellPosition) else{
print("Indexpath not found")
return
}
//Get Ceollection view cell
guard let cell = self.collectionView.cellForItem(at: indexPath) as? CellMainCategoryList else{
print("Could't found cell")
return
}
//Get String of label of collection view cell
let strMyLable = cell. myLabel.text
//you can process addition on based of your requirement here by getting Int from above string.
}
instead of above, you can manage it like below code also:
#objc func tap(_ sender: UITapGestureRecognizer) {
let cellPosition = sender.location(in: self.collectionViewCategaries)
guard let indexPath = self.collectionViewCategaries.indexPathForItem(at: cellPosition) else{
print("Indexpath not found")
return
}
let railroadName = railroadNames[indexPath.item]
let myLableText = railroadName.myLabelValue
//update value
railroadName.myLabelValue = "2"
self.collectionView.reloadItems(at: [indexPath])
}
Hope this helps.
I have a table view and the cells are populated with data from Firebase. In each cell there is a like button, when I like a button in a particular cell, it captures the ID of that cell and creates a node in Firebase to let me know the button was clicked (liked). Before the button is clicked, it is white and after it is clicked it turns red. Then if it is clicked again (unliked) it turns white.
#IBAction func LikeClicked(_ sender: UIButton) -> Void {
let LikedRef = FIRDatabase.database().reference().child("Likes").child((self.loggedInUser?.uid)!)
let indexPath = self.selectedIndex
let post = self.posts![(indexPath?.row)!] as! [String: AnyObject]
self.key = post["postID"] as? String
let cell = TableView.cellForRow(at: indexPath!) as! ProfileTableViewCell
if cell.Like.currentImage == #imageLiteral(resourceName: "icons8-Hearts Filled-50 (2)"){
cell.Like.setImage(#imageLiteral(resourceName: "icons8-Heart-50"), for: .normal)
// cell.RedLike.isHidden = true
FIRDatabase.database().reference().child("Likes").child((self.loggedInUser?.uid)!).child(self.key!).removeValue(completionBlock: { (error, ref) in
if error != nil {
print("error \(error)")
}else{
}})
} else{
LikedRef.observeSingleEvent(of: .value, with: { (snapshot:FIRDataSnapshot) in
if let postsDictionary = snapshot .value as? [String: AnyObject] {
var LikeStatus = postsDictionary[self.key!] as? String ?? ""
if self.key == LikeStatus
{
// cell.Like.isHidden = true
cell.Like.setImage(#imageLiteral(resourceName: "icons8-Hearts Filled-50 (2)"), for: .normal)
}
}})
LikedRef.updateChildValues([self.key!: self.key!])
}
}
cell.Like.addTarget(self, action: #selector(LikeClicked), for: UIControlEvents.touchUpInside)
cell.Like.tag = indexPath.row
print(indexPath.row)
cell.Like.isUserInteractionEnabled = true
My problem is when I like one button on a specific cell, all the like buttons in every cell turns red. but I only want the cell I click to turn red and when I leave the app and come back all the buttons are back to being white. I want whatever button the logged in user likes to remain red regardless of if the user exits the app or not.
Okay so I spent an hour or so to just give you an idea how you can do what you need to do. Liking and unliking from your custom UITableViewCell. I've explained in each line of code I made the details. I hope this helps you out. Let me know if you have questions. Remember this is just one of the many ways you can do your task.
MyViewController.swift
class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, MyCustomCellDelegate {
// This is the array of keys that we
var likedDataKeys = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Load here the 'Likes' stuff and store its in a datasource for reference or store as well its keys.
// If data is liked, store to likedDayaKeys the key of your data.
FirebaseCall {
if liked {
self.likedDataKeys.append(keyOfYourData)
}
}
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ....
// Set the image.
let dataKey = yourDatasource[indexPath.row] // get the key or whatever data you need
// Set the delegate and key
cell.delegate = self
cell.dataKey = dataKey
if likedDataKeys.contains(dataKey) {
cell.image = redImageLike
} else {
cell.image = whiteNormalLikeImage
}
return cell
}
// MARK: - MyCustomCellDelegate
func myCustomCell(userDidTapLikeWithDataKey dataKey: String) {
// So now we can get the dataKey of the cell that is being liked or unliked.
// Check from the self.likedDataKeys if the tapped cell is liked or not.
if self.likedDataKeys.contains(dataKey) {
// If it is there, then we should call the unlike Firebase.
// Also remove it from the self.likedIndexPath and reload the tableView to update the image.
let index = self.likedDataKeys.index(of: dataKey)
self.likedDataKeys.remove(at: index)
// Call now the unlike Firebase.
} else {
// If it is not there, then we should call the like Firebase.
// Also store it to the self.likedIndexPAth
}
}
}
MyCustomCell.swift
protocol MyCustomCellDelegate: NSObjectProtocol {
// This is the delegate that will help us send the dataKey reference to the viewController
// Whenever the user taps on the like button in the cell.
func myCustomCell(userDidTapLikeWithDataKey dataKey: String)
}
class MyCustomCell: UITableViewCell {
// This will be called in the viewController, pass here the self of the viewController
weak var delegate: MyCustomCellDelegate?
// Make sure to pass here the key from the cellForRow of the viewController's tableView delegate.
var dataKey = ""
#IBAction func LikeClicked(_ sender: UIButton) -> Void {
// Call the delegate to inform the viewController
self.delegate?.myCustomCell(userDidTapLikeWithDataKey: self.dataKey)
}
}
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
}
I'm writing an app to control home kit accessories (lightbulbs), except in my view controller, when I update say the Boolean on/off status of a characteristic, it takes me two iterations of going back and forth between the view controller for it to reflect the changed values.
Could this be a problem with the delegate? I don't know what to do with this.
I have an accessory view UISwitch for each table cell that controls the power state of a lightbulb, and I have a UISwitch at the toolbar at the top that is supposed to turn all of the lights on or off.
here is my cellForRowAtIndexPath method, where powercharacteristic is an array of the power Status characteristics of all the lightbulbs,
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Configure the cell...
let cellIdentifier = "DeviceTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! DeviceTableViewCell
cell.deviceName.text = devices[indexPath.row].name
//creating the switch
let deviceSwitch = UISwitch(frame: CGRect.zero) as UISwitch
//init the switch
updateValues()
let isOn = powerCharacteristic[(indexPath as NSIndexPath).row].value as! Bool
deviceSwitch.setOn(isOn, animated: true)
//letting switch know what cell called it
deviceSwitch.tag = (indexPath as NSIndexPath).row
//adding selector
deviceSwitch.addTarget(self, action: #selector(changeStatusFromButton), for: .valueChanged)
//adding cell in table
cell.accessoryView = deviceSwitch
//on Off Label
if isOn {
cell.isOnOrOff.text = "On"
} else {
cell.isOnOrOff.text = "Off"
}
return cell
}
and this part works. But when I flip the switch at the top,
#IBAction func mainSwitchToggled(_ sender: AnyObject) {
writeAllValue()
}
which calls
func writeAllValue() {
for characteristic in powerCharacteristic{
characteristic.writeValue(mainSwitch.isOn, completionHandler: {(error) in
if error != nil {
print("Something went wrong with writing all value")
} else {
print("Writing all Value Successful")
}})
}
tableView.reloadData()
}
It gets off cycle, and ends up turning them !on or !off depending on the state of the switch.