Trigger viewWillAppear when using a transitioning delegate - ios

I want to present a view controller:
Modally
Using custom transitions
At the same time, I want to make sure that when it is dismissed, the view controller behind it is aware of being pushed to the foreground.
My basic idea is to try some different configurations and see which one will lead to viewWillAppear being called on the view controller behind.
Attempt 1
presentedViewController.modalPresentationStyle = .Custom
presentedViewController.transitioningDelegate = someTransitioningDelegate
The results of this approach:
The custom transition works perfectly well
viewWillAppear does not get called on the view controller behind presentedViewController when I call presentedViewController.dismissViewControllerAnimated(true)
I do want viewWillAppear to be called on the view controller below the one being dismissed, so I did this:
Attempt 2
presentedViewController.modalPresentationStyle = .FullScreen
presentedViewController.transitioningDelegate = someTransitioningDelegate
or
presentedViewController.modalPresentationStyle = .FullScreen
presentedViewController.modalTransitionStyle = .CoverVertical
presentedViewController.transitioningDelegate = someTransitioningDelegate
The results of this approach:
viewWillAppear gets called on the view controller behind presentedViewController when dismissing presentedViewController
The transition occurs as expected when presenting the view controller.
When dismissing the view controller, the background during the transition is black, which is undesirable.
Seems that .FullScreen causes the view controllers behind presentedViewController to be removed from the display hierarchy - which is good because presumably that's what triggers the viewWillAppear call.
Attempt 3
presentedViewController.modalPresentationStyle = .FullScreen
presentedViewController.modalTransitionStyle = .CoverVertical
The results of this are:
viewWillAppear gets called on the view controller behind presentedViewController.
The background during the transition is the view controller located behind presentedViewController, which is desired.
No custom transition.
The project I'm working on is structured in a way that makes it difficult to use delegation (which seems to be the suggested answer here). Making use of NSNotificationCenter is another alternative which lets me call the code that is supposed to be called by viewWillAppear, but from attempt 3, I'm hoping there is a more elegant approach to achieve all these:
Trigger viewWillAppear
Use a custom transition
See the view controller being presented in the background during the transition animation

Seems Apple considers it foul play to invoke viewWillAppear etc., but it's okay to invoke beginAppearanceTransition and endAppearanceTransition, which in turn will invoke viewWillAppear. I'm going with this.

The way I have achieved this in the past is by calling viewWillAppear and viewDidAppear from the transition animator. Here's a simplified example:
public func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
fromVC.viewWillDisappear(true)
toVC.viewWillAppear(true)
UIView.animateWithDuration(0.3, animations: {
//Animations here
}, completion: { (success) in
toVC.viewDidAppear(success)
fromVC.viewDidDisappear(success)
transitionContext.completeTransition(true)
})
}
I call the "will" methods before I do animations, and the "did" methods after completion

Related

testing "presentingViewController is UIViewController" works fine in one case, fails in other

