I'm attempting a project using the Xcode (version 9.1 9B55) Master-Detail template, using one Storyboard for both iPhone and iPad.
I want to use the built in popover segue, which ideally will show a popover when the size class is appropriate, or a modal view controller when the size class is compact (iPhone in portrait).
What I am finding is that it works just fine for iPad, but when I run it on an iPhone, you can't touch outside the popover to dismiss as I would expect.
When on iPad, popoverPresentationControllerDidDismissPopover is called and all is well.
When on iPhone, the popoverPresentationControllerDidDismissPopover never gets called and you cannot dismiss the popover.
To reproduce, I did this:
Create new Master-Detail App
New - File. Cocoa Touch Class, called MyPopoverViewController
In Storyboard:
Create new view controller, change class to MyPopoverViewController.
On MasterViewController, add bar button item "Popover". Control-drag from this to MyPopoverViewController. Set segue Kind to "Present As Popover". Set Identifier to "thePopover".
In MasterViewController.h, add UIPopoverPresentationControllerDelegate:
#interface MasterViewController : UITableViewController <UIPopoverPresentationControllerDelegate>
In MasterViewController.m:
#import "MyPopoverViewController.h"
In viewDidLoad, comment out two lines which create the "Add Button".
In prepareForSegue:
} else if ([[segue identifier] isEqualToString:#"thePopover"]) {
NSLog(#"MVC prepareForSegue thePopover");
MyPopoverViewController *myPopoverController = segue.destinationViewController;
myPopoverController.popoverPresentationController.delegate = self;
}
Add three UIPopoverPresentationControllerDelegate delegate methods:
- (void) prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController {
NSLog(#"MVC prepareForPopoverPresentation");
}
- (void) popoverPresentationControllerDidDismissPopover:(UIPopoverPresentationController *)popoverPresentationController {
NSLog(#"MVC popoverPresentationControllerDidDismissPopover");
}
- (BOOL) popoverPresentationControllerShouldDismissPopover:(UIPopoverPresentationController *)popoverPresentationController {
NSLog(#"MVC popoverPresentationControllerShouldDismissPopover");
return TRUE;
}
I tried this also, but all it does is force popover in portrait mode (which I don't want); doesn't change the lack of popover delegate calls and doesn't allow us to dismiss popover:
-(UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
return UIModalPresentationNone;
}
I'm hoping there is something simple I'm missing here. I have uploaded a sample project here, which is exactly what I've described above:
https://github.com/johnstewart/MasterDetailPopoverTestProject
How do I allow iPhone to also dismiss popovers by touching outside the popover?
If I understand your question correctly your problem appears on iPhone 8 Plus in landscape mode.
In this situation, the presented popover actually is not a popover but a normal presented view. Visually it looks like a sheet that appears from the botton of the screen. In order to close such a view, you have to add your own button to do this.
If you want to show a real popover, you must implement:
adaptivePresentationStyleForPresentationController:traitCollection:
to return UIModalPresentationNone. Note the additional parameter traitcollection:. UIAdaptivePresentationControllerDelegate contains two similar methods. In your project you already implemented the method:
adaptivePresentationStyleForPresentationController:
Change this to the former method and everything should work.
Related
Update for iOS 9 beta: Apple may have fixed this for iOS 9. If you work(ed) around this issue for iOS 8, make sure it also works correctly on iOS 9.
In storyboard, I've created a popover presentation segue to present a navigation and view controller from a button, as well as creating an unwind segue.
In portrait orientation, the modal (fullscreen) presentation is unwound/dismissed, as expected.
In landscape orientation, the unwind segue also gets called, however the popover presentation is not automatically dismissed.
Did I miss hooking something up? Do I have to dismiss the popover presentation myself?
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)__unused sender
{
if ([[segue identifier] isEqualToString:#"showSelectBookChapter"])
{
UINavigationController *navigationController = segue.destinationViewController;
if ([navigationController.topViewController isKindOfClass:[BIBLESelectViewController class]])
{
BIBLESelectViewController *selectViewController = (BIBLESelectViewController *)navigationController.topViewController;
selectViewController.initialBookChapterVerse = self.bookChapterVerse;
}
}
}
- (IBAction)unwindToBIBLEChapterViewController:(UIStoryboardSegue *)segue
{
if ([segue.identifier isEqualToString:#"unwindToBIBLEChapterViewController"]) {
if ([segue.sourceViewController isKindOfClass:[BIBLESelectViewController class]])
{
BIBLESelectViewController *sourceViewController = (BIBLESelectViewController *)segue.sourceViewController;
self.bookChapterVerse = sourceViewController.selectedBookChapterVerse;
[self.tableView reloadData];
}
}
}
Update:
After looking at gabbler's sample code, I've narrowed the problem down to popover dismissing fine in a Single View Application, but not in a Master-Detail Application.
Update 2:
Here's what the hierarchy looks like (omitting navigation controllers for simplicity's sake), in answer to the question Luis asked:
Split view controller
Master view controller
Detail view controller
Chapter view controller (modal page sheet)
Select view controller (the problematic popover that unwinds to chapter view controller, but doesn't dismiss)
As I mentioned in the previous update, I created an new master/detail template, and simply presented a popover directly from (a button in) the detail view. It won't dismiss.
I ran into this problem too. I present a View Controller modally (as a form sheet), from the Master View Controller (UISplitViewController). The problem only occurred on the iPad (probably the iPhone 6+ in landscape mode too, but I didn't check it). I ended up doing the following in my unwind action method (using Swift), and it works good.
if !segue.sourceViewController.isBeingDismissed() {
segue.sourceViewController.dismissViewControllerAnimated(true, completion: nil)
}
If you segue as a popover from a view controller embedded in a navigation controller, the corresponding unwind fails to dismiss the popover.
It's a bug in -[UINavigationController segueForUnwindingToViewController:fromViewController:identifier]. The embedding navigation controller is supposed to supply a segue that will dismiss the popover but it doesn't. The fix then is to override this and supply a working segue, which we can get from the embedded view controller.
Here's a partial solution that will only handle unwinding to the top view controller of the navigation stack:
#implementation MyNavigationController
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController
fromViewController:(UIViewController *)fromViewController
identifier:(NSString *)identifier
{
if (toViewController == self.topViewController && fromViewController.presentingViewController == self)
return [toViewController segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
else
return [super segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
}
#end
It works on iOS 8 for both landscape/portrait iPad and landscape/portrait iPhone. The logic should be robust enough to survive on iOS 9.
It is/must be a behavior of the popOver segue, in normal situations or regularly we need that the popOver keeps in view, if the segue show something important is annoying that we lost that information just because we rotate the device, I guess that that is the reason of that native behavior. So if we want for it to dismiss automaticly we have to make that behaivor by our own, this works:
in the method - (void)viewDidLoadin the detailViewController.m add this:
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:[UIDevice currentDevice]];
then create this method:
- (void) orientationChanged:(NSNotification *)note{
UIDevice * device = note.object;
//CGRect rect = [[self view] frame];
switch(device.orientation)
{
default:
[self dismissViewControllerAnimated:YES completion:nil];
break; }}
You said that in a single view happens what you want, but I've never seen that behavior when I used popOvers.
mbeaty's fix is great but as others have pointed out, this bug seems to know be fixed in iOS 9 and it also doesn't work well for universal device design. I have adapted his answer to handle both situations. Here is the code:
#IBAction func yourUnwindSegue(segue: UIStoryboardSegue) {
if #available(iOS 9, *) {
return // bug fixed in iOS 9, just return and let it work correctly
}
// do the fix for iOS 8 bug
// access your SplitViewController somehow, this is one example
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let splitVC = appDelegate.window!.rootViewController as! YourSplitViewController
// if the source isn't being dismissed and the splitView isn't
// collapsed (ie both windows are showing), do the hack to
// force it to dismiss
if !segue.sourceViewController.isBeingDismissed() && splitVC.collapsed == false {
segue.sourceViewController.dismissViewControllerAnimated(true, completion: nil)
}
}
This first checks if iOS 9 is running and just exit as the bug seems to be fixed. This will prevent the multiple views getting dismissed issue. Also to make sure this fix is only done when the splitView is showing two windows (to make it happen only on iPads and iPhone 6 Plus in landscape as well as future devices) I added the check to make sure it is not collapsed.
I have not exhaustively check this but it seems to work. Also not that my app is set for a min of iOS 7, I don't know if this bug existed then so you may need to look into that if you support below iOS 8.
I have an app, that works fine under iOS 7, but when built for iOS 8 the unwind segues are not working.
I created a new project and added a modal (navigationcontroller with tableviewcontroller)and tried to use an unwind modal. Unfortunately it doesn't work either. The methods that are being unwind to, are in the desination view controller. The unwind segue is created through the storyboard (a Navigationbar button in the tableviewcontroller) When I tap the button, nothing happens. There is no log output and the modal does not disappear. It also only seems to affect modal segues. push/popover are unwound normally.
Has anyone had a similar problem and has an Idea how I could solve it?
Apple has FIXED this bug in iOS 8.1
Temporary solutions for iOS 8.0
The unwind segue will not work only in next situation:
View structure: UITabBarController -> UINagivationController -> UIViewController1 -> UIViewController2
Normally (in iOS 7, 8.1), When unwind from UIViewController2 to UIViewController1, it will call viewControllerForUnwindSegueAction in UIViewController1.
However in iOS 8.0 and 8.0.x, it will call viewControllerForUnwindSegueAction in UITabBarController instead of UIViewController1, that is why unwind segue no longer working.
Solution: override viewControllerForUnwindSegueAction in UITabBarController by create a custom UITabBarController and use the custom one.
For Swift
CustomTabBarController.swift
import UIKit
class CustomTabBarController: UITabBarController {
override func viewControllerForUnwindSegueAction(action: Selector, fromViewController: UIViewController, withSender sender: AnyObject?) -> UIViewController? {
var resultVC = self.selectedViewController?.viewControllerForUnwindSegueAction(action, fromViewController: fromViewController, withSender: sender)
return resultVC
}
}
For old school Objective-C
CustomTabBarController.h
#import <UIKit/UIKit.h>
#interface CustomTabBarController : UITabBarController
#end
CustomTabBarController.m
#import "CustomTabBarController.h"
#interface CustomTabBarController ()
#end
#implementation CustomTabBarController
-(UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
return [self.selectedViewController viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
}
#end
==============================================================================
DO NOT USE ANY SOLUTIONS BELOW THIS POINT (they are out of date and just for reference)
Latest update on Sep 23
My new solution is pushing to a view that embedded in a navigation controller, and config that navigation controller to hide bottom bar on push(a tick box in IB). Then you will have a view looks like a modal view, the only different is the animate of pushing and popping. You can custom if you want
Updated: The solution below actually present the modal view under the tab bar, which will cause further view layout problems.
Change the segue type to Present As Popover will work only on iOS8 for iPhones, on iOS7 your app will crash.
Same here, to fix this, I set segue's presentation to current context(my app is for iphone only).
Default and full screen will not work.
[UPDATE: Bug fixed on iOS 8.1 beta but you'll need it for 8.0 and 8.0.2 support]
The only way I could make my unwind segue work was by mixing Aditya's and viirus' answers.
My setup going in:
[View Controller 1] > custom modal segue > [Navigation Controller] > root > [View Controller 2]
Unwind:
[View Controller 2] > custom unwind segue > [View Controller 1]
Fix:
Subclass the [Navigation Controller], add a property called sourceViewController and pass "self" to that property when prepare for segue is called when going from [View Controller 1] to [Navigation Controller]
In the [Navigation Controller] subclass .m override/add this two methods:
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
if ([self.sourceViewController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender]) {
return self.sourceViewController;
}
return [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
}
Then I override this in that [Navigation Controller] subclass only because I have a custom unwind segue:
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
return [fromViewController segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
}
This is a problem with iOS 8.0, 8.0.1, and 8.0.2. It was resolved in 8.1; unwind segues are calling the appropriate method now.
Note that on iOS 8, modally presented view controllers may not be automatically dismissed when performing an unwind segue, unlike iOS 7. To ensure it's always dismissed, you may detect if it's being dismissed and if not then manually dismiss it. These inconsistencies are resolved in iOS 9.0.
With iOS 8.4 running on iPhone, all of the modally presented segues with all presentation styles do dismiss upon unwind, except Over Full Screen and Over Current Context. That's also the case for iPad, with the addition of Form Sheet and Page Sheet also not auto-dismissing. With iOS 9, all presentation styles auto dismiss on both iPhone and iPad.
Yep it kinda happen to me too, I think for your case you have to subclass the UINavigationController and override the following:
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
for(UIViewController *vc in self.viewControllers){
// Always use -canPerformUnwindSegueAction:fromViewController:withSender:
// to determine if a view controller wants to handle an unwind action.
if ([vc canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender])
return vc;
}
return [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
}
Same problem here. Unwind method is not called. Only happens when
using modal segue
Presentation is anything but "current context"
NavigationController is not extended (using default from storyboard)
Also happens in IOS8 GM Seed, therefore I think we need to find a workaround.
Sounds like a bug to me...
Extending UINavigationController and implementing viewControllerForUnwindSegueAction didn't help, as it is not fired. The only thing which gets fired is canPerformUnwindSegueAction() within the extended UINavigationController. Strange.
Woah there! I'm still getting user reports of getting stuck on a modal view in iOS 8.1.1 (on an iPad 3).
I'm jettisoning all this unwind from a modal view stuff. Just a good old-fashioned...
[self dismissViewControllerAnimated:NO completion:nil];
...works fine on all those various iOS 8.x.x versions.
It seems that both iOS 7.1 and iOS 8.1/8.2 create unwind segue from navigation controller however unwind segue is registered on a child controller inside of navigation controller.
So manually creating an unwind segue from the controller where it's registered in storyboard solves the problem.
#implementation RootNavigationController
- (UIStoryboardSegue*)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
return [toViewController segueForUnwindingToViewController:toViewController fromViewController:fromViewController identifier:identifier];
}
#end
I encountered the same problem when unwinding to a source view controller from a destination view controller. The destination was presented through a "show" segue from the source.
I was using iPhone simulator that shows iPhone 6, iOS8.3.
XCode 6.3.2
The solution of subclassing NavigationViewController worked for me. Here is the swift code which is essentially swift translation of Raul's answer. I am puzzled that if Apple has fixed it in iOS8.1 per Raul, how am I getting hit by it in 8.3.
var sourceViewController: UIViewController?
override func viewControllerForUnwindSegueAction(action: Selector, fromViewController: UIViewController, withSender sender: AnyObject?) -> UIViewController? {
if(self.sourceViewController! .canPerformUnwindSegueAction(action, fromViewController: fromViewController, withSender: sender!)){
return self.sourceViewController
}
return super.viewControllerForUnwindSegueAction(action, fromViewController: fromViewController, withSender: sender)
}
I just ran into this problem, and after some digging discovered that with modal segues (at least ones with the default and fullscreen presentation modes), you can't rely on the normal unwind mechanism, but rather you have to call the presented UIViewController's dismissViewControllerAnimated method.
Steps to be followed:
Link the unwind segue to the button in Storyboard.
Create IBAction for the button and add the below code in it:
[self dismissViewControllerAnimated:NO completion:nil];
This should work for all versions.
I am (still) in the process of converting an iPhone app to a universal app.
I want to push a UISplitViewController onto a UIView. As discussed here I am trying to create this flow:
UIView -> UISplitViewController (containing two UITableViews that I use in the iPhone version) -> UIView
I want the button attached to this IBAction to make the tableview appear on iPhone (which is all set up and working) and the SplitViewController appear on the iPad:
-(IBAction)makeStory:(id)sender{
NSLog(#"makeStory:");
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
//Code here to push split view.
} else {
//I am an iPhone!
makeStoryTableViewController = [[MakeStoryTableViewController alloc] initWithNibName:#"MakeStoryTableViewController" bundle:nil];
[self.navigationController pushViewController:makeStoryTableViewController animated:YES];
[self.navigationController setNavigationBarHidden:NO animated:YES];
}
}
I've read tutorials here and here - but I can't get my head around how to add it to an existing app correctly. I would appreciate some help / direction so I can implement a UISplitViewController correctly in this universal app.
You can't "push" a split view controller or add it on top of another view. It has to be the root view controller of the window.
In your IBACtion you could do something as simple as:
appWindow.rootViewController = aSplitViewController;
Of course, you have to get a reference to your app's window, and you have to get aSplitViewController which is initialized with left and right view controllers from somewhere...
I've created a project from the master detail template in Xcode 4.2/iOS5. I am trying to preform a modal segue from the splitviewcontroller. I call the following from my detailviewcontroller.m:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.splitViewController performSegueWithIdentifier:#"DisplayLoginView" sender:self.splitViewController];
}
The login view shows fine but it does not rotate and stays in portrait. Any ideas on how to get it to autorotate?
I fixed this by implementing a custom class for the destination view controller and adding the
shouldAutorotateToInterfaceOrientation: method making sure it returned YES.
Hi there, Now I'm trying to create a Pop-OverView using an Xcode
storyboard. Firstly, I have
rootViewController, UIViewController, and UITableViewController
I want the UIView to act as a page flip and the UITableView will show popOver under the navigationBar item controller.
For the UITableView, I want to make a Pop-Over under NavigationBar controller. The problem is, when I touch the Navigation item to show the UITableViewController, it shows correctly, but when I try to close the Pop-Over View, it won't close. And then, the navigation item doesn't work well. It shows multiple instances of popOverView when I touch it multiple times.
This doesn't seem to make sense to me. Can anyone help me out or tell me where to find documentation / tutorials on this?
UPDATE:
For the UIPopOverController, it seems to work well now, but it is still bugging me when I touch a Navigation Item multiple times. It will show multiple instances of PopOver. How can I handle it, so it will show only one instance?
I had the same problem and mostly found the solution here. Basically you change the action of the button each time it's pressed to either display or dismiss the popover. Here's the code I ended up with:
#interface FilterTableViewController : UITableViewController {
UIPopoverController *editPopover;
id saveEditSender;
id saveEditTarget;
SEL saveEditAction;
}
-(void)prepareForSegue:(UIStoryboardPopoverSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"EditFilterSegue"]){
// Save the edit button's info so we can restore it
saveEditAction = [sender action];
saveEditTarget = [sender target];
saveEditSender = sender;
// Change the edit button's target to us, and its action to dismiss the popover
[sender setAction:#selector(dismissPopover:)];
[sender setTarget:self];
// Save the popover controller and set ourselves as the its delegate so we can
// restore the button action when this popover is dismissed (this happens when the popover
// is dismissed by tapping outside the view, not by tapping the edit button again)
editPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
editPopover.delegate = (id <UIPopoverControllerDelegate>)self;
}
}
-(void)dismissPopover:(id)sender
{
// Restore the buttons actions before we dismiss the popover
[saveEditSender setAction:saveEditAction];
[saveEditSender setTarget:saveEditTarget];
[editPopover dismissPopoverAnimated:YES];
}
-(BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
// A tap occurred outside of the popover.
// Restore the button actions before its dismissed.
[saveEditSender setAction:saveEditAction];
[saveEditSender setTarget:saveEditTarget];
return YES;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Before we navigate away from this view (the back button was pressed)
// remove the edit popover (if it exists).
[self dismissPopover:saveEditSender];
}