【问题标题】:Timer in UITableViewsUITableViews 中的计时器
【发布时间】:2015-10-17 03:30:07
【问题描述】:

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

我正在创建一个应用程序,其中有几个计时器显示在 UITableView 中,如这些图像 Timers ListMenu 所示。

我遵循 MVC 范例,并将模型、控制器和视图相互分离。所以我有

  • 计时器类,计时器所在的位置。
  • 一个 UITableViewController
  • 一个 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!()
    }
  }
}

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

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

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

【问题讨论】:

  • 您可以看到cell.photo.image = time.photo,但看不到cell.timeLabel.text = "\(time.time)"
  • 是的,照片和时间标签都可以看到,但那是您创建一个包含这些值的单元格的时候。但是当计时器运行时,标签不会更新。
  • 这可能是因为您在计时器运行时没有重新加载单元格,因此单元格没有最新数据。尝试在单元格中创建一个视图并在视图中附加计时器标签。
  • 等等什么?大声笑我,我是这些事情的新生儿哈哈。
  • 即使您正在运行计时器,单元格也不会更新,因为没有调用tableView.reloadData(),因此标签不知道您提供的新信息。我建议尝试将其附加到视图而不是单元格本身并让它以这种方式运行。

标签: ios iphone swift uitableview timer


【解决方案1】:

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

就协议而言,您的想法是正确的,但是您的协议缺少一个关键信息——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)"
        }
    }
} 

【讨论】:

  • 嘿,保罗,非常感谢您帮助我。即使我让它工作了,我也想尝试你的方法,因为我是新手,不仅在 swift 方面,而且在编码方面。我做了您建议的更改,并且在 reloadTime 方法上出现错误。错误指向“self.timers.indexOf(timer)”,说“无法将 Timer 类型的值转换为预期的参数 @noescape (Timer) throws -> Bool”。任何想法为什么?
  • Timer 类需要实现Equatable== 运算符才能使indexOf 工作。我已经更新了我的答案
  • 好吧,我的新朋友保罗大声笑,你已经把它带到了一个完全不同的水平。非常感谢您的回答,它完成了工作。也让我感到困惑,因为我从未见过 Equatable。你能解释一下它在做什么吗?我还必须在您的 reloadTimer 方法建议中修复 2 个小问题。 (forRow: timerIndex, inSection:0) 缺少逗号。而cell.timeLabel.text = timer.displayTime我将其更改为“(timer.displayTime)”。再次感谢您
  • equatable 协议允许您为您的类定义 == 运算符。没有它,Swift 不知道如何测试两个计时器的相等性,因此它无法确定 Timer 是否在数组中。抱歉,我假设 displayTime 是一个字符串(像 'display' 这样的名字暗示你要显示的人是一个字符串)。
【解决方案2】:

试试这个方法:

在时间类替换

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

【讨论】:

  • 我做了您在此处发布的更改,但它仍然不会更改标签.. 加上“弱”会给我一个错误,所以我不得不取消关键字“弱” .
  • @JhoanArango 将您的协议定义为一个类:protocol Reloadable: class { },它将照顾弱者。
  • 会这样做!谢谢,我正忙于工作,所以我稍后会处理它。谢谢你的帮助!!
  • 嘿,非常感谢,我得到了它与您的解决方案一起使用,我忘记委派“TimerTableViewController”。在它运行良好之后,我还必须进行一些配置,因为每次我想删除一个单元格时,tableView.reloadData() 都会关闭我的“删除”按钮。但我也解决了这个问题。谢谢您的帮助。我还在这里更新了我的代码,以便您可以看到最新的更改。如果您有更多意见,我们将不胜感激。
  • @JhoanArango 很高兴你能够让它工作! :) 我喜欢 Paul 使用 reloadTime 函数的方法,而不是重新加载表格。
【解决方案3】:

出于我自己的好奇心和一些快速的练习,我制定了自己的解决方案。我让计时器单元按预期更新和维护状态。我没有在单元格上设置图像,因为您已经弄清楚了那部分。我就是这样做的。任何人都可以随意纠正我的 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]
        )
    }
}

【讨论】:

  • 感谢分享。我想制作一些 cmet 来帮助那些正在学习 iOS 的人。使某些东西成为单例,比如这里的表格视图数据源,当它不需要是单例时,违背了 iOS 中存在的约定。我不认为它对于这个问题是必要的。此外,与使用委托模式更新视图相比,使用通知中心更新计时器视图具有更大的开销。委派通常比观察快,它可能是更新此问题中的视图的首选解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多