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
Related
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.
If I have multiple view controllers being presented and dismissed in any order, can I be sure that iOS calls viewWillAppear methods in the right order (i.e. order of appearance)?
I cannot find any specific information about this in the documentation.
I think this is all you need to know about viewWillAppear from the docs:
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view. You can override this method to perform custom tasks associated with displaying the view. For example, you might use this method to change the orientation or style of the status bar to coordinate with the orientation or style of the view being presented. If you override this method, you must call super at some point in your implementation.
Only thing that comes to mind that might not be absolutely clear is that this callback is called on the presenting view controller when presented view controller is going to be dismissed (so presenting view controller is going to appear again).
Therefore if A is a root, A.viewWillAppear will be called before it will appear on the screen. Then, if A presents B, just before B becomes visible, B.viewWillAppear will be called. And when B is being dismissed, A.viewWillAppear will get called again, since its view will appear again.
viewWillAppear() is called the first time the view is displayed and it is also called when the view is displayed again, so it can be called multiple times during the life of the view controller object.
It’s called when the view is about to appear as a result of the user tapping the back button, closing dialog, or when the view controller’s tab is selected in a tab bar controller, or a variety of other reasons. Make sure to call super.viewWillAppear() at some point in the implementation
How can I make my UIView (which has no access to the ViewController) perform some task after an orientation change has taken place? That is, no task should take place until the animation has completely finished and the frame is no longer changing due to animation.
I've tried registering an observer on the UIView to respond to UIDeviceOrientationDidChangeNotifications, but a breakpoint I've placed in the handler is triggered before the animation even starts.
You would need to involve the view controller. Only the view controller can get a reference to the transition coordinator, so that it can call animate(alongsideTransition:completion:) and do something in the completion handler. That is the moment when the animation has finished.
So the view controller could then talk directly to the view, or, if you want the view to be completely agnostic, the view controller could post a custom notification for which the view has registered.
I have a pretty simple application with a couple of UIViewControllers (say VC_A and VC_B). Every screen has a button that allows to switch to another screen (no UINavigation is used).
App schedules a local notification, which, when expired, should present another view controller (VC_N - no matter what screen is active at the moment).
The problem is that sometimes application throws:
'NSInternalInconsistencyException', reason: 'Attempting to begin a modal transition from
<VC_A: 0x2021e0> to <VC_N: 0xf84b970> while a transition is already in progress.
Wait for viewDidAppear/viewDidDisappear to know the current transition has completed'
What is the proper way to implement such behaviour?
A. Use UIViewController's new presentViewController:animated:completion to present all three controllers instead of the old modal method.
B. Create a Boolean flag and initialize it to NO.
C. Before any view controller presentation, check for this flag. If YES, set to NO. And then present the VC. In the completion block, set the flag to YES again.
D. The app should ignore this flag when present the initial view controller whether VC_A or VC_B.
One pitfall to this is that if a button got pressed or the local notification expired while already a view controller was in a transition state, then the new VC won't get presented. One can improve upon this logic to present it after if needed.
When the timer expires don't call the view presentation in that timer handler method. Instead try putting the view presentation call in a separate method and use [self performSelector: withObject: afterDelay:] to call that method (delay could be 0.1]. This should get the view presentation done when current transitions complete.
We have a container view controller and want to be able to call "PerformSelector" on one of the "sub" view controllers in that container, right after starting a transition, i.e.
[self navigateSubViewControllerTo:newSubViewController];
... some time later, elsewhere in the stack, a selector will be performed on the top visible VC
[subViewController performSelector:#selector(foo)];
The call to transitionFromViewController happens in navigateSubViewController.Unfortunately, since transitionFromViewController happens asynchronously, we are finding that the performSelector call gets applied to the "before" sub view controller, not newSubViewController. I.e. it is happening before the transition happens.
Any thoughts on how to have performSelect not happen until the sub view controller transition happens?
You can just call performSelector in the completion block of
transitionFromViewController:toViewController:duration:options:animations:completion:
UIViewController provides a callback beginAppearanceTransition:animated: which is exactly for this purpose. Simply implement it in your subviewcontrollers and your good to go ;)