Just started using Xcode 4.5 and I got this error in the console:
Warning: Attempt to present < finishViewController: 0x1e56e0a0 > on < ViewController: 0x1ec3e000> whose view is not in the window hierarchy!
The view is still being presented and everything in the app is working fine. Is this something new in iOS 6?
This is the code I'm using to change between views:
UIStoryboard *storyboard = self.storyboard;
finishViewController *finished =
[storyboard instantiateViewControllerWithIdentifier:#"finishViewController"];
[self presentViewController:finished animated:NO completion:NULL];
Where are you calling this method from? I had an issue where I was attempting to present a modal view controller within the viewDidLoad method. The solution for me was to move this call to the viewDidAppear: method.
My presumption is that the view controller's view is not in the window's view hierarchy at the point that it has been loaded (when the viewDidLoad message is sent), but it is in the window hierarchy after it has been presented (when the viewDidAppear: message is sent).
Caution
If you do make a call to presentViewController:animated:completion: in the viewDidAppear: you may run into an issue whereby the modal view controller is always being presented whenever the view controller's view appears (which makes sense!) and so the modal view controller being presented will never go away...
Maybe this isn't the best place to present the modal view controller, or perhaps some additional state needs to be kept which allows the presenting view controller to decide whether or not it should present the modal view controller immediately.
Another potential cause:
I had this issue when I was accidentally presenting the same view controller twice. (Once with performSegueWithIdentifer:sender: which was called when the button was pressed, and a second time with a segue connected directly to the button).
Effectively, two segues were firing at the same time, and I got the error: Attempt to present X on Y whose view is not in the window hierarchy!
viewWillLayoutSubviews and viewDidLayoutSubviews (iOS 5.0+) can be used for this purpose. They are called earlier than viewDidAppear.
For Display any subview to main view,Please use following code
UIViewController *yourCurrentViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (yourCurrentViewController.presentedViewController)
{
yourCurrentViewController = yourCurrentViewController.presentedViewController;
}
[yourCurrentViewController presentViewController:composeViewController animated:YES completion:nil];
For Dismiss any subview from main view,Please use following code
UIViewController *yourCurrentViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (yourCurrentViewController.presentedViewController)
{
yourCurrentViewController = yourCurrentViewController.presentedViewController;
}
[yourCurrentViewController dismissViewControllerAnimated:YES completion:nil];
I also encountered this problem when I tried to present a UIViewController in viewDidLoad. James Bedford's answer worked, but my app showed the background first for 1 or 2 seconds.
After some research, I've found a way to solve this using the addChildViewController.
- (void)viewDidLoad
{
...
[self.view addSubview: navigationViewController.view];
[self addChildViewController: navigationViewController];
...
}
Probably, like me, you have a wrong root viewController
I want to display a ViewController in a non-UIViewController context,
So I can't use such code:
[self presentViewController:]
So, I get a UIViewController:
[[[[UIApplication sharedApplication] delegate] window] rootViewController]
For some reason (logical bug), the rootViewController is something other than expected (a normal UIViewController). Then I correct the bug, replacing rootViewController with a UINavigationController, and the problem is gone.
Swift 5 - Background Thread
If an alert controller is executed on a background thread then the "Attempt to present ... whose view is not in the window hierarchy" error may occur.
So this:
present(alert, animated: true, completion: nil)
Was fixed with this:
DispatchQueue.main.async { [weak self] in
self?.present(alert, animated: true, completion: nil)
}
TL;DR You can only have 1 rootViewController and its the most recently presented one. So don't try having a viewcontroller present another viewcontroller when it's already presented one that hasn't been dismissed.
After doing some of my own testing I've come to a conclusion.
If you have a rootViewController that you want to present everything then you can run into this problem.
Here is my rootController code (open is my shortcut for presenting a viewcontroller from the root).
func open(controller:UIViewController)
{
if (Context.ROOTWINDOW.rootViewController == nil)
{
Context.ROOTWINDOW.rootViewController = ROOT_VIEW_CONTROLLER
Context.ROOTWINDOW.makeKeyAndVisible()
}
ROOT_VIEW_CONTROLLER.presentViewController(controller, animated: true, completion: {})
}
If I call open twice in a row (regardless of time elapsed), this will work just fine on the first open, but NOT on the second open. The second open attempt will result in the error above.
However if I close the most recently presented view then call open, it works just fine when I call open again (on another viewcontroller).
func close(controller:UIViewController)
{
ROOT_VIEW_CONTROLLER.dismissViewControllerAnimated(true, completion: nil)
}
What I have concluded is that the rootViewController of only the MOST-RECENT-CALL is on the view Hierarchy (even if you didn't dismiss it or remove a view). I tried playing with all the loader calls (viewDidLoad, viewDidAppear, and doing delayed dispatch calls) and I have found that the only way I could get it to work is ONLY calling present from the top most view controller.
I had similar issue on Swift 4.2 but my view was not presented from the view cycle. I found that I had multiple segue to be presented at same time. So I used dispatchAsyncAfter.
func updateView() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
// for programmatically presenting view controller
// present(viewController, animated: true, completion: nil)
//For Story board segue. you will also have to setup prepare segue for this to work.
self?.performSegue(withIdentifier: "Identifier", sender: nil)
}
}
My issue was I was performing the segue in UIApplicationDelegate's didFinishLaunchingWithOptions method before I called makeKeyAndVisible() on the window.
In my situation, I was not able to put mine in a class override. So, here is what I got:
let viewController = self // I had viewController passed in as a function,
// but otherwise you can do this
// Present the view controller
let currentViewController = UIApplication.shared.keyWindow?.rootViewController
currentViewController?.dismiss(animated: true, completion: nil)
if viewController.presentedViewController == nil {
currentViewController?.present(alert, animated: true, completion: nil)
} else {
viewController.present(alert, animated: true, completion: nil)
}
You can call your segues or present, push codes inside this block:
override func viewDidLoad() {
super.viewDidLoad()
OperationQueue.main.addOperation {
// push or present the page inside this block
}
}
I had the same problem. I had to embed a navigation controller and present the controller through it. Below is the sample code.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIImagePickerController *cameraView = [[UIImagePickerController alloc]init];
[cameraView setSourceType:UIImagePickerControllerSourceTypeCamera];
[cameraView setShowsCameraControls:NO];
UIView *cameraOverlay = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 768, 1024)];
UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"someImage"]];
[imageView setFrame:CGRectMake(0, 0, 768, 1024)];
[cameraOverlay addSubview:imageView];
[cameraView setCameraOverlayView:imageView];
[self.navigationController presentViewController:cameraView animated:NO completion:nil];
// [self presentViewController:cameraView animated:NO completion:nil]; //this will cause view is not in the window hierarchy error
}
If you have AVPlayer object with played video you have to pause video first.
I had the same issue. The problem was, the performSegueWithIdentifier was triggered by a notification, as soon as I put the notification on the main thread the warning message was gone.
It's working fine try this.Link
UIViewController *top = [UIApplication sharedApplication].keyWindow.rootViewController;
[top presentViewController:secondView animated:YES completion: nil];
In case it helps anyone, my issue was extremely silly. Totally my fault of course. A notification was triggering a method that was calling the modal. But I wasn't removing the notification correctly, so at some point, I would have more than one notification, so the modal would get called multiple times. Of course, after you call the modal once, the viewcontroller that calls it it's not longer in the view hierarchy, that's why we see this issue. My situation caused a bunch of other issue too, as you would expect.
So to summarize, whatever you're doing make sure the modal is not being called more than once.
I've ended up with such a code that finally works to me (Swift), considering you want to display some viewController from virtually anywhere. This code will obviously crash when there is no rootViewController available, that's the open ending. It also does not include usually required switch to UI thread using
dispatch_sync(dispatch_get_main_queue(), {
guard !NSBundle.mainBundle().bundlePath.hasSuffix(".appex") else {
return; // skip operation when embedded to App Extension
}
if let delegate = UIApplication.sharedApplication().delegate {
delegate.window!!.rootViewController?.presentViewController(viewController, animated: true, completion: { () -> Void in
// optional completion code
})
}
}
This kind of warning can mean that You're trying to present new View Controller through Navigation Controller while this Navigation Controller is currently presenting another View Controller. To fix it You have to dismiss currently presented View Controller at first and on completion present the new one.
Another cause of the warning can be trying to present View Controller on thread another than main.
I fixed it by moving the start() function inside the dismiss completion block:
self.tabBarController.dismiss(animated: false) {
self.start()
}
Start contains two calls to self.present() one for a UINavigationController and another one for a UIImagePickerController.
That fixed it for me.
I fixed this error with storing top most viewcontroller into constant which is found within while cycle over rootViewController:
if var topController = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(controller, animated: false, completion: nil)
// topController should now be your topmost view controller
}
You can also get this warning when performing a segue from a view controller that is embedded in a container. The correct solution is to use segue from the parent of container, not from container's view controller.
Have to write below line.
self.searchController.definesPresentationContext = true
instead of
self.definesPresentationContext = true
in UIViewController
With Swift 3...
Another possible cause to this, which happened to me, was having a segue from a tableViewCell to another ViewController on the Storyboard. I also used override func prepare(for segue: UIStoryboardSegue, sender: Any?) {} when the cell was clicked.
I fixed this issue by making a segue from ViewController to ViewController.
I had this issue, and the root cause was subscribing to a button click handler (TouchUpInside) multiple times.
It was subscribing in ViewWillAppear, which was being called multiple times since we had added navigation to go to another controller, and then unwind back to it.
It happened to me that the segue in the storyboard was some kind of broken. Deleting the segue (and creating the exact same segue again) solved the issue.
With your main window, there will likely always be times with transitions that are incompatible with presenting an alert. In order to allow presenting alerts at any time in your application lifecycle, you should have a separate window to do the job.
/// independant window for alerts
#interface AlertWindow: UIWindow
+ (void)presentAlertWithTitle:(NSString *)title message:(NSString *)message;
#end
#implementation AlertWindow
+ (AlertWindow *)sharedInstance
{
static AlertWindow *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[AlertWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
});
return sharedInstance;
}
+ (void)presentAlertWithTitle:(NSString *)title message:(NSString *)message
{
// Using a separate window to solve "Warning: Attempt to present <UIAlertController> on <UIViewController> whose view is not in the window hierarchy!"
UIWindow *shared = AlertWindow.sharedInstance;
shared.userInteractionEnabled = YES;
UIViewController *root = shared.rootViewController;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
alert.modalInPopover = true;
[alert addAction:[UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
shared.userInteractionEnabled = NO;
[root dismissViewControllerAnimated:YES completion:nil];
}]];
[root presentViewController:alert animated:YES completion:nil];
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
self.userInteractionEnabled = NO;
self.windowLevel = CGFLOAT_MAX;
self.backgroundColor = UIColor.clearColor;
self.hidden = NO;
self.rootViewController = UIViewController.new;
[NSNotificationCenter.defaultCenter addObserver:self
selector:#selector(bringWindowToTop:)
name:UIWindowDidBecomeVisibleNotification
object:nil];
return self;
}
/// Bring AlertWindow to top when another window is being shown.
- (void)bringWindowToTop:(NSNotification *)notification {
if (![notification.object isKindOfClass:[AlertWindow class]]) {
self.hidden = YES;
self.hidden = NO;
}
}
#end
Basic usage that, by design, will always succeed:
[AlertWindow presentAlertWithTitle:#"My title" message:#"My message"];
Sadly, the accepted solution did not work for my case. I was trying to navigate to a new View Controller right after unwind from another View Controller.
I found a solution by using a flag to indicate which unwind segue was called.
#IBAction func unwindFromAuthenticationWithSegue(segue: UIStoryboardSegue) {
self.shouldSegueToMainTabBar = true
}
#IBAction func unwindFromForgetPasswordWithSegue(segue: UIStoryboardSegue) {
self.shouldSegueToLogin = true
}
Then present the wanted VC with present(_ viewControllerToPresent: UIViewController)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if self.shouldSegueToMainTabBar {
let mainTabBarController = storyboard.instantiateViewController(withIdentifier: "mainTabBarVC") as! MainTabBarController
self.present(mainTabBarController, animated: true)
self.shouldSegueToMainTabBar = false
}
if self.shouldSegueToLogin {
let loginController = storyboard.instantiateViewController(withIdentifier: "loginVC") as! LogInViewController
self.present(loginController, animated: true)
self.shouldSegueToLogin = false
}
}
Basically, the above code will let me catch the unwind from login/SignUp VC and navigate to the dashboard, or catch the unwind action from forget password VC and navigate to the login page.
I found this bug arrived after updating Xcode, I believe to Swift 5. The problem was happening when I programatically launched a segue directly after unwinding a view controller.
The solution arrived while fixing a related bug, which is that the user was now able to unwind segues by swiping down the page. This broke the logic of my program.
It was fixed by changing the Presentation mode on all the view controllers from Automatic to Full Screen.
You can do it in the attributes panel in interface builder. Or see this answer for how to do it programatically.
Swift 5
I call present in viewDidLayoutSubviews as presenting in viewDidAppear causes a split second showing of the view controller before the modal is loaded which looks like an ugly glitch
make sure to check for the window existence and execute code just once
var alreadyPresentedVCOnDisplay = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we call present in viewDidLayoutSubviews as
// presenting in viewDidAppear causes a split second showing
// of the view controller before the modal is loaded
guard let _ = view?.window else {
// window must be assigned
return
}
if !alreadyPresentedVCOnDisplay {
alreadyPresentedVCOnDisplay = true
present(...)
}
}
You can see on the gif below that on the first scroll of UITableView cell's content moves a tiny bit. You can barely see it, margins become 1 pixel wider.I've never encountered this before. It seems like there's some layout issue before the first scroll and it resolves itself after the fact. There's no warning in XCode, these custom cells are pretty straightforward, with no layout code overrides.
I don't know where to start, how do I catch this glitch?
UPDATE. I've implemented an obvious workaround for now:
- (void)scrollTableToFixGlitch {
[self.tableView setContentOffset:CGPointMake(0, 1)];
[self.tableView setContentOffset:CGPointMake(0, -1)];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self scrollTableToFixGlitch];
}
Still looking into the problem. I've tried generic UITableViewCells, nothing changed. Seems like it's the problem with View Controller's root view or tableview layout, and not with the table cells.
UPDATE 2.
I ruled out all the animations out of the question, problem lies somewhere in a different region. The glitch is easy to recreate on a much simplified project. My Tab Bar controller is based on MHCustomTabBarController with custom segues and some other additions. Here's what you do to recreate a glitch. Setup a project where your initial VC is embedded in Navigation Controller. The next controller either MHCustomTabBarController or a subclass is pushed to the navigation stack. First visible VC in tab bar is generic Table View Controller. That's it. Glitch appears only if tab bar controller is pushed in navigation stack.
Here's some code that I think matters from tab bar controller:
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.childViewControllers.count < 1) {
[self performSegueWithIdentifier:#"viewController1" sender:[self.buttons objectAtIndex:0]];
}
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if (![segue isKindOfClass:[MHTabBarSegue class]]) {
[super prepareForSegue:segue sender:sender];
return;
}
self.oldViewController = self.destinationViewController;
//if view controller isn't already contained in the viewControllers-Dictionary
if (![self.viewControllersByIdentifier objectForKey:segue.identifier]) {
[self.viewControllersByIdentifier setObject:segue.destinationViewController forKey:segue.identifier];
}
[self.buttons setValue:#NO forKeyPath:#"selected"];
[sender setSelected:YES];
self.selectedIndex = [self.buttons indexOfObject:sender];
self.destinationIdentifier = segue.identifier;
self.destinationViewController = [self.viewControllersByIdentifier objectForKey:self.destinationIdentifier];
[[NSNotificationCenter defaultCenter] postNotificationName:MHCustomTabBarControllerViewControllerChangedNotification object:nil];
}
And a custom segue code:
#implementation MHTabBarSegue
- (void) perform {
MHCustomTabBarController *tabBarViewController = (MHCustomTabBarController *)self.sourceViewController;
UIViewController *destinationViewController = (UIViewController *) tabBarViewController.destinationViewController;
//remove old viewController
if (tabBarViewController.oldViewController) {
[tabBarViewController.oldViewController willMoveToParentViewController:nil];
[tabBarViewController.oldViewController.view removeFromSuperview];
[tabBarViewController.oldViewController removeFromParentViewController];
}
destinationViewController.view.frame = tabBarViewController.container.bounds;
[tabBarViewController addChildViewController:destinationViewController];
[tabBarViewController.container addSubview:destinationViewController.view];
[destinationViewController didMoveToParentViewController:tabBarViewController];
}
#end
UPDATE 3
During my research I've found that - viewWillAppear is not called the first time when child controller appears. But it's called in all subsequent times.
Maybe the scrollviews contentSize is wider than your scrollView's frame(width specifically in this case) causing scrolling for both directions.
You can either try to decrease the contentSize width to the scrollView's width and
self.scrollView.alwaysBounceHorizontal = NO;
If this doesn't work, the solution would be to disable horizontal scrolling programatically by the help of the UIScrollView delegate
self.scrollView.delegate = self;
[self.scrollView setShowsHorizontalScrollIndicator:NO];
//for the below UIScrollView delegate function to work do the necessary step in the bottom of my answer.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x > 0)
scrollView.contentOffset = CGPointMake(0, scrollView.contentOffset.y);
}
And in your .h file you should change the interface line to below by adding UIScrollViewDelegate
#interface ViewController : UIViewController <UIScrollViewDelegate>
You most probably know this delegate part very well but for others it might be needed:D
Original answer
Ah, I've finally found the origin of this behaviour. I was almost sure this is happening due to some of the preparation methods are not called properly. As I stated in the update 3 I've found that -viewWillAppear method is not called in TableViewController when my TabBarController is pushed to the navigation stacked. I've found a ton of coverage of this matter on SO, it's a very well known issue apparently.
I've added a simple fix just to check if I'm right in my Custom Segue:
//added this line
[destinationViewController viewWillAppear:YES];
[tabBarViewController.container addSubview:destinationViewController.view];
And it works like a charm, no flickering! Now I have to figure out a more suitable place for this call, since explicit calls to methods like this can break a lot of stuff.
Probably the best place is in -navigationController:willShowViewController:animated: method of UINavigationControllerDelegate.
Anyway, problem solved. Hope it will help someone with the same issue.
UPDATE Actually, I was not completely correct on that. -viewWillAppear is called on my tab bar controller when it's pushed to navigation stack. It's not being translated to TableViewController. So there's no need to access NavigationControllerDelegate. Simple fix to a custom segue is enough.
I've got a HomeViewController that has different modal segues to several other UIViewControllers. If I try to show the keyboard on a UITextField within the HomeView, everything works fine. However, if I try to show the keyboard on a UITextField (using becomeFirstResponder) after returning from any of the modal View Controllers, the keyboard never shows.
Here's some sample code from one of the setups I've tried:
In HomeViewController:
- (void)viewDidAppear:(BOOL)animated
{
static BOOL firstTimeComplete = false;
if (!firstTimeComplete) {
firstTimeComplete = true;
} else {
UITextField *textField = [[UITextField alloc] init];
[self.view addSubview:textField];
[textField performSelector:#selector(becomeFirstResponder) withObject:nil afterDelay:3]
}
}
In ModalViewController:
- (IBAction)done:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Where done: is linked to the "Done" button via a touch up inside event.
A few things I've tried:
Converting the modal segues to push segues fixes the issue, but I don't want a Nav bar in any of the child views
I've tried disabling and enabling animations when dismissing the
modal view controller (using dismissViewControllerAnimated:)
Using unwind segues in the storyboard rather than doing it programmatically
Anyone have an idea of what may be going on?
After deleting tons of code, I finally found out that a custom NavigationController was being used and this was the root cause:
#implementation MSLNavigationController
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationPortrait;
}
- (BOOL)shouldAutorotate
{
return NO;
}
#end
The app doesn't need this code, so I've nuked the file. (But an explanation as to why this would be hiding the keyboard would be awesome :))
You did not call [super viewDidAppear:animated]
In place like that i have workaround that works pretty well
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
{
if (self.textView.text.isNotEmpty)
{
[self.textView becomeFirstResponder];
}
});
}
I have been struggling with this problem for some time, so I'll post here what I found out.
I was calling textField.becomeFirstResponder() in viewWillAppear but on iOS 7, after the modal was dismissed, the keyboard would not show again, even when you would tap on the textField.
For me calling textField.resignFirstResponder() when the modal is presented, solved the issue. It seems like the input field was already marked as first responder and then would not react to the new calls.
I am presenting a modal view using a storyboard segue set as Form Sheet.
The problem is, when I rotate the iPad after this view is displayed, the view is removed from the view/dismissed.
I have no idea why. It only seems to occur when starting in Portrait then rotating to Landscape.
If I start in Landscape then show the view then rotate it stays on the screen fine.
Any ideas?
EDIT ----
It also seems that full screen modal views are also dismissed after rotation!
There's nothing special going on in the presentation code, this is a full screen modal:
EditViewController *editView = [self.navigationController.storyboard instantiateViewControllerWithIdentifier:#"editViewController"];
editView.delegate = self;
editView.image = image;
editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:editView animated:YES completion:nil];
This happens on both iOS 6 and iOS 7
EDIT 2 ----
Forgot to mention, i'm presenting the modal from the left/master view controller of a UISplitViewController
late, but what it worked for me was just before
[self presentViewController:aController animated:YES completion:nil];
dismiss the master controller, adding this lines
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAutomatic];
and then present your controller
Get rid of: editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
That will solve the issue you are experiencing. Modal view controllers presented on iPad as a Form Sheet do not rotate correctly using that transition style.
its really hard to get the cause how and why this is happening as i found that this also happen with UIPopover also as when you rotate it UIPopover hide because ???
So if you want to keep your view then just call again your controller after rotation will do fine user experience
This is not a bug its a limitation on UISplitViewController. The problem exists when the masterViewController (which is a UIPopoverController) is able to be dismissed. Heres how it works with the assumption that your app does allow the masterViewController to be dismissed in portrait and does not allow in landscape.
In portrait while the masterViewController is visible, if you were to present a modal from a viewController in the masterViewController and then rotate to landscape, the modal would disappear in iOS7 and the app would not rotate in iOS8. iOS8 introduces a condition to prevent the bad experience of iOS7. iOS7 losses the modal in the process of moving the masterViewController from the popoverController to a contained viewController in the splitViewController.
The modal needs to be presented from the splitViewController and not from the masterViewController. The only problem with this is the modal gets presented below the masterViewController in portrait. My solution is to dismiss the masterViewController and then present the modal.
There are several ways to achieve this result depending on how complex your code needs to be. Here's how I do this in my app.
I first subclass UISplitViewController in order to have a reference to the popoverController. I use delegate forwarding in order to access the delegate methods internally and externally. Heres the .h
// MainSplitViewController.h
#import <UIKit/UIKit.h>
#interface MainSplitViewController : UISplitViewController
#property (nonatomic, weak, readonly) UIPopoverController* primaryColumnController;
#end
And the .m
// MainSplitViewController.m
#import "MainSplitViewController.h"
#interface MainSplitViewController () <UISplitViewControllerDelegate>
#property (nonatomic, weak) id<UISplitViewControllerDelegate> externalDelegate;
#property (nonatomic, weak) UIPopoverController* primaryColumnController;
#end
#implementation MainSplitViewController
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
#pragma mark - Split View Controller Delegate
- (void)splitViewController:(UISplitViewController *)svc popoverController:(UIPopoverController *)pc willPresentViewController:(UIViewController *)aViewController {
self.primaryColumnController = pc;
if ([(id)self.externalDelegate respondsToSelector:_cmd]) {
[self.externalDelegate splitViewController:svc popoverController:pc willPresentViewController:aViewController];
}
}
#pragma mark - Delegate Forwarder
- (void)setDelegate:(id<UISplitViewControllerDelegate>)delegate {
[super setDelegate:nil];
self.externalDelegate = (delegate != self) ? delegate : nil;
[super setDelegate:delegate ? self : nil];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [super respondsToSelector:aSelector] || [delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [delegate respondsToSelector:aSelector] ? delegate : [super forwardingTargetForSelector:aSelector];
}
#end
Next I create a class extension on UIViewController
// UIViewController+Popover.h
#import <UIKit/UIKit.h>
#interface UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController;
#end
And the .m
// UIViewController+Popover.m
#import "UIViewController+Popover.h"
#import "MainSplitViewController.h"
#implementation UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController {
UIViewController* viewController = self;
if ([self.splitViewController isKindOfClass:[MainSplitViewController class]]) {
viewController = self.splitViewController;
MainSplitViewController* mainSplitViewController = (MainSplitViewController *)self.splitViewController;
if (mainSplitViewController.primaryColumnController.popoverVisible) {
[mainSplitViewController.primaryColumnController dismissPopoverAnimated:YES];
}
}
return viewController;
}
#end
Now where ever you present the modal, instead of calling [self presentViewController: ... call [self.popoverPresentingViewController presentViewController: ...]. Remember to import UIViewController+Popover.h
your question came closest to my bug, On returning from modalView the parentView will switch to orientation in which the application was opened.
Visually it appears that the modal view is rotated and then returns.
I solved it by removing the modal view altogether, and using
[self.navigationController pushViewController: <the View(not modal now)>]
instead of using-
[self presentViewController:<Modal View>]
I think this is because the navigation controller doesn't own the Modal View, hence it reloads - when returning from the modal view - to incorrect orientation
Problem:
When presenting a view controller modally, it gets dismissed on rotation.
Approach:
Set the UISplitViewControllerDelegate
Use the UISplitViewControllerDelegate methods
Hold a reference to your modal view controller in an instance variable
Check if your modal view controller's presenting view controller exists.
If it exists, nothing needs to be done, else just present without any animation.
UISplitViewControllerDelegate methods:
func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
Note:
UISplitViewControllerDelegate has quite a methods, it can be daunting initially, if you spend some time experimenting, you can achieve what you want.
It has fine grained access.
I'm very late but try this. It works for me.
[self.splitViewController presentViewController:editView animated:YES completion:nil];