How to update tableView cells? - ios

I have a tableView and I want update it every 10 seconds. For it I do:
var timer = NSTimer.scheduledTimerWithTimeInterval(10, target: self, selector: "reloadTable", userInfo: nil, repeats: true)
func reloadTable() {
print("reload")
self.tableView.reloadData()
}
but it doesn't reload correctly. What it means:
Yes, it reloads, but my data doesn't update, but when I drag my tableView to top and leave it, my tableView cells' data update. How can I achieve this effect programmatically?
UPDATE
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> onlineUserCell {
let cell:onlineUserCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! onlineUserCell
let user = OneRoster.userFromRosterAtIndexPath(indexPath: indexPath)
cell.username.text = user.displayName
if user.unreadMessages.intValue > 0 {
cell.backgroundColor = .orangeColor()
} else {
cell.backgroundColor = .whiteColor()
}
configurePhotoForCell(cell, user: user)
cell.avatarImage.layer.cornerRadius = cell.avatarImage.frame.size.height / 2
cell.avatarImage.clipsToBounds = true
return cell;
}

I'm not 100% sure, but if the table isn't visually updating, you probably aren't calling the UI code on the main thread. All UI code needs to be executed on the main thread in iOS.
Look up dispatch_async and/or performSelectorOnMainThread.

You need to update your datasource programmatically too.The reloadData() method just refresh the cell.If you don't refresh the datasource(the actually array) there is noting different.

Related

Not able to call a function from itself after an interval

I want to keep calling following function for 'n' number of times, I'll write that code later. Now am facing a problem.
func keepHighlighting(myLbl : UILabel)
{
myLbl.text = "hi"
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for : indexPath) as! TableViewCell
let mySelector = #selector(self.keepHighlighting(duaLbl : cell.tempLbl))
let timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: mySelector, userInfo: nil, repeats: true);
timer.fire()
return cell
}
Error:
Argument of '#selector' does not refer to an '#objc' method, property, or initializer
You've misunderstood selectors. self.keepHighlighting(duaLbl : cell.tempLbl) is a function call. A selector is the name of a method (it's technically a message, but those generally resolve into a method call).
You cannot pass parameters via a selector. The method you're calling from a timer should have the signature void someMethod(_ timer: Timer). For that, the selector would be #selector(someMethod). (That compiles down to someMethod:, but you generally won't have to deal with that fact.)
Depending on what you're trying to do here, there are several approaches, but it's not quite clear what you're trying to do. My recommendation, though, is to put this logic into the cell itself rather than trying to manage it from the controller. It is generally much easier to have each cell manage its own behaviors than try to have the controller keep track of every cell and tweak it.
You have to annotate your method with #objc and use the Timers userInfoto parse arguments:
#objc func keepHighlighting(timer:Timer)
{
let myLbl = timer.userInfo as! UILabel
myLbl.text = "hi"
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for : indexPath) as! UITableViewCell
let mySelector = #selector(self.keepHighlighting)
let timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: mySelector, userInfo: cell.tempLbl, repeats: true);
timer.fire()
return cell
}

Reloading Table View to Create Dynamic Label Changes iOS Swift

