8

NOTE: Asking for answers in Swift please.

What I'm trying to do:

  • Have tableview cells update every 1 second and display a real-time countdown.

How I'm doing it currently:

  • I have a tableview with cells that contain a label.

  • When the cells are generated, they call a function that calculates the time between today and a stored target date, and it displays the countdown time remaining in the label.

  • To get the cells to update the label every second, I use an NSTimer that reloads the tableview every second.

  • PROBLEM: The countdown works, but when I try to do a Swipe-to-Delete action, because the NSTimer reloads the tableview, it resets the swiped cell so that it is no longer swiped and of course this is not acceptable for end users.

What I'm trying / Potential Solutions

  • I heard that a better way to do this is to use NSNotificationCenter that is triggered by NSTimer and add listeners to each cell. Every 1 second a "broadcast" is sent from the NSNotificationCenter, the cell will receive the "broadcast" and will update the label.

  • I tried adding a listener to each cell in the part of the code where it generates a new cell:

    // Generate the cells
    
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    
    var cell = tableView.dequeueReusableCellWithIdentifier("Tablecell", forIndexPath: indexPath) as UITableViewCell
    let countdownEntry = fetchedResultController.objectAtIndexPath(indexPath) as CountdownEntry
    
    // MARK: addObserver to "listen" for broadcasts
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateCountdownLabel", name: mySpecialNotificationKey, object: nil)
    
    func updateCountdownLabel() {
        println("broadcast received in cell")
        if let actualLabelCountdown = labelCountdown {
            // calculate time between stored date to today's date
            var countdownValue = CountdownEngine.timeBetweenDatesRealTime(countdownEntry.date)
            actualLabelCountdown.text = countdownValue
        }
    }
    
  • but the observer's selector only seems to be able to target functions at the view controller level, not in the cell. (I can't get "updateCountdownLabel" to fire from within the cell. When I create a function of the same name at view controller level, the function can be called)

Questions

  • So my question is: Is this is the right approach?
  • If yes, how can I get the listener to fire a function at the cell level?
  • If no, then what is the best way to achieve this real-time countdown without interrupting Swipe on Cell actions?

Thanks in advance for your help!

Dharmesh Dhorajiya
  • 3,976
  • 9
  • 30
  • 39
learning_swift
  • 107
  • 1
  • 5
  • Have you tried using `reloadRowsAtIndexPaths(_ indexPaths: [AnyObject], withRowAnimation animation: UITableViewRowAnimation)` instead of `reloadData`? – Ian MacDonald Feb 16 '15 at 04:32
  • this is how I used to reload the table cells `func timerAction() { // update the countdown label with the countdown value //tableView.reloadData() }` Would using "reloadRowsAtIndexPaths" avoid cancelling out a swipe state for the cell? And where would I call "reloadRowsAtIndexPaths" if I was to use it in this scenario? – learning_swift Feb 16 '15 at 04:38
  • You could avoid the swiped cell in your list of index paths. If you have access to the label directly to update the countdown value, you don't need to call `reload` at all. – Ian MacDonald Feb 16 '15 at 04:45
  • Great. So with this method would NSTimer's selector target a function that uses "reloadRowsAtIndexPaths" that also has a condition: if swiped, don't reload? Also, does NSNotificationCentre still play a role or should it be discarded? – learning_swift Feb 16 '15 at 04:54

2 Answers2

15

It might be a solution not reloading cells at all. Just make the cells listen to an update notification and change their label accordingly. I assume you subclass UITableViewCell and give the cell a storedDate property. You will set that property when preparing the cell.

The timer will just fire the notification.

Remember to unregister the cell from notification center in dealloc

Here is a quick an dirty example.

The View Controller containing your TableView:

class ViewController: UIViewController, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!

    var timer: NSTimer!


    //MARK: UI Updates

    func fireCellsUpdate() {
        let notification = NSNotification(name: "CustomCellUpdate", object: nil)
        NSNotificationCenter.defaultCenter().postNotification(notification)
    }

    //MARK: UITableView Data Source

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cellIdentifier = "CustomCell"
        let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! CustomTableViewCell
        cell.timeInterval = 20
        return cell
    }

    //MARK: View Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.timer = NSTimer(timeInterval: 1.0, target: self, selector: Selector("fireCellsUpdate"), userInfo: nil, repeats: true)
        NSRunLoop.currentRunLoop().addTimer(self.timer, forMode: NSRunLoopCommonModes)
    }


    deinit {
        self.timer?.invalidate()
        self.timer = nil
    }

}

The custom cell subclass:

class CustomTableViewCell: UITableViewCell {

    @IBOutlet weak var label: UILabel!

    var timeInterval: NSTimeInterval = 0 {
        didSet {
            self.label.text = "\(timeInterval)"
        }
    }

    //MARK: UI Updates

    func updateUI() {
        if self.timeInterval > 0 {
            --self.timeInterval
        }
    }

    //MARK: Lifecycle

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code

        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: Selector("updateUI"), name: "CustomCellUpdate", object: nil)
    }

    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }

}

I'm pretty sure this example doesn't adhere to your app's logic. Just showing how things are glowed together.

Matteo Piombo
  • 6,688
  • 2
  • 25
  • 25
  • This was what I was trying to do, but I'm not sure if I'm assigning the listener to the cell properly. `// MARK: addObserver to "listen" for broadcasts` `NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateCountdownLabel", name: mySpecialNotificationKey, object: nil)` I'm using Core Data to store the dates and yes, I give the cell a property for that date. The listener successfully receives the notification, but I don't know how to target a function inside the cell to update the date label. The selector can only target functions at ViewController level... – learning_swift Feb 16 '15 at 20:40
  • When a listener receives the notification-- where should I put the function the selector targets? In the cell or at the view controller level? – learning_swift Feb 16 '15 at 20:41
  • thanks! I am still pretty new to Swift so the sample code really helps. I will attempt this later tonight and post back my results. – learning_swift Feb 18 '15 at 15:43
  • when I changed to custom cells, I was no longer able to access Core Data. Could use your help here if possible: http://stackoverflow.com/questions/28653764/storing-retrieving-core-data-in-custom-cell-in-swift Thanks! – learning_swift Feb 22 '15 at 23:49
  • a lot of things changed in Swift 4 but it still works :) – J. Doe Sep 08 '17 at 22:34
  • @J.Doe really nice it can still give some help :-) – Matteo Piombo Sep 14 '17 at 19:24
1

As Ian MacDonald has suggested you should avoid reloading the cell when the timer ticks, if you are swiping. Also drop the NSNotification, as It and timer are essentially doing the samething

Raj
  • 326
  • 1
  • 7
  • If I drop `NSNotification` and strictly use `NSTimer` to update the cells, then I assume the `NSTimer` calls a function that loops through each cell and updates it's label? – learning_swift Feb 16 '15 at 20:43