Container View Controller - Unbalanced calls to begin/end appearance transitions - ios

I'm trying to develop Container View Controller as it is shown in Apple documentation.
For now I have simple init code in viewDidAppear:
presentedVC = self.storyboard!.instantiateViewControllerWithIdentifier(Storyboard.yesNoControllerID)
self.addChildViewController(presentedVC)
presentedID = Storyboard.yesNoControllerID
presentedVC.view.frame = containerView.bounds
self.containerView.addSubview(presentedVC.view)
presentedVC.didMoveToParentViewController(self)
I have implemented exchange method, like in Apple doc:
private func exchangeVC(withVC viewController: UIViewController){
presentedVC.willMoveToParentViewController(nil)
self.addChildViewController(viewController)
viewController.view.frame = newViewStartFrame
let endFrame = oldViewEndFrame
self.containerView.addSubview(viewController.view)
self.transitionFromViewController(presentedVC, toViewController: viewController, duration: 0.25, options: UIViewAnimationOptions.CurveLinear, animations: {
viewController.view.frame = self.presentedVC.view.frame
self.presentedVC.view.frame = endFrame
}) { (finished) in
self.presentedVC.view.removeFromSuperview()
self.presentedVC.removeFromParentViewController()
viewController.didMoveToParentViewController(self)
self.presentedVC = viewController
}
}
Then, I have button that is calling simply:
let controller = self.storyboard!.instantiateViewControllerWithIdentifier(presentedID)
exchangeVC(withVC: controller)
With this code, my controllers are animating on screen on button press. But at the end of animation I'm getting:
Unbalanced calls to begin/end appearance transitions for
UIViewController: 0x7aecf730.
Can You tell me what I have done wrong? How to get rid of this error/warning?

This is an easy mistake to make, but Apple's documentation for transition(from:to:duration:options:animations:completion:) states that:
This method adds the second view controller's view to the view
hierarchy and then performs the animations defined in your animations
block. After the animation completes, it removes the first view
controller's view from the view hierarchy.
You were getting that Unbalanced calls to begin/end appearance transitions warning because you were adding the subview once with:
self.containerView.addSubview(viewController.view)
... then adding it again when calling transition(from:to:duration:options:animations:completion:)
Removing the call to addSubview would fix the problem.

I had a similar issue. Generally, that warning appears when you try to present the same view controller without dismissing it first, but the transition method should handle all that for you. I ended up fixing it by using
UIView.animate(withDuration:animations:completion:)
instead of
UIViewController.transition(from:to:duration:options:animations:completion:)
as you are above. Of course, I had to manually add and remove the subviews as well. I'm not sure why this change worked, but my best guess is that there's something wrong with the UIViewController transition method that issues that warning. Fortunately the fix is really easy.

I was seeing this unbalanced calls error on iPhones, in button tap handler. traced it to a dismiss(animated: no) followed by a present(.. animated: no)
solution was to do the present in a completion handler for the dismiss call.

Related

Why viewDidLoad function is not called?

I want to change UIBarButton's color in the navigationBar. To achieve this, in viewDidLoad: I put this line:
navigationController?.navigationBar.tintColor = .white
Everything works fine until I started to notice something strange. That UIBarButton is used to dismiss the UIViewController. When it is pressed, I just dismiss the viewController. But, if I present it (viewController) again, the color of the UIBarButton is not white, it gets tintColor of the application.
After doing some debugging, I noticed that viewDidLoad: is not called again after the viewController is just dismissed and presented again. The reason why my UIBarButton has a different color is because I change its color in viewDidLoad:. When viewDidLoad: is not called, of course, color is not changed.
It was an interesting discovery for me the fact that iOS doesn't call viewDidLoad: for UIViewController that was presented already. Possibly, it is due to the optimisation, because it is not efficient to draw the whole UI every time.
My solution to this problem can be to change color, not in viewDidLoad:, but in viewDidAppear:. But, is it right approach to solve a problem? And why viewDidLoad: is not called in the above situation?
It looks like you create and store you view controller, but present it wrapped in UINavigationController:
let controller = YourModalViewController()
...
func presentMyModal() {
present(UINavigationController(rootViewController: controller))
}
In this case your viewDidLoad method will be called just once and you'll have visual bug. If you want to leave styling code of your modal inside it's file you can create instance func which will return this controller wrapped and styled.
extension YourModalViewController {
func wrappedInNC() -> UINavigationController {
let nc = UINavigationController(rootViewController: controller)
// Styling code.
return nc
}
}

How to set an animation once the view controller is loaded

My goal is to set off an animation when my second viewController is loaded. How do call the function once the view is loaded?
Here is the code:
UIView.animate(withDuration: 1.5, animations: {self.greyScreen.frame.origin.y = -0.39*self.screenHeight}, completion: nil)
Any advice much appreciated!
viewDidAppear() is the method of UIViewContoller Life Cycle which is called once the screen is completely visible i.e. loaded all views into the memory hierarchy. So put your this block of animation into this method. You'll get your animation effect
Place that code inside viewDidAppear.
viewDidAppear is called when the view is shown on the device, so adding that code to this method will trigger the animation as soon as the view is displayed.

