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
}
}
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 need help concerning the logic behind a problem I have regarding delegate and protocol. I made a timer app that allows you set a timer. The timer selection is within SettingsController and I used protocols and delegates to pass data into my TimerController. This works as the timer change. But I am having problems concerning creating the logic for the reset button. I want the reset button to have the same data. Example, if I choose 5mins as the timer, the reset button will reset as 5min. Currently its reseting to 0. The problem is really extracting the data from the delegated function into the TimerController so I can change the value of counter which is nil.
SettingsController
import UIKit
protocol SettingsVCDelegate {
func didSelectReadingSpeed(counter: Int)
}
class SettingsVC: UIViewController {
var counter = Int()
var settingsDelegate: SettingsVCDelegate!
#IBAction func changeReadingSpeed(_ sender: UISlider) {
speedSlider.value = roundf(speedSlider.value)
if speedSlider.value == 1 {
speedLabel.text = "1 min (60 pgs / hr) Extreme"
counter = 1
print(counter)
} else if speedSlider.value == 2 {
speedLabel.text = "2 min (30 pgs / hr) Fast"
counter = 2
}
}
#IBAction func didTappedStart(_ sender: Any) {
settingsDelegate.didSelectReadingSpeed(counter: counter)
dismiss(animated: true, completion: nil)
}
TimerController
class TimerController: UIViewController, CountdownTimerDelegate, SettingsVCDelegate {
lazy var countdownTimer: CountdownTimer = {
let countdownTimer = CountdownTimer()
return countdownTimer
}()
var resetBtn: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(resetBtnDidTouch), for: .touchUpInside)
return button
}()
#objc func resetBtnDidTouch() {
countdownTimer.reset()
countdownTimer.setMinuteTimer(minutes: 0, seconds: counter)
countdownTimer.setHourTimer(hour: 0, minutes: 0, seconds: duration)
}
var counter = Int()
var isRunning = false
var resumeTapped = false
//MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
countdownTimer.delegate = self
countdownTimer.setMinuteTimer(minutes: 0, seconds: counter)
countdownTimer.setHourTimer(hour: 0, minutes: 0, seconds: duration)
}
//MARK: - Settings Delegate
func didSelectReadingSpeed(counter: Int) {
let counter = counter
countdownTimer.setMinuteTimer(minutes: 0, seconds: counter)
}
Very much appreciate if someone can help me.
You are using three(!) counter variables in your code
The property on the top level
The parameter in didSelectReadingSpeed
The local variable in didSelectReadingSpeed
This is pretty confusing because all three objects are different. To avoid the naming confusion name the parameter in the delegate method speed (you could even omit Speed in the method name) for example
protocol SettingsVCDelegate {
func didSelectReading(speed: Int)
}
The issue occurs because the property whose value is used to reset the timer never changes.
Solution: When the delegate method is called delete the local variable counter and assign speed to the property counter.
func didSelectReading(speed: Int) {
counter = speed
countdownTimer.setMinuteTimer(minutes: 0, seconds: counter)
}
Two notes:
Don't initialize a scalar property with the default initializer e.g. Int(), assign a default value which is more descriptive:
var counter = 0.
Initializing countdownTimer with a closure looks pretty cool but it's pointless if the property is only initialized.
lazy var countdownTimer = CountdownTimer() does the same thing.
I agree with the previous answer. Additionally, you declared and initialized "counter" as a minute.
countdownTimer.setMinuteTimer(minutes: counter, seconds: 0) should be in this way or if "counter" declared as a seconds:
minutes = counter / 60
seconds = counter % 60
I am trying to create a table view such that I am able to play videos . I am able to do this using AVPlayer and layer.
I want to add a custom play and pause button with a slider to the bottom of a video view.
AVPlayerController comes built in with these controls.
How can I implement these in AVPlayer. I have been looking for examples. But I haven't found any.
Are there any GitHub examples or code samples that I can follow? Any help will be really appreciated.
here I add the points , you need to customize based on your need.
Step-1
initially hide your AVPlayer controls,
YourAVPlayerViewController.showsPlaybackControls = false
Step-2
create the structure like
one label for current Duration, One label for overall Duration, one UIbutton for pause and play your current player and one UISlider for seek The video.
step-3
initially close the simple steps.
first stop and play the player using button action , currentPlayer is your AVPlayer name.
#IBAction func handlePlayPauseButtonPressed(_ sender: UIButton) {
// sender.isSelected ? currentPlayer.pause() : currentPlayer.play()
if sender.isSelected {
currentPlayer.pause()
}
else {
currentPlayer.play()
}
}
second set the video duration, as like
let duration : CMTime = currentPlayer.currentItem!.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
lblOverallDuration.text = self.stringFromTimeInterval(interval: seconds)
third set the player current time to current duration label
let duration : CMTime = currentPlayer.currentTime()
let seconds : Float64 = CMTimeGetSeconds(duration)
lblcurrentText.text = self.stringFromTimeInterval(interval: seconds)
the following method is convert from NSTimeinterval to HH:MM:SS
func stringFromTimeInterval(interval: TimeInterval) -> String {
let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let hours = (interval / 3600)
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
finally we go for slider control for calulate the seek time
_playheadSlider.addTarget(self, action: #selector(self.handlePlayheadSliderTouchBegin), for: .touchDown)
_playheadSlider.addTarget(self, action: #selector(self.handlePlayheadSliderTouchEnd), for: .touchUpInside)
_playheadSlider.addTarget(self, action: #selector(self.handlePlayheadSliderTouchEnd), for: .touchUpOutside)
_playheadSlider.addTarget(self, action: #selector(self.handlePlayheadSliderValueChanged), for: .valueChanged)
lets we go for action, initially when touchbegin is start then stop the player
handlePlayheadSliderTouchBegin
#IBAction func handlePlayheadSliderTouchBegin(_ sender: UISlider) {
currentPlayer.pause()
}
set the current item label for calculate the sender.value * CMTimeGetSeconds(currentPlayer.currentItem.duration)
#IBAction func handlePlayheadSliderValueChanged(_ sender: UISlider) {
let duration : CMTime = currentPlayer.currentItem!.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration) * sender.value
// var newCurrentTime: TimeInterval = sender.value * CMTimeGetSeconds(currentPlayer.currentItem.duration)
lblcurrentText.text = self.stringFromTimeInterval(interval: seconds)
}
finally move the player based on seek
#IBAction func handlePlayheadSliderTouchEnd(_ sender: UISlider) {
let duration : CMTime = currentPlayer.currentItem!.asset.duration
var newCurrentTime: TimeInterval = sender.value * CMTimeGetSeconds(duration)
var seekToTime: CMTime = CMTimeMakeWithSeconds(newCurrentTime, 600)
currentPlayer.seek(toTime: seekToTime)
}
In addition to Anbu.Karthik's answer, the slider handing can be handled better in a single function like below:
slider.addTarget(self, action: #selector(self.handlePlayheadSliderValueChanged(sender:event:)), for: .valueChanged)
Slider Value Change:
Note switch cases handling all phases of slider event
#objc func handlePlayheadSliderValueChanged(sender: UISlider, event: UIEvent) {
if let duration: CMTime = player?.currentItem?.asset.duration {
let newCurrentTime: Float64 = CMTimeGetSeconds(duration) * Double(sender.value)
videoLengthLabel.text = self.stringFromTimeInterval(interval: newCurrentTime)
if let touchEvent = event.allTouches?.first {
switch (touchEvent.phase) {
case .began:
// on slider touch begin
pauseVideo()
break
case .moved:
// on slider movement
let seekToTime: CMTime = CMTimeMakeWithSeconds(newCurrentTime, preferredTimescale: 600)
player?.seek(to: seekToTime, completionHandler: { (completedSeek) in
// any additional operation upon seek completion
})
break
case .ended:
// on slider touch end (finger lift)
playVideo()
break
default:
break
}
}
}
}
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.