Following Apple's documentation for adding and editing information Apple guide here I have a Viewcontroller with a tableview. The tableview contains a header with a "Add new" Button. If a table row is selected the detailViewController is pushed onto the stack. The detailViewController is also embedded in a UINavigationController, as in Apple's docs. If "Add new" is pressed, another segue is performed which presents the UINavigationController modally, which in turns shows the detailViewController. This works fine and the animation clearly shows a modally presented ViewController.
The detailViewController contains a Cancel Button in the NavigationBar. If it is pressed the following code is run:
#IBAction func cancel(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
var isPresentingInAddActionMode = false
if let presentingVC = self.presentingViewController{
isPresentingInAddActionMode = presentingVC is UINavigationController
}
streekgidsModel.undoManager.endUndoGrouping()
print("undo grouping ended and undone")
streekgidsModel.undoManager.undo()
if isPresentingInAddActionMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The MealViewController is not inside a navigation controller.")
}
}
The first if-statement checks if the property presentingViewController is present, and if so if it is of type UINavigationController. If so, the viewController is presented modally and should be dismissed. If not it is pushed onto the stack and the owningNavigationController should pop the detailViewController.
Running this code does not work as described by Apple. The check on the presentingViewController shows it is present, but the type check gives back "invalid". This is treated as false. The test on the owningNavigationController succeeds (I think it should fail) and the popViewController is executed. As there was no push, the view controller is not popped or dismissed and is still visible. A second press on Cancel executes the func cancel again, which results in an error as there is no longer a group started in the undo manager.
Baffling thing is that I have the same code in another viewcontroller, with similar UIViewTable and navigation and it works fine.
So to frame the question: why does this not work the way Apple describes it, why does my other view controller work as it is supposed to? Any input is appreciated.
BTW, the fatal error text is straight from the docs so the naming is not relevant and it is never executed.
I would start with checking who is presenter.
According to Apple docs on this:
When you present a view controller modally (either explicitly or implicitly) using the present(_:animated:completion:) method, the view controller that was presented has this property set to the view controller that presented it. If the view controller was not presented modally, but one of its ancestors was, this property contains the view controller that presented the ancestor. If neither the current view controller or any of its ancestors were presented modally, the value in this property is nil.
If the docs are correct then your presenter should be your "Viewcontroller with a tableview" which, I guess, is not UINavigationController. If that is the case then you should understand why your code fails.
It depends on your context of course, but I would just simplify a check this way:
var isPresentingInAddActionMode = self.presentingViewController != nil
... // your other code
if isPresentingInAddActionMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
If I understood your question and intent correctly then it doesn't matter for you who (which class) presented your detailVC and you care only about how your detailVC was presented - either pushed in navigation view controller or presented modally. I think just by checking presentingViewController property you can get that information.

No animation when pushing view controllers in UINavigationController