I am creating an app that I want to display different strings inside of a label that is within a UITableViewCell. However, I cannot use indexPath.row because the strings are in not particular order.
let one = "This is the first quote"
let two = "This is the second quote"
var numberOfRows = 1
var quoteNumber = 0
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let items = [one, two]
myTableView = tableView
let cell = tableView.dequeueReusableCellWithIdentifier("customcell", forIndexPath: indexPath) as! cellTableViewCell
let cellWidth = cell.frame.width
let cellHeight = cell.frame.height
cell.QuoteLabel.frame = CGRectMake(cellWidth/8, cellHeight/4, cellWidth/1.3, cellHeight/2)
let item = items[quoteNumber]
cell.QuoteLabel.text = item
cell.QuoteLabel.textColor = UIColor.blackColor()
if quoteNumber == 0 {
quoteNumber = 1
}
if numberOfRows == 1 {
numberOfRows = 2
}
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
var timer = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update() {
dispatch_async(dispatch_get_main_queue()) {
self.myTableView.reloadData()
}
This code builds and runs, but when the timer I have set expires and the update function is ran, both of the two available cells change to the same label. What I am trying to do is make it so that the first cell can remain static while the newly added cell can receive the new element from the 'items' array without using indexpath.row (because the quotes that I am going to be using from the items array will be in no particular order). Any help would be appreciated.
Instead of reloadData try using reloadRowsAtIndexPaths: and pass in the rows you want to reload. This does use indexPath, however. The only way I'm aware of to reload specific rows as opposed to the whole tableView is to specify the rows by index path.

How to update the button tag which is part of UICollectionViewCell after a cell is deleted in UICollectionView?

Here's a problem which I have been stuck at for quite some time now.
Here's the code
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({() -> Void in
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
self.collectionViewLove?.reloadData()}, completion: nil)}
I have a button inside each UICollectionViewCell which deletes it on clicking. The only way for me to retrieve the indexPath is through the button tag. I have initialized the button tag in
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
However every time I delete, the first time it deletes the corresponding cell whereas the next time it deletes the cell follwing the one I clicked. The reason is that my button tag is not getting updated when I call the function reloadData().
Ideally, when I call the reloadData() ,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
should get called and update the button tag for each cell. But that is not happening. Solution anyone?
EDIT:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: UIImage(named: "preloader"))
let item = self.wishlist?.results[indexPath.row]
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.text = item?.title
cell.itemName.numberOfLines = 1
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.price.adjustsFontSizeToFitWidth = true
cell.deleteButton.tag = indexPath.row
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = indexPath.row
cell.buyButton.backgroundColor = UIColor.blackColor()
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
A couple of things:
You're doing too much work in cellForItemAtIndexPath--you really want that to be as speedy as possible. For example, you only need to register the nib once for the collectionView--viewDidLoad() is a good place for that. Also, you should set initial state of the cell in the cell's prepareForReuse() method, and then only use cellForItemAtIndexPath to update with the custom state from the item.
You shouldn't reload the data until the deletion is complete. Move reloadData into your completion block so the delete method is complete and the view has had time to update its indexes.
However, it would be better if you didn't have to call reloadData in the first place. Your implementation ties the button's tag to an indexPath, but these mutate at different times. What about tying the button's tag to, say, the wishlist item ID. Then you can look up the appropriate indexPath based on the ID.
Revised code would look something like this (untested and not syntax-checked):
// In LoveListCollectionViewCell
override func prepareForReuse() {
// You could also set these in the cell's initializer if they're not going to change
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.numberOfLines = 1
cell.price.adjustsFontSizeToFitWidth = true
cell.buyButton.backgroundColor = UIColor.blackColor()
}
// In your UICollectionView class
// Cache placeholder image since it doesn't change
private let placeholderImage = UIImage(named: "preloader")
override func viewDidLoad() {
super.viewDidLoad()
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: placeholderImage)
let item = self.wishlist?.results[indexPath.row]
cell.itemName.text = item?.title
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.deleteButton.tag = item?.id
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = item?.id
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
func removeFromLoveList(sender: AnyObject?) {
let id = sender.tag
let index = wishlist?.results.indexOf { $0.id == id }
let indexPath = NSIndexPath(forRow: index, inSection: 0)
collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
wishlist?.results.removeAtIndex(index)
}
It's probably not a good idea to be storing data in the cell unless it is needed to display the cell. Instead your could rely on the UICollectionView to give you the correct indexPath then use that for the deleting from your data source and updating the collectionview.
To do this use a delegate pattern with cells.
1.Define a protocol that your controller/datasource should conform to.
protocol DeleteButtonProtocol {
func deleteButtonTappedFromCell(cell: UICollectionViewCell) -> Void
}
2.Add a delegate property to your custom cell which would call back to the controller on the delete action. The important thing is to pass the cell in to that call as self.
class CustomCell: UICollectionViewCell {
var deleteButtonDelegate: DeleteButtonProtocol!
// Other cell configuration
func buttonTapped(sender: UIButton){
self.deleteButtonDelegate.deleteButtonTappedFromCell(self)
}
}
3.Then back in the controller implement the protocol function to handle the delete action. Here you could get the indexPath for the item from the collectionView which could be used to delete the data and remove the cell from the collectionView.
class CollectionViewController: UICollectionViewController, DeleteButtonProtocol {
// Other CollectionView Stuff
func deleteButtonTappedFromCell(cell: UICollectionViewCell) {
let deleteIndexPath = self.collectionView!.indexPathForCell(cell)!
self.wishList.removeAtIndex(deleteIndexPath.row)
self.collectionView?.performBatchUpdates({ () -> Void in
self.collectionView?.deleteItemsAtIndexPaths([deleteIndexPath])
}, completion: nil)
}
}
4.Make sure you set the delegate for the cell when configuring it so the delegate calls back to somewhere.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//Other cell configuring here
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("identifier", forIndexPath: indexPath)
(cell as! CustomCell).deleteButtonDelegate = self
return cell
}
}
I was facing the similar issue and I found the answer by just reloading collection view in the completion block.
Just update your code like.
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
}, completion: {
self.collectionViewLove?.reloadData()
})
which is mentioned in UICollectionView Performing Updates using performBatchUpdates by Nik

