J Arango
J Arango

Reputation: 951

Timer in UITableViews

UPDATE: This code has been updated with the latest fixes suggested by the answers below. Thanks to everyone who helped.

I am creating an app, where I have several timers being displayed in a UITableView, as shown in these images Timers List and Menu.

I am following the MVC paradigm, and have the Model, Controller, and View separated from each other. So I have

Basically everything is working fine, except that I am not able to "Display" the timers in the label of each cell. Please note that I am 6 months into coding and this will be my first app that has UITableView, and learning the basics of MVC.

So how the application works is that a user adds a new timer, then by tapping on the button "start" the timer should start counting down. These are NSTimers. The timers ARE being triggered and running once you click on start, but they are not being displayed to the user on the label. That's where my problem is.

If anyone has an advice or can help me figure it out, I will really appreciate it.

Here is my code.

Timer Class:

@objc protocol Reloadable: class {
@objc optional func reloadTime()
}

class Timer {

// MARK: Properties
var time: Int
var displayTime: Int
var photo: UIImage
weak var dataSource: Reloadable?

// MARK: - Methods
init(time: Int, displayTime: Int, photo: UIImage){
    self.time = time
    self.displayTime = displayTime
    self.photo = photo
}

/// The Timer properties and Methods start from here ------

// MARK: - Timer Properties
var counterRun = NSTimer()
var colorRun = NSTimer()
var startTime = NSTimeInterval()
var currentTime = NSTimeInterval()

// MARK: - Timer Mothods
func startTimeRunner(){
    counterRun = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector:"timeRunner:", userInfo: nil, repeats: true)
    startTime = NSDate.timeIntervalSinceReferenceDate()
}

@objc func timeRunner(timer: NSTimer){
    currentTime = NSDate.timeIntervalSinceReferenceDate()

    let elapsedTime: NSTimeInterval = currentTime - startTime
    ///calculate the minutes in elapsed time.
    let minutes = UInt8(elapsedTime / 1)
    let minutesInt = Int(minutes)
    displayTime = time - minutesInt

     "reloadTime()" in the TimerTableVIewController.
    if let myDelegate = self.dataSource {
        myDelegate.reloadTime!()
    }
  }
}

The TableViewController

class TimerTableViewController: UITableViewController, ButtonCellDelegate, Reloadable{

// MARK: Properties
var timers = [Timer]()

override func viewDidLoad() {
    super.viewDidLoad()

    /// Loads one timer when viewDidLoad
    let time = Timer(time: 30, displayTime: 30, photo: UIImage(named: "Swan")!)
    timers.append(time)

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

     self.navigationItem.leftBarButtonItem = self.editButtonItem()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Table view data source

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

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return timers.count
}


override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("TimerCell", forIndexPath: indexPath) as! TimerTableViewCell

    let time = timers[indexPath.row]

    cell.timeLabel.text = "\(time.displayTime)"
    cell.photo.image = time.photo

    /// Makes TimerTableViewController (self) as the delegate for TimerTableViewCell.
    if cell.buttonDelegate == nil {
        cell.buttonDelegate = self
    }
    return cell
}

// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    // Return false if you do not want the specified item to be editable.
    return true
}

// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        timers.removeAtIndex(indexPath.row)
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    } else if editingStyle == .Insert {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }    
}

// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {

}

// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    // Return false if you do not want the item to be re-orderable.
    return true
}

/*
// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
}
*/

/// Unwind segue, the source is the MenuViewController.
@IBAction func unwindToTimerList(sender: UIStoryboardSegue){
    if let sourceViewController = sender.sourceViewController as? MenuViewController, time = sourceViewController.timer {

        let newIndexPath = NSIndexPath(forRow: timers.count, inSection: 0)
        timers.append(time)
        tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
    }
}

/// With the help of the delegate from TimerTableViewCell, when the "start" button is pressed, it will 
func cellTapped(cell: TimerTableViewCell) {
    let cellRow = tableView.indexPathForCell(cell)!.row
    let timer = timers[cellRow]

    timer.dataSource = self
    timer.startTimeRunner()
}

func reloadTime(){
    if self.tableView.editing == false {
        self.tableView.reloadData()
    }
  }
}

The TableViewCell

protocol ButtonCellDelegate {
func cellTapped(cell: TimerTableViewCell)
}

class TimerTableViewCell: UITableViewCell, UITextFieldDelegate{

// MARK: Properties
@IBOutlet weak var startButtonOutlet: UIButton!
@IBOutlet weak var refreshButtonOutlet: UIButton!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var photo: UIImageView!

var buttonDelegate: ButtonCellDelegate?

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

    /// UITextFieldDelegate to hide the keyboard.
    textField.delegate = self
}

override func setSelected(selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)

    // Configure the view for the selected state
}
@IBAction func startButton(sender: UIButton) {
    if let delegate = buttonDelegate {
        delegate.cellTapped(self)
    }
}

func textFieldShouldReturn(textField: UITextField) -> Bool {
    /// Hide the keyboard
    textField.resignFirstResponder()
    return true
  }
}