I have a storyboard set up with a UITabBarController which contains a UINavigationController for each tab. For one of the UINavigationControllers there are no transition animations when pushing or presenting a view controller.
There are, at least, two different cases when this happens
1. I have a storyboard segue set up to push the child view controller. The segue triggers when selecting a cell in a table view. The "Animates"-box is checked.
Attempting to programatically push the child view controller yields the same result.
self.navigationController?.pushViewController(nextController, animated: true)
2. There is also no animation when attempting to modally present another view controller from the root view controller of the navigation controller.
modalViewController.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal
self.present(modalViewController, animated: true, completion: nil)
If I present the modalViewController from another view controller the transition is animated which leads me to believe that there is something wrong in the root view controller that is presenting.
Is there a way to disable animations on a UIViewController that I might accidentally have triggered? I have checked and verified that there are no UIView.setAnimationsEnabled(false)
Use self.navigationController?.pushViewController( instead of self.present(
You set up animation in UINavigationController. But you called the function self.present( which is provided by UIViewController. UIViewController of course cannot provide the animation.
In my overridden viewWillDisappear(_ animated: Bool) of the parent view controller I had some code that reset the state of a custom view. The reset did in turn disable actions via CATransaction.setDisableActions(true), thus disabling the transition animations.
Moving the reset to viewDidDisappear(_ animated: Bool) resolved the issue.

Presenting view controller's viewDidLoad getting called after presented view controller dismissed

I have a view controller that presents another view controller like so
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let qrScannerViewController = QRScannerViewController()
qrScannerViewController.presentedBy = self
self.present(qrScannerViewController, animated:true, completion: nil)
// Do any additional setup after loading the view.
}
qrScannerViewController (the presented view controller) then calls
self.dismiss(animated:true, completion: nil)
which to my understanding calls the presenting view controllers dismiss function anyway.
Problem is, once the presented view controller has been dismissed, the presenting view controller's viewDidLoad gets called again, meaning the view controller is presented again.
Any ideas how to get around this?
Even if I use delegation the presenting view controller's viewDidLoad gets called again
Thanks
The presenting view controller is defined in a UITabController:
let qrPlaceholderViewController = QRPlaceholderViewController()
let controllers = [restaurantNavController,favouritesViewController, qrPlaceholderViewController, profileViewController]
self.viewControllers = controllers
Ok so the problem here was ARC doing its job.
When the presenting view controller presented the other view controller, ARC was unloading the presenting controller. This meant that when the presented view controller was dismissed, the presenting one was reinstantiated, hence forcing the viewDidLoad method to get called again
Solution:
A few solutions are available:
First of all I just stored a flag in a helper that I could check in the viewDidLoad method to see if it had already been loaded before and if it had, dont present the view controller again
Alternatively, I changed to once a qr code had been scanned, call a function in the presented view controllers delegate (the presenting controller) that navigated to the view that I wanted, therefore skipping the issue of the viewDidLoad being executed again.

Unwind Segue not dismissing View Controller with UIModalPresentationCustom

I'm presenting a modal view controller using a custom transition (by setting its modelPresentationStyle to UIModalPresentationCustom, providing a transitioning delegate, and UIViewControllerAnimatedTransitioning object).
In the presented view controller, I have an unwind segue hooked up to a button. The segue fires just fine; the IBAction method in the presenting view controller is called, and so is prepareForSegue in the presented view controller. However, the presented view controller is not dismissed, and the appropriate transitioning delegate method (animationControllerForDismissedController:) is not called.
If, however, I set the presented view controller's modalPresentationStyle to UIModalPresentationFullScreen (the default), the view controller is dismissed properly (this breaks my custom transition, though).
I'm at a complete loss at what to do here. I've looked through Apple's documentation, and didn't notice anything saying that one had to do special things with unwind segues when dealing with custom transitions.
I'm aware that I could call dismissViewControllerAnimated:completion: in the IBAction method of the presenting view controller, but I'd rather use that as a last resort, and get the unwind segue working the way it should (or at least know why it's not working :) ).
Any help would be much appreciated,
Thanks in advance
It seems that if you use UIModalPresentationCustom to present the controller with a custom transition manager, you also need to use a custom transition manager to dismiss it (which makes sense I guess, since you can do all kinds of weird stuff in the animator object and UIKit can't be sure that just dismissing the screen as usual will completely restore the original state - I just wish it told you that explicitly...).
Here's what I've done to fix this in my app:
override segueForUnwindingToViewController in the parent view controller (the one to which you're moving after the dismiss animation) and return an instance of your UIStoryboardSegue, either the one you've used for the original transition or a new separate class
if the unwind segue's target view controller is in a navigation hierarchy, then you need to override that method in the navigation controller instead
in the perform method call dismissViewControllerAnimated
the presented view controller needs to still hold a valid reference to the transitioning delegate, or you'll get an EXC_BAD_ACCESS (see DismissViewControllerAnimated EXC_Bad_ACCESS on true) - so either make it keep the delegate as a strong reference as described in that thread, or assign a new one before calling dismissViewControllerAnimated (it's possible that changing modelPresentationStyle to e.g. full screen before dismissing would work too, but I haven't tried that)
if the dismiss animation needs to do any non-standard things (mine luckily didn't), override animationControllerForDismissedController in the transition manager object and return a proper animator
if the target view controller is in a navigation hierarchy, then you also need to manually pop the navigation stack to the target controller before dismissing the presented screen (i.e. target.navigationController!.popToViewController(target, animated: false))
Complete code sample:
// custom navigation controller
override func segueForUnwindingToViewController(toViewController: UIViewController,
fromViewController: UIViewController,
identifier: String?) -> UIStoryboardSegue {
return CustomSegue(
identifier: identifier,
source: fromViewController,
destination: toViewController
)
}
// presented VC
var customTransitionManager: UIViewControllerTransitioningDelegate?
// custom segue
override func perform() {
let source = sourceViewController as! UIViewController
if let target = destinationViewController as? PresentedViewController {
let transitionManager = TransitionManager()
target.modalPresentationStyle = .Custom
target.customTransitionManager = transitionManager
target.transitioningDelegate = transitionManager
source.presentViewController(target, animated: true, completion: nil)
} else if let target = destinationViewController as? WelcomeViewController {
target.navigationController!.popToViewController(target, animated: false)
target.dismissViewControllerAnimated(true, completion: nil)
} else {
NSLog("Error: segue executed with unexpected view controllers")
}
}
I also met this problem when I need to pass data back from the modalpresented view.
I wandering around Google and here for a couple of hours but I couldn't find an answer that is easy to understand for me. But I did get some hint and here's a work around.
It seems that because it has to pass data back, and the dismissing process from the automatic Unwind is prior before the data passing which prevented the ViewController being dismissed. So I think that I have to manually dismiss it once one more time.
I got some luck here. I didn't notice that it was a child viewcontroller. I just configured it from the storyboard.
And then in the Unwind function, I added to lines to remove the child viewcontroller and the child view. I have no code in the sourceViewController.
Swift 4.1
#IBAction func unwindToVC(sender :UIStoryboardSegue){
if let source = sender.source as? CoreLocationVC{
if source.pinnedCity != nil{
clCity = source.pinnedCity
}
if source.pinnedCountry != nil {
clCountry = source.pinnedCountry
}
if source.pinnedTimeZone != nil {
clTimeZone = source.pinnedTimeZone
}
if source.pinnedLocation != nil {
clLocation = source.pinnedLocation
}
// I added 2 lines here and it just worked
source.view.removeFromSuperview()
source.removeFromParentViewController()
}

Present view controller modally at app launch

My application has a setup screen that should be presented modally on the root view controller if certain conditions are met.
I have looked around on SO and the internet and the closest answer so far to how to go about doing this is here:
AppDelegate, rootViewController and presentViewController
There are 2 problems with this approach however:
In iOS 8, doing it this way makes a log appear in the console, which doesn't seem to be an error, but is probably not good nonetheless:
Unbalanced calls to begin/end appearance transitions for UITabBarController: 0x7fe20058d570.
The root view controller actually shows up very briefly when the app launches, and then fades into the presented view controller (even though I explicitly call animated:NO on my presentViewController method).
I understand that I can set my root controller dynamically in applicationDidFinishLaunchingWithOptions: but I specifically want to present the setup screen modally, so that when the user is done with it, it dismisses and the true first view of the application is revealed. This is to say, I don't want to dynamically change my root view controller to my setup screen, and present my app experience modally when the user is done setting up.
Presenting the view controller on my root view controller viewDidLoad method also leads to a noticeable blink of the UI when the app is launched for the first time.
Is it possible to programmatically present a view controller modally, before the application has rendered anything so that the first view in place is the modal view controller?
UPDATE: Thank you for the comments, adding my current code as suggested:
In my AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
[self.window makeKeyAndVisible];
[self.window.rootViewController presentViewController:[storyboard instantiateViewControllerWithIdentifier:#"setupViewController"] animated:NO completion:NULL];
return YES;
}
This does what I need except for the fact that it briefly shows the window's root view controller for a second when the application launches, then fades the setupViewController, which I find odd given that I am presenting it without animation and fading is not how a modal view controller is presented anyway.
The only thing that has gotten me close is manually adding the view in the root view controller's view did load method like so:
- (void)viewDidLoad
{
[self.view addSubview:setupViewController.view];
[self addChildViewController:setupViewController];
}
The problem with this approach is that I can no longer "natively" dismiss the setupViewController, and will now need to deal with the view hierarchy and animated it out myself, which is fine if it's the only solution, but I was hoping there was a sanctioned way of adding a view controller modally without animation before the root view controller displays.
UPDATE 2: After trying a lot of things out and waiting for an answer for 2 months, this question proposes the most creative solution:
iOS Present modal view controller on startup without flash
I guess it's time to accept that it's just not possible to present a view modally without animation before the root view controller appears. However the suggestion in that thread is to create an instance of your Launch Screen and leave that on for longer than default until the modal view controller has had a chance to present itself.
I guess it's time to accept that it's just not possible to present a view modally without animation before the root view controller appears.
Before it appears, no, you can't present. But there are multiple valid approaches to solve this visually. I recommend solution A below for its simplicity.
A. add launchScreen as subview, then present, then remove launchscreen
Solution is presented here by ullstrm and does not suffer from Unbalanced calls to begin/end appearance transitions:
let launchScreenView = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!.view!
launchScreenView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
launchScreenView.frame = window!.rootViewController!.view.bounds
window?.rootViewController?.view.addSubview(launchScreenView)
window?.makeKeyAndVisible()
// avoiding: Unbalanced calls to begin/end appearance transitions.
DispatchQueue.global().async {
DispatchQueue.main.async {
self.window?.rootViewController?.present(myViewControllerToPresent, animated: false, completion: {
launchScreenView.removeFromSuperview()
})
}
}
B. addChildViewController first, then remove, then present
Solution is presented here by Benedict Cohen.
I was in the same boat as you and found the same answer. I learned that you can get rid of the first problem (unbalanced calls warning) by setting the modalPresentationStyle of your setupViewController to .OverCurrentContext or .OverFullScreen. Problem solved - so I thought.
Only later I noticed the second problem and that was something I couldn't live with... back to square one.
As you, I wanted a solution with a normal view hierarchy and I didn't want to 'fake' something. I think the most elegant solution is switching your windows rootViewController on first dismissal of your setupViewController.
So, at launch, you set the setupViewController as the rootViewController (if needed):
var window: UIWindow?
var tabBarController: UITabBarController!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
tabBarController = window!.rootViewController as! UITabBarController
if needsToShowSetup() {
let setupViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SetupViewController") as! SetupViewController
window?.rootViewController = setupViewController
}
return true
}
When setup is done you call a method in your appDelegate to switch to the 'real' rootViewController:
func switchToTabBarController() {
let setupUpViewController = window!.rootViewController!
tabBarController.view.frame = window!.bounds
window!.insertSubview(tabBarController.view, atIndex: 0)
let height = setupUpViewController.view.bounds.size.height
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1, options: .allZeros, animations: { () -> Void in
setupUpViewController.view.transform = CGAffineTransformMakeTranslation(0, height)
}) { (completed) -> Void in
self.window!.rootViewController = self.tabBarController
}
}
I was after a 'cover vertical' dismiss animation. For crossfade and others, you could use UIView.transitionFromView(fromView: UIView, toView: UIView...). Hereafter you can present/dismiss your setupController the normal way, so your doneButton action could be something like this:
#IBAction func doneButtonSelected(sender: UIButton) {
if presentingViewController != nil {
presentingViewController!.dismissViewControllerAnimated(true, completion: nil)
} else {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.switchToTabBarController()
}
}
Actually, I implemented this through delegation with the appDelegate being the delegate the first time around.
I understand that I can set my root controller dynamically in applicationDidFinishLaunchingWithOptions: but I specifically want to present the setup screen modally, so that when the user is done with it, it dismisses and the true first view of the application is revealed.
I have two suggestions. One is to try doing this in viewDidAppear:. I tried it and although you do see the root view controller's view if you look carefully, you barely see it, and sometimes you don't see it at all if you blink:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self presentViewController:[self.storyboard instantiateViewControllerWithIdentifier:#"setupViewController"] animated:NO completion:NULL];
}
Of course you'd need to add a flag so that you don't do that every time viewDidAppear: is called - otherwise you'll never be able to get back to this view controller at all! But that's trivial and I leave it as an exercise for the reader.
My other suggestion - and you have clearly thought about doing this - is to use a custom embedded (child) view controller instead. That works around the limitations of the whole "presentation" thing.
I'd launch and set up things dynamically, as you say, with the child view controller present if needed, configuring it all during the launch process. The child view controller's view would just cover the root view controller's view. So that's what the user would see as the app launches.
And then when the user's setup procedure is over and the user "dismisses" this view, you tear that view down, with animation, and remove the child view controller - revealing the root view controller's view underneath. The animation will make this all indistinguishable from the dismissal of presented view, even though it isn't really one.

Resources