iOS 13 UIViewController doesn't state restore its modalPresentationStyle to fullscreen - ios

I've adopted the new iOS 13 modal presentation style across most of the modals in my app, however one navigation controller doesn't really fit the new style well so I set it to
UIModalPresentationFullScreen
when presenting it which works fine.
I just noticed a bug that when my app undergoes state preservation and restoration whilst on a ViewController being presented modally its loaded back in after restoration with a modalPresentationStyle of UIModalPresentationPageSheet.
As built in properties of a ViewController their state restoration is Apple's responsibility and I guess they just missed out handling it properly.
Thankfully I've been able to fix the issue by adding the following to my existing state restoration handling
- (void)decodeRestorableStateWithCoder:(NSCoder *)aDecoder
{
...
self.navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
}
Anyone else encountered this or got an alternative solution?
Cheers

Related

iOS 13 new presentation styles - presenting VC not shrinking down

I am having a problem with the new VC presentation styles in iOS 13. I want to implement the new "stack" presentation.
However I do have an issue where the PRESENTING view controller is not shrinking down as it should causing a really ugly look.
The only solution I found to this problem is setting the background (ie presenting) VC as the new root view controller with
window.rootViewController = self
However this is not recommended by Apple and causes lots of odd bugs like dissapearing status bar or cutting of segue animations.
Can someone please explain to me how to fix this issue or how to properly use the new presentation styles or how to properly set a new root VC.
The intended behaviour:
The buggy behaviour:

modal view controller animates on state restoration

I have a strange problem with state restoration for a Universal app with Split View Controller.
The strange thing that I am doing things in a very standard way using a Storyboard and segues and with a restoration identifier for alle relevant view controllers. There is not really any code, as the logic is in the Storyboard and a minimal XCode project shows this.
The problem is with a settings screen that is shown modally as a form sheet presented from the split view controller. My view controller hierarchy ends up correct, but the transition doesn't really make sense. For some reason state restoration animates the modal controller into place.
Since the screen starts out with a screenshot from the last time the app was running, with the settings controller already present, the animation is just visual noise.
I have tried to disable animation on the segue which is respected when entering the settings interactively, but when state restoration does the same thing, the animation is there.
What is the standard way to avoid this?
Calling self.window?.makeKeyAndVisible() in application(_:willFinishLaunchingWithOptions:) solved the issue for me.
More info in the docs:
Important
If your app relies on the state restoration machinery to restore its
view controllers, always show your app’s window from this method. Do
not show the window in your app’s
application:didFinishLaunchingWithOptions: method. Calling the
window’s makeKeyAndVisible method does not make the window visible
right away anyway. UIKit waits until your app’s
application:didFinishLaunchingWithOptions: method finishes before
making the window visible on the screen.

Exception "Application tried to present modally an active controller" crash in iOS 8 only

