Is it bad practice to modify view after viewDidDisappear called? - ios

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).

Related

Is `DispatchQueue.main.async` block in viewWillAppear always called after `viewDidLayoutSubviews`?

I wanted to change the collection view's contentOffset.x right after pushing VC.
So I called collectionView.setContentOffset(~) in viewWillAppear.
But It didn't work because of auto layout cycle.
However, if I call collectionView.setContentOffset inside DispatchQueue.main.async block, IT WORKS!
The code is below:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
collectionView.setContentOffset(
CGPoint(x: currentFolderIndex * collectionView.bounds.width), y: 0),
animated: false
)
}
}
I figured out why it had worked when I printed the order of layout methods.
DispatchQueue.main.async block is called after viewDidLayoutSubviews.
Does it always work like this?
Why does it work like this?
I'm so curious!!
Using async basically says "do it when you have time". So it will be done at an indeterminate time in the future (generally not too long after), and probably after finishing what is currently being done. And the calling function does not wait for the code to be executed on the main queue before continuing.
So in your case, when calling async, the UI thread finishes what he is doing, calling viewWillLayoutSubViews, doing autolayout, etc,... and when he has finished and has time, he executes the setContentOffset asynchronously.
However, in theory it does not guarantees it will always be the same order : UI thread could find time before, or could be overbooked and not have time before viewDidAppear... But in practical, I think you are safe assuming it will be the same order 99,99% of the time.
Note : In other situations, you could think of using DispatchQueue.main.sync, which would say "Do it ASAP and I'm waiting for you". However, this is probably is a terrible idea, especially if you are in the UI thread : you would be waiting in the UI thread for the UI thread to do something else, and you cannot get out of that - it is a deadlock. More details here.

How can I force update my UI immediately in Swift 5 using UIKit?

I ran into a problem with my UI, which is not updating immediately.
I am calling someCustomView.isHidden = false first. After that I create a new instance of a new View Controller. Inside the new VCs viewDidLoad(), I am loading a "new Machine Learning Model", which takes some time.
private func someFuncThatGetsCalled() {
print("1")
self.viewLoading.isHidden = false
print("2")
performSegue(withIdentifier: "goToModelVCSegue", sender: nil)
}
As soon as I press the button that calls this function, "1" and "2" is printed in the console. However the view is not getting visible before the viewDidLoad() of my new VC is finished.
Is there any possibility to force update a UIView immediately? setNeedsDisplay() did not work for me.
Thanks for your help!
Use layoutIfNeeded() Apple Docs
layoutIfNeeded()
Lays out the subviews immediately, if layout updates are pending.
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.
So As a rule of thumb,
layoutifneeded : Immediate (current update cycle) , synchronous call
setNeedsLayout :relaxed ( wait till Next Update cycle) , asynchronous call
So, layoutIfNeeded says update immediately please, whereas setNeedsLayout says please update but you can wait until the next update cycle.
how to use
yourView.layoutIfNeeded()
You can also refer to the diagram to better remember the order of these passes
Source Apple docs on layoutIfNeeded
Image credit Medium artcle
A couple problems...
If you have a view controller that "takes some time" to load, you should not try to do it in that manner.
The app will be non-responsive and appear "frozen."
A much better approach would be:
on someFuncThatGetsCalled()
hide viewLoading and replace it with an activity indicator (spinner, or something else that let's the user know the app is not stuck)
instantiate your ModelVC
when ModelVC has finished its setup, have it inform the current VC (via delegate)
current VC then shows / navigates to the already instantiated and prepared ModelVC
Or, probably a better option... Move your time-consuming setup in ModelVC to a point after the view has appeared. You can show an activity indicator in viewDidLoad(). That is really the most common UX - you see it all the time when the new VC has to retrieve remote data to display - and it would fit wit what users have come to expect.

NotificationCenter.default.addObserver keep getting called multiple times with Unwind Segue

I am using an show segue and an unwind segue to navigate between two iOS viewControllers, VC1 and VC2. In the viewDidLoad() of VC2 I make VC2 an observer. Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "buzzer updated"), object: nil, queue: OperationQueue.main) { _ in
print("set beeperViewImage")
}
}
Every time I use the unwind segue to go from VC2 back to VC1 the addObserver() gets called an additional time, e.g., on the fourth return segue addObserver is called 4 time; on the fifth segue, five times, etc. This behavior happens even when the app is sent to the background and recalled. It remembers how many segues happened in the previous session and picks up the count from there.
I have no problems with multiple calls in VC1, which is the initial VC.
I have tried to set VC2 to nil after unwind segueing.
Looking forward to any guidance.
This is undoubtedly a case where your view controllers are not getting released. Perhaps you have a strong reference cycle.
For example, consider this innocuous example:
extension Notification.Name {
static let buzzer = Notification.Name(rawValue: Bundle.main.bundleIdentifier! + ".buzzer")
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: .buzzer, object: nil, queue: .main) { _ in
self.foo()
}
}
func foo() { ... }
}
If I then enter and leave this view controller three times and then click on the “debug memory graph” button, I will see the following:
I can see three instances of my second view controller in the panel on the left and if they were properly deallocated, they wouldn’t show up there. And when I click on any one of them in that panel, I can see a visual graph of what still has a strong reference to the view controller in question.
In this case, because I turned on the “Malloc Stack” feature under “Product” » “Scheme” » “Edit Scheme...” » “Run” » “Diagnostics” » “Logging”, I can see the stack trace in the right most panel, and can even click on the arrow button and be taken to the offending code:
In this particular example, the problem was that I (deliberately, for illustrative purposes) introduced a persistent strong reference where the Notification Center is maintaining a strong reference to self, imposed by the closure of the observer. This is easily fixed, by using the [weak self] pattern in that closure:
NotificationCenter.default.addObserver(forName: .buzzer, object: nil, queue: .main) { [weak self] _ in
self?.foo()
}
Now, I don’t know if this is the source of the strong reference cycle in your case because the code in your snippet doesn’t actually reference self. Perhaps you simplified your code snippet when you shared it with us. Maybe you have something completely else that is keeping reference to your view controllers.
But by using this “Debug Memory Graph” button, you can not only (a) confirm that there really are multiple instances of your relevant view controller in memory; but also (b) identify what established that strong reference. From there, you can diagnose what is the root cause of the problem. But the code in your question is insufficient to produce this problem, but I suspect a strong reference cycle somewhere.
Thank you all for your comments on my problem. Based on them, I started searching for what might be holding on to my VC2. Turns out that a call to read a bluetooth radio link in my VC2 viewWillAppear() was the culprit but I don't understand why:
self.radio?.peripheral?.readValue(for: (self.radio?.buzzerChar)!)
Everything works fine after removing the above line of code. Thanks again for pointing out in which direction to search.