How to send cell number to the function in TableViewCell

I'd like to know how to get cell number(indexPath.row) in the following tapPickView function. topView is on the UITableViewCell, and pickView is on the topView. If pickView is tapped, tapPickView is activated.
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(
"QuestionAndAnswerReuseIdentifier",
forIndexPath: indexPath) as! QuestionAndAnswerTableViewCell
cell.topView.pickView.userInteractionEnabled = true
var tap = UITapGestureRecognizer(target: self, action: "tapPickView")
cell.topView.pickView.addGestureRecognizer(tap)
return cell
}
func tapPickView() {
answerQuestionView = AnswerQuestionView()
answerQuestionView.questionID = Array[/*I wanna put cell number here*/]
self.view.addSubview(answerQuestionView)
}
First of all, you need to append : sign to your selector upon adding gesture recognizer in order for it to get the pickView as its parameter.
var tap = UITapGestureRecognizer(target: self, action: "tapPickView:")
Besides that, cells are reusable objects, so you should prevent adding same gesture again and again to the same view instance by removing previously added ones.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("QuestionAndAnswerReuseIdentifier", forIndexPath: indexPath) as! QuestionAndAnswerTableViewCell
cell.topView.pickView.userInteractionEnabled = true
cell.topView.pickView.tag = indexPath.row
for recognizer in cell.topView.pickView.gestureRecognizers ?? [] {
cell.topView.pickView.removeGestureRecognizer(recognizer)
}
cell.topView.pickView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "tapPickView:"))
return cell
}
While populating the cell, you can set tag value of the pickView as indexPath.row so you can easily query that by cellForRowAtIndexPath(_:).
cell.topView.pickView.tag = indexPath.row
Assuming you already know the section of the cell you tap on. Let's say it is 0.
func tapPickView(recognizer: UIGestureRecognizer) {
let indexPath = NSIndexPath(forRow: recognizer.view.tag, inSection: 0)
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) {
print("You tapped on \(cell)")
}
}
Hope this helps.
Assuming that this was not as simple as didSelectRowAtIndexPath, which I strongly recommend to first look into, passing the information to your method could look like this:
#IBAction func tapPickView:(sender: Anyobject) {
if let cell = sender as? UITableViewCell {
let indexPath = self.tableView.indexPathForCell(cell: cell)
println(indexPath)
}
}
Use didSelectRowAtIndexPath delegate method.

NSTimer update in real time?

I have an NSTimer and I want to update a label in real time which shows a timer's time. The following is my implementation of how I came about doing so:
override func viewDidAppear(animated: Bool) {
self.refreshTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "refreshView:", userInfo: nil, repeats: true)
var currentRunLoop = NSRunLoop()
currentRunLoop.addTimer(refreshTimer, forMode: NSRunLoopCommonModes)
}
func refreshView(timer: NSTimer){
for cell in self.tableView.visibleCells(){
var indexPath = self.tableView.indexPathForCell(cell as! UITableViewCell)
self.tableView(self.tableView, cellForRowAtIndexPath: indexPath!)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return offerCellAtIndexPath(indexPath)
}
func offerCellAtIndexPath(indexPath: NSIndexPath) -> OfferCell{
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(offerCellIdentifier) as! OfferCell
setCellContents(cell, indexPath: indexPath)
return cell
}
func setCellContents(cell:OfferCell, indexPath: NSIndexPath!){
let item = self.offers[indexPath.row]
var expirDate: NSTimeInterval = item.dateExpired()!.doubleValue
var expirDateServer = NSDate(timeIntervalSince1970: expirDate)
//Get current time and subtract it by the predicted expiration date of the cell. Subtract them to get the countdown timer.
var timer = self.modelStore[indexPath.row] as! Timer
var timeUntilEnd = timer.endDate.timeIntervalSince1970 - NSDate().timeIntervalSince1970
if timeUntilEnd <= 0 {
cell.timeLeft.text = "Finished."
}
else{
//Display the time left
var seconds = timeUntilEnd % 60
var minutes = (timeUntilEnd / 60) % 60
var hours = timeUntilEnd / 3600
cell.timeLeft.text = NSString(format: "%dh %dm %ds", Int(hours), Int(minutes), Int(seconds)) as String
}
}
As seen by the code, I try to do the following:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.timeLeft.text = NSString(format: "%dh %dm %ds", Int(hours), Int(minutes), Int(seconds)) as String
})
Which I thought would help me update the cell in real time, however when I view the cells, they are not being updated in real time. When I print out the seconds, minutes, and hours, they are being updated every 1 second which is correct. However, the label is just not updating. Any ideas on how to do this?
EDIT: DuncanC's answer has helped a bunch. I want to also delete the timers when their timers go to 0. However, I am getting an error saying that there is an inconsistency when you delete and insert most likely extremely quickly without giving the table view any time to load. Here is my implmentation:
if timeUntilEnd <= 0 {
//Countdown is done for timer so delete it.
self.offers.removeAtIndex(indexPath!.row)
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
cell.timeLeft.text = "Finished."
}
Your problem isn't with timers. Your problem is that you are handling table views totally wrong. In addition to the problems pointed out by Matt in his answer, your timer method refreshView makes no sense.
func refreshView(timer: NSTimer)
{
for cell in self.tableView.visibleCells()
{
var indexPath = self.tableView.indexPathForCell(cell as! UITableViewCell)
self.tableView(self.tableView, cellForRowAtIndexPath: indexPath!)
}
}
You are looping through the visible cells, asking for the indexPath of each cell, and then asking the table view to give you that cell again. What do you think this will accomplish? (The answer is "nothing useful".)
What you are supposed to do with table views, if you want a cell to update, is to change the data behind the cell (the model) and then tell the table view to update that cell/those cells. You can either tell the whole table view to reload by calling it's reloadData method, or use the 'reloadRowsAtIndexPaths:withRowAnimation:' method to reload only the cells who's data have changed.
After you tell a table view to reload certain indexPaths, it calls your cellForRowAtIndexPath method for the cells that need to be redisplayed. You should just respond to that call and build a cell containing the updated data.
This is madness:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return offerCellAtIndexPath(indexPath)
}
func offerCellAtIndexPath(indexPath: NSIndexPath) -> OfferCell{
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(offerCellIdentifier) as! OfferCell
setCellContents(cell, indexPath: indexPath)
return cell
}
Your job in cellForRowAtIndexPath: is to return the cell - now. But setCellContents does not return any cell to offerCellAtIndexPath, so the cell being returned is merely the empty OfferCell from the first line. Moreover, setCellContents cannot return any cell, because it contains an async, which will not run until after it has returned.
You need to start by getting off this triple call architecture and returning the actual cell, now, in cellForRowAtIndexPath:. Then you can worry about the timed updates as a completely separate problem.
You could use a dispatch timer:
class func createDispatchTimer(
interval: UInt64,
leeway: UInt64,
queue: dispatch_queue_t,
block: dispatch_block_t) -> dispatch_source_t?
{
var timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
if timer != nil
{
dispatch_source_set_timer(timer, dispatch_walltime(nil, 0), interval, leeway)
dispatch_source_set_event_handler(timer, block)
return timer!
}
return nil
}

Resources