Multiple Timers in Tableview Swift - ios

I am trying to have timer for each row which is added manually when user clicks on add button.
Start time is set to 100units(not important for the question) and it should count down.
When new row is added it will have it's own timer started and display the value on this new row.
I tried to have timer in each cell but this is creating issue when dequeuing, so I created array of timer to hold corresponding timer for each cell. Problem I am facing right now is how to update the cell value every second(timer interval)
MultipleTimersTableView_Gist is the link for the code I wrote so far. I thought of using delegate to update the cell but not sure with the best approach.
Any help would be appreciated.
EDIT:
Timers should show the time in increasing order because each row is created(along with timer)from top to bottom meaning first will have less time than next. Looks like while dequeueing something messed up.
Here is the gist I used for above screenshot

Here is how you can handle timers in each UITableViewCell.
Create a custom UITableViewCell
class CustomCell: UITableViewCell {
//MARK: Outlets
#IBOutlet weak var label: UILabel!
//MARK: Internal Properties
var handler: ((Int)->())?
//MARK: Private Properties
private var timer: Timer?
private var counter = 100 {
didSet {
DispatchQueue.main.async {
self.label.text = "\(self.counter)"
self.handler?(self.counter)
}
}
}
//MARK: Internal Methods
func configure(with counter: Int) {
self.counter = counter
self.setTimer()
}
//MARK: Private Methods
private func setTimer() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {[weak self] (timer) in
if let counter = self?.counter, counter > 0 {
self?.counter -= 1
} else {
timer.invalidate()
}
})
}
}
In the above code,
I've created a label that will update the counter value in UI.
handler - it will and store the updated counter value somewhere (in ViewController, explained further) when the cell is moved out of the screen
timer - schedule the timer in the cell with timeinterval = 1
counter - current counter value for each cell
In the ViewController,
class VC: UIViewController, UITableViewDataSource {
let numberOfCells = 20
var timerArr = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
self.timerArr = [Int](repeating: 100, count: numberOfCells)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.numberOfCells
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.configure(with: self.timerArr[indexPath.row])
cell.handler = {[weak self] (counter) in
self?.timerArr[indexPath.row] = counter
}
return cell
}
}
In the above code,
timerArr - keeps track of the counter value for each cell in the tableView.
In tableView(_:cellForRowAt:), the counter for each cell is updated using the handler we created previously in CustomCell.

I would use one timer for all the cells. When creating your object and adding it to the datasource, record the dateAdded = Date(), then when the cells are rebuilt on the timer fire, get the seconds count for each cell from the dateAdded field and update the cell in cellForRowAtIndex.

Related

UITableView: reloadRows(at:) takes two hits for table to be updated and scroll every time

