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
}
}
Related
I'm using a splitViewController to display a master view and a detail view.
When I tap on a row, the detail view updates correctly.
Then, when I'm in portrait view, I collapse the splitview detail view, so that that master list items are shown as follows:
And when I tap on a row, I correctly move to the detail view, as shown:
The problem I'm having is that if I rotate the device in the detail view shown above, while I'm in the detail view, the rotation correctly goes back to the splitView, however, now when I select a row, the delegate method does not update the detail view. It only seems to work if I start out in the splitView and stay in that view, or if I start out in the collapsed view and stay in that. If I rotate, then the delegate method does not seem to work.
I found a prior post, which shows how to use the delegate method to update the detail view using objective C code, using the didSelectRow function. I tried to duplicate this code with the following swift code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let navigationVC = UINavigationController()
var detailVC = TestsDetailAdvertVC()
if let tests = controller.fetchedObjects, tests.count > 0 {
//if there is, keep track of the test which is selected
selectedTest = tests[indexPath.row]
if let isCollapsed = splitViewController?.isCollapsed {
if isCollapsed {
//solves problem of navigating to the detail view in compact view
// on the iPhone (compact) the split view controller is collapsed
// therefore we need to create the navigation controller and detail controller
detailVC = self.storyboard!.instantiateViewController(withIdentifier: "detailVC") as! TestsDetailAdvertVC
navigationVC.setViewControllers([detailVC], animated: false)
self.splitViewController?.showDetailViewController(detailVC, sender: self)
detailVC.testToEdit = selectedTest
} else {
// if the split view controller shows the detail view already there is no need to create the controllers
// so we just pass the correct test using the delegate
// if the test variable is set, then it calls the showDetail function
delegate?.testToEdit = selectedTest
}
}
}
}
I think that somehow when the one or the other method is used to update the detail view it works, but then when it switches back and forth, it stops working. I wonder if anyone has solved this issue using swift code who could point me to an example.
Note: After some additional searching, I realized that there are a few of delegate methods for the splitViewController, including:
func primaryViewControllerForExpandingSplitViewController:
and
func primaryViewControllerForCollapsingSplitViewController:
and
splitViewController:separateSecondaryViewControllerFromPrimaryViewController:
I've been fiddling around with these methods, but so far haven't been able to get them to work, and I haven't found any posts that show examples of how they are used.
Thanks.
I figured out how to make the detail view update properly, using an answer from a prior post at:
In UISplitViewController, can't make showDetailViewController:sender: push onto detail navigationController
my code to solve the problem is updated using swift code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var detail = UINavigationController()
var testVC = TestsDetailAdvertVC()
if let tests = controller.fetchedObjects, tests.count > 0 {
//if there is, keep track of the test which is selected
selectedTest = tests[indexPath.row]
if let isCollapsed = splitViewController?.isCollapsed {
if isCollapsed {
//in collapsed view, the correct detail view controller is not
//yet substantiated, so we need to substantiate it
testVC = self.storyboard?.instantiateViewController(withIdentifier: "detailVC") as! TestsDetailAdvertVC
detail.setViewControllers([testVC], animated: true)
testVC.testToEdit = selectedTest
} else {
//in expanded view, the correct view controller needs
//to be identified, using the appropriate view controller for
//the splitview controller
let vc = self.splitViewController?.viewControllers[1]
//which is a navigation controller
if vc is UINavigationController {
detail = vc as! UINavigationController
//which we then use to identify the correct detail view
testVC = detail.viewControllers.first as! TestsDetailAdvertVC
testVC.testToEdit = selectedTest
}
}
}
}
self.splitViewController?.showDetailViewController(detail, sender: self)
}
The key solution is that on the collapsed splitviewcontroller, the detail view has to be instantiated form the storyboard. However, on the expanded splitviewcontroller, the detail view has to come from the expanded navigation controller. Then when I rotate the correct detail view controller updates correctly.
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()
}
}
}
My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.
How can I detect if the UISplitViewController is currently just showing 1 view controller or it's in dual-pane with 2 views controllers shown side-by-side?
The split view controller reflects the actual display mode in the displayMode property:
AllVisible: The primary and secondary UIViewControllers are displayed side-by-side.
PrimaryHidden: The primary UISplitViewController is hidden.
PrimaryOverlay: The primary UISplitViewController overlays the secondary, which is partially visible.
When the isCollapsed property is true, the value of displayMode property is ignored. A collapsed split view interface contains only one view controller so the display mode is superfluous.
Resume: To find out the detailed situation on screen use isCollapsed property and (if isCollapsed = false) displayMode property.
Here is a simple case:
You are on the MasterViewController and you select a cell. Now, depending if the UISplitViewController is collapsed or not you want to either perform a segue (circled in red)
to the DetailViewController (collapsed) or update the DetailViewController (not collapsed).
In your "didSelectRowAtIndexPath" method on your MasterViewController get a reference to the UISplitViewController and choose what to do like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//Reference to Split View
guard let splitView = self.splitViewController else {
return
}
//Check the collapsed property
if splitView.collapsed {
self.performSegueWithIdentifier("segueToDetail", sender: self)
}else {
//Get reference to your details navigation controller
guard let detailViewNavigationController = self.splitViewController?.viewControllers[1] as? UINavigationController else {
return
}
//Get a reference to your custom detail view controller
guard let detailController = detailViewNavigationController.viewControllers[0] as? MyCustomViewController else {
return
}
//Call your custom function to update the detail view controller
detailController.updateSomething()
}
}
If you don't want to use the "collapsed" property of the UISplitViewController you can check the number of view controllers property like this.
if splitView.viewControllers.count == 1 {
self.performSegueWithIdentifier("segueToDetail", sender: self)
}else splitView.viewControllers.count == 2 {
guard let detailViewNavigationController = self.splitViewController?.viewControllers[1] as? UINavigationController else {
return
}
guard let detailController = detailViewNavigationController.viewControllers[0] as? MyCustomViewController else {
return
}
detailController.updateSomething()
}
Another option is to set up delegation from your master view controller to your detail view controller. This will work well if you don't want to have to reach up the view controller chain like this example does. Here is a tutorial on this method. Note the "Hooking Up The Master With the Detail" section.
Just a note: I tested switching on the UISplitViewControllers "displayMode" property. This property did not give me enough info to figure out what to do. The reason is that the property is set to .AllVisible when you are in the horizontal compact mode and the horizontal expanded mode.
Last, before I go. I like the way I do it because lots of times you know you are going to need a UISplitViewController so you create a project from the template. You will notice the template comes with the segue set up. This template is great for phones but doesn't cut it for iPads and iPhone6+'s. If you drag and drop a UISplitViewController onto a story board after project creation you will notice the detail view is neither imbedded in a UINavigationController nor is there a segue from the master to the detail. Just more to set up I guess!
There is a property of UISplitViewController named 'collapsed'.
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
}
}