Reputation: 951
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
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
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
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