willMoveToSuperview is called twice

I'm adding a view to my view controller. In this view I have implemented willMoveToSuperview. Now I experienced that this function is called twice:
When the view is added to the superview (as intended)
When the current view controller is dismissed (e.g. a new view controller is pushed on the stack of the navigation controller)
Is this the intended behavior? What other method could I use to detect if the current view is added to a superview only? didMoveToSuperview seems to do the same. Or should I use a variable which remembers how often the function is called?
Edit:
Now I think I found the reason why it is called twice. I'm using a hide method to dismiss the view. It's in C# but it shouldn't matter here:
UIView.Animate (
0.5, // duration
() => { Alpha = 0; },
() => { RemoveFromSuperview(); }
);
If I comment this out it isn't called twice. How can I keep the animation and assure that it is only called once?
When a view is added to a superview, the system sends willMoveToSuperview: to the view. The parameter is the new superview.
When a view is removed from a superview, the system sends willMoveToSuperview: to the view. The parameter is nil.
You can't prevent the system from sending willMoveToSuperview: when you remove the view from its superview, but you can check the parameter:
- (void)willMoveToSuperview:(UIView *)newSuperview {
if (newSuperview != nil) {
// not a removeFromSuperview situation
}
}

UIPageViewController prevent hiding keyboard on scroll

I have a UIPageViewController with multiple UIViewController, each one containing a UITextField.
Whenever I slide the UIPageViewController, the keyboard automatically dismisses. Is there a way to prevent this behavior?
Invoking becomeFirstResponder on the new UITextfield but the animation the won't fix the problem.
You can try to embed the PageViewController as a ChildViewController of an other viewController.
I believe this way the navigation in the PageViewController will not effect the keyboard.
I am not sure if that is needed, but if it is still not working you can set your ParentViewController as the firstResponder when transition occurs.
unfortunately this seems to come from the animation of the transition showing as done, before it is actually done,
the workarounds I can think of are
1. made the animating false
2. set textFieldShouldEndEditing in the next VC to return NO or handle it with a bool
3. add a delay in the animation, or in the next VC viewWillAppear
When using setViewControllers on UIPageViewController (setting a controller without the scroll), it seems that the completion block is called just before the scrollView has reached its final position. When it does, it dismisses the first responder.
The solution we found was to first, grab the scroll view:
// In UIPageViewController subclass
for subview in self.view.subviews {
if let scrollV = subview as? UIScrollView {
scrollV.delegate = self
self.scrollView = scrollV // Optional, we don't really need it
}
}
The scroll view position is done when its offset is the x position of the middle controller. That is, the offset will equal the view's length.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x == self.view.width {
// Transition is 'really' done.
}
}
At that point, you can send a notification that transition is completed.
What we did, is hold a completion block and call it when the transition is done.
Create a function, so that a controller can pass a block in:
func transitionCompleted(completed:(()->())?) {
self.transitionCompletedBlock = completed
}
When transition is completed:
self.transitionCompletedBlock?()
The controller with the keyboard will look like:
pagerController.transitionCompleted(completed: {
self.textfield.becomeFirstResponder()
})

Navigation bar title bug with interactivePopGestureRecognizer

I am having a weird problem with UINavigationBar's title in an app when interactivePopGestureRecognizer comes into play. I have made a demo app to showcase this bug.
Setup:
The rootViewController is a UINavigationController.
FirstViewController has the navigation bar hidden, and interactivePopGestureRecognizer.enabled = NO;
Second and ThirdViewControllers have the navigation bar visible and the popgesture enabled.
Bug:
The bug occurs when going back from the Second to the First view using the popgesture. If you pull the second view halfway and then go back to the second view, the navigation title will show "Second View" (as expected).But when you go to the Third view, the title will not change to "Third View". And then on clicking the back button of the Third view, the navbar will get messed up.
Please check out my demo app. Any help explaining why this bug is happening will be appreciated. Thanks!
Remove Red Herrings
First of all, your example can be greatly simplified. You should delete all the viewDidLoad stuff, as it is a complete red herring and just complicates the issue. You should not be playing around with the pop gesture recognizer delegate on every change of view controller; and turning the pop gesture recognizer off and on is irrelevant to the example (it is on by default, and should just be left on for this example). So delete this kind of thing in all three view controllers:
- (void)viewDidLoad {
[super viewDidLoad];
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
self.navigationController.interactivePopGestureRecognizer.delegate = self;
}
}
(Don't delete the code that sets self.title, though you could have made things even simpler by doing that in the xib file for each view controller.)
You can also get rid of other unused methods throughout, such as the init... methods and memory alert methods.
Another issue, by the way, is that you have forgotten to call super in your implementations of viewWillAppear:. It is required that you do this. I don't think that affects the bug, but it is well to obey all the rules before you start trying to track these things down.
Now the bug still happens but we have much simpler code, so we can start to isolate the issue.
How The Pop Gesture Works
So what's the cause of the problem? I think the most obvious way to understand it is to realize how the pop gesture works. This is an interactive view controller transition animation. That's right - it's an animation. The way it works is that the pop animation (slide from the left) is attached to the superview layer, but with a speed of 0 so that it doesn't actually run. As the gesture proceeds, the timeOffset of the layer is constantly being updated, so that the corresponding "frame" of the animation appears. Thus it looks like you are dragging the view, but you are not; you are just making a gesture, and animation is proceeding at the same rate and to the same degree. I have explained this mechanism in this answer: https://stackoverflow.com/a/22677298/341994
Most important (pay attention to this part), if the gesture is abandoned in the middle (which it almost certainly will be), a decision is made as to whether the gesture is more than half-way completed, and based on this, either the animation is rapidly played to the end (i.e. the speed is set to something like 3) or the animation is run backwards to the start (i.e. the speed is set to something like -3).
Solutions And Why They Work
Now let's talk about the bug. There are two complications here that you've accidentally banged into:
As the pop animation and pop gesture begin, viewWillAppear: is called for the previous view controller even though the view may not ultimately appear (because this is an interactive gesture and the gesture may be cancelled). This can be a serious issue if you are used to the assumption that viewWillAppear: is always followed by the view actually taking over the screen (and viewDidAppear: being called), because this is a situation in which those things might not happen. (As Apple says in the WWDC 2013 videos, "view will appear" actually means "view might appear".)
There is a secondary set of animations, namely, everything connected with the navigation bar - the change of title (it is supposed to fade into view) and, in this case, the change between not hidden and hidden. The runtime is trying to coordinate the secondary set of animations with the sliding view animation. But you have made that difficult by calling for no animation when the bar is hidden or shown.
Thus, as you've already been told, one solution is to change animated:NO to animated:YES throughout your code. This way, the showing and hiding of the navigation bar is ordered up as part of the animation. Therefore, when the gesture is cancelled and the animation is run backwards to the start, the showing/hiding of the navigation is also run backwards to the start - the two things are now staying coordinated.
But what if you really don't want to make that change? Well, another solution is to change viewWillAppear: to viewDidAppear: throughout. As I've already said, viewWillAppear: is called at the start of the animation, even if the gesture won't be completed, which is causing things to get out of whack. But viewDidAppear: is called only if the gesture is completed (not canceled) and when the animation is already over.
Which of those two solutions do I prefer? Neither of them! They both force you to make changes you don't want to make. The real solution, it seems to me, is to use the transition coordinator.
The Transition Coordinator
The transition coordinator is an object supplied by the system for this very purpose, i.e., to detect that we're involved in an interactive transition and to behave differently depending on whether it is canceled or not.
Concentrate just on the OneViewController implementation of viewWillAppear:. This is where things are getting messed up. When you're in TwoViewController and you start the pan gesture from the left, OneViewController's viewWillAppear: is being called. But then you cancel, letting go of the gesture without completing it. In just that one case, you want not to do what you were doing in OneViewController's viewWillAppear:. And that is exactly what the transition coordinator allows you to do.
Here, then, is a rewrite of OneViewController's viewWillAppear:. This fixes the problem without your having to make any other changes:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
id<UIViewControllerTransitionCoordinator> tc = self.transitionCoordinator;
if (tc && [tc initiallyInteractive]) {
[tc notifyWhenInteractionEndsUsingBlock:
^(id<UIViewControllerTransitionCoordinatorContext> context) {
if ([context isCancelled]) {
// do nothing!
} else { // not cancelled, do it
[self.navigationController setNavigationBarHidden:YES animated:NO];
}
}];
} else { // not interactive, do it
[self.navigationController setNavigationBarHidden:YES animated:NO];
}
}
The fix is simple , but I don't have any explanation at the moment why this is happening.
One your OneViewController change your viewWillAppear to ,
-(void)viewWillAppear:(BOOL)animated{
// [self.navigationController setNavigationBarHidden:YES animated:NO];
self.navigationController.navigationBar.hidden = YES;
}
and on the second and third view controllers change it to,
-(void)viewWillAppear:(BOOL)animated{
//[self.navigationController setNavigationBarHidden:NO animated:NO];
self.navigationController.navigationBar.hidden = NO;
}
Strange but this will fix the issue when we directly use the hidden property of the UINavigationBar.
I don't know how do you make "FirstViewController has the navigation bar hidden".
I have the same problem, and I fixed it by replacing
self.navigationController.navigationBarHidden = YES / NO;
by
[self.navigationController setNavigationBarHidden:YES / NO animated:animated];
I gave up trying to make this work used my own swipe recognizer that pops the navigation stack:
override func viewDidLoad() {
super.viewDidLoad()
// disable system swipe back gesture and add our own
navigationController?.interactivePopGestureRecognizer?.enabled = false
let swipeBackGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipeBackAction:")
swipeBackGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Right
tableView.addGestureRecognizer(swipeBackGestureRecognizer)
}
func swipeBackAction(sender: UISwipeGestureRecognizer) {
navigationController?.popViewControllerAnimated(true)
}
Disable the system interactivePopGestureRecognizer
Create your own UISwipeGestureRecognizer with a Right direction
Pop the navigation stack animated when he swipe is detected
Here's what fixed it for me (Swift)
1st view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
2nd and 3rd view controllers:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}

Resources