Update UITableViewCell using Timer without blocking UI - ios

I'm trying to create a UITableView that contains a bunch of cells that count down from a value. Initially I was just running an NSTimer in the ViewController on repeat to run a function that would update the labels and then run a reloadData on the table, and that does technically work, but with some issues.
To fill things in a bit more, I'm well aware NSTimer isn't accurate as a timer, so I use a date calculation for the time on the labels. I also considered using CADisplayLink so the refresh would sync with the screen, but this ran into problems with scrolling the table, making it very jumpy. Another issue is when issuing the Edit command on the table, the UI is unable to delete rows because of the refresh.
So, I've since considered moving my Timer into each UITableViewCell, and I can tell that it is running via the console, but the table doesn't update. This makes sense, given that it can't run the reloadData on the table, but I'm a bit stuck as to where to go next.
I found a similar thread here and even tried the AsyncTimer suggested here but either I'm doing something wrong, or these aren't quite the same issues? Any help is appreciated, thank you!
var timer = Timer()
self.timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.checkTimers), userInfo: nil, repeats: true)
#objc func checkTimers() {
for counter in counters {
if counter.isActive {
counter.updateSeconds()
}
}
tableView.reloadData()
}

Related

Memory Allocations Profiler and Steadily Increasing Persistent Memory - Signs of Trouble?

I have an app I am developing and the stakeholder using it said that the app becomes slow and unusable/unresponsive after consistent usage all day. Killing it and starting over causes it to run fine.
I don't seem to have this trouble on my device, but I started looking at the memory usage in both simulator/phone in debugger, and observed my memory would steadily increase if I took the basic action of going between screen to screen. These are pretty involved screens, but if I just go forward to the 'add new item' screen, then back to the product listing screen, the memory jumps up 30mb. If I keep doing this same action, over and over and over, I can get it to 1.1gb of memory
I then took it a step further, hooked up my phone, and ran profiler (specifically memory leaks). I found one leak involving my usage of ads, so I just commented out all the code for a test and while the leaks are gone, the memory continues to go up steadily.
I then ran the allocations tool, and after a few min of going back and forth in the same manner, here is the output:
As you can see, it's 1.53GB and if I kept doing the same action I can get it to 2GB+. Oddly enough, my phone never seems to mind, and the screens are just slightly laggy at times otherwise not too bad. Certainly usable.
Before I start ripping out the floor boards, I wanted to confirm this is a likely sign of a problem. Any suggestions on where I can start looking? If persistent memory is the issue, what would be some typical gotchas or pitfalls? What is "anonymous vm?"
Thank you so much if you're reading this far, and appreciate any guidance!
UPDATE/EDIT
After some guidance here, I noticed, oddly enough, that on the "add product" page it causes the memory to jump ~10MB each time I visit it. After commenting out code, I narrowed it down to this section (and even the line of code) causing the jump. Removing this code causes it to remain stable and not increase.
//Render collection views
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath)
let member: MemberDto = groupMembers[indexPath.item]
let contactInitials = cell.viewWithTag(1) as! UILabel
let contactAvatar = cell.viewWithTag(2) as! UIImageView
contactAvatar.image = UIImage(named: "anonymous")
contactInitials.text = member.displayName
contactAvatar.layer.cornerRadius = contactAvatar.frame.size.width / 2
contactAvatar.clipsToBounds = true
contactAvatar.contentMode = UIViewContentMode.scaleAspectFill
contactAvatar.layer.borderWidth = 5.0
if (member.profileImage.trimmingCharacters(in: CharacterSet.whitespaces) != "") {
UserService.getProfilePicture(userId: member.userId) {
response in
contactAvatar.image = response.value
}
}
So, the offending line of code is here:
contactAvatar.image = response.value
Adding it in, and going back and forth to this tableviewcontroller causes the memory to go up and up and up all the way to 2gb. Removing that one line of code (where I set the image) keeps it stable at ~40-70mb, or it goes up but very very slowly (dozens of repeats only got it to 80mb)
I realized I was not caching this image
I decided to try caching this with my framework, and that immediately resolved the issue. I suppose the line of code was pulling the image into memory or something like that? It doesn't seem like the networking call is the actual issue, since I left that in (and even went so far to make additional calls to my API) and that doesn't seem to do much by way of memory increase.
Just a few pieces of info:
From the main screen, you tap on a + symbol in the navigation menu bar to come to this screen.
I am using a regular segue on my storyboard, associated with the navigationbutton, to take the user here
Placing deinit on this vc does not seem to ever hit, even with print/code in there and breakpoints
Making API calls from within my uitableviewcontroller doesn't seem to cause the image to load UNLESS I combine that with SETTING the image. If I make a network call, but don't set the image, it doesn't increase.
What mistake did I make? I feel like caching the image is a bandaid - I recall reading that you're not supposed to make calls to images within a UITableViewController but what is the alternative, to pull all user images from the collection in advance and cache them before the tableview loads?
EDIT 2
As #matt suggested, this was just a bandaid. The true problem still lingered as I knew deinit() was not being called. After pulling out major chunks of code, I found this
lblMessage.addTapGestureRecognizer {
self.txtMessage.becomeFirstResponder()
}
which maps to an extension class:
public func addTapGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.tapGestureRecognizerAction = action
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
self.addGestureRecognizer(tapGestureRecognizer)
}
public func addLongPressGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.longPressGestureRecognizerAction = action
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
self.addGestureRecognizer(longPressGestureRecognizer)
}
// Every time the user taps on the View, this function gets called,
// which triggers the closure we stored
#objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
if let action = self.tapGestureRecognizerAction {
action?()
} else {
print("no action")
}
}
So somewhere in here the problem must lie. I'm taking this to a new thread:
Deinit not calling - Cannot find why something is retaining (code provided)
Thanks! Hope this helps someone.
Yes it's a problem, and yes you need to fix it. The two usual causes of this sort of thing are:
You've got a retain cycle such that at least some of your view controllers are never able to go out of existence.
You've designed the storyboard (or manual segue) sequence incorrectly, so that (for example) you present from view controller A to view controller B, and then in order to get "back" you present from controller B to view controller A. Thus you are not actually going "back"; instead, you are piling up a second view controller A on top of the first one, and so on, forever.
Either way, you can rapidly test that that sort of thing is going on just by implementing deinit to print(self) in all your view controllers. Then play with the app. If you don't see the printout in the log every time you go "back", you've got a serious memory problem, because the view controller is not being released when it should be, and you need to fix it.

Is it bad practice to modify view after viewDidDisappear called?

I have UITabBarController and in one of the UIViewController there I scroll UICollectionView each 5 second using Timer. Here is short code how I do it:
override func viewDidLoad() {
super.viewDidLoad()
configureTimer()
}
private func configureTimer() {
slideTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(scrollCollectionView), userInfo: nil, repeats: true)
}
#objc func scrollCollectionView() {
collectionView.scrollToItem(at: someIndexPath, at: .centeredHorizontally, animated: true)
}
It perfectly works. But I think it has a big issue. Of course, I can open another screen from this UIViewController (for example, I can tap to another tab or push another UIViewController). It means, my UIViewController's view, containing UICollectionView, disappears. In another words, viewDidDissapear will be called. But my timer still exists and I am having strong reference to it, possibly, there is retain cycle. It keeps working and each 5 second scrollCollectionView method is called even my view dissapeared. I don't know how, but iOS somehow handles it. In other words, it can modify view even it is not visible. How is that possible and is it good practice? Of course, I can invalidate my timer in viewDidDissapear and start it in viewDidAppear. But I don't want to loose my timer value and don't want to start it from zero again. Or may be it is ok to invalidate my timer in deinit?
My question covers pretty common situation. For instance, if I make network request and open another UIViewController. After that request finished, I should modify UI, but now am on another screen. Is it ok to allow iOS to modify UI even it is not visible?
A couple of thoughts:
If the timer is updating the UI at some interval, you definitely should start it in viewDidAppear and stop it in viewDidDisappear. There’s no point in wasting resources updating a view that is not visible.
By doing this, you can solve your strong reference cycle, too.
In terms of “losing” your timer value and starting at zero, we generally would just save the time you’re counting from or to and calculate the necessary value when restarting the timer later.
We do this, anyway, because you really shouldn’t be using timers to increment values because you’re technically not assured that they’ll be called with the frequency you expect.
All of that said, I don’t know what timer “value” you’re worried about losing in this example.
But definitely don’t waste time updating a UI that is no longer visible. It’s not scalable and blurs the distinction between the model (what you’re counting to or from) from the UI (the update that happens every five seconds).

