Brian
Brian

Reputation: 3

IOS: Updating Labels in a UITableViewCell using an NSTimer

I'm trying to create a countdown app that will show the number of Years....Seconds until an upcoming date. I want to be able to view several countdowns in a table view. I have some basic logic that works idependently of a tableview cell to create the countdown:

.
.
.
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "countdown", userInfo: nil, repeats: true)

func  countdown() {
    let hourMinuteComponents: NSCalendarUnit =
        NSCalendarUnit.CalendarUnitYear |
        NSCalendarUnit.CalendarUnitMonth |
        NSCalendarUnit.CalendarUnitDay |
        NSCalendarUnit.CalendarUnitHour |
        NSCalendarUnit.CalendarUnitMinute |
        NSCalendarUnit.CalendarUnitSecond
    let timeDifference = userCalendar.components(
        hourMinuteComponents,
        fromDate: NSDate(),
        toDate: date1.enteredDate as NSDate,
        options: nil)
// Sample Output
    println("Year: \(timeDifference.year)")
    println("Month: \(timeDifference.month)")
    println("Day: \(timeDifference.day)")
    println("Hour: \(timeDifference.hour)")
    println("Minute: \(timeDifference.minute)")
    println("Second: \(timeDifference.second)")
}

This works well and the console shows the countdown. However, I can't get past an error when I try to call it from a cell.

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFTimer row]: unrecognized selector sent to instance

Here's the code that isn't working.

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    println("Index Path: \(indexPath)")
    let cell = tableView.dequeueReusableCellWithIdentifier("countDownCell") as! UITableViewCell
    let cellIndex = indexPath
    var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "update:", userInfo: cellIndex, repeats: true)
    //let secondlabel = cell.viewWithTag(1005) as! UILabel
    //secondlabel.text = "Test"


    return cell

}


func  update(cellIndex: NSIndexPath) {
    let hourMinuteComponents: NSCalendarUnit =
    NSCalendarUnit.CalendarUnitYear |
    NSCalendarUnit.CalendarUnitMonth |
    NSCalendarUnit.CalendarUnitDay |
        NSCalendarUnit.CalendarUnitHour |
        NSCalendarUnit.CalendarUnitMinute |
        NSCalendarUnit.CalendarUnitSecond
    let timeDifference = userCalendar.components(
        hourMinuteComponents,
        fromDate: NSDate(),
        toDate: date1.enteredDate as NSDate,
        options: nil)
    println("Year: \(timeDifference.year)")
    println("Month: \(timeDifference.month)")
    println("Day: \(timeDifference.day)")
    println("Hour: \(timeDifference.hour)")
    println("Minute: \(timeDifference.minute)")
    println("Second: \(timeDifference.second)")


    if let cell = tableView.cellForRowAtIndexPath(cellIndex) {
        let yearlabel = cell.viewWithTag(1000) as! UILabel
        let secondlabel = cell.viewWithTag(1005) as! UILabel!
    }

    secondlabel.text = "\(timeDifference.second)"


}

I appreciate any help you can provide. I initially focused my fixing attempts on changing how I'm calling update() from the timer. I tried passing the cell explicitly and then thought it better to try calling the indexPath of the cell. I also checked my syntax as was suggested in a number of NSTimer posts. I think I have it right (and believe I've tried pretty much all other options).

The code breaks at this line:

if let cell = tableView.cellForRowAtIndexPath(cellIndex) { ...

Upvotes: 0

Views: 1678

Answers (3)

edelaney05
edelaney05

Reputation: 6987

I would recommend a different approach to how you've constructed your app.

UITableView will "recycle" its cells using the dequeueReusableCellWithIdentifier method. So, if a user were to create 100 countdowns with your app, but there are only ever a maximum of 4 cells visible on screen, when the user scrolls those four cells will be enqueued into table view's cache of cells and then dequeued by your data source code. This is done to give performance enhancements so the table view scrolls smoothly. You can read more about this behavior in Apple's Table View Programming Guide.

If it were me...

  1. I'd start by having an object that can track the state of a countdown: countdown start date, countdown end date, countdown name, etc.

  2. I'd use an array of those objects to be the data that is represented by the table view.

  3. Since all the countdowns update every second, you could update what's currently displayed by asking the table view what index paths is it currently showing, match those index paths to the countdown objects in your array, and then changing the text of all the different labels. This means you would only have one NSTimer instance in your view controller responsible for driving the updates.

There are plenty of other possible ways to structure this, but I think trying to manage many timer instances is going to cause some gray hairs.

Upvotes: 7

Arun Gupta
Arun Gupta

Reputation: 2636

More simplistic approach will be to keep running your nstimer az it isvand call the countdown method and reload your UITableView at the end. Keep the values to be display in an NSArray everytime before reloading and pass it as object to UITableView.

Upvotes: 0

Marcos Crispino
Marcos Crispino

Reputation: 8218

The selector passed to NSTimer's scheduledTimerWithTimeInterval(_:target:selector:userInfo:repeats:) receives a NSTimer object, not a NSIndexPath.

That is, your update(_:) method should be defined as

func update(timer: NSTimer) {
    cellIndex = timer.userInfo as! NSIndexPath
    ...
}

See the method's documentation for more information

Upvotes: 1

Related Questions