I have a UITableView in which I have numerous timers set on each UITableViewCell. Each timer starts from when a user creates a "post" on my application and should expire within 24 hours. However, I want it so that when all 24 hours is over, the UITableViewCell deletes itself in real time but I can't seem to figure out where or when I should be deleting the timer. I have a method that will constantly refresh the timer every second using NSTimer.scheduledTimerWithTimeInterval and it updates the timers on each UITableViewCell every second. However, I can't find a method or find how I can find if each timer inside each UITableViewCell is finished. Obviously I can find if the timer is finished in viewDidLoad but that is only called right when the view becomes active. Is there any method I am missing or anything I can use to find if a timer via the scheduledTimerWithTimeInterval method is finished, and if it is, to delete it? Here is my code below:
//I have a self.offers array declared in the beginning of my class whcih will act as the UITableView data source.
var offers = [Offer]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(offerCellIdentifier) as! OfferCell
setCellContents(cell, indexPath: indexPath)
return cell
}
func setCellContents(cell:OfferCell, indexPath: NSIndexPath!){
let item = self.offers[indexPath.row]
cell.offerName.text = item.offerName()
cell.offerPoster.text = item.offerPoster()
var expirDate: NSTimeInterval = item.dateExpired()!.doubleValue
//Get current time and subtract it by the predicted expiration date of the cell. Subtract them to get the countdown timer.
var timeUntilEnd = expirDate - NSDate().timeIntervalSince1970
if timeUntilEnd <= 0 {
//Here is where I want to delete the countdown timer but it gets difficult to do so when you are also inserting UITableViewCells and deleting them at the same time.
self.offers.removeAtIndex(indexPath!.row)
self.offersReference = Firebase(url:"<Database Link>")
self.offersReference.removeValue()
self.tableView.reloadData()
cell.timeLeft.text = "Finished."
}
else{
//Display the time left
var seconds = timeUntilEnd % 60
var minutes = (timeUntilEnd / 60) % 60
var hours = timeUntilEnd / 3600
cell.timeLeft.text = NSString(format: "%dh %dm %ds", Int(hours), Int(minutes), Int(seconds)) as String
}
}
override func viewDidLoad() {
super.viewDidLoad()
var timeExpired = false
//I set up my offers array above with completion handler
setupOffers { (result, offer) -> Void in
if(result == true){
//Insert each row one by one.
var currentCount = self.offers.count
var indexPaths: [NSIndexPath] = [NSIndexPath]()
indexPaths.append(NSIndexPath(forRow:currentCount, inSection: 0))
self.offers.append(offer)
currentCount++
self.tableView.reloadData()
}
}
// Do any additional setup after loading the view, typically from a nib.
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.rowHeight = 145.0
}
//Called when you click on the tab
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.refreshTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "refreshView:", userInfo: nil, repeats: true)
//Should fire while scrolling, so we need to add the timer manually:
//var currentRunLoop = NSRunLoop()
//currentRunLoop.addTimer(refreshTimer, forMode: NSRunLoopCommonModes)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
refreshTimer.invalidate()
refreshTimer = nil
}
//Constantly refreshes the data in the offers array so that the time will continuously be updating every second on the screen.
func refreshView(timer: NSTimer){
self.tableView.reloadData()
}
You have a number of misconceptions, a few problems with your code, and your description isn't clear.
If you create a timer using scheduledTimerWithTimeInterval, you don't need to, and shouldn't, add it to the runloop. The scheduledTimerWithTimeInterval method does that for you.
If you create a repeating timer, it never "finishes." It keeps repeating forever. If instead you create a non-repeating timer, it fires once and then goes away. You don't need to delete it. Just don't keep a strong reference to it and it will be deallocated once it fires.
You say you create a timer for each table view cell, but the code you posted only creates a single timer for the view controller. You say "...it updates the timers on each UITableViewCell every second. However, I can't find a method or find how I can find if each timer inside each UITableViewCell is finished." That doesn't match the code you posted. Your code is running a timer once a second that simply tells the table view to reload it's data.
I guess you mean that you re-display a remaining time counter in each cell when your timer fires?
So, are you asking how to figure out if the time remaining for all of your cell's item.timeLeft() has reached zero? You haven't posted the code or the requirements for your timeLeft() function, so your readers can't tell what is supposed to be happening.
Related
I have a table view containing a dynamic number of UITableViewCells. The data that populates the cell originates from Firebase. Each record (and therefore each cell) has a field for recordTimestamp which is the Unix timestamp for when that record was added to Firebase.
I'm trying to find a way/best practice for making a "counter" in each cell that updates every second to show how many minutes/seconds it has been since recordTimestamp. The counter should continue to increment every second until a button in the cell is tapped.
I've tried using a timer object to call a function once per second which compares recordTimestamp against the current time. Then, on button tap it'd fire timer.invalidate. This method kind of worked, but after about 20 seconds of running the counters were getting all out of sync and some were a few seconds behind. It was also incredibly laggy - there are only about 10 rows in the table and you could barely scroll smoothly.
Any suggestions for achieving this?
EDIT: Code as requested. For now, I'm just trying to get it to count from 0 properly. Once I get that sorted I'll add the logic to first calculate the time difference and then increment every second.
class newRequestCell: UITableViewCell {
#IBOutlet weak var acceptButton: UIButton!
#IBOutlet weak var timeSinceRequest: UILabel!
var timer = Timer()
var counter = 0
func setRequestCell(customerRequest: customerRequest) {
customerName.text = String(customerRequest.customerName)
orderNumber.text = String(customerRequest.orderNumber)
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(self.updateCounter), userInfo: nil, repeats: true)
}
#objc func updateCounter() {
counter += 1
timeSinceRequest.text = String(counter)
}
#IBAction func acceptButtonTapped(_ sender: Any) {
//TODO: stop timer, make call to Firebase updating record status
}
}
Here's an example of the inconsistent counting. These two cells were loaded in at the same time, yet after about 6 minutes the second timer is behind by about 2 minutes.
Many thanks to #DanielStorm and #Paulw11 for the insights which led to this answer.
All logic for the timer has been moved out of the cell and into the primary ViewController.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customerRequest = activeRequestsArray[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "newRequestCell") as! newRequestCell
cell.setRequestCell(customerRequest: customerRequest)
let requestTime = NSDate(timeIntervalSince1970: TimeInterval(activeRequestsArray[indexPath.row].requestTimestamp/1000))
let calendar = Calendar.current
let timeComponents = calendar.dateComponents([.hour, .minute, .second], from: requestTime as Date)
let nowComponents = calendar.dateComponents([.hour, .minute, .second], from: Date())
let difference = calendar.dateComponents([.second], from: timeComponents, to: nowComponents).second!
cell.timeSinceRequest.text = String(difference)
cell.buttonPressed = {
print(indexPath.row)
//TODO: delete cell on button tap
}
return cell
}
So I have a UITableView and on top cell(1st cell) I have a timer on the cell.detailText. The timer displays but the tableview can’t scroll past the first cell because the cell text label is constantly being updated by this timer. What can I do to make the table view scroll properly without stopping the timer when the user scrolls?
Please help
Thanks in advance.
You don't need to keep calling reloadData(). You can do all of your timer work in the cell itself - you just need to remember to invalidate timers when you are done with them. I maintain an app that does something similar and use the following:
#IBOutlet weak var timeLeftLbl: UILabel!
var secondTimer: Timer!
var secondsLeft: Int = 0
func configureCell(item: PreviousAuctionItem) {
//Timers
secondsLeft = getSecondsLeft(endTime: item.endDate)
timeLeftLbl.text = secondsLeft
secondTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.decreaseTimeLeft), userInfo: nil, repeats: true)
}
#objc func decreaseTimeLeft() {
if secondsLeft > 0 {
secondsLeft -= 1
timeLeftLbl.text = secondsLeft.formatTimeAsString()
} else {
timeLeftLbl.text = ""
secondTimer?.invalidate()
}
}
override func prepareForReuse() {
secondTimer?.invalidate()
}
The getSeconds method is an API method I use to get how long an item has left.
Hope this helps!
I have a UITableViewCell with some buttons that have time values like the hours app. I would like to track the time on each cell whenever I click on the button related to that cell like the hours app does - as shown in the screen shot below.
I already know how deal with timers: The function below is used to update the time on a general label :
var startTime = TimeInterval()
var timer : Timer?
func updateTime() {
let currentTime = NSDate.timeIntervalSinceReferenceDate
//Find the difference between current time and start time.
var elapsedTime: TimeInterval = currentTime - startTime
//calculate the minutes in elapsed time.
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
self.timeLabel?.text = "\(strMinutes):\(strSeconds)"
//labelShake(labelToAnimate: self.timeLabel!, bounceVelocity: 5.0, springBouncinessEffect: 8.0)
}
I can put the following code in ViewDidLoad to start the timer:
timer = Timer()
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
For tapping a button in a cell, I add a tag on the cell's button to track the cell that I tapped on as shown below
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...
cell.timerViewButton.tag = indexPath.row
cell.timerViewButton.addTarget(self, action: #selector(startTimerForCell), for: UIControlEvents.touchUpInside)
//...
return cell
}
// I can track the button clicked here.
func startTimerForCell(sender : UIButton) {
print("SelectedCell \(sender.tag)")
}
Can anyone help with how can I change the button title on the cell clicked to the do the counting and potential stopping the timer when I click on the button
From my understanding, your problem is want to track each timer states. Just keep in mind that UITableViewCell is reusable and it will keep change when you scroll or view it.
Normally I will use array to easily keep track all those states or values in cell.
So, in your controller will have class variables probably as below
struct Timer {
let start = false
let seconds = 0
// can have more properties as your need
}
var timers = [Timer]()
Then, in your start function startTimerForCell will keep monitor those array.
func startTimerForCell(sender : UIButton) {
let timer = timers[sender.tag]
if timer.start {
// stop the timer
timers[sender.tag].start = false
}
else {
// stop the timer
timers[sender.tag].start = true
}
}
I am trying to print a value from a slider at regular intervals. But only print the value if it is different to that last printed. I also do not want to miss any of the output values from the slider.
To do this I have created an array and added an element to the start of that array if it is different to the one already at the start. I have then used a repeating NSTimer to regularly call a function that prints the last element in the array before removing it from the array.
What happens when I run the app is the NSTimer stops anything being printed for it's set time, but then all of the elements print at once and more than one of each print. I've tried messing about with lots of different things - this is the closest I have got to making it work.
If you need to know any more info let me know.
I really appreciate any help given, thanks very much.
var sliderArray: [Float] = []
var timer: NSTimer!
let step: Float = 1
#IBAction func sliderValueChanged(sender: AnyObject)
{
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
func sendSliderPosition()
{
if sliderArray.count > 0
{
print(self.sliderArray.last)
sliderArray.removeLast()
}
}
I would suggest using CADisplayLink. A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display. Which is perfect for your slider case.
This will also not trigger unnecessary call when the slider or the UI is at rest.
class C: UIViewController {
var displayLinkTimer: CADisplayLink?
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
displayLinkTimer = CADisplayLink(target: self, selector: #selector(printSliderValue))
let runLoop = NSRunLoop.mainRunLoop()
displayLinkTimer?.addToRunLoop(runLoop, forMode: runLoop.currentMode ?? NSDefaultRunLoopMode )
displayLinkTimer?.paused = true
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
displayLinkTimer?.paused = true
}
deinit {
displayLinkTimer?.invalidate()
}
func printSliderValue()
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
print(roundedValue)
}
}
The basic idea is this:
--> Every time the screen needs to redraw (this will happen at max around 60 frames per second taking into consideration this is fps rate), we get a chance to perform function.
--> to do so, we add the displayLink to the Run Loop. (Run lopp processes input/ refreshes UI and time slices)
--> NOTE This method wont be called if there is no redraw needed on the screen. This is not a timer per say that fires periodically. It fires when redraw is needed. In Sliders case, we want this to fire when we move slightest of the slider too.
For more info on how it actually works try it out and see the apple documentation. Make sure to invalidate before deinitializing the ViewController.
Figured out the answer, thank to everyone for the help and suggestions:
override func viewDidLoad()
{
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(0.02, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
#IBAction func sliderValueChanged(sender: AnyObject)
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
}
func sendSliderPosition()
{
if sliderArray.count > 1
{
let end1 = sliderArray.count-2
print(sliderArray[end1])
sliderArray.removeLast()
}
}
Explanation:
If the new slider value is different to the one already in the array then add it to the array at the start. Use an NSTimer to repeatedly call the sendSliderPosition function from viewDidLoad. The function will only be performed if there is more than one element in the array. If there is, print the element before the last one and remove the last. This always ensures that there is one element in the array so the function does not always run and that the element printed is the most recent one that hasn't already been printed.
I'm trying to activate a function inside my custom cell by setting the value of a boolean inside the custom cell class. This is my best attempt at doing this:
func blurViewActive(gestureRecognizer:UIGestureRecognizer) {
if (gestureRecognizer.state == UIGestureRecognizerState.Began) {
println("STATE BEGAN")
var point = gestureRecognizer.locationInView(self.tv)
if let indexPath = self.tv.indexPathForRowAtPoint(point) {
let data = messageList[indexPath.row] as Messages
let mcell: TableViewCell = self.tv.dequeueReusableCellWithIdentifier("cell") as TableViewCell
mcell.read = true
}
}
}
but this doesn't work, and I really have no idea how to do this any other way.
Here is the code for my custom cell class:
class TableViewCell: UITableViewCell, UIGestureRecognizerDelegate {
#IBOutlet weak var labelOutl: UILabel!
var timer = NSTimer()
var counter = 10
var read = Bool()
#IBOutlet weak var dateLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
if read == true{
println("hello")
}
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func timerStarted(){
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update(){
println(--counter)
}
}
My expected outcome is that once read has been set to "true" in my view controller, the function inside awakeFromNib-function should be executed instantaneously.
There seems to be a number of points of confusion here.
dequeueReusableCellWithIdentifier returns a cell for use in the table view it is called on. This will be either a newly instantiated cell or an existing cell not currently displayed in any of the table's visible rows. Setting the read property on this cell will therefore have no immediate visible effect.
If you want access to a visible cell you could use cellForRowAtIndexPath but even then changes made to that cell will not necessarily update the UI. Instead you probably want to update whatever model backs that cell and call reloadRowsAtIndexPaths to update a specific cell.
Additionally awakeFromNib will be called only when a new cell is created. That will be before it is returned from dequeueReusableCellWithIdentifier and therefore well before you can take any action on it, like setting its read property. It will also not be called once per row in your table or per displayed row since you are using a reuse identifier. Instead the table view will create at least one cell for each visible row and reuse them as row scroll into and out of sight. This is convenient because minimizing the number of objects created helps reduce memory use and reduces load which could slow down scrolling performance. However it means that your data source needs to be prepared to update these cells as they are reused from one row to the next.