UITableView 中的计时器



UPDATE:此代码已使用以下答案建议的最新修复进行了更新。感谢所有帮助过的人。

我正在创建一个应用程序,其中在UITableView中显示多个计时器,如这些图像计时器列表和菜单中所示。

我遵循 MVC 范式,并将模型、控制器和视图彼此分开。所以我有

  • 计时器类,计时器所在的位置。
  • 一个UITableViewController
  • A UITableViewCell
  • 一个UIViewController,我在其中配置计时器。

基本上一切都工作正常,除了我无法在每个单元格的标签中"显示"计时器。请注意,我已经编码 6 个月了,这将是我的第一个具有 UITableView 的应用程序,并学习 MVC 的基础知识。

因此,应用程序的工作方式是用户添加新的计时器,然后通过点击"开始"按钮,计时器应该开始倒计时。这些是NSTimers。单击开始后,计时器将被触发并运行,但它们不会在标签上向用户显示。这就是我的问题所在。

如果有人有建议或可以帮助我弄清楚,我将不胜感激。

这是我的代码。

定时器类:

@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!()
    }
  }
}

表视图控制器

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()
    }
  }
}

表视图单元格

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
  }
}

和菜单视图控制器

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!)
    }
}
}

您的主要问题是您将计时器的委托分配为视图控制器的新实例 - 委托必须是屏幕上的现有视图控制器。

就协议而言,您有正确的想法,但您的协议缺少一条关键信息 - reloadTime函数需要提供计时器实例作为参数。 这将使视图控制器能够知道它正在处理哪个计时器,而不必重新加载整个表,这在视觉上没有吸引力。

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)
  }
}

为了简洁起见,我只展示您需要更改的表视图控制器方法

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)"
        }
    }
} 

试试这个方式:

在时间类中替换

var delegate: Reloadable = TimerTableViewController()

weak var dataSource: Reloadable?

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

var delegate: Reloadable = TimerTableViewController()是指TimerTableViewController()的单独实例

将来,如果您有多个计时器关闭,您将需要使用 tableView.reloadRowsAtIndexPathstableView.reloadSections .

只是出于我自己的好奇心和一些快速的练习,我制定了自己的解决方案。我让计时器单元格按预期更新和维护状态。我没有在单元格上设置图像,因为您已经弄清楚了该部分。我就是这样做的。任何人都可以随意纠正我的 Swift,我主要使用 Obj-C 工作。

这是我的UITableViewController子类

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)
    }
}

这是我的UITableViewCell子类

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
    }
}

这是我的计时器数据源

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]
        )
    }
}

最新更新