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

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.

Related

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

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

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

How to decide if a statement needs to be placed in DispatchQueue.main.async?

I am trying to get a subview's bound size in viewDidLoad: (the canvas.bounds)
Initially I did not use the DispatchQueue.main.async wrapper, and the size is not returned correctly. So by experiment, I wrapped the statement in main thread queue. Then it worked.
I know there is a guideline saying that "UI related operation needs to be placed in main thread". But how does this translates into actual coding rule of thumb:
If I am just querying a UI property, like getting bounds size, do I
need to wrap it inside main thread queue?
If I am changing a UI
property, e.g. change the bounds size, do I need to wrap it inside
the main thread queue? (likely yes I guess)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// stack views
view.addSubview(photoView)
view.addSubview(canvas)
DispatchQueue.main.async {
self.canvas.startPoint = CGPoint.zero
self.canvas.endPoint = CGPoint(x: self.canvas.bounds.width / 2.0, y: self.canvas.bounds.height)
self.canvas.setNeedsDisplay()
}
Actually this leads to another question I want to ask naturally: having to wrap code in dispatch main looks “unclean”. If u have to wrap it , doesn’t it mean ‘viewDidLoad’ is not the correct life cycle to use? There should be a more appropriate life cycle that doesn’t require dispatch main wrapper?
Your issue is that viewDidLoad is not the correct place to get the size of anything. It's too soon.
Using DispatchQueue.main.async simply delays that code just enough that the size has been updated (maybe).
The proper solution is to get the size in the proper place. Use either viewDidAppear or viewDidLayoutSubviews.

Update UIView before calling other function?

I am working in Swift. When a user presses a UIButton it calls a function ButtonPressed(). I would like ButtonPressed() to do two things:
Update the UIView by removing the current buttons and texts, then uploading some new text.
Call function TimeConsumingCalculation(). TimeConsumingCalculation is the complicated part of my app and does some calculations which take about 20 seconds or so to complete.
Right now, I have the code in the basic order:
ButtonPressed(){
self.Button.removeFromSuperview()
TimeConsumingCalculation()
}
However, it will not remove the button or do any other UI updates or additions until after the TimeConsumingCalculation is complete. I have read and attempted a few guides on closures and asynchronous functions, but have had no luck. Is there a special property with UIView that is causing it to be updated last?
As a side note - I have already attempted putting all UI actions in a separate function and calling it first. It doesn't work. The time consuming function does not take any variables from the buttons or UI or anything like that.
Thanks!
It seems like timeConsumingCalculation() is blocking the main queue, which is in charge of UI updates. Try calling it like this instead and use the isHidden property to hide the button instead of removing it from the view completely.
ButtonPressed(){
self.Button.isHidden = true
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
self.timeConsumingCalculation()
}
}
here you call timeConsumingCalculation() asynchronously on a background thread. The quality of service we give it is userInitiated, read more about quality of service classes here

Make UIView Appear Before Network Operation

I have a seemingly simple problem that I cannot for the life of me seem to figure out. In my iOS App, I have a UICollectionView that triggers network operation upon tapping it that can take a few seconds to complete. While the information is being downloaded, I want to display a UIView that fills the cell with a UIActivityIndicatorView that sits in the square until the loading is done, and the segue triggered. The problem is that it never appears. Right now my code looks like:
myLoadView.hidden = NO;
//Network Operation
myLoadView.hidden = YES;
The App simply stops for a couple seconds, and then moves on the the next view. I'd imagine Grand Central Dispatch has somthing to do with the solution, however please keep in mind that this code takes place in prepareForSegue, and the network info needs to be passed to the next View. For this reason not finishing the download before switching scenes has an obvious problem. Any help would be VASTLY appreciated. Thanks!
iOS commits changes in the interfaces after working out a routine. Hence you should perform your network operation in a background thread and then get back back on the main and perform the "show my view now thing". Have a look the below code for reference.
myLoadView.hidden = NO;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//Network Operation
dispatch_async(dispatch_get_main_queue(), ^{
myLoadView.hidden = YES;
});
});
Your network operation seems to be carried out on the main thread, aka UI thread. This blocks all further UI calls, including the call to unhide a view, until completion.
To resolve this, make your call asynchronous.
You should read this in full, if you haven't already.
As mentioned by other answers, the problem is that the UIView change doesn't happen until the current method finishes running, which is where you are blocking. Before GCD was available I would split methods in two and use performSelector:withObject:afterDelay (to run the second part also on the UI loop) or performSelectorInBackground:withObject: at the end of the first method. This would commit all the waiting animaations first, then do the actual tasks in the second method.
Well the better option for this type of indication is by using the custom HUD libraries like SVProgressHUD or MBProgressHUD

Resources