I have a table view (controller: MetricsViewController) which gets updated from a CoreData database. I have used prototype cells (MetricsViewCell) which I have customized for my needs. It contains a segmented control, a UIView (metricsChart, which is used to display a chart - animatedCircle), and some UILabels.
MetricsViewCell:
class MetricsViewCell: UITableViewCell {
var delegate: SelectSegmentedControl?
var animatedCircle: AnimatedCircle?
#IBOutlet weak var percentageCorrect: UILabel!
#IBOutlet weak var totalPlay: UILabel!
#IBOutlet weak var metricsChart: UIView! {
didSet {
animatedCircle = AnimatedCircle(frame: metricsChart.bounds)
}
}
#IBOutlet weak var recommendationLabel: UILabel!
#IBOutlet weak var objectType: UISegmentedControl!
#IBAction func displayObjectType(_ sender: UISegmentedControl) {
delegate?.tapped(cell: self)
}
}
protocol SelectSegmentedControl {
func tapped(cell: MetricsViewCell)
}
MetricsViewController:
class MetricsViewController: FetchedResultsTableViewController, SelectSegmentedControl {
func tapped(cell: MetricsViewCell) {
if let indexPath = tableView.indexPath(for: cell) {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
var container: NSPersistentContainer? = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer { didSet { updateUI() } }
private var fetchedResultsController: NSFetchedResultsController<Object>?
private func updateUI() {
if let context = container?.viewContext {
let request: NSFetchRequest<Object> = Object.fetchRequest()
request.sortDescriptors = []
fetchedResultsController = NSFetchedResultsController<Object>(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: "game.gameIndex",
cacheName: nil)
try? fetchedResultsController?.performFetch()
tableView.reloadData()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Object Cell", for: indexPath)
if let object = fetchedResultsController?.object(at: indexPath) {
if let objectCell = cell as? MetricsViewCell {
objectCell.delegate = self
let request: NSFetchRequest<Object> = Object.fetchRequest()
...
...
}
}
}
return cell
}
When a user selects one of the segments in a certain section's segmented control, MetricsViewController should reload the data in that particular row. (There are two sections with one row each). Hence, I've defined a protocol in MetricsViewCell to inform inform my controller on user action.
Data is being updated using FetchedResultsTableViewController - which basically acts as a delegate between CoreData and TableView. Everything is fine with that, meaning I am getting the correct data into my TableView.
There are two issues:
I have to tap segmented control's segment twice to reload the data in the row where segmented control was tapped.
The table scrolls back up and then down every time a segment from segmented control is selected.
Help would be very much appreciated. I've depended on this community for a lot of issues I've faced during the development and am thankful already :)
For example, in Animal Recognition section, I have to hit "Intermediate" two times for its row to be reloaded (If you look closely, the first time I hit Intermediate, it gets selected for a fraction of second, then it goes back to "Basic" or whatever segment was selected first. Second time when I hit intermediate, it goes to Intermediate). Plus, the table scroll up and down, which I don't want.
Edit: Added more context around my usage of CoreData and persistent container.
Instead of using indexPathForRow(at: <#T##CGPoint#>) function to get the indexPath object of cell you can directly use indexPath(for: <#T##UITableViewCell#>) as you are receiving the cell object to func tapped(cell: MetricsViewCell) {} and try to update your data on the UI always in main thready as below.
func tapped(cell: MetricsViewCell) {
if let lIndexPath = table.indexPath(for: <#T##UITableViewCell#>){
DispatchQueue.main.async(execute: {
table.reloadRows(at: lIndexPath, with: .none)
})
}
}
Your UISegmentedControl are reusing [Default behaviour of UITableView].
To avoid that, keep dictionary for getting and storing values.
Another thing, try outlet connection as Action for UISegmentedControl in UIViewController itself, instead of your UITableViewCell
The below code will not reload your tableview when you tap UISegmentedControl . You can avoid, delegates call too.
Below codes are basic demo for UISegmentedControl. Do customise as per your need.
var segmentDict = [Int : Int]()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...29 // number of rows count
{
segmentDict[i] = 0 //DEFAULT SELECTED SEGMENTS
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SOTableViewCell
cell.mySegment.selectedSegmentIndex = segmentDict[indexPath.row]!
cell.selectionStyle = .none
return cell
}
#IBAction func mySegmentAcn(_ sender: UISegmentedControl) {
let cellPosition = sender.convert(CGPoint.zero, to: tblVw)
let indPath = tblVw.indexPathForRow(at: cellPosition)
segmentDict[(indPath?.row)!] = sender.selectedSegmentIndex
print("Sender.tag ", indPath)
}

UItextField data disappears when I scroll through the TableView -- Swift

I'm having problems with my app. I have a table view where every cell consists of a textfield. When i write in it and scroll down, than scroll back up, the data i wrote in it disappears.
These are some of my functions in ViewController
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, UIScrollViewDelegate {
var arrayOfNames : [String] = [String]()
var rowBeingEdited : Int? = nil
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return initialNumberOfRows
}
var count: Int = 0;
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableViewCell
if(arrayOfNames.count > 0 && count < arrayOfNames.count) {
cell.TextField.text = self.arrayOfNames[indexPath.row]
}else{
cell.TextField.text = ""
}
count += 1
cell.TextField.tag = indexPath.row
cell.TextField.delegate = self
return cell
}
func textFieldDidEndEditing(_ textField: UITextField) {
let row = textField.tag
if row >= arrayOfNames.count {
for _ in ((arrayOfNames.count)..<row+1) {
arrayOfNames.append("") // this adds blank rows in case the user skips rows
}
}
arrayOfNames[row] = textField.text!
rowBeingEdited = nil
}
func textFieldDidBeginEditing(_ textField: UITextField) {
rowBeingEdited = textField.tag
}
}
As you can see, I'm saving all of my written text in the textfield into an array. Any help would be appreciated.
Thanks in advance
When you scroll back up tableView(tableView: cellForRowAt:) gets called again. Inside that method you increment count every time that is called, thus instead of using the first condition it goes to the second conditional statement that sets cell.TextField.text = "" as count is probably greater than arrayOfNames.count. What are you using count for? Maybe rethink how you could code that part a little better.
You cells are recreated. So you lose them. You could use the method PrepareForReuse to set the text back when they are recreated.

every TableView Cell updated instead of just one that is clicked.

I have two Buttons (like/dislike) that works like thumbs up and down voting system style. When I click on the button though, every cell gets updated. Is there a way to solve this so that only the one that gets clicked updated?
Also, How do I reload the cell so that I don't have to pull to refresh to update the value of the button.setTitle value?
Here is my code:
PFTableViewCell:
class UserFeedCell: PFTableViewCell {
#IBOutlet weak var likeButton: UIButton!
#IBOutlet weak var dislikeButton: UIButton!
var vote: Int = 0 // initialize to user's existing vote, retrieved from the server
var likeCount: Int = 0 // initialize to existing like count, retrieved from the server
var dislikeCount: Int = 0 // initialize to existing dislike count, retrieved from the server
#IBAction func dislikeButton(sender: UIButton) {
buttonWasClickedForVote(-1)
print(likeCount)
print(dislikeCount)
}
#IBAction func likeButton(sender: UIButton) {
buttonWasClickedForVote(1)
print(likeCount)
print(dislikeCount)
}
private func buttonWasClickedForVote(buttonVote: Int) {
if buttonVote == vote {
// User wants to undo her existing vote.
applyChange(-1, toCountForVote: vote)
vote = 0
}
else {
// User wants to force vote to toggledVote.
// Undo current vote, if any.
applyChange(-1, toCountForVote: vote)
// Apply new vote.
vote = buttonVote
applyChange(1, toCountForVote: vote)
}
}
private func applyChange(change: Int, toCountForVote vote: Int ) {
if vote == -1 { dislikeCount += change }
else if vote == 1 { likeCount += change }
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "TableViewCell" // your cell identifier name in storyboard
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! PFTableViewCell
cell.likeButton.selected = vote == 1
cell.dislikeButton.selected = vote == -1
cell.likeButton.titleLabel!.text = "\(likeCount)"
cell.dislikeButton.titleLabel!.text = "\(dislikeCount)"
return cell
}
You can do it with delegate mechanism
Create protocol for your action:
protocol LikeProtocol {
func likeOrDislikeActionAtRow(row: Int)
}
Make your TableViewController class confirm this protocol:
class YourTableViewController: UITableViewController, LikeProtocol {
...
func likeOrDislikeActionAtRow(row: Int) {
...
// Reload only you want cell
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: row, inSection: 1)], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.endUpdates()
...
}
...
}
Add to your PFTableViewCell delegate object with type on protocol and row variable:
var delegate: LikeProtocol?
var rowValue: Int?
Set delegate and row at your func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath):
cell.delegate = self
cell.row = indexPath.row
Call protocol method in your like and dislike actions:
#IBAction func likeButton(sender: UIButton) {
....
delegate!.likeOrDislikeActionAtRow(row!)
....
}

NSTimer update in real time?

I have an NSTimer and I want to update a label in real time which shows a timer's time. The following is my implementation of how I came about doing so:
override func viewDidAppear(animated: Bool) {
self.refreshTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "refreshView:", userInfo: nil, repeats: true)
var currentRunLoop = NSRunLoop()
currentRunLoop.addTimer(refreshTimer, forMode: NSRunLoopCommonModes)
}
func refreshView(timer: NSTimer){
for cell in self.tableView.visibleCells(){
var indexPath = self.tableView.indexPathForCell(cell as! UITableViewCell)
self.tableView(self.tableView, cellForRowAtIndexPath: indexPath!)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return offerCellAtIndexPath(indexPath)
}
func offerCellAtIndexPath(indexPath: NSIndexPath) -> OfferCell{
//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]
var expirDate: NSTimeInterval = item.dateExpired()!.doubleValue
var expirDateServer = NSDate(timeIntervalSince1970: expirDate)
//Get current time and subtract it by the predicted expiration date of the cell. Subtract them to get the countdown timer.
var timer = self.modelStore[indexPath.row] as! Timer
var timeUntilEnd = timer.endDate.timeIntervalSince1970 - NSDate().timeIntervalSince1970
if timeUntilEnd <= 0 {
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
}
}
As seen by the code, I try to do the following:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.timeLeft.text = NSString(format: "%dh %dm %ds", Int(hours), Int(minutes), Int(seconds)) as String
})
Which I thought would help me update the cell in real time, however when I view the cells, they are not being updated in real time. When I print out the seconds, minutes, and hours, they are being updated every 1 second which is correct. However, the label is just not updating. Any ideas on how to do this?
EDIT: DuncanC's answer has helped a bunch. I want to also delete the timers when their timers go to 0. However, I am getting an error saying that there is an inconsistency when you delete and insert most likely extremely quickly without giving the table view any time to load. Here is my implmentation:
if timeUntilEnd <= 0 {
//Countdown is done for timer so delete it.
self.offers.removeAtIndex(indexPath!.row)
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
cell.timeLeft.text = "Finished."
}
Your problem isn't with timers. Your problem is that you are handling table views totally wrong. In addition to the problems pointed out by Matt in his answer, your timer method refreshView makes no sense.
func refreshView(timer: NSTimer)
{
for cell in self.tableView.visibleCells()
{
var indexPath = self.tableView.indexPathForCell(cell as! UITableViewCell)
self.tableView(self.tableView, cellForRowAtIndexPath: indexPath!)
}
}
You are looping through the visible cells, asking for the indexPath of each cell, and then asking the table view to give you that cell again. What do you think this will accomplish? (The answer is "nothing useful".)
What you are supposed to do with table views, if you want a cell to update, is to change the data behind the cell (the model) and then tell the table view to update that cell/those cells. You can either tell the whole table view to reload by calling it's reloadData method, or use the 'reloadRowsAtIndexPaths:withRowAnimation:' method to reload only the cells who's data have changed.
After you tell a table view to reload certain indexPaths, it calls your cellForRowAtIndexPath method for the cells that need to be redisplayed. You should just respond to that call and build a cell containing the updated data.
This is madness:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return offerCellAtIndexPath(indexPath)
}
func offerCellAtIndexPath(indexPath: NSIndexPath) -> OfferCell{
//Dequeue a "reusable" cell
let cell = tableView.dequeueReusableCellWithIdentifier(offerCellIdentifier) as! OfferCell
setCellContents(cell, indexPath: indexPath)
return cell
}
Your job in cellForRowAtIndexPath: is to return the cell - now. But setCellContents does not return any cell to offerCellAtIndexPath, so the cell being returned is merely the empty OfferCell from the first line. Moreover, setCellContents cannot return any cell, because it contains an async, which will not run until after it has returned.
You need to start by getting off this triple call architecture and returning the actual cell, now, in cellForRowAtIndexPath:. Then you can worry about the timed updates as a completely separate problem.
You could use a dispatch timer:
class func createDispatchTimer(
interval: UInt64,
leeway: UInt64,
queue: dispatch_queue_t,
block: dispatch_block_t) -> dispatch_source_t?
{
var timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
if timer != nil
{
dispatch_source_set_timer(timer, dispatch_walltime(nil, 0), interval, leeway)
dispatch_source_set_event_handler(timer, block)
return timer!
}
return nil
}

How to update UITableViewCells using NSTimer and NSNotificationCentre in Swift

NOTE: Asking for answers in Swift please.
What I'm trying to do:
Have tableview cells update every 1 second and display a real-time
countdown.
How I'm doing it currently:
I have a tableview with cells that contain a label.
When the cells are generated, they call a function that calculates the time between today and a stored target date, and it displays the countdown time remaining in the label.
To get the cells to update the label every second, I
use an NSTimer that reloads the tableview every second.
PROBLEM: The countdown works, but when I try to do a Swipe-to-Delete action, because the NSTimer reloads the tableview, it resets the
swiped cell so that it is no longer swiped and of course this is not acceptable for end users.
What I'm trying / Potential Solutions
I heard that a better way to do this is to use NSNotificationCenter that is triggered by NSTimer and add listeners to each cell. Every 1 second a "broadcast" is sent from the NSNotificationCenter, the cell will receive the "broadcast" and will update the label.
I tried adding a listener to each cell in the part of the code where it generates a new cell:
// Generate the cells
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Tablecell", forIndexPath: indexPath) as UITableViewCell
let countdownEntry = fetchedResultController.objectAtIndexPath(indexPath) as CountdownEntry
// MARK: addObserver to "listen" for broadcasts
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateCountdownLabel", name: mySpecialNotificationKey, object: nil)
func updateCountdownLabel() {
println("broadcast received in cell")
if let actualLabelCountdown = labelCountdown {
// calculate time between stored date to today's date
var countdownValue = CountdownEngine.timeBetweenDatesRealTime(countdownEntry.date)
actualLabelCountdown.text = countdownValue
}
}
but the observer's selector only seems to be able to target functions at the view controller level, not in the cell. (I can't get "updateCountdownLabel" to fire from within the cell. When I create a function of the same name at view controller level, the function can be called)
Questions
So my question is: Is this is the right approach?
If yes, how can I get the listener to fire a function at the cell level?
If no, then what is the best way to achieve this real-time countdown without interrupting Swipe on Cell actions?
Thanks in advance for your help!
It might be a solution not reloading cells at all. Just make the cells listen to an update notification and change their label accordingly.
I assume you subclass UITableViewCell and give the cell a storedDate property. You will set that property when preparing the cell.
The timer will just fire the notification.
Remember to unregister the cell from notification center in dealloc
Here is a quick an dirty example.
The View Controller containing your TableView:
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var timer: NSTimer!
//MARK: UI Updates
func fireCellsUpdate() {
let notification = NSNotification(name: "CustomCellUpdate", object: nil)
NSNotificationCenter.defaultCenter().postNotification(notification)
}
//MARK: UITableView Data Source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "CustomCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! CustomTableViewCell
cell.timeInterval = 20
return cell
}
//MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.timer = NSTimer(timeInterval: 1.0, target: self, selector: Selector("fireCellsUpdate"), userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.timer, forMode: NSRunLoopCommonModes)
}
deinit {
self.timer?.invalidate()
self.timer = nil
}
}
The custom cell subclass:
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
var timeInterval: NSTimeInterval = 0 {
didSet {
self.label.text = "\(timeInterval)"
}
}
//MARK: UI Updates
func updateUI() {
if self.timeInterval > 0 {
--self.timeInterval
}
}
//MARK: Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: Selector("updateUI"), name: "CustomCellUpdate", object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
I'm pretty sure this example doesn't adhere to your app's logic. Just showing how things are glowed together.
As Ian MacDonald has suggested you should avoid reloading the cell when the timer ticks, if you are swiping. Also drop the NSNotification, as It and timer are essentially doing the samething

Resources