And the MenuViewController

class MenuViewController: UIViewController {

// MARK: Properties 

@IBOutlet weak var swanPhoto: UIImageView!
@IBOutlet weak var duckPhoto: UIImageView!
@IBOutlet weak var minsPhoto: UIImageView!
@IBOutlet weak var okButtonOutlet: UIButton!

var timer: Timer?
var photo: UIImage? = UIImage(named: "Swan")
var time: Int? = 30
var displayTime: Int? = 30


override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


// MARK: Actions

@IBAction func swantButton(sender: UIButton) {
     photo = UIImage(named: "Swan")
}

@IBAction func duckButton(sender: UIButton) {
    photo = UIImage(named: "Duck")
}

@IBAction func okButton(sender: UIButton) {
}
@IBAction func cancelButton(sender: UIButton) {
    self.dismissViewControllerAnimated(true, completion: nil)
}

@IBAction func min60(sender: UIButton) {
    time = 60
    displayTime = 60
}

@IBAction func min30(sender: UIButton) {
    time = 30
    displayTime = 30
}

@IBAction func min15(sender: UIButton) {
    time = 15
    displayTime = 15
}

// MARK: Navegation

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if okButtonOutlet === sender {
        let photo = self.photo
        let time =  self.time
        let displayTime = self.displayTime

        timer = Timer(time: time!, displayTime: displayTime!, photo: photo!)
    }
}
}

Upvotes: 1

Views: 1247

Answers (3)

Paulw11
Paulw11

Reputation: 114828

Your main issue is that you are assigning the delegate for your timer as a new instance of the view controller - the delegate needs to be the existing view controller that is on screen.

As far as the protocol goes, you have the right idea, but your protocol is lacking one crucial piece of information - the reloadTime function needs to provide the timer instance as an argument. This will enable the view controller to know which timer it is dealing with rather than having to reload the entire table, which is visually unappealing.

protocol Reloadable {
    func reloadTime(timer:Timer)
}

func ==(lhs: Timer, rhs: Timer) -> Bool {
    return lhs.counterRun == rhs.counterRun
}

class Timer : Equatable {

// MARK: Properties
var time: Int
var displayTime: Int
var photo: UIImage
var delegate?

// MARK: - Methods
init(time: Int, displayTime: Int, photo: UIImage){
    self.time = time
    self.displayTime = displayTime
    self.photo = photo
}

// MARK: - Timer Properties
var counterRun = NSTimer()
var colorRun = NSTimer()
var startTime = NSTimeInterval()
var currentTime = NSTimeInterval()

// MARK: - Timer Mothods
func startTimeRunner(){
    counterRun = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector:"timeRunner:", userInfo: nil, repeats: true)
    startTime = NSDate.timeIntervalSinceReferenceDate()
}

@objc func timeRunner(timer: NSTimer){
    currentTime = NSDate.timeIntervalSinceReferenceDate()

    let elapsedTime: NSTimeInterval = currentTime - startTime
    ///calculate the minutes in elapsed time.
    let minutes = UInt8(elapsedTime / 1)
    let minutesInt = Int(minutes)
    displayTime = time - minutesInt

    print(displayTime)
    delegate?.reloadTime(self)

  }

}

For the sake of brevity, I will only show the table view controller methods that you need to change

override func viewDidLoad() {
    super.viewDidLoad()

    let time = Timer(time: 30, displayTime: 30, photo: UIImage(named: "Swan")!)
    time.delegate=self
    self.timers.append(time)

    self.navigationItem.leftBarButtonItem = self.editButtonItem()
}

@IBAction func unwindToTimerList(sender: UIStoryboardSegue){
    if let sourceViewController = sender.sourceViewController as? MenuViewController, time = sourceViewController.timer {

        let newIndexPath = NSIndexPath(forRow: timers.count, inSection: 0)
        time.delegate=self
        timers.append(time)
        tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
    }
}

func reloadTime(timer:Timer){
    if let timerIndex=self.timers.indexOf(timer) {
        let indexPath=NSIndexPath(forRow:timerIndex, inSection:0)
        if let cell=self.tableView.cellForRowAtIndexPath(indexPath) as? TimerTableViewCell {
            cell.timeLabel.text = "\(timer.displayTime)"
        }
    }
} 

Upvotes: 1

Dare
Dare

Reputation: 2587

Just for my own curiosity and some swift practice I worked out my own solution to this. I have the timer cells updating and maintaining state as expected. I did not set an Image on the cells since you have already figured that part out. This is how I did it. Anyone feel free to correct my Swift, I work in Obj-C primarily.

Here is my UITableViewController subclass

import UIKit