Update UITableViewCell using Timer without blocking UI

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()
}

Swift IOS app won't start after idle for over 1 day

I have a simple app in Swift with just a few views:
A UIWebView
some TableViews
and another view with some data I download from my server
It all works well until when using the app I press the home button, leave there for a while then the iPad goes on sleep mode. A few days later I tap on the app icon and it won't start:
first tap on the icon will select the icon (goes a little darker) and deselect it a few seconds later
second tap will launch the LaunchScreen and crash a few seconds later
double tap the home button and quit the app will sometimes work
I'm just wondering if there is something I need to set on my code to handle idle/long periods of inactivity in something like viewWillDisappear or other methods?
If so I already have this in all my controllers:
override func viewWillDisappear(animated: Bool) {
timer.invalidate()
webView.removeFromSuperview()
}
Maybe I need to call super. in there too? or something else I'm missing?
You should definitely call super in your viewWillDisappear(animated:) method. See UIViewController Class Reference documentation. Also you might want to confirm why you are removing your webView from the view controller's hierarchy.
Discussion
This method is called in response to a view being removed
from a view hierarchy. This method is called before the view is
actually removed and before any animations are configured.
Subclasses can override this method and use it to commit editing
changes, resign the first responder status of the view, or perform
other relevant tasks. For example, you might use this method to revert
changes to the orientation or style of the status bar that were made
in the viewDidDisappear: method when the view was first presented. If
you override this method, you must call super at some point in your
implementation.
You probably have some null pointer exception and crash. Maybe you are calling some variable that is not set (and checked if not null).
Try disabling app funcionality (like downloading, storing and using data from server) and see where you app starts working normal again and then procede from there.
Sorry for vague answer but withouth code and maybe some log it is really hard to give specific answer.
And NO, you dont have to do anything special to handle idle/long periods of inactivity.

Resources