In MainViewController, in viewDidLoad() I'm calling a function which in turn tests if Auth.auth().currentUser == nil; If the condition is met then the statement to execute presents another view controller.
In the if statement, why does the statement to execute need to be preceded by DispatchQueue.main.async (if I don't write DispatchQueue.main.async then the view controller doesn't present and it's just stuck on MainViewController).
Because at the time viewDidLoad is called your view controller has not yet been added to the view hierarchy. A view controller that is not part of the view hierarchy can't present another view controller. You should get a log message in the console saying something similar to that when you try to present the other view controller without async dispatch.
Putting the call in the DispatchQueue.main.async causes the presentation to be delayed until the next runloop cycle which happens to be enough that your view controller has been added to the view hierarchy once it gets called.
A better solution would be to put your current user check in a more appropriate place, possibly viewDidAppear.
Dispatch.main.async is used for reason being that all the UI related have to be performed on the mainQueue. Since UIViewController presentation is a UI task, hence performed on mainQueue
Also, to answer why it's not presenting when Dispatch.main is not used is perhaps you are doing it on a thread which isn't main.
Related
I am using Xcode 10 Beta 6, so this might just be a bug.
I am trying to present a view controller (colorPickerController) as a popover. Within that view controller I will be able to set some properties, which I want to read once the popover is dismissed.
Here's the code:
In lines 93...97 I define a completion handler.
In line 99 I present the colorPickerController modally, including the completion handler.
When running the code the color picker controller was successfully shown in a popover. But when I tapped outside of the popover (to dismiss it), the callback was not called.
I thought maybe a UIPopoverPresentationController does not dismiss "normally", so I tried manually dismissing the popover before it would do so itself, by calling dismiss in popoverPresentationControllerShouldDismissPopover (line 110).
Now this still didn't work, so I set a breakpoint as seen in the picture, to check if the delegate method is even being called.
That's when I noticed, that when running the app, the completion handler is called right when the popover appears, not when it dismisses.
I was logging Completion handler was called. in the console, before even reaching the breakpoint.
How is this possible?
The way your code is in your question, your updateColor closure is only getting called when the presentation animation finishes, not when the view controller you are presenting is done with whatever it needs to do.
See the docs for UIViewController.present(_:animated:completion:):
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621380-present
completion
The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
Note the "after the presentation finishes" (emphasis mine). This means the closure will be executed literally right after the 0.2 seconds of time it takes to animate the presentation of the new view controller up from the bottom of the screen (or however long it takes and in whatever fashion if you're doing some fancy custom presentation animation).
To get a callback for when your new view controller is done doing whatever it needs to do, subclass UIViewController (call it, say, ColorPickerViewController), and use delegation of some sort to notify your current view controller to dismiss the color picker view controller (and, presumably, to tell it what color was picked).
#TylerTheCompiler is correct, the completion you pass runs after the animation completes. In order to accomplish what you want here's what I recommend:
1) Subclass the UIViewController and make a custom type.
2) Add a property to the new class of type:
var functionToFinish: (() -> Void)
3) Change update color's definition to be:
let updateColor: (() -> Void) = ...
3a) Insert the rest of your function for the ...
4) On line 98 write:
colorPickerController.functionToFinish = updateColor
5) Now, within the subclassed controller, you can call that function from the viewDidDisappear event
In the app I'm working on, we sometimes get random crashes in the completion part of a network request.
The chain of events is as follows:
Add a view controller as a child VC. The parent is registered as the child's delegate.
The child calls delegate method to call some API.
The parent gets response JSON data on the main queue.
The parent updates views in the child according to the received data as completion.
In the last part, however, crashes happen randomly. It's pretty hard to reproduce, and Crashlytics shows the crash happens due to referencing a nil variable, a subview in the child view controller's view hierarchy. The parent is alive at this point.
Instead of crashing upon calling a function of the child view controller, it crashes when referencing the view inside the function, so my only guess is that the view is released before the child view controller is.
So my question is, as the title says, is it possible or expected that the view controller outlives its view?
This is what is says in the UIViewController document:
A view controller is the sole owner of its view and any subviews it
creates. It is responsible for creating those views and for
relinquishing ownership of them at the appropriate times such as when
the view controller itself is released.
I always thought the view is released as a result of the view controller's reference count being 0.
UPDATE Instead of presenting, I'm adding it as a child view controller. Added a more detailed explanation of what happens.
The problem is not that your View Controller is outliving the View but its the fact that once a Queue i.e. a closure or a block (In your case the callback of your API calling method) is dispatched the system keeps it alive and keeps a strong reference to it even when the owner of the closure or block (In your case your View Controller) is deallocated. So for the solution you can use weak self in your closure or block.
I try to perform a segue programatically from one view to another, way after it's been loaded (server sends an order to proceed) but it won't happen because "whose view is not in view hierarchy". Any ideas on how to solve it?
P.S. I don't perform any segues from viewDidLoad() method, all of them are performed from a method which is also called from another method in this class (which reads a dictionary, sent from the server). Also I tried to perform segues like this
dispatch_async(dispatch_get_main_queue(), {
self.performSegueWithIdentifier("seg_wait_ready", sender: self)
})
and just like this (even though I know this is wrong):
performSegueWithIdentifier("seg_wait_ready", sender: self)
None of these work, I still get the same warning.
I solved this issue by changing the logic of how my application changes VCs. Now I do it directly through the delegate by doing the following (roughly):
delegate.window!.rootViewController = destinationViewController
It cost me all the system transition animations but I like to do custom ones anyway. But remember to keep it in main queue. To do it safe put it in the main queue manually like this:
dispatch_async(dispatch_get_main_queue(), {delegate.window! ... })
I recently ran into a headache with iOS view controller containment.
Everything works as expected, except in one particular case:
If a child view controller is contained by a parent while the parent is in-between its viewWillAppear and viewDidAppear calls, then viewWillAppear will never be sent to the child VC (the child VC sees viewDidLoad and then viewDidAppear).
This edge case can come up e.g. if you create and contain a child VC as the result of an asynchronous network call, which might land in-between the parent's viewWillAppear and viewDidAppear.
I've put together a demo here: https://gist.github.com/cellularmitosis/8205610a80112eebd96c
To reproduce this locally, create a new "Single View Application" iOS project in Xcode, then replace the contents of ViewController.swift with the above gist.
Am I missing something obvious here, or is this a bug on Apple's part?
I'm guessing this means I need to override shouldAutomaticallyForwardAppearanceMethods() to return false, and then manually call beginAppearanceTransition and endAppearanceTransition?
Follow-up: This is the workaround which I'm currently using: https://gist.github.com/cellularmitosis/56d734ab087a3f283455
I implemented a view controller transition state tracker, and if the parent VC is in-between viewWillAppear and viewDidAppear, then we answer false to shouldAutomaticallyForwardAppearanceMethods.
containChildViewController is updated to handle both true and false for shouldAutomaticallyForwardAppearanceMethods.
Definitely a kludge, but it appears to work.
TODO: There may be an analogous bug during the viewWillDisappear -> viewDidDisappear transition. I haven't checked.
EDIT: edited the workaround gist to use beginAppearanceTransition/endAppearanceTransition instead of directly calling viewWillAppear/viewDidAppear
So my setup is very simple I have:
Controller A
Controller B
Controller A segue's to Controller B, and it's a modal display.
When Controller B's viewDidLoad fires, I instantiate the NSURLConnection with request and start immediately.
If there's no network connection, then the didFailWithError gets fired.
From within here, i call dismissViewController, but when I do, I receive:
"Attempt to dismiss from view controller while a presentation or dismiss is in progress!"
What could possibly be causing this error?
I also have a button on Controller B, that when clicked also does the dismissViewController, but it does it correctly.
Could this be a race condition where Controller B has not finished animating/displaying once it hits viewDidLoad, and the URL connection spawning and failing immediately?
If this is the case, what's the correct way to fix this?
Thanks.
You can't dismiss a view controller until it has been completely presented. If you're still in viewDidLoad, this certainly isn't going to be the case, and I think this mostly will not be the case in viewWillAppear. viewDidAppear is the earliest possible state you can guarantee the view controller's presentation is complete and ready to be dismissed.
With that said... it's better from a UI perspective to instead try to start the NSURLConnection in the background and if you have a good network connection, then you present view controller B, and if not, simply never present it, rather than present and immediately dismiss.
Seems to be race condition. Can you try doing this in failToLoad delegate and see?
//Goes into the failed delegate
dispatch_async(dispatch_get_main_queue(), ^{
//dismiss the View Controller here??
});