class TimerTable: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.registerClass(TimerTableViewCell.self, forCellReuseIdentifier: TimerTableViewCell.reuseIdentifier())

        self.startObserving()
    }

    // MARK: - Notifications

    func startObserving()
    {
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "timerFired:", name: TimerTableDataSourceConstants.TimerTableDataSource_notification_timerFired, object: nil)
    }

    // MARK: - Table view data source / delegate

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

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

    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return TimerTableViewCell.cellHeight()
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(TimerTableViewCell.reuseIdentifier(), forIndexPath: indexPath) as! TimerTableViewCell
        let time = TimerTableDataSource.sharedInstance.currentTimeForIndexPath(indexPath)
        cell.configureLabelWithTime("\(time)")

        return cell
    }

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

        TimerTableDataSource.sharedInstance.toggleTimerForIndexPath(indexPath)
    }

    // MARK: - Imperatives

    func timerFired(note: AnyObject?){
        let ip = note?.valueForKey("userInfo")?.valueForKey(TimerTableDataSourceConstants.TimerTableDataSource_userInfo_timerIndexPath) as! NSIndexPath
        self.tableView.reloadRowsAtIndexPaths([ip], withRowAnimation: .Automatic)
    }

}

Here is my UITableViewCell subclass

import UIKit

    // MARK: - Constants

struct TimerTableViewCellConstants {
    static let reuseIdentifier = "TimerTableViewCell_reuseIdentifier"
    static let cellHeight : CGFloat = 60.0
}


class TimerTableViewCell: UITableViewCell {

    var timerLabel = UILabel()

    // MARK: - Lifecycle
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.setup()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - Setup
    func setup()
    {
        let label = UILabel()
        label.textAlignment = .Center
        self.addSubview(label)

        let leadingConstraint = NSLayoutConstraint(item: label, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1.0, constant: 0)
        let trailingConstraint = NSLayoutConstraint(item: label, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1.0, constant: 0)
        let topConstraint = NSLayoutConstraint(item: label, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1.0, constant: 0)
        let bottomConstraint = NSLayoutConstraint(item: label, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1.0, constant: 0)

        label.translatesAutoresizingMaskIntoConstraints = false;
        self.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])

        self.timerLabel = label
    }

    // MARK: - Imperatives

    func configureLabelWithTime(time: String)
    {
        self.timerLabel.text = time
    }

    // MARK: - Accessors

    class func reuseIdentifier() -> String
    {
        return TimerTableViewCellConstants.reuseIdentifier
    }

    class func cellHeight() -> CGFloat
    {
        return TimerTableViewCellConstants.cellHeight
    }

}

Here is my data source for the timers

import UIKit

//NSNotificationCenter Constants
struct TimerTableDataSourceConstants {
    static let TimerTableDataSource_notification_timerFired = "TimerTableDataSource_notification_timerFired"
    static let TimerTableDataSource_userInfo_timerIndexPath = "TimerTableDataSource_userInfo_timerIndexPath"
}

class TimerTableDataSource: NSObject {
    //Datasource Singleton
    static let sharedInstance = TimerTableDataSource()

    var timerDict = NSMutableDictionary()

    // MARK: - Accessors

    func currentTimeForIndexPath(ip: NSIndexPath) -> Int {
        if let timerDataArray = timerDict.objectForKey(ip) as? Array<AnyObject>
        {
            return timerDataArray[1] as! Int
        }

        return 30
    }

    // MARK: - Imperatives

    func toggleTimerForIndexPath(ip: NSIndexPath)
    {
        if let timer = timerDict.objectForKey(ip) as? NSTimer{
            timer.invalidate()
            timerDict.removeObjectForKey(ip)
        }else{
            let timer = NSTimer(timeInterval: 1.0, target: self, selector: "timerFired:", userInfo: ip, repeats: true)
            NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
            timerDict.setObject([timer, 30], forKey: ip)
        }
    }

    func timerFired(sender: AnyObject)
    {
        let timer = sender as! NSTimer
        let indexPath = timer.userInfo as! NSIndexPath
        let timerDataArray = timerDict.objectForKey(indexPath) as! Array<AnyObject>
        var timeRemaining: Int = timerDataArray[1] as! Int

        if (timeRemaining > 0){
            timeRemaining--
            timerDict.setObject([timer, timeRemaining], forKey: indexPath)
        }else{
            timer.invalidate()
        }


        NSNotificationCenter.defaultCenter().postNotificationName(
            TimerTableDataSourceConstants.TimerTableDataSource_notification_timerFired,
            object: nil,
            userInfo: [TimerTableDataSourceConstants.TimerTableDataSource_userInfo_timerIndexPath : indexPath]
        )
    }
}

Upvotes: 0

wxcoder
wxcoder

Reputation: 655

Try it this way:

In the time class replace

var delegate: Reloadable = TimerTableViewController() with

weak var dataSource: Reloadable?

func timeRunner(timer: NSTimer) {
...
if let myDelegate = self.dataSource {
    myDelegate.reloadTime()
}

var delegate: Reloadable = TimerTableViewController() refers to a separate instance of TimerTableViewController()

In the future if you have multiple timers going off you will want to use tableView.reloadRowsAtIndexPaths or tableView.reloadSections.

Upvotes: 0

Related Questions