UIModalPresentationPopover for iPhone 6 Plus in landscape doesn't display popover - ios

I want to always present a ViewController in a popover on all devices and all orientations. I tried to accomplish this by adopting the UIPopoverPresentationControllerDelegate and setting the sourceView and sourceRect.
This works very well for all devices and orientations, except the iPhone 6 Plus in landscape. In that case the view controller slides up from the bottom of the screen in a form sheet. How can I prevent that so that it will always appear in a popover?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let popoverPresentationController = segue.destinationViewController.popoverPresentationController
popoverPresentationController?.delegate = self
popoverPresentationController?.sourceView = self.titleLabel!.superview
popoverPresentationController?.sourceRect = self.titleLabel!.frame }
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None }
All device are under iOS 8.2 or higher

Implement the new adaptivePresentationStyleForPresentationController:traitCollection: method of UIAdaptivePresentationControllerDelegate:
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {
// This method is called in iOS 8.3 or later regardless of trait collection, in which case use the original presentation style (UIModalPresentationNone signals no adaptation)
return UIModalPresentationNone;
}
UIModalPresentationNone tells the presentation controller to use the original presentation style which in your case will display a popover.

In Swift 3, if you implemented the original adaptivePresentationStyle method, simply adding this code works:
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return adaptivePresentationStyle(for: controller)
}

Apple designed the iPhone 6 Plus presentation to behave that way, based on its size class.
To prevent the modal presentation on the iPhone 6 Plus, you'll have to override the trait collection (horizontal size).
You should be able to set the overrideTraitCollection property for the presentation controller:
presentedVC.presentationController.overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
(Sorry for the Objective C! I haven't learned Swift yet.)

A note to people having issues with this:
This
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *) controller traitCollection:(UITraitCollection *)traitCollection {
return UIModalPresentationNone;
}
Is not the same as this
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController: (UIPresentationController * ) controller {
return UIModalPresentationNone;
}
The later will not get called / work the same as the former.

Related

Modal UIViewController with presentation style formSheet is not displayed correctly on iPhone XS Max and iPhone XR

I have a view controller presented from a segue modally. Its presentation style is set to Form Sheet.
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .formSheet
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
segue.destination.presentationController?.delegate = self
}
On iPhone X and iPhone 8 Plus it works as expected, on iPhone Xs Max and Xr the width of the controller is respected, but the height is wieredly stretched. I have no way of confirming if this is simulator bug, iOS bug or expected behaviour as I don't have Xs Max myself.
Better use the modal presentation style overFullScreen for compact horizontal size clases and leave the formSheet for the horizontally regular ones.
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
if controller.traitCollection.horizontalSizeClass == .regular {
return .formSheet
}
return .overFullScreen
}

Popover covers entire screen iOS

I am trying to implement a popover whose anchor is a bar button item for my iphone app. I have connected the bar button item to the view controller via a segue pathway configured as show as "present as popover." I have also set my own size for the view controller and selected "use preferred explicit size." Based on previous posts about this similar topic, I have implemented the following code for my popover. However, the popover is still covering the entire screen, probably because my adaptivePresentationStyle method is not being called ("hello" does not print to the screen). Note that I have also implemented the UIPopoverPresentationControllerDelegate. Where did I go wrong?
override func prepare (for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "popoverLogin" {
let popoverViewController = segue.destination
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.popover
popoverViewController.popoverPresentationController!.delegate = self }
}
// MARK: - UIPopoverPresentationControllerDelegate method
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
// Force popover style
print ("hello")
return UIModalPresentationStyle.none
}
Thanks!
In my case your code works and popover is presented normally!
Make sure your segue has the right "Kind": "Present As Popover".

swift - Popover is not displayed correctly in landscape mode

