Will dismissing a vc before a DispatchGroup finishes crash the app? - ios

I know that you have to balance out the calls with DispatchGroup for both .enter() and .leave().
The question is if I started a dispatchGroup.enter() and I dismissed/popped the vc before the calls were balanced, would the app crash? Basically, would deinit() change the outcome because the vc is no longer in memory?
eg. This starts, but before the 3rd background task is completed, the vc is dismissed or popped. .leave() only ran twice but it's supposed to run 3 times. This is just a simple example to get the idea across:
func viewDidLoad()
let group = DispatchGroup()
// there are 3 background tasks
for task in backgroundTasks {
group.enter()
someBackgroundTask(... completion: { (_) in
group.leave()
})
}
group.notify...
}

No, failing to call leave will not, itself, cause a crash. (It likely is not relevant here, but calling leave too many times will crash.)
What will happen, though, is that until all of the enter calls are offset by leave calls, the dispatch group will not be released. Worse, anything captured strongly by the completion handler and the notify closures will not get released. So you really do want to ensure that all enter calls are eventually offset by leave calls.
But I would first suggest confirming that leave is not getting called enough times. Typically the exact opposite problem occurs, that leave is called the correct number of times, but that one or more of these background tasks finish after the view controller was dismissed, and does not handle this scenario gracefully.
For what it is worth, if someBackgroundTask is doing something that is no longer needed after the view controller is dismissed, we would:
make someBackgroundTask a cancelable task, if possible;
make sure the various closures do not maintain a strong reference back to the view controller (by using [weak self] pattern), eliminating strong reference cycles;
make sure the closure gracefully handles if the view controller is deallocated by the time the closure runs, e.g.
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
...
}
and
in deinit of the view controller (or somewhere equivalent) cancel the tasks that are no longer needed (again, assuming it is a cancelable task).

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.

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.

What happens to a completion handler when a view is popped?

When a request is sent from a viewcontroller having a completion handler in place, but before the request can be received by the device, we go back to the previous view. What will happen to that completion handler block?
When are objects deallocated from memory?
Is it when the related object is moved away from the screen? Wrong!
Or is it When the related object is removed from memory? Right!
All objects are removed from memory when 'there is nothing left to strongly point to them'. This could happen when the view goes offscreen. See AutomaticReferenceCounting from docs.
Closures or completionHandlers, point to the instance. Hence they will hold the instance in memory. The instance itself will also point to the completionHandler. So both would be waiting for the other to be removed from memory. As if two people each say I would like you exit the house first. Ending result is that that none of them leave.
You need to avoid that from happening. As Sh_Khan said you do that with [weak self]. For more on why we do that. See Reference to property in closure requires explicit 'self.' to make capture semantics explicit
This case is the reason why we need to put [Weak self] in completion , to avoid strong references to that vc and so to be deallocated , if you skipped it , then what you did in completion will run but you'll not see anything as the vc's view is hidden after the pop while this instance is still in memory and will cause memory drains by time

Checking the current view state after block/closure completion

Within a asynchronously executed block/closure, I want to get a check on my current state before I executed anything within that block.
A common example of where this presents itself is segueing to the next View Controller after a NSURLsession request.
Here's an example:
#IBAction func tappedButton(sender: UIButton) {
//This closure named fetchHistorical goes to the internet and fetches an array
//The response is then sent to the next view controller along with a segue
Order.fetchHistorical(orderID, completionHandler: { (resultEnum) -> () in
switch resultEnum {
case .Success(let result):
let orderItemsArray = result.orderItems!.allObjects
self.performSegueWithIdentifier("showExchanges", sender: orderItemsArray)
default:
let _ = errorModal(title: "Error", message: "Failed!")
}
})
}
Assume that the user has been impatient and tapped this button 3 times.
That would mean this function will be called three times and each time it would attempt to segue to the next view controller (iOS nicely blocks this issue with "Warning: Attempt to present on whose view is not in the window hierarchy!")
I wanted to know how do you folks tackle this problem? Is it something like ... within the closure, check if you are still in the present viewcontroller ... if you are, then segueing is valid. If not, you probably have already segued and don't execute the segue again.
***More generally, how are you checking the current state within the closure because the closure is executed asynchronously?
Since the closure isn't executing on the main thread the state would be in accurate if you check it here (as you stated). You can use GCD to go to the main thread and check the state there. There are a couple of ways you can keep this code from running multiple times. If it will take some time to perform the calculations you can use an acitivity indicator to let the user know the app is busy at the moment. If you want the user to still have the option of pressing the button you can put a tag like:
var buttonWasTapped:Bool = false //class property
#IBAction func tappedButton(sender: UIButton) {
if !self.buttonWasTapped{
self.buttonWasTapped = true
}
}
Then change it back to false on viewDidAppear so they can press once every time that page is shown.
When starting some task that will take some time to complete I would do two things;
Show some sort of activity indicator so that the user knows something is happening
Disable the button so that there is further indication that the request has been received and to prevent the user from tapping multiple times.
It is important that you consider not only the correct operation of your app but also providing a good user experience.

Swift Xcode6 UIActivityIndicatorView Slow to display

How do I get the UIActivityIndicatorView to display first, then execute other code?
I've experimented with using sleep, and it works but it doesn't "feel" right and adds an extra second to processing a bunch of core data stuff. I've also tried dispatching it to the main thread which only works some of the time. (I'm guessing when the rest of the block is executed outside of the main thread).
Ideally as soon as a user touches the button the instance of the UIActivityIndicatorView would display (which seems to happen where I've used it in other apps by itself or with other minimal processing).
Details: I have an IBAction connected to a button that executes a bunch of core data stuff, sometimes including images, that takes between 1 - 3 seconds to finish. When it finishes it dismisses the view controller. The view controller where this is executed is presented as a modal over current context.
Here is a code snippet:
// get the background queue
let bg_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(bg_queue, {
// long running code here...
dispatch_async(dispatch_get_main_queue(), {
self.activityIndicator.stopAnimating()
})
})

Resources