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
}
}
Related
I'm running into an issue where after changing the rootViewController on my UINavigationController and changing it back to my original UINavigationController, a UISplitViewController begins to show both it's master and detail view in a phone device on compact/portrait orientation (so not only on plus size phones, but also others).
Basic overview of architecture:
A TabBarController houses several tabs. One of these tabs is a UISplitViewController. I currently override the following to ensure that the MasterViewController is shown on compact orientations:
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
// this prevents phone from going straight to detail on showing the split view controller
return true
}
This works fine and displays the master on portrait as expected. At any point pressing a button on another tab can create a new UINavigationController instance and display it, in which I'm doing the below to change the rootViewController to the newly created UINavigationController to display:
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.rootViewController = newNavVC
On dismiss, I'm just swapping the UINavigationController back to the original one through the same code above. However, once I do this one time (create nav/display/dismiss), and I switch my tab back to the one with the UISplitViewController, it changes itself to show a side-by-side master detail view. I didn't know this was possible in portrait mode for compact sizing. I tried changing to any of the 4 preferred display modes in the UISplitViewController, but that didn't fix it.
Below is what it looks like (iPhone 6 simulator), am I missing delegates or misunderstanding collapsing?
Before:
After:
You can replace the the logic that assigned the rootViewController with the code snippet found at this link:
Leaking views when changing rootViewController inside transitionWithView
Basically you just create an extension for the UIWindow class that will set the root view controller correctly.
extension UIWindow {
/// Fix for https://stackoverflow.com/a/27153956/849645
func set(rootViewController newRootViewController: UIViewController, withTransition transition: CATransition? = nil) {
let previousViewController = rootViewController
if let transition = transition {
// Add the transition
layer.add(transition, forKey: kCATransition)
}
rootViewController = newRootViewController
// Update status bar appearance using the new view controllers appearance - animate if needed
if UIView.areAnimationsEnabled {
UIView.animate(withDuration: CATransaction.animationDuration()) {
newRootViewController.setNeedsStatusBarAppearanceUpdate()
}
} else {
newRootViewController.setNeedsStatusBarAppearanceUpdate()
}
/// The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
if let transitionViewClass = NSClassFromString("UITransitionView") {
for subview in subviews where subview.isKind(of: transitionViewClass) {
subview.removeFromSuperview()
}
}
if let previousViewController = previousViewController {
// Allow the view controller to be deallocated
previousViewController.dismiss(animated: false) {
// Remove the root view in case its still showing
previousViewController.view.removeFromSuperview()
}
}
}
I am trying to get device rotation right.
I am testing on iPad 8.x/9.x simulator
I have 4 VCs
VC1 - Both Portrait and Landscape
VC2 - Both Portrait and Landscape
VC3 - Only Portrait
VC4 - Both Portrait and Landscape
Goal: to have VC3 display PortraitView at all times (same as if app orientation was fixed to portrait).
I tried
#implementation RotationAwareNavigationController
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIViewController *top = self.topViewController;
return top.supportedInterfaceOrientations;
}
-(BOOL)shouldAutorotate {
UIViewController *top = self.topViewController;
return [top shouldAutorotate];
}
#end
In VC which is portrait
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
But it does not work meaning view not displayed in Portrait dimensions Am I missing something?
I am sure it can be done as when I use ImagePickerController provided my iOS, it is fixed to Portrait. I just dont know how to do it.
To support differing orientations in different view controllers, you will need to do a few things. First, you need to check all the checkboxes for orientations you want to support in your target's General settings tab.
Second, anytime you call presentViewController or dismissViewController in your app, the UIApplicationDelegate method application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask will be called. You can use this method to restrict (or allow) specific orientations each time a new view controller is presented or dismissed. Unfortunately, it isn't as simple as just returning a UIInterfaceOrientationMask here. You will need to find the view controller that is going to be showing on screen and return the orientations that it supports. Here is an example of this:
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
guard let window = window, let rootViewController = window.rootViewController else {
return UIDevice.currentDevice().userInterfaceIdiom == .Pad ? .All : .AllButUpsideDown // iOS defaults
}
// Let the view controller being shown decide what orientation it wants to support. This method will be called anytime a new view controller is presented on screen.
return findVisibleViewController(rootViewController: rootViewController).supportedInterfaceOrientations()
}
/// Searches the view hierarchy recursively and finds the view controller that is currently showing.
private func findVisibleViewController(rootViewController rootViewController: UIViewController) -> UIViewController {
if let presentedViewController = rootViewController.presentedViewController {
// Search for a modal view first.
return self.findVisibleViewController(rootViewController: presentedViewController)
} else if
let navigationController = rootViewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
// Then search navigation controller's views to find its visible controller.
return self.findVisibleViewController(rootViewController: visibleViewController)
} else if let splitViewController = rootViewController as? UISplitViewController {
// Then try the split view controller. This will be the true root view controller. Use the master here since the detail just shows web views.
return self.findVisibleViewController(rootViewController: splitViewController.viewControllers[0])
} else {
// Guess we found the visible view controller, because none of the other conditions were met.
return rootViewController
}
}
findVisibleViewController(_:) is an example from one of my projects and is tailored to the exact view controller hierarchy in my app. You will need to edit this for your own app in a way that makes sense for your hierarchy.
Third, you will need to implement supportedInterfaceOrientations() for most, if not all, of your view controllers.
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return // portrait, landscape, or combinations of those.
}
Finally, this only handles situations where you presented or dismissed something modally. For show (push) segues, the orientation of the navigation controller will be used for every new view controller pushed onto the stack. If you need more fine grained control here, you will need to force the orientation to happen. Here is an example of that:
// Some other view had the screen in landscape, so force the view to return to portrait
UIDevice.currentDevice().setValue(UIInterfaceOrientation.Portrait.rawValue, forKey: "orientation")
I am building an iPad only/landscape only app in iOS 8 and want to take advantage of the new features are available in the UISplitViewController, in that I want to create a collapsable primary controller that can disappear when I need a fullscreen detail view.
I am using the basic storyboard with a Container VC containing my splitVC...
I am using this code to achieve that function...
import UIKit
class ContainerViewController: UIViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let splitViewController = self.childViewControllers[0] as UISplitViewController
splitViewController.delegate = self
splitViewController.preferredDisplayMode = .AllVisible
let navViewController = splitViewController.childViewControllers.last as UINavigationController
navViewController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem();
}
func splitViewController(splitViewController: UISplitViewController!, collapseSecondaryViewController secondaryViewController:UIViewController!, ontoPrimaryViewController primaryViewController:UIViewController!) -> Bool {
return true;
}
}
Note I am using .AllVisible as the preferred display mode. This works except my detail view is pushed to the right when I show the primary view.
What I'd actually like to do is use the Preferred display mode of .PrimaryOverlay which 'should' overlay the primary on the secondary. It does, but is incorrect size and misplaced on the initial showing of the controllers. The first time the display appears, I get this effect....
... and after tapping to hide/reshow the system seems to sort itself and the primary is properly overlaid onto the secondary...
Can anyone help so this works initially as expected?
I'm using a split-view controller. When I start it in some devices, the master view is hidden and just the detail is shown. The detail is empty because a row hasn't been selected in master yet.
So, I need a solution that is one of the following:
1) Default the detail view to the first item in the master view.
2) Automatically show the master view, either by making it visible some how.
It is using the automatic [< Master View] bar button in the navigation bar which swift automatically adds for you.
As others have shared, this is unrelated to unwind segues.
If you look in the AppDelegate.swift code generated by the Master-Detail template, you'll see this UISplitViewControllerDelegate method which determines whether or not to show the detail view while collapsed:
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController:UIViewController!, ontoPrimaryViewController primaryViewController:UIViewController!) -> Bool {
if let secondaryAsNavController = secondaryViewController as? UINavigationController {
if let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController {
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
}
The sample code is checking the detail view controller's detailItem property, to see if it has any details. If it does, the detail view is shown while collapsed, otherwise the master view is shown.
You'll have to modify this code to check the particular property you're using which holds the detail item that the master would be passing to the detail in its "showDetail" prepareForSegue.
Once you've done this, the detail view will not be shown while collapsed, if it is empty.
I was able to get the first row of the items into the detail view when it loads. here is the viewDidLoad of the master which is called even though its not shown to the user.
override func viewDidLoad() {
getItems() // gets the items from the web service
if let split = self.splitViewController{
let controllers = split.viewControllers
self.detailViewController = controllers[controllers.count-1].topViewController as? DetailViewController
// this line sets the "default" item
self.detailViewController?.detailItem = items.items[0]
}
}
now after the user logs in and the detail view is displayed it already is populated with the first item.
I had the same problem and came to the attached solution. Maybe it's for some help.
The shown class of a splitViewController is linked with the splitViewController in the storyboard.
//
// InfoMainSplitViewController.swift
//
import UIKit
class InfoMainSplitViewController: UISplitViewController, UISplitViewControllerDelegate {
// MARK: - Global variables
// variable to control if the detail view should be collapsed on launch
var forceDetailToColapse : Bool = false
// MARK: - Lifetime management
override func viewDidLoad() {
super.viewDidLoad()
// set the delegate to get access to the delegate methods
self.delegate = self
// if this is an iPad, show both scenes (selection and detail) side by side
if (UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad) {
// we have an iPad, so set the mode
self.preferredDisplayMode = UISplitViewControllerDisplayMode.allVisible
// we do not want to collapse the detail view on launch
forceDetailToColapse = false
} else {
// we have an iPhone, set the mode
self.preferredDisplayMode = UISplitViewControllerDisplayMode.automatic
// make sure we collapse the detail view on launch
forceDetailToColapse = true
}
}
// MARK: - Delegate methods
// used to collapse the detail view
// BTW: this method will not be called if preferredDisplayMode == UISplitViewControllerDisplayMode.allVisible
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
// true: detail view will collapse, false: detail View will not collapse
return forceDetailToColapse
}
}
In XCode 6, if you create a new project based on the Master-Detail Application template, you get a universal storyboard that is supposed to be good for all devices.
When selecting a cell in the master view, the detail view is updated via an adaptive "show detail" segue. On an iPhone 4, 5, 6 or 6+ in portrait, this segue will take the form of a push as expected. On an iPad or an iPhone 6+ in landscape, it will cause the detail view to be updated as expected.
Now, if you insert a UITabBarController as the master view controller which has a tab to the original master view controller, the adaptive segue that occurs when selecting a cell in the master view does not behave as expected on iPhones. Instead of getting a push transition, you now get a modal transition. How can I fix that? Seems odd that this is not supported by default.
I found the following post useful: iOS8 TabbarController inside a UISplitviewController Master
But when using the suggested method, I don't get the right behaviour on an iPhone 6 Plus when I rotate to landscape after a push in portrait. The content of the detail view appears in the master view which is not surprising since that's what the suggested solution does.
Thanks!
Re-watching videos from WWDC14 I think I've found a better answer.
Use a custom UISplitViewController (subclass)
Override the showDetailViewController operation
Use the traitCollection to determine the class of the UISplitViewController
If the horizontal class is Compact, get the navigationController to call showViewController
Here is the the code of the custom UISplitViewController :
import UIKit
class CustomSplitViewController: UISplitViewController {
override func showDetailViewController(vc: UIViewController!, sender: AnyObject!) {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact) {
if let tabBarController = self.viewControllers[0] as? UITabBarController {
if let navigationController = tabBarController.selectedViewController as? UINavigationController {
navigationController.showViewController(vc, sender: sender)
return
}
}
}
super.showDetailViewController(vc, sender: sender)
}
}
Do not forget to the set the custom class in the storyboard.
Tested in the simulator of iPhone 6, iPhone 6+ and iPad Air and worked as expected.
Unfortunately, the selected answer didn't work for me. However, I did eventually manage to solve the problem:
Subclass UISplitViewController and set the new class in Interface Builder.
Make the new class conform to UISplitViewControllerDelegate:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.delegate = self
}
Implement these two methods:
func splitViewController(_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController:UIViewController,
onto primaryViewController:UIViewController) -> Bool {
return true
}
func splitViewController(_ splitViewController: UISplitViewController,
showDetail vc: UIViewController,
sender: Any?) -> Bool {
if splitViewController.isCollapsed {
guard let tabBarController = splitViewController.viewControllers.first as? UITabBarController else { return false }
guard let selectedNavigationViewController = tabBarController.selectedViewController as? UINavigationController else { return false }
// Push view controller
var detailViewController = vc
if let navController = vc as? UINavigationController, let topViewController = navController.topViewController {
detailViewController = topViewController
}
selectedNavigationViewController.pushViewController(detailViewController, animated: true)
return true
}
return false
}
The docs state when the split controller is collapsed, it handles showDetail by calling show on the master view controller, which in your case is a tab controller. You need to forward that on to the child nav controller as follows:
Make a tab controller subclass.
In the storyboard set the tab controller to use the new subclass.
Add this method to the subclass:
- (void)showViewController:(UIViewController *)vc sender:(id)sender{
[self.viewControllers.firstObject showViewController:vc sender:sender];
}
This forwards it on to the nav controller in the first tab.