Popover takes the complete screen when displayed in landscape mode, it works correctly in portrait mode though. Also, it is does not disappear when i click outside the popover in landscape mode.
I connected the popover through the storyboard. Inside the popoverviewcontroller I placed a view which contains the buttons. The code for the viewdidload() of the popoverviewcontroller is:
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = popoverView.frame.size
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
Portrait:
landscape:
The answer from #Jake2Finn works for Swift 4.0.
The trait parameter specifically is required to fix the landscape problem:
traitCollection: UITraitCollection
Without it the function adaptive... only works for portrait.
You have to add UIPopoverPresentationControllerDelegateto your class like this:
swift 3
import UIKit
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {
...
As a second step, add the following function:
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
Explanation: By returning UIModalPresentationStyle as none, the original presentation style is kept and your popover is not streched to the bottom of your screen in landscape orientation.

Setting UIModalPresentationStyle for iPhone 6 Plus in landscape?

I want to always present a view controller in a popover on all devices and all orientations. I tried to accomplish this by adopting the UIPopoverPresentationControllerDelegate and setting the sourceView and sourceRect. The segue in the storyboard is configured as a Present As Popover segue. This works very well for all devices and orientations, except the iPhone 6 Plus in landscape. In that case the view controller slides up from the bottom of the screen in a form sheet. How can I prevent that so that it will always appear in a popover?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let popoverPresentationController = segue.destinationViewController.popoverPresentationController
popoverPresentationController?.delegate = self
popoverPresentationController?.sourceView = self.titleLabel!.superview
popoverPresentationController?.sourceRect = self.titleLabel!.frame
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
Implement the new (as of iOS 8.3) adaptivePresentationStyleForPresentationController:traitCollection: method of UIAdaptivePresentationControllerDelegate:
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {
// This method is called in iOS 8.3 or later regardless of trait collection, in which case use the original presentation style (UIModalPresentationNone signals no adaptation)
return UIModalPresentationNone;
}
UIModalPresentationNone tells the presentation controller to use the original presentation style which in your case will display a popover.

Open UISplitViewController to Master View rather than Detail

I have a split-view interface with a target iPhone 6 application. On the first launch of the application, it opens to the Detail View; I would like it to open to the Master View. I have tried:
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryOverlay
Which was suggested elsewhere (Prior StackOverFlow Question) but it doesn't seem to do anything, and does not open the Master view on launch. I also tried to add the following line to my AppDelegate:
splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:
But despite returning true or false (Another Prior Stack Overflow Question) I had no success.
I did launch up the example Master-Detail application in Xcode, and it loads to the Master view based on the splitViewController: call returning false; however, I'm not sure how to make this work in a more complicated layout.
Swift
UISplitViewController display master view above detail in portrait orientation is not about showing the Master view, it is about presenting the Detail view in full width, underneath the Master view.
UISplitViewController in portrait on iPhone shows detail VC instead of master is about the principle of the collapse mechanism.
This present answer addresses:
Master → Detail (Compact width)
iPhone 4s, 5, 5s, SE, 6, 6s, 7 (any orientation)
iPod Touch
any iPhone Plus (portrait)
side-by-side (all other sizes)
iPad
any iPhone Plus (landscape)
You must set preferredDisplayMode. You would want is .primaryVisible if it existed! Using .allVisible, iOS picks Detail if only 1 view fits (Compact width); in that size, the code below will pick Master.
The trick is to change both the preferredDisplayMode to .allVisible and to return true in collapseSecondary:onto.
class PrimarySplitViewController: UISplitViewController,
UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.preferredDisplayMode = .allVisible
}
func splitViewController(
_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController: UIViewController,
onto primaryViewController: UIViewController) -> Bool {
// Return true to prevent UIKit from applying its default behavior
return true
}
}
iOS 14
I wasn't getting a callback for splitViewController(_:collapseSecondary:onto:) and instead used the following new method.
func splitViewController(_ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
return .primary
}
Step 1 - Open MasterViewController
Step 2 - ensure the table view has the UISplitViewControllerDelegate protocol. Eg:
class ListVC: UITableViewController,UISplitViewControllerDelegate {}
Step 3 - Add it in ViewDidLoad
splitViewController?.delegate = self
Step 4 - Then override this method to say the master view controller should always collapse onto the detail view controller:
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
On the first launch of the application, it opens to the Detail View; I would like it to open to the Master View
Assuming you want that only on the first launch, but not always; for example in the case that the Master View shows an empty data set; then the solution is just as the Master-Detail template shows:
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController:UIViewController, ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
}
return false
}
iOS 14
From WWDC 2020 - Build for iPad, You can add a specific view controller for the compact width class (e.g. iPhone in portrait, iPad in Slide Over) by checking Use Separate View Controller in the Attribute Inspector of SplitViewController.
So you can set any view controller as an initial view controller as you want by setting relationship segue.
iOS 14 -- Two Column Mode Updates
I struggled with this for a while before eventually finding that the Split View Controller has been reworked in iOS14, so none of the answers above are relevant anymore.
I'd recommend starting with this article here.
But in case you are looking for a quick fix:
You'll need to set the "compact view controller" relationship on your Split View Controller. You can do this by right-clicking the Split View Controller and dragging a new relationship to the view controller you would like to display in compact mode.
My app has a TableView, and in compact mode I want to push a Detail View Controller when a cell is tapped. In the new iOS 14 SplitView Controller, this has to be done manually. I did this by adding the following to my didSelectRowAt function:
// If we are in compact mode, we need to push the detail view controller
if let splitViewController = splitViewController {
if splitViewController.isCollapsed {
let shipmentDetailViewController = storyboard?.instantiateViewController(identifier: "shipmentDetailViewController") as! ShipmentDetailViewController
shipmentDetailViewController.shipment = selectedShipment
self.navigationController?.pushViewController(shipmentDetailViewController, animated: true)
}
}
This is an oldish question and none of the answers were for Objective C, and even when I ported the Swift answers, none worked for me. One was close, by #SwiftArchitect.
But he recommended setting the content mode to .allVisible (UISplitViewControllerDisplayModeAllVisible in Objective C) - this makes the master view display all the time, splitting the view into master on one side, detail on the other. Which is kinda cool, but the OP asked specifically to display the master view on initial launch, which is what I needed to do.
The change was to use UISplitViewControllerDisplayModePrimaryOverlay for the display mode.
This answer is for Xcode 9.4.1, deployment target 11.4.
Here is MasterViewController.h - you need to add UISplitViewControllerDelegate in the protocols declaration:
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
#import "MasterDetailDemo+CoreDataModel.h"
#class DetailViewController;
#interface MasterViewController : UITableViewController
<UISplitViewControllerDelegate,
NSFetchedResultsControllerDelegate>
#property (strong, nonatomic) DetailViewController *detailViewController;
#property (strong, nonatomic) NSFetchedResultsController<Event *> *fetchedResultsController;
#property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
#end
And then in your MasterViewController.m, you need to set the split view controller delegate and the content mode in ViewDidLoad, and following along with #SwiftArchitect's answer, to also add the split view controller delegate method:
- (void)viewDidLoad {
[super viewDidLoad];
// needed to "slide out" MasterView on startup on iPad
self.splitViewController.delegate = self;
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
self.navigationItem.leftBarButtonItem = self.editButtonItem;
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;
self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];
}
// split view delegate method
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
return true;
}
NOTE: After some testing, I found that the split view delegate method and the split view protocol was not necessary. Without it, it appears to work exactly the same. Perhaps this is a result of changes in iOS since the question was originally asked and answered.
I got it working fine just by putting this line in my ViewDidLoad method:
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
Or just inherit from UISplitViewController and use this new class in the storyboard (based on SwiftArchitect's answer):
class MasterShowingSplitViewController :UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.preferredDisplayMode = .allVisible
}
func splitViewController(
_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController: UIViewController,
onto primaryViewController: UIViewController) -> Bool {
// Return true to prevent UIKit from applying its default behavior
return true
}
}
Swift 5, iOS 13
I found other answers useful, but not-quite-there in that they produced the behavior I wanted on iPad or iPhone, but not both.
The solution below is what I used for:
iPhone: Master view always appears first
iPad Portrait: detail always appears, but with master overlaying it; detail is full-screen (not just right-of-master)
iPad Landscape: Master always on left, detail always on right
class RootSplitViewController: UISplitViewController {
override func viewDidLoad() {
if UIDevice.current.userInterfaceIdiom == .pad {
self.preferredDisplayMode = .automatic
}
else {
self.preferredDisplayMode = .allVisible
}
self.delegate = self
}
}
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController:UIViewController,
onto primaryViewController:UIViewController)
-> Bool
{
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
// Or: return false if your application logic makes this appropriate
// return false
}
}

Resources