I have a disconcerting issue in that I have a UITableViewCell that does not update the displayed value of its underlying data. To the code:
class ReviewInspectionViewController: UIViewController {
private lazy var locationsDataSource: ReviewInspectionDataSource = ReviewInspectionDataSource(tableView: tableView, delegate: self)
override func viewWillAppear(_ animated: Bool) {
.. retrieve data from Realm
.. process data and place in data object defined as var data : [Any] = []
locationsDataSource.data.append((location.title,data))
}
}
class ReviewInspectionDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let location = data[indexPath.section]
let item = location.content[indexPath.row]
if let item = item as? String {
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ReviewChecklistItemCell.self)
cell.descriptionLabel.text = item
return cell
}
....
}
}
Works fine the first time and the correct string is shown on the screen. I tab to a different view (the underlying UIViewController is in a UITabViewController), make a change and then tab back, I can confirm that the changed data is being set correctly in this line:
cell.descriptionLabel.text = item
I can even print out the value of cell.description.text by adding a line like this:
cell.descriptionLabel.text = item
print("Cell value", cell.descriptionLabel.text)
and it prints out the changed value BUT the screen shows the old value. The UITableViewCell itself is extremely simple:
class ReviewChecklistItemCell: UITableViewCell, NibReusable {
#IBOutlet weak var descriptionLabel: UILabel!
}
The datasource class is loaded from the UIViewController.viewWillAppear method holding onto the UITableView. I have never seen this happen before, thoughts on what the issue is?
It sounds like you are updating the cell description label instead of updating the actual source of the data.
So instead of
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ReviewChecklistItemCell.self)
cell.descriptionLabel.text = item
I would update your data source at that indexPath
dataSource[indexPath.row] = item
tableView.reloadData()
Related
I try to sort the tableViewCells by numbers inside a label, so the cell which includes the highest number in a label should be last, and vice versa.
I tried it with different solutions like following, but it's simply not working, it also doesn't show any error code
I don't know if there is just a small mistake or if it is all completely wrong, but if so, I hope that you know a completely different way to solve it.
TableView:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// download jobs
jobsRef.observe(.value, with: { (snapshot) in
self.jobs.removeAll()
for child in snapshot.children {
let childSnapshot = child as! DataSnapshot
let job = Job(snapshot: childSnapshot)
print(job)
self.jobs.insert(job, at: 0)
}
filterLocation()
self.tableView.reloadData()
})
}
var jobArr = JobTableViewCell.jobDistance!.jobArr
func filterLocation() {
jobArr.sort() { $0.distance.text > $1.distance.text}
}
TableViewCell:
#IBOutlet weak var distance: UILabel!
static var jobDistance: JobTableViewCell?
var jobArr = [JobTableViewCell.jobDistance!.distance.text]
override func layoutSubviews() {
super.layoutSubviews()
JobTableViewCell.jobDistance = self
}
lets check out apple doc for the table view https://developer.apple.com/documentation/uikit/uitableviewdatasource
as it says there is method:
func tableView(UITableView, cellForRowAt: IndexPath) -> UITableViewCell
we can read it like "give me[UITableView] cell[-> UITableViewCell] for this index[cellForRowAt]"
so all we need is just map our data source to tableview indexes:
e.g.
we have datasource array of strings
var dataSource = ["String", "Very long string", "Str"]
sort...
> ["Str", "String", "Very long string"]
and then just provide our data to cell (your tableview must conform UITableViewDataSource protocol)
// Provide a cell object for each row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch a cell of the appropriate type.
let cell = tableView.dequeueReusableCell(withIdentifier: "cellTypeIdentifier", for: indexPath)
// Configure the cell’s contents.
cell.textLabel!.text = dataSource[indexPath]
return cell
}
The problem is you sort another array jobArr
jobArr.sort() { $0.distance.text > $1.distance.text}
and append values to another one jobs
Before anyone suggests to pull the Firebase data from within the PlayerController's viewWillAppear, I already know how to do that and if I did it that way I know how to pass the data to the ScoreController. In this situation I need to pull the data directly from within the cell and somehow pass the data back from there.
I have a tableView inside a PlayerController that displays the randomPower, name, and score of each player. Inside the tableView's cell I pull the name and score from Firebase using a function getScoreDataFromFirebase(). The function is located directly inside the tableView's PlayerCell and once I get the values from Firebase I initialize the cell's name and score outlets right then and there.
Inside the tableView's cellForRowAtIndexPath I call cell.getScoreDataFromFirebase() and everything works fine because both outlets display the correct values.
From that point on I have a ScoreController. When a tableView cell is chosen the score is sent to the ScoreController.
The problem is since I'm pulling the data directly from within the cell itself the only way I could pass the score (pulled from Firebase) to ScoreController was to 1st set a didSet score property inside the cell.
Still inside the cell when I pull the score data from Firebase 2nd I initialize the score property with it
3rd inside the tableView's cellForAtIndexPath I use an if let to pass the value from the cell's score property to the the tableData.
When I first try to send the indexPath of that tableData over to the ScoreController sometimes it's nil even though the cell's score property definitely has a value (I used to break points to check). If I select any of the very first few tableView cells that are visible they will have a nil value for the score property. However if I scroll further down through the cells and back up then those same cells will no longer have a nil score property.
What I found out was the if let statement was running before the Firebase code was pulled so the score property was nil for first few cells that are on scene. The odd thing is everything works fine once I start scrolling.
How can I pass a value pulled directly from a cell to the tableView's didSelectRow?
PlayerModel:
class PlayerModel{
name:String?
score:String?
randomPower:String?
}
TableViewCell SubClass:
class PlayerCell:UITableViewCell{
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var scoreLabel: UILabel!
#IBOutlet weak var randomPowerLabel: UILabel!
internal var score: String?{
didSet{
print("**********\(score ?? "*********")")
}
}
override func prepareForReuse() {
super.prepareForReuse()
nameLabel.text = " "
scoreLabel.text = " "
}
func getScoreDataFromFirebase(){
let scoreRef = usersRef?.child("score")
scoreRef?.observe( .value, with: {
(snapshot) in
for child in snapshot.children{
let user = child as! DataSnapshot
for player in user.children{
let eachPlayer = player as! DataSnapshot
if let dict = eachPlayer.value as? [String:Any]{
let name = dict["name"] as? String ?? " "
let score = dict["score"] as? String ?? " "
self.nameLabel.text = name
self.scoreLabel.text = score
self.score = score
}
}
}
}
}
}
TableView:
class PlayerController: UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak fileprivate var tableView: UITableView!
var players = [PlayerModel]() // this array is populated with data from a previous vc. The number of players in the array are the same exact number of players that's pulled from the getScoreDataFromFirebase() function
override func viewDidLoad() {
super.viewDidLoad()
tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return players.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PlayerCell", for: indexPath) as! PlayerCell
let cellData = players[indexPath.row]
cellData.randomPowerLabel.text = cellData.power
cell.getScoreDataFromFirebase()
if let score = cell.score{
cellData.score = score
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let scoreVC = storyboard?.instantiateViewController(withIdentifier: "ScoreController") as! ScoreController
scoreVC.score = players[indexPath.row].score
}
You can achieve this using delegation :
Create a protocol
protocol UpdateValueDelegate: class {
func changeValue(score: String, row: Int)
}
Your UIViewController should look like this :
PlayController : UITableViewDataSource, UITableViewDelegate, UpdateValueDelegate
{
var scoreDict:[String:String] = [:]
//
//
func changeValue(score: String, row: Int)
{
self.scoreDict["\(row)"] = score
}
}
In cellForRowAtIndexPath set cell.delegate = self and cell.row = indexPath.row
Your UITableViewCell should look like this :
class PlayerCell:UITableViewCell: UITableViewCell
{
weak var delegate: UpdateValueDelegate?
var row: Int?
//
//
}
Finally pass score from getScoreDataFromFirebase by calling delegate function:
func getScoreDataFromFirebase()
{
//
//
delegate. changeValue(score: localScore, row: self.row)
}
Now you have the value in your viewController from where it can be easily passed to didSelectRow using the global dictionary ** scoreDict**.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
var score = self.scoreDict["\(indexPath.row)"]
// Use score
}
I have a table view (controller: MetricsViewController) which gets updated from a CoreData database. I have used prototype cells (MetricsViewCell) which I have customized for my needs. It contains a segmented control, a UIView (metricsChart, which is used to display a chart - animatedCircle), and some UILabels.
MetricsViewCell:
class MetricsViewCell: UITableViewCell {
var delegate: SelectSegmentedControl?
var animatedCircle: AnimatedCircle?
#IBOutlet weak var percentageCorrect: UILabel!
#IBOutlet weak var totalPlay: UILabel!
#IBOutlet weak var metricsChart: UIView! {
didSet {
animatedCircle = AnimatedCircle(frame: metricsChart.bounds)
}
}
#IBOutlet weak var recommendationLabel: UILabel!
#IBOutlet weak var objectType: UISegmentedControl!
#IBAction func displayObjectType(_ sender: UISegmentedControl) {
delegate?.tapped(cell: self)
}
}
protocol SelectSegmentedControl {
func tapped(cell: MetricsViewCell)
}
MetricsViewController:
class MetricsViewController: FetchedResultsTableViewController, SelectSegmentedControl {
func tapped(cell: MetricsViewCell) {
if let indexPath = tableView.indexPath(for: cell) {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
var container: NSPersistentContainer? = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer { didSet { updateUI() } }
private var fetchedResultsController: NSFetchedResultsController<Object>?
private func updateUI() {
if let context = container?.viewContext {
let request: NSFetchRequest<Object> = Object.fetchRequest()
request.sortDescriptors = []
fetchedResultsController = NSFetchedResultsController<Object>(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: "game.gameIndex",
cacheName: nil)
try? fetchedResultsController?.performFetch()
tableView.reloadData()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Object Cell", for: indexPath)
if let object = fetchedResultsController?.object(at: indexPath) {
if let objectCell = cell as? MetricsViewCell {
objectCell.delegate = self
let request: NSFetchRequest<Object> = Object.fetchRequest()
...
...
}
}
}
return cell
}
When a user selects one of the segments in a certain section's segmented control, MetricsViewController should reload the data in that particular row. (There are two sections with one row each). Hence, I've defined a protocol in MetricsViewCell to inform inform my controller on user action.
Data is being updated using FetchedResultsTableViewController - which basically acts as a delegate between CoreData and TableView. Everything is fine with that, meaning I am getting the correct data into my TableView.
There are two issues:
I have to tap segmented control's segment twice to reload the data in the row where segmented control was tapped.
The table scrolls back up and then down every time a segment from segmented control is selected.
Help would be very much appreciated. I've depended on this community for a lot of issues I've faced during the development and am thankful already :)
For example, in Animal Recognition section, I have to hit "Intermediate" two times for its row to be reloaded (If you look closely, the first time I hit Intermediate, it gets selected for a fraction of second, then it goes back to "Basic" or whatever segment was selected first. Second time when I hit intermediate, it goes to Intermediate). Plus, the table scroll up and down, which I don't want.
Edit: Added more context around my usage of CoreData and persistent container.
Instead of using indexPathForRow(at: <#T##CGPoint#>) function to get the indexPath object of cell you can directly use indexPath(for: <#T##UITableViewCell#>) as you are receiving the cell object to func tapped(cell: MetricsViewCell) {} and try to update your data on the UI always in main thready as below.
func tapped(cell: MetricsViewCell) {
if let lIndexPath = table.indexPath(for: <#T##UITableViewCell#>){
DispatchQueue.main.async(execute: {
table.reloadRows(at: lIndexPath, with: .none)
})
}
}
Your UISegmentedControl are reusing [Default behaviour of UITableView].
To avoid that, keep dictionary for getting and storing values.
Another thing, try outlet connection as Action for UISegmentedControl in UIViewController itself, instead of your UITableViewCell
The below code will not reload your tableview when you tap UISegmentedControl . You can avoid, delegates call too.
Below codes are basic demo for UISegmentedControl. Do customise as per your need.
var segmentDict = [Int : Int]()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...29 // number of rows count
{
segmentDict[i] = 0 //DEFAULT SELECTED SEGMENTS
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SOTableViewCell
cell.mySegment.selectedSegmentIndex = segmentDict[indexPath.row]!
cell.selectionStyle = .none
return cell
}
#IBAction func mySegmentAcn(_ sender: UISegmentedControl) {
let cellPosition = sender.convert(CGPoint.zero, to: tblVw)
let indPath = tblVw.indexPathForRow(at: cellPosition)
segmentDict[(indPath?.row)!] = sender.selectedSegmentIndex
print("Sender.tag ", indPath)
}
(Before you mark as duplicate you have to read the whole question and I am posting this cause I din't found the relevant and proper solution also need the solution in swift)
I have created one demo project and load and displayed name and area from array on custom cell.
I have noticed that after every 5th cell means 6th row is repeating with contents of 0th cell
for e.g.
the demo code is given below
class demoTableCell: UITableViewCell {
#IBOutlet var name : UILabel!
#IBOutlet var area : UILabel!
}
extension ViewController:UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.arrDemo.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
var cell : demoTableCell = demoTable.dequeueReusableCell(withIdentifier: cellIdentifier)! as! demoTableCell
cell.name.text = (arrDemo.object(at: indexPath.row) as! NSDictionary).value(forKey: "Name") as? String
cell.area.text = (arrDemo.object(at: indexPath.row) as! NSDictionary).value(forKey: "Area") as? String
if indexPath.row == 0{
cell.name.isHidden = true
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
}
As I hide the first label on 0th cell so I found that 6th row is also effected with implemented functionality of 0th cell. It means that also hide label1 of every 6th cell as I have attached the screenshot below so you can get the exact issue (This issue happened only if table view is scrollable)
As I have try to solve this issue from this link also but facing the same issue and cannot find the proper solution, and I am stuck here.
Cells are reused, you have to make sure that every UI element is set to a defined state.
You are using an if clause but there is no else case or a default value.
Simple solution:
Just replace
if indexPath.row == 0 {
cell.name.isHidden = true
}
with
cell.name.isHidden = indexPath.row == 0
this sets the hidden property always to a defined state.
And the usual dont's
Do not use NSDictionary in Swift.
Do not valueForKey unless you really need KVC (actually here you don't).
Remember - the cells are being reused.
You hide the cell, but you never explicitly unhide the cell
When you come to row 6, you are re-using the cell that was at row 0, and isHidden = true
All you need to do is extend your check, and hide the rows that you need to be hidden, and explicitly show the cells that you need to see. If you also have a moving banner that you add - you will also need to check to see if it's been loaded, and remove it if not required. Remember - it may not be row 6 - that's just how it works out with the current screensize
If you do have significant differences between the cells you want to use, you might be better using two different classes - and then you don't have to think about hiding labels
class demoTableCell: DemoTableCellNormalRow {
#IBOutlet var name : UILabel!
#IBOutlet var area : UILabel!
}
class demoTableCell: DemoTableCellFirstRow {
#IBOutlet var area : UILabel!
#IBOutlet var movingBannerView : LCBannerView!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
if row == 0 {
var cell : demoTableCell = demoTable.dequeueReusableCell(withIdentifier: cellIdentifier)! as! DemoTableCellFirstRow
cell.area.text = (arrDemo.object(at: indexPath.row) as! NSDictionary).value(forKey: "Area") as? String
// populate the bannerview which already exists, or create a new one
return cell
} else {
var cell : demoTableCell = demoTable.dequeueReusableCell(withIdentifier: cellIdentifier)! as! DemoTableCellNormalRow
cell.name.text = (arrDemo.object(at: indexPath.row) as! NSDictionary).value(forKey: "Name") as? String
cell.area.text = (arrDemo.object(at: indexPath.row) as! NSDictionary).value(forKey: "Area") as? String
return cell
}
}
Implement prepareForReuse in your cell class
override func prepareForReuse() {
name.isHidden = false
}
I have made a custom cell class which fetches data from firebase. Everything's working fine but what happens is when new data is added, is gets displayed at the bottom not the top. In the code i have mentioned to insert new item to index path 0. Still the code is not working
Here's the code..
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
#IBOutlet weak var save: UIButton!
#IBOutlet weak var table: UITableView!
var firebase = Firebase(url: "https://meanwhile.firebaseio.com")
var messages = [String]()
var uid:String = ""
override func viewDidLoad() {
super.viewDidLoad()
var localArray = [String]()
firebase.observeEventType(.ChildAdded, withBlock: { snapshot in
//print(snapshot.value)
let msgText = snapshot.value.objectForKey("text") as! String
localArray.insert(msgText, atIndex: 0)
self.messages = localArray
self.table.reloadData()
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.table.dequeueReusableCellWithIdentifier("Vish", forIndexPath: indexPath) as! CustomTableViewCell
let fdata = self.messages[indexPath.item]
cell.data.text = fdata as? String
return cell
}
}
I don't see the code here, but you mention you are using a custom table cell to load data.
If you are loading data inside of your CustomTableViewCell class, you either have to be very careful about how you do it, or move the data loading for the cells elsewhere.
This is because of dequeueReusableCellWithIdentifier. UITableView does not create a UITableViewCell for ever row of the table, but instead it maintains a smaller queue of cells that it reuses to reduce memory usage and increase performance.
The returned UITableViewCell object is frequently one that the application reuses for performance reasons.
Check if you are sharing state (that you didn't expect to be) and/or have race-conditions inside of your CustomTableViewCell.
These bugs often manifest as table view data appearing in the wrong cell.
Source: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/index.html#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath: