Swift RunLoop blocks table view - swift

So, I have a timer which increments a variable by one in my app delegate every 0.1 seconds. In my tableViewController, I have this number displayed in a cell. In the controller there is ANOTHER timer which reloads visible cells. Everything worked, apart for the fact that once scrolling through the tableView, the number stopped changing until the table view wasn't released. I have read that the fix for this was:
RunLoop.current.add(tableTimer, forMode: RunLoopMode.commonModes)
Where tableTimer is my cell reload timer. Nevertheless, this works, but once scrolling through the view it is extremely laggy and is 0% fluid, as it normally is. Any fix? Thanks.
EDIT:
Creating the timer:
func scheduledTimerWithTimeInterval(){
reloadTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.reloadTV), userInfo: nil, repeats: true)
RunLoop.current.add(earningTimer, forMode: RunLoopMode.commonModes)
}
Updating table view:
#objc func reloadTV() {
self.tableView.beginUpdates()
self.tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
self.tableView.endUpdates()
}

Reloading a table view every .1 seconds for a label in a cell is quite stupid. For many several reasons. The optimum way to solve this issue is to loop through all visible index paths, create a cellForRowAtIndexPath with the index paths and than modify the cell title directly from there, in the following way:
#objc func reloadTV() {
for myIP in (tableView.indexPathsForVisibleRows) {
let cell = tableView.cellForRow(at: myIP)
cell.textLabel?.text = changingVariable
}
}

Related

Timer on table view Cell

there are 5 cells in table view, each have countdown on it, but countdown running fast, it decreases more than 1 second in one call.
this is my table view cell class, I have created a labelSatus here
class ActivityCell: UITableViewCell {
#IBOutlet weak var labelStatus: UILabel!
//Variable Declaration
var timer: Timer?
var totalTime:Double = 0
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func designCell(data:ActivityModal, removeTimer:Bool){
if removeTimer {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
else{
let timeStampLimit = (data.date)/1000 + 86400 //for 24 hours
let currentTimeStamp = NSDate().timeIntervalSince1970
if timeStampLimit > currentTimeStamp{
self.totalTime = timeStampLimit - currentTimeStamp
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
}
}
#objc func updateTimer() {
self.labelStatus.text = String.getString(CommonUtils.convertTimeStampToHour(unixtimeInterval: self.totalTime, dateFormat:"HH:mm:ss"))
if self.totalTime != 0 {
self.totalTime -= 1 // decrease counter timer
} else {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
}
}
There are a few issues to take care of when working with timers on table view cells. Note that the UITableView reuses the cell objects, so after a cell disappears from the screen after scrolling, it will be reused to be displayed in another place inside the table view.
Supposing you call designCell() in tableView:(_:cellForRowAt:) method, you might be creating more timers for a single cell (i.e. a lot of timers which will trigger the same cell's updateTimer() method). Note that a Timer will not be deallocated after you drop all your references to it, if the timer is still valid. At this point, you might be creating a new timer for a specific cell without having the chance to invalidate the old one.
This would be handled by stopping the timer in preapreForReuse() method from the table view cell subclass. This method is called when the cell is going to be reused:
class ActivityCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
self.timer?.invalidate()
self.timer = nil
}
}
This will solve (one of) the timer problem(s), but there is one more issue: you are going to lose the time tracked by that cell. There is no way to preserve that state into the cell itself. You must do it in the view controller and pass the displayed cell the time it needs to track in tableView(_:cellForRowAt:) method. If you are already doing it, then this is not a problem.
Another problem is that the timers will live forever (or until it's cell stops it). For some of the timers, if the cell object lost the reference to it, nobody will ever stop that timer anymore, so it will just fire every second until the app is killed.
I'm not really sure at this point whether the timer will retain the cell object using the target-selector method for creating a timer, but there is an issue regardless of that:
If the timer retains the cell object, that means that you will have memory leaks: that means that the timers and the cells will never be deallocated and will continuously use memory and processing resources;
If the timer does not retain the cell object, then there might occur a crash once the cell is being deallocated.
You should stop the cell's timer in tableView(_:didEndDisplaying:forRowAt) method. At this point, the cell is being hidden, so the timer makes no sense anymore.
Of course, all of this will lead you to another issue: preserving the time state for the cells. This handling should be done from the view controller presenting the table view, and not from the table view cell.
TL;DR:
You will have a lot of headaches having a timer on each cell, in fact it's almost impossible to handle things that way. You should have your timers corresponding to each cell in the view controller that presents the table view and the things will simplify a lot.

Variables are not updated in timer completion block

Here is the scenario:
I want to scroll(or shake) a horizontal collectionView after a page showing up so user could see it scrolling...
I do it with no problem 3 sec after page pops up but I don't want it to do the dance when user reach the collectionView before 2 sec & scrolls it itself.
So here is my solution for it:
var collectionDidScroll = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionDidScroll = true
}
override func awakeFromNib() {
super.awakeFromNib()
Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(showScrollTutorial), userInfo: nil, repeats: false)
}
#objc func showScrollTutorial() {
if !collectionDidScroll {
collectionView.shakeCells()
}
}
The Problem:
The "collectionDidScroll" is update just fine in class, but in timer completion its always false!
It means "collectionDidScroll" is not updating in completion & it has its launch time value.
Notice:
My class is a subclass of UICollectionViewCell
I even tried dispatchQueue with timer & timer with completion block but the results are the same
The comments above answered your question, but to help you tackle similar problems in the future yourself:
Whenever you have problems like this, you should try littering your code (especially scrollViewDidScroll and showScrollTutorial) with print-statements to see if they get called at all and which values they contain. Or you can set breakpoints!
Problem Solved!
I placed 'showScrollTutorial()' function contents inside a 'DispatchQueue' to force it to run in main thread, now it works like a charm!
Thanks for everyone.

Swift - Calling a method in a UIViewController from a separate UIView class

Okey, I have been struggling this for a long time now.. Cant get it to work, and I know there is an easy solution that I cannot find.
I have a UIViewController displaying the main app. When it launches there is two containerViews overlaid that displays a count down for the app to start. Countdown is done, and the "overlay" viewcontrollers are removed from superview.
BUT, I need to call the function to remove VC from superview and start the app in the main UIViewController. But I am not able to trigger this function from the separate "overlay" UIView..
If I make an instance of the UIViewController to trigger the startGame() function it works, but then all the labels return nil, and the app crashes.. I believe this is because the UIViewController is triggered twice?
A lot of explaining here.. I will try to show some outtakes in code:
Main UIViewController:
class DMStartVC: UIViewController {
.....
func startGame() {
self.view.viewWithTag(100)?.removeFromSuperview()
self.view.viewWithTag(101)?.removeFromSuperview()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(DMStartVC.update), userInfo: nil, repeats: true)
UITimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(DMStartVC.UIUpdate), userInfo: nil, repeats: true)
}
....
}
The UIView handling the "overlay" container views:
class countdown: UIView {
let dmStartVC = DMStartVC()
...
//when animation is done:
dmStartVC.startGame()
}
This causes crash because all labels in the UIViewController is nil.
How can I reach that .startGame() function?
Only way I have been able to solve this is adding a timer inside the DMStartVC to trigger .startGame()... But that is not a good solution as the timer has to trigger at the exact time the countdown ends.. and it never does.

reloadData in UITableView on uibutton click - swift

I've been playing around with the UITableView in swift.
Firstly, my goal is to append a cell with array data on click of the UIButton.
I've managed to do this with the following action
#IBAction func Option1Click(sender: UIButton) {
arrayOfQuestions.append(QuestionMark(Label:"Grocery List", option1:"yes", option2:"no")) }
But the issue is i cannot get to reload the tableView, because the tableView.reloadData() isn't inherited.
Secondly, i want to apply a timer. So that the appending of the cell would take a specific time interval.
I've managed to find a function on the forums
var timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self,
selector: Selector("Option1Click"), userInfo: nil, repeats: true)
But i don't know where exactly to place this code to work.
Would appreciate any insights greatly!
Thank you.
You should get a reference to your tableview as an outlet and name it whatever you like.
After you can use the reference of the tableview and call the reload method
like this:
nameofyourreference.reloadData()

How to loop the CollectionViewCell

I have a set of collectionviewcell in UICollectionView and i am using function NSTimer.scheduledTimerWithTimeInterval to command each cell to show up and move to the left in every 4 second. Currently, they are not in loop and it ends up blank on the screen once the last cell shows up. How can I create the loop for these cells and make them to move continuously? Please advise.
func timeSlide() {
var timeslide = NSTimer.scheduledTimerWithTimeInterval(4, target: self, selector: Selector("onTime"), userInfo: nil, repeats: true)
}
func onTime(){
collection.contentOffset = CGPointMake(collection.contentOffset.x+375, 0)
}
You need to know how many items are there in the collection view, and when it gets to the end you need to set the contentOffset back to the beginning.