UIPopoverController *popCtrl = [[UIPopoverController alloc] initWithContentViewController:self.rootViewController.navigationController];
popCtrl.delegate = self;
[popCtrl presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
This code is in a button action, where the button is the "sender".
The line with presentPopoverFromBarButtonItem causes an exception to be thrown with the reason: Application tried to present modally an active controller DetailViewController: 0x15a54c00. DetailViewController is "self" in this case and it is only a delegate to popCtrl, so I don't see how it could be trying to present modally. It's supposed to be presenting rootViewController.navigationController.
As you may have guessed from the names, rootViewController and detailViewController are inside a SplitViewController, but prior to trying to present rootViewController with the the popover, it is removed from the SplitViewController.
This only happens on iOS 8 when built with the iOS 8 SDK. It's also not 100% reproducible. Most of the time this exception occurs, but sometimes after I restart the app it does not occur at all until I rerun the app, then it starts happening all the time again. (I put it in a try/catch so I know it can occur more than once per run.)
I'm almost positive this is yet another iOS 8 bug in the SDK, but has anyone come up with a workaround?
I faced the same problems while updating some app, which was initially developed at the times of iOS 5.0. Removing the controller from the UISplitViewController right before using it in the popover did not work, neither did it help to switch to the newer UIPopoverPresentationController.
However, I was able to swipe-in my (master) controller from the left side. More or less, I discovered that "feature" by accident, so I looked up where this came from and found this in Apple's iOS SDK 5.1 release notes:
In 5.1 the UISplitViewController class adopts the sliding presentation style when presenting the left view (previously only seen in Mail). This style is used when presentation is initiated either by the existing bar button item provided by the delegate methods or by a swipe gesture within the right view. No additional API adoption is required to obtain this behavior, and all existing API, including that of the UIPopoverController instance provided by the delegate, will continue to work as before. If the gesture cannot be supported in your app, set the presentsWithGesture property of your split view controller to NO to disable the gesture. However, disabling the gesture is discouraged because its use preserves a consistent user experience across all applications.
(Source: iOS 5.1 Release Notes, requires Apple Developer Login)
I didn't test what happens if you set the mentioned property to NO and if it releases the controller, but I wouldn't put too much hope on that.
So even after removing it manually from the UISplitViewController, my view controller was still active on that hidden swipeable pane, which appears to happen internally in the SDK. I'm aware of the fact that this still worked fine until iOS 7.x, but I actually consider that as tolerated bug now, closed with iOS 8.0.
I ended up abandoning the popover completely and using the default UISplitViewController behaviour of iOS 5.1 and above. For some extra tweaking, you can change UISplitViewController.preferredDisplayMode to fit your needs, this saved me a lot of time to upgrade old code which never heard of auto layout.
I am using a popover in iOS 8 programmatically in an IBAction. I don't know if this is a bug or not but I do know that they did make some changes to modal views and presentations. There is a good WWDC video on it, see if you can find it. The way I am doing it (keep in mind this is Swift, so you will need to do a little bit of translation) is the following:
let controller = self.settingsVC
controller.preferredContentSize = CGSizeMake(345, 234)
controller.modalPresentationStyle = UIModalPresentationStyle.Popover
var settingsPopController = controller.popoverPresentationController
settingsPopController?.delegate = self
settingsPopController?.sourceView = self.view
settingsPopController?.sourceRect = sender.frame
controller.modalPresentationStyle = UIModalPresentationStyle.Popover
self.presentViewController(controller, animated: true, completion: nil)
In this code, self.settingsVC is a property of the ViewController I set which is initialized to another ViewController in the storyboard, but you can replace controller with the ViewController you need to present as a popover. Also, please note that your UIViewController class must implement UIPopoverPresentationControllerDelegate.
If you need any help with the translation, I'd be happy to give you a hand.

iOS 8 viewDidLoad modal presentation causes multiple presentations

So I've got a screen that does a check for certain attributes and under defined circumstances will instantly load another view modally in viewDidLoad, without animation, over the currently-loading view (so as not to show the view below). Prior to iOS 8 when this was done, the original view would pause its loading (would not proceed with viewWillAppear, viewDidLayoutSubviews etc.) until the overlaying controller was dismissed. This behaviour I found was appropriate for my needs, as any animation on elements in the original view, could then be done. However, in iOS 8 I'm getting a completely different chain of events. First off, for some reason viewDidLayoutSubviews is being called twice (what's up with that?) but more importantly the view is not liking another controller being popped up at all anytime before viewDidAppear, complaining about unbalanced calls to begin/end appearance transitions. Not only that, but the underlying viewController continues with it's loading (viewWillAppear,viewDidLayoutSubviews etc.) even though it's not being shown which causes all the methods in those events to fire. I appreciate if Apple have updated the way something like this is meant to be achieved, so if the new meta is a completely different process I'm willing to adopt, however, as it is I can't get this to work appropriately.
I'd appreciate any help on how to get this modal view to interject without causing the underlying view to continue it's loading.
Thanks,
Mike
UPDATE: Going to bring some code in. Below is the viewDidLoad of the main viewController that presents the modal VC if need.
-(void) viewDidLoad{
if(hasNotSeenTutorial){
TutVC* vc = [[TutVC alloc] initWithNibName:#"tutNib" bundle:nil]
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self.navigationController presentViewController:vc animated:NO completion:^{
NSLog(#"Has Completed Presentation");
}];
}
}
This is where the issues are. Calling the presentation here in viewDidLoad, causes the presentation of the presenting VC to continue. Prior to iOS 8 the presenting VC if not yet presented, would pause, until the modal VC had been dismissed, it would then complete as usual. This is not the case in iOS 8, as per my original post.
Apple has made its rules stricter with ios 8. To give you an example and I ll drive my point through this:- In my app i used to pop some view controllers off the navigation stack and just after that, push the a new one, but that pop was never seen in ios7, only a push transition appeared to happen (when logically, pop should have been seen and then the push). And in ios 8 this thing changed. Now a push is seen only after the pop is seen and noticed. which breaks the UX rather badly.
I have noticed this strictness in other areas as well but those are not UI/UX related so i wont go into its detail right now.
As far as your situation go, With my experience I can tell you that you ve been doing stuff in a wrong manner. As apple has gone strict your implementation seems to break.
The only solution in my opinion is to shift every check in viewdidAppear.
If you wish to continue the way you were doing for ios7 earlier you might use this check:
if([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
{
// Code for ios 8 implementation
}
else
{
// Code for ios 7 implementation
}
Though i would reccomend you to avoid because wat u are aiming is perfectly achievable.
Also what you are doing can easily cause inconsistency in the navigation stack which can crash the application.

UISplitViewController state restoration in iOS 8

On iOS 8, UISplitViewController appears to save and restore the state of its subviews, for example, whether the master view is hidden or not.
This is undesirable because my app should always show the master view in landscape and always hide it in portrait. If the user closes the app in landscape (landscape state is saved) and reopens it in portrait (landscape state is restored), then the UISplitViewController shows the master view in the wrong configuration.
I still need to supply a restoration identifier to the UISplitViewController so that is subview controllers have their own state saved and restored. So how does one prevent UISplitViewController from restoring its own state, or override this behavior?
I solved this by subclassing UISplitViewController and overriding - (void)decodeRestorableStateWithCoder:(NSCoder *)coder to do nothing. This way the split view controller does not have an opportunity to restore its views, but its child view controllers still participate in state restoration.
The first thing needed when implementing UI State Restoration is changing from using didFinishLaunchingWithOptions to willFinishLaunchingWithOptions. If you set the delegate now in the willFinish the collapse will be called as expected. The issue was likely the delegate was set too late and it had already collapsed without your special handling.
Another issue is that the restoration paths to the controllers are different when in landscape and portrait so could be getting in a weird state. Because of the change it can't automatically find the existing detail view controller and creates new instances and either both or one of them likely get thrown away by the split view delegate because of misconfiguration of the detail item. In the State Restoration docs under "Recreate Your View Controllers" on step 3 it says it looks for an already created view controller with the same path which sadly fails when restoring after an orientation/trait change because the path is different. So it falls back to step 4 and creates a brand new empty misconfigured detail controller and is the reason you see the wrong configuration of controllers.
To understand the restoration identifier paths, implement application:viewControllerWithRestorationIdentifierPath:coder: in the app delegate and output the path components you'll see in portrait the last to be restored looks like:
SplitViewController,
MasterNavigationController,
DetailNavigationController,
DetailViewController
...which corresponds the single hierarchy primary of the split view controller (note: DetailNavigationController is a hidden nested navigation controller in this configuration).
And in landscape the last two to be restored are:
SplitViewController,
MasterNavigationController,
MasterViewController
and
SplitViewController,
DetailNavigationController,
DetailViewController
...which corresponds to the primary and secondary controller hierarchies of the split view.
So now knowing the restoration path to DetailViewController can be different, you can understand that if you try to automatically restore a portrait path while the storyboard has initlaised in landscape it won't find that detail view controller and resort to creating a new one. So I think the solution is to give it a helping hand to find it regardless of how the restoration path was saved:
- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder{
if([identifierComponents.lastObject isEqualToString:#"DetailViewController"]){
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *secondaryNavigationController = splitViewController.viewControllers.lastObject;;
DetailViewController *detail = (DetailViewController *)secondaryNavigationController.viewControllers.firstObject;
return detail;
}
return nil;
}
Now the restoration will correctly use the existing detail controller which is configured correctly and it won't be thrown away by the split view delegate which was resulting in you being left with the master.
Another way this issue can manifest is seeing two detail controllers pushed onto the navigation stack after restoration, which is what happens if you force the split view delegate to not throw away the initial detail controller, and when the restoration creates another one you end up with two pushed on!

Resources