I have a number of entries (cells) in a UITableView in Swift which each represent a "song" that each user can select. The titles of the songs that have not yet been completed are represented by question marks and the others are displayed normally. The songs completed by the user are stored in a firebase database which is immediately read at the beginning of the UITableViewController lifecycle and stored in a global dictionary I called temp_dict.
The dictionary temp_dict is of the form
Key = "02": Value = "Complete",
Key = "03": Value = "Complete",
etc.
The problem is that the loading of the data into this Dictionary does not happen immediately and the initial call of the following "loading the cells" function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}
occurs before I have the information from the firebase to decide which ones have been completed or not.
I am now in a strange situation where the question marks will only appear after I have scrolled down to the bottom (there are around 30 cells) then scroll back up again. I believe this occurs because each cell is removed and added again when it is removed and reappears on the screen
What I've tried: I've tried using self.tableView.reloadData() at various stages but it does nothing for some reason.
class MainTableViewController: UITableViewController {
...
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCell(withIdentifier: "cell",
for: indexPath)
myCell.textLabel?.text = songs[indexPath.row].title
myCell.detailTextLabel?.text = songs[indexPath.row].artist
if (self.temp_dict[String(indexPath.row+1)] != "Complete") {
myCell.textLabel?.text = question_marks(s :
songs[indexPath.row].title)
myCell.detailTextLabel?.text = question_marks(s :
songs[indexPath.row].artist)
}
num_times_called = num_times_called + 1
return myCell
}
...
}
The function question_marks() just takes a string and replaces every character with a question mark to signify that the level has not been completed and so is hidden.
** songs is an array of song objects which have attributes of title, artist, song number etc.
Results:
Right now, I am greeted with all cells being entirely question marks when I start up this viewcontroller. When I scroll to the bottom and scroll back up, the songs that have been completed are now suddenly displayed as no longer question marks (which is what I want but I want this to happen immediately).
Summary:
I need a way to automatically reload all the cells on a uitableview immediately following the occurence of a particular event.
Thanks for any help.
Mr.P's answer is almost correct. I suggest to reload tableview on main thread:
var temp_dict: [String: String] {
didSet {
DispatchQueue.main.async {
tableView.reloadData()
}
}
}
Add a property observer to your temp_dict object so that it will reload the table view automatically when it is updated
var temp_dict: [String: String] {
didSet {
tableView.reloadData()
}
}
Related
I have a TableView that I want to reuse for different categories of data (essentially as plugins.. the tableView being a skeleton and being filled with whatever I want it to be). The TableView is filled with different categories of data (and related actions) depending on essentially what ViewController the user came from. I understand how to make it display the various data (just send it whatever array I want it to display), but I can't figure out how I could control the actions for the data at the specific index selected in didSelectRowAtIndexPath.
How could I do this? How could I create an array that has both Strings and executable actions associated with each indices? For example:
arrayOneNames = ["Tigris", "Leo", "Barko"]
arrayOneActions = [displayTheTigers, displayTheCats, displayTheDogs]
If "Leo" is selected in the tableView, then "displayTheCats" is executed. Again, I want each array to be a separate Class that I can use as a plugin, so that I can fill the tableView with whichever Class of data I want it to display and execute, depending on which ViewController the user came from previously. Please answer in Swift.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell=UITableViewCell() // cell you've created
cell.data = arrayOneNames[indexPath.row] // passing relevant data
cell.tag = indexPath.row // the tag you want to pass for particular data
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)! as UITableViewCell // take selected cell
if(cell.tag == 0) { // call relevant function accordingly.
self.tigrisTouch()
} else if (cell.tag == 1) {
self.leoTouch()
} else {
self.barkoTouch()
}
}
private func tigrisTouch() {
}
private func leoTouch() {
}
private func barkoTouch() {
}
I have a viewControl called PostViewController which has a UITableView of posts. I also have a class called PostCell which defines the UITableViewCell. I made a button function in PostCell called likeButtonClicked to favour a post similar to twitter.
#IBAction func likesButtonClicked(_ sender: Any) { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil, userInfo: ["cell":self, "likesButton":likesButton!]) }
This is to pass the cell indexPath and the button name to PostViewController. I need indexPath to increase the likes by 1 and the button name to change its image to pink when post is favoured.
I then subscribed to the notification in viewDidLoad of PostViewController.
NotificationCenter.default.addObserver(self, selector: #selector(postLiked), name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil)
I then wrote this function in the same viewController
#objc func postLiked(notification: Notification){
if let cell = notification.userInfo?["cell"] as? UITableViewCell{
let likesButton = notification.userInfo?["likesButton"] as? SpringButton
if let indexPath = postsTableView.indexPath(for: cell){
let post = posts[indexPath.row]
postId = post.id
PostAPI.getPostById(postId: postId) { post in
//Check if the same post were already favoured.
if !self.liked || self.oldPostId != self.postId{
self.newLikes = post.likes + 1
self.liked = true
self.oldPostId = self.postId
}else{
self.newLikes = self.newLikes - 1
self.liked = false
}
PostAPI.favourPost(postId: self.postId, likes: self.newLikes) {
PostAPI.getPostById(postId: self.postId) { postResponse in
let post = postResponse
self.posts[indexPath.row] = post
let cellNumber = IndexPath(row: indexPath.row, section: indexPath.section)
self.reloadRowData(cellNumber: cellNumber){
if !self.liked{
likesButton?.tintColor = .systemPink
}else{
likesButton?.tintColor = .darkGray
}
}
}
}
}
}
}
}
func reloadRowData(cellNumber: IndexPath, completion: #escaping () -> ()) {
self.postsTableView.reloadRows(at: [cellNumber], with: .none)
completion()
}
Please tell me why the last 4 lines of postLiked function is executed before reloadRowData function, which causes the button to change its color to pink then returns immediately to gray when it should stay pink.
Any help will be most appreciated.
Thank you.
I expect the specific problem is your call to reloadRows. As the docs note:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
So this is likely creating an entirely new cell, and then the later code modifies the old cell that is being removed from the table. (So you see the change, and then the cell is removed and replaced.)
I would start by getting rid of the entire reloadRowData call.
Generally, though, this code is fragile, and I'd redesign it. The cell should take care of setting the tint colors itself based on the data. You generally shouldn't be reaching into a cell and manipulating its subviews. This will cause you a problem when cells are recycled (for example, when this scrolls off screen). All the configuration should be done in cellForRow(at:), and the cell should observe its Post and update itself when there are changes.
Data should live in the Model. The View should observe the Model and react. The Model should not reach into the View and manipulate anything.
As a side: your reloadRowData looks async, but it's not. There's no reason for a completion handler. It could just call return.
A table view's .reloadRows(at:...) (and .reloadData()) functions are async processes.
So your reloadRowData() func is returning before the table view actually reloads the row(s).
This is a rather unusual approach - both in using NotificationCenter for your cells to communicate with the controller, and in trying to change the button's tint color by holding a reference to the button.
The tint color really should be set in cellForRowAt, based on your data source.
Edit
My description of table view data reloading being asynchronous was misleading.
The point I wanted to make was this...
If I change my data, call .reloadData(), and then change my data again:
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
the table view will not display "You Say Hello" even though we call .reloadData() when those are the values of myData.
Here's a complete, simple example to demonstrate:
class TestTableViewController: UITableViewController {
var myData: [String] = ["Some", "Song", "Lyrics"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = myData[indexPath.row]
return c
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// call my func when a row is selected
self.myReload()
}
func myReload() {
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
// table view doesn't reload itself we exit this func
// so we'll get
// I
// Say
// Goodbye
}
}
So, among the other issues described by Rob Napier in his answer, your original code was trying to change the tint color of an object in a cell before the table reloaded its data.
I am having issues getting Realm's result object to be accessed correctly using UITableView's cellForRowAt.
Here's the setup:
UITableViewController is divided into sections based on the Objects's category (a string defined in the object).
UITableViewController has a segue to a UIViewController which takes form input. That view controller writes to Realm and then makes a call back via delegation to refresh the table view data.
When that screen dismisses and returns to the UITableViewController, as I try to add the row via category, I am getting empty objects. However, when I use a for loop within cellForRowAt, I can access the data.
Here is what I'm running in this section:
func loadItems() {
itemsList = try! Realm().objects(Items.self).filter("list_id = \(list_id)").sorted(byKeyPath: "item_category")
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemListCell", for: indexPath)
let categoryName = categories.categories[indexPath.section]
let currItem = itemsList[indexPath.row]
if currItem.item_category == categoryName {
cell.textLabel!.text = currItem.item_name
}
return cell
}
It seems to be evaluating category correctly and stepping into that block, but the object's item_name and item_category is null. Here is a screenshot of the debugger inside the if statement:
Debugger Image
Is there something I need to change with how I'm using the object, pulling data, etc, to get the data correct in the object?
Found my answer here: UITableView with Multiple Sections using Realm and Swift
This is the change I made to cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemListCell", for: indexPath)
let currItem = itemsList.filter("item_category = '\(categories[indexPath.section])'")[indexPath.row]
cell.textLabel!.text = currItem.item_name
return cell
}
The issue I was having was I was constantly pulling the first position results object and not the first position within a section. I needed to narrow down to my section and then pull the first row.
I think it may have to do with your filters predicate. Try changing
itemsList = try! Realm().objects(Items.self).filter("list_id = \(list_id)").sorted(byKeyPath: "item_category"
to this
itemsList = try! Realm().objects(Items.self).filter("list_id = '\(list_id)'").sorted(byKeyPath: "item_category"
Add single quotations around '(list_id)'.
Realm Filtering
I am developing a Facebook like application, where if the post is too long, it gets cut down and at the end of it "See more" appears that has some click events. (I use FRHyperLabel for it)
Everything works fine, but I've got a question: If the formatting+event is decleared inside tableView: cellForRowAt indexPath: is it bad for the performance?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var message : message
var cell : MessageCell
let message = self.messages[indexPath.row]
// World Message
cell = tableView.dequeueReusableCell(withIdentifier: "essageCell", for: indexPath) as! MessageCell
cell.messageLabel.attributedText = message.limitMessageMax250char()
//Add link if needed
if message.count > appSettings.maxCharacters.message {
let handler = {
(hyperLabel: FRHyperLabel!, range: NSRange!) -> Void in
self.alert(title: "Test", message: "Alert: because clicked on link")
}
let text = cell.messageLabel.text!
let nsRange = text.calculateUnicodeScalar(start: text.unicodeScalars.count-8, len: 8) // See more is 8 characters
cell.messageLabel.setLinkFor(nsRange, withLinkHandler: handler)
}
return cell
}
Or should I do it in the Cell's file, when it's awakeFromNib + with delegate
IMO you don't want to do this in the for cellForRowAt call mostly because you want to make that call return quickly and also because you are only returning the formatted cell.
I would argue/recommend that you set your handler for the row when it is being initialized and that way the handler value is only being set once.
Remember that cellForRowAt is called multiple times as the user scrolls through and as cells come into view.
review the https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614861-tableview for more information as to how cells are reused.
The problem I'm facing is probably some lack of understanding on the concept of reusable cells. I have, let's say, 30 rows to be created and each of them has a UISwitch.
When I toggle one of the switches, it's behavior should affect the other 29. The point is: as far as I know, iOS doesn't create all of them at once, but rather wait to reuse the cells when the TableView is scrolled up and down.
How can I keep a copy of those reused objects and tell iOS to set the proper value to the switches?
I've thought on having the cells appended to a [UISwitch] but I can't manage to have all the 30 cells in there, look:
...
var switches = [UISwitch]()
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Field10Cell", for: indexPath) as! Field10TableViewCell
...
//cell.value is a UISwitch
if !switches.contains(cell.value) {
switches.append(cell.value)
}
return cell
}
You could create a set which stores the indexes of the cells whose switches have been pressed.
var activeSwitches = Set<IndexPath>()
Whenever a user presses the switch on a cell, you store it on the set like this:
activeSwitches.insert(indexPath)
If you need to check if a switch was activated, just check if its container cell's indexPath is in active switches like so:
if activeSwitches.contains(indexPath) {
// do something
}
In order to know when a user pressed a specific switch I recommend the folliwing:
In cellForRowAtIndexPath save the current indexPath into your Field10TableViewCell.
Create a protocol on Field10TableViewCell and add a delegate.
protocol Field10Delegate {
func didChangeSwitch(value: Bool, indexPath: IndexPath)
}
class Field10TableViewCell {
var delegate: Field10Delegate?
var indexPath: IndexPath?
#IBOutlet weak var fieldSwitch: UISwitch! // Can't use 'switch' as a variable name
#IBAction func switchValueChanged(_ sender: UISwitch) {
if let indexPath = indexPath {
delegate?.didChangeSwitch(value: sender.isOn, indexPath: indexPath)
}
}
When you create a cell, set the view controller as a delegate
let cell = tableView.dequeueReusableCell(withIdentifier: "Field10Cell", for: indexPath) as! Field10TableViewCell
cell.delegate = self
cell.indexPath = indexPath
Make your view controller comply with the protocol:
extension ViewController: Field10Delegate {
/* Whenever a switch is pressed on any cell, this delegate will
be called. This is a good place also to trigger a update to
your UI if it has to respond to switch changes.
*/
func didChangeSwitch(value: Bool, indexPath: IndexPath) {
if value {
activeSwitches.insert(indexPath)
} else {
activeSwitches.remove(indexPath)
}
updateUI()
}
}
With the above, at any point you will know which switches are active or not and you can process the dequeued cells with this information.