Showing scroll indicator all the time UITextView

I would like to show my scrollview all the time. I have looked around for some answers and i found this which i implemented:
override func viewDidLoad() {
super.viewDidLoad()
_ = NSTimer.scheduledTimerWithTimeInterval(0.001, target: self, selector: #selector(flashIndicator), userInfo: nil, repeats: true)
and the function:
func flashIndicator(){
informationView.flashScrollIndicators()
}
To make sure it is getting called i put a simple print in the function and its getting called many times...
But the scroll is not being shown? Just when im actually scrolling. Anybody knows why?
To the best of my knowledge, Apple frowns upon scroll indicators been constantly shown. Thus, I believe that have deliberately made it a private API. So in short, it may be possible, but I don't think the app would be allowed into the app store.

How to make NSTimer more accurate?

In my app, I put a label on the view controller and I want that label to display a number which is incremented by 1 every millisecond. (I know I can just update it every second by 1000, but I want it to look smooth)
#IBOutlet var label: UILabel!
var number = 0
var timer: NSTimer!
override func viewDidLoad() {
super.viewDidLoad()
timer = NSTimer.scheduledTimerWithTimeInterval(0.001, target: self, selector: Selector("addNumber"), userInfo: nil, repeats: true)
}
func addNumber () {
number++
label.text = number.description
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
}
The code all looks very sensible to me, but when I run the app, I see that the number is increasing by about 50 every second, not 1000 every second. That error is far from acceptable!
Surprisingly, when I lock the screen for a few seconds and unlock it again, the number suddenly increases by a few thousands!
I think this is because I am updating the UI when the timer fires so it requires more time. But how can I fix this?
Your question is asking how to make NSTimer more accurate and I have some bad news: you can't. A reasonable expectation of NSTimer is that it can be called ~30 times per second and, if you're lucky, up to ~60 times per second. And that's okay.
Thinking of this from a practical point of view: the display can only be updated up to 60 times per second, so there is no need to give user feedback more often than that.
If you're trying to max out the hardware and rely on it firing as fast as possible, then you're going to have issues on older and slower hardware where it might only fire ~20 times per second.
Relying on the time interval of NSTimer will get you in trouble quickly.
Since you're looking for milliseconds, you can set a start time and every time your function is called look at the number of milliseconds since your start time and that's the number of transpired milliseconds.
Your current method does everything Apple's documentation says it should: number is just the count of times that method has been called (not the number of transpired milliseconds).
Building on #Fennelouski's answer, what I suggest is you swap to a CADisplayLink which is pretty much made for this kind of task. It will call your method once every screen update (or once every 2nd or 3rd, or whatever you make the frameInterval property). it also has a timestamp variable that you can use to calculate how much time has passed and update your label accordingly

Adding time to NSTimer

I'm creating an iOS test game as practice and want a game over method to be ran at the end of a 15 second timer.
I also want time to be added to this timer and taken away if certain actions take place.
Not having much luck figuring out how to add time or take away time from a certain timer depending on actions by the user.
So far I have the timer in viewDidLoad and a method with some conditional formatting that will do things depending on what the user does.
Once it has been created, the period of an NSTimer cannot be changed, the timer can only be cancelled or allowed to fire after the requested period.
In order to implement a timer for your game I would suggest that you set up a repeating timer that fires every second. The actual 'time remaining' value is stored in an integer and you decrement this value each time the timer fires. When this value reaches 0, end the game.
This way you can easily add additional time by simply changing the value of the time remaining variable.
This approach also makes it simple to display the remaining time on the screen.
Once NSTimer has been created, you cannot change time interval. But here's method I've done for me:
func addTimeToTimer(timer: NSTimer, time: NSTimeInterval, target: AnyObject, selector: Selector, repeats: Bool) -> NSTimer {
let currentInterval = Float(timer.timeInterval)
let targetInterval = NSTimeInterval(currentInterval + Float(time))
let newTimer = NSTimer.scheduledTimerWithTimeInterval(targetInterval, target: target, selector: selector, userInfo: timer.userInfo, repeats: repeats)
return newTimer
}
You will need to supply a lot of parameters because NSTimer does not store it all. Hope it helps.

Resources