更新:此代码已使用以下答案建议的最新修补程序进行了更新。感谢所有帮助过的人。
我正在创建一个应用,我在UITableView中显示了几个定时器,如图Timers List和Menu所示。
我遵循MVC范例,并将模型,控制器和视图相互分离。所以我有
基本上一切正常,但我无法在每个单元格的标签中“显示”定时器。请注意,我是编程的6个月,这将是我的第一个具有UITableView的应用程序,并学习MVC的基础知识。
所以应用程序的工作原理是用户添加一个新的计时器,然后通过点击按钮“start”,计时器应该开始倒计时。这些是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!)
}
}
}
答案 0 :(得分: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)"
}
}
}
答案 1 :(得分:0)
以这种方式尝试:
在时间类中替换
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.reloadRowsAtIndexPaths
或tableView.reloadSections
。
答案 2 :(得分:0)
为了我自己的好奇心和一些快速的练习,我制定了自己的解决方案。我有计时器单元更新和维护状态按预期。我没有在单元格上设置图像,因为你已经找到了那个部分。这就是我做到的。任何人都可以随意纠正我的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]
)
}
}