You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sometimes we need to show an alert, apply a gradient, or conditionally show another view controller on the startup of a view controller. We wish to do such thing in viewDidLoad, however we end up doing it in viewDidAppear(_:). This is because such purposes have requirements that are not fulfilled when viewDidLoad executes; e.g. frames are not yet correct, view hierarchy is not ready, ...etc.
One annoyance with viewDidAppear(_:) is that it can get called multiple times. For example, if you present a new view controller, then dismiss it sometime after, it gets called again. You have to handle such logic that shouldn't be repeated.
Solutions that work, but I don't quite like
1. Booleans
One can introduce a Bool like viewAppeared; checking and setting it for once:
varviewAppeared= false
overridefunc viewDidAppear(_ animated:Bool){
super.viewDidAppear(animated)
if !viewAppeared {// Do non-repeatable logic...}
viewAppeared = true
}
It works. However, I tend to try to avoid "state variables" as much as I can (doesn't mean I succeed much 😅). Although they look simple, bugs find their way around them. And in this particular situation, you may need to repeat some check you did earlier in viewDidLoad, and it gets less nice:
varname:String?overridefunc viewDidLoad(){
super.viewDidLoad()
if let name = name {// conditional initial setup }}overridefunc viewDidAppear(_ animated:Bool){
super.viewDidAppear(animated)
if !viewAppeared {// this check again
if let name = name {}}
viewAppeared = true
}
2. Deferring with GCD
Magic. GCD helps in executing code later. There are two ways to do it for our need: async and asyncAfter(deadline:execute:). We can use either in viewDidLoad to execute non-repeatable code "later enough".
asyncAfter(deadline:execute:) is straight forward:
Why 0.1 seconds? No idea. It's just late enough. It works, but it depends on a magic number, we don't know exactly when this code runs.
What about just async? It works too, no explicit delay needed. That's because code in a DispatchQueue.main.async block is executed in the next run loop.
Believe me, I've read a lot about run loops, I still don't understand them much. However, for the main thread, you can interpret them as time slices in which the main thread accepts input, updates UI, calculate layouts, and more importantly "polls" code enqueued via DispatchQueue.main.async.
So, When we dispatch code async on the main queue from the main thread, it doesn't run immediately; it waits to be polled in the next run loop, leaving enough time for the requirements mentioned above to be fulfilled. Remember our blog post? 😂 Now let's continue it. This GCD thing worth it's own blog post.
If you look at code I wrote (I hope you don't), you'll find me guilty of using this to hack my way through. It's not good, I don't recommend it. We should invest more time to really solve latency problems rather than working around them.
A Better Solution?
Here I suggest a solution that I think is better. GCD gave us a hint, we need to queue tasks on viewDidLoad; but execute it later. So, what about queuing it on viewDidLoad, then execute it on viewDidAppear(_:)?
privatevarviewDidAppearQueue:[()->()]=[]overridefunc viewDidLoad(){
super.viewDidLoad()
viewDidAppearQueue.append{// Our non-repeatable logic}}overridefunc viewDidAppear(_ animated:Bool){
super.viewDidAppear(animated)// Dequeue tasks and execute them in FIFO order
while !viewDidAppearQueue.isEmpty {
viewDidAppearQueue.removeFirst()()}}
We introduce a simple array of closures. We add our tasks as closures in viewDidLoad. Then in viewDidAppear(_:) we execute each closure and remove it from the array. If there are no tasks; nothing happens, just what we need.
We also don't need to weakly capture self (i.e. [weak self] in) when appending closures. This is because strong references to the closures are lost when we remove them from the array. So, we should be safe.
Thanks for reading. Feedback is welcome.
The text was updated successfully, but these errors were encountered:
Another - obvious - alternative that I wasn't aware of at the time of writing this is the isSuspended property of OperationQueue. However, looks like that doesn't work with the shared main OperationQueue. So, it should be used as the following:
classViewController:UIViewController{privateletoperationQueue:OperationQueue={letqueue=OperationQueue()
queue.maxConcurrentOperationCount =1return queue
}()overridefunc viewDidLoad(){
super.viewDidLoad()
operationQueue.isSuspended = true
operationQueue.addOperation{DispatchQueue.main.async{// If we need to execute on the main thread// Some code that needs to be executed once in viewDidAppear}}}overridefunc viewDidAppear(_ animated:Bool){
super.viewDidAppear(animated)
operationQueue.isSuspended = false
}}
(Originally published 2019-02-05)
Sometimes we need to show an alert, apply a gradient, or conditionally show another view controller on the startup of a view controller. We wish to do such thing in
viewDidLoad
, however we end up doing it inviewDidAppear(_:)
. This is because such purposes have requirements that are not fulfilled whenviewDidLoad
executes; e.g. frames are not yet correct, view hierarchy is not ready, ...etc.One annoyance with
viewDidAppear(_:)
is that it can get called multiple times. For example, if you present a new view controller, then dismiss it sometime after, it gets called again. You have to handle such logic that shouldn't be repeated.Solutions that work, but I don't quite like
1. Booleans
One can introduce a
Bool
likeviewAppeared
; checking and setting it for once:It works. However, I tend to try to avoid "state variables" as much as I can (doesn't mean I succeed much 😅). Although they look simple, bugs find their way around them. And in this particular situation, you may need to repeat some check you did earlier in
viewDidLoad
, and it gets less nice:2. Deferring with GCD
Magic. GCD helps in executing code later. There are two ways to do it for our need:
async
andasyncAfter(deadline:execute:)
. We can use either inviewDidLoad
to execute non-repeatable code "later enough".asyncAfter(deadline:execute:)
is straight forward:Why 0.1 seconds? No idea. It's just late enough. It works, but it depends on a magic number, we don't know exactly when this code runs.
What about just
async
? It works too, no explicit delay needed. That's because code in aDispatchQueue.main.async
block is executed in the next run loop.Believe me, I've read a lot about run loops, I still don't understand them much. However, for the main thread, you can interpret them as time slices in which the main thread accepts input, updates UI, calculate layouts, and more importantly "polls" code enqueued via
DispatchQueue.main.async
.So, When we dispatch code async on the main queue from the main thread, it doesn't run immediately; it waits to be polled in the next run loop, leaving enough time for the requirements mentioned above to be fulfilled. Remember our blog post? 😂 Now let's continue it. This GCD thing worth it's own blog post.
A Better Solution?
Here I suggest a solution that I think is better. GCD gave us a hint, we need to queue tasks on
viewDidLoad
; but execute it later. So, what about queuing it onviewDidLoad
, then execute it onviewDidAppear(_:)
?We introduce a simple array of closures. We add our tasks as closures in
viewDidLoad
. Then inviewDidAppear(_:)
we execute each closure and remove it from the array. If there are no tasks; nothing happens, just what we need.We also don't need to weakly capture
self
(i.e.[weak self] in
) when appending closures. This is because strong references to the closures are lost when we remove them from the array. So, we should be safe.Thanks for reading. Feedback is welcome.
The text was updated successfully, but these errors were encountered: