Hiding the master view controller with UISplitViewController in iOS8

Objective CSwiftIos8Uisplitviewcontroller

Objective C Problem Overview


I have an iOS7 application, which was based on the Xcode master-detail template, that I am porting to iOS8. One area that has changed a lot is the UISplitViewController.

When in portrait mode, if the user taps on the detail view controller, the master view controller is dismissed:

enter image description here

I would also like to be able to programmatically hide the master view controller if the user taps on a row.

In iOS 7, the master view controller was displayed as a pop-over, and could be hidden as follows:

[self.masterPopoverController dismissPopoverAnimated:YES];

With iOS 8, the master is no longer a popover, so the above technique will not work.

I've tried to dismiss the master view controller:

self.dismissViewControllerAnimated(true, completion: nil)

Or tell the split view controller to display the details view controller:

self.splitViewController?.showDetailViewController(bookViewController!, sender: self)

But nothing has worked so far. Any ideas?

Objective C Solutions


Solution 1 - Objective C

Extend the UISplitViewController as follows:

extension UISplitViewController {
    func toggleMasterView() {
        let barButtonItem = self.displayModeButtonItem()
        UIApplication.sharedApplication().sendAction(barButtonItem.action, to: barButtonItem.target, from: nil, forEvent: nil)
    }
}

In didSelectRowAtIndexPath or prepareForSegue, do the following:

self.splitViewController?.toggleMasterView()

This will smoothly slide the master view out of the way.

I got the idea of using the displayModeButtonItem() from this post and I am simulating a tap on it per this post.

I am not really happy with this solution, since it seems like a hack. But it works well and there seems to be no alternative yet.

Solution 2 - Objective C

Use preferredDisplayMode. In didSelectRowAtIndexPath or prepareForSegue:

self.splitViewController?.preferredDisplayMode = .PrimaryHidden
self.splitViewController?.preferredDisplayMode = .Automatic

Unfortunately the master view abruptly disappears instead of sliding away, despite the documentation stating:

> If changing the value of this property leads to an actual change in > the current display mode, the split view controller animates the > resulting change.

Hopefully there is a better way to do this that actually animates the change.

Solution 3 - Objective C

The code below hides the master view with animation

UIView.animateWithDuration(0.5) { () -> Void in
            self.splitViewController?.preferredDisplayMode = .PrimaryHidden
        }

Solution 4 - Objective C

I was able to have the desired behavior in a Xcode 6.3 Master-Detail Application (universal) project by adding the following code in the MasterViewController's - prepareForSegue:sender: method:

if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
    let animations: () -> Void = {
        self.splitViewController?.preferredDisplayMode = .PrimaryHidden
    }
    let completion: Bool -> Void = { _ in
        self.splitViewController?.preferredDisplayMode = .Automatic
    }
    UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}

The complete - prepareForSegue:sender: implementation should look like this:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "showDetail" {
        if let indexPath = self.tableView.indexPathForSelectedRow() {
            let object = objects[indexPath.row] as! NSDate
            let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
            controller.detailItem = object
            controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
            controller.navigationItem.leftItemsSupplementBackButton = true

            if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
                let animations: () -> Void = {
                    self.splitViewController?.preferredDisplayMode = .PrimaryHidden
                }
                let completion: Bool -> Void = { _ in
                    self.splitViewController?.preferredDisplayMode = .Automatic
                }
                UIView.animateWithDuration(0.3, animations: animations, completion: completion)
            }
        }
    }
}

Using traitCollection may also be an alternative/supplement to displayMode in some projects. For example, the following code also works for a Xcode 6.3 Master-Detail Application (universal) project:

let traits = view.traitCollection
if traits.userInterfaceIdiom == .Pad && traits.horizontalSizeClass == .Regular {
    let animations: () -> Void = {
        self.splitViewController?.preferredDisplayMode = .PrimaryHidden
    }
    let completion: Bool -> Void = { _ in
        self.splitViewController?.preferredDisplayMode = .Automatic
    }
    UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}

Solution 5 - Objective C

Swift 4 update:

Insert it into prepare(for segue: ...

if splitViewController?.displayMode == .primaryOverlay {
    let animations: () -> Void = {
        self.splitViewController?.preferredDisplayMode = .primaryHidden
    }
    let completion: (Bool) -> Void = { _ in
        self.splitViewController?.preferredDisplayMode = .automatic
    }
    UIView.animate(withDuration: 0.3, animations: animations, completion: completion)
}

Solution 6 - Objective C

Modifying the answers above this is all I needed in a method of my detail view controller that configured the view:

 [self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];

Of course it lacks the grace of animation.

Solution 7 - Objective C

try

let svc = self.splitViewController
svc.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden

Solution 8 - Objective C

My solution in the Swift 1.2

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
    var screen = UIScreen.mainScreen().currentMode?.size.height
    if (UIDevice.currentDevice().userInterfaceIdiom == UIUserInterfaceIdiom.Pad) || screen >= 2000 && UIDevice.currentDevice().orientation.isLandscape == true  && (UIDevice.currentDevice().userInterfaceIdiom == .Phone){
        performSegueWithIdentifier("showDetailParse", sender: nil)
        self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
    } else if (UIDevice.currentDevice().userInterfaceIdiom == .Phone) {
        performSegueWithIdentifier("showParse", sender: nil)
    }
}

Solution 9 - Objective C

for iPad add Menu button like this

UIBarButtonItem *menuButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"burger_menu"]
                                                                       style:UIBarButtonItemStylePlain
                                                                      target:self.splitViewController.displayModeButtonItem.target
                                                                      action:self.splitViewController.displayModeButtonItem.action];
[self.navigationItem setLeftBarButtonItem:menuButtonItem];

This work great with both landscape and portrait mode. To programmatically close the popover vc you just need to force the button action like this

[self.splitViewController.displayModeButtonItem.target performSelector:appDelegate.splitViewController.displayModeButtonItem.action];

Solution 10 - Objective C

Very similar to the method by phatmann, but a bit simpler in Swift 5. And it's not technically a 'hack', as it is what the iOS doc suggested.

In your prepareForSegue or other methods that handle touches, in

let barButton = self.splitViewController?.displayModeButtonItem
_ = barButton?.target?.perform(barButton?.action)

According to Apple, the splitViewController's displayModeButtonItem is set up for you to display the master view controller in a way that suits your device orientation. That is, .preferHidden in portrait mode.

All there's to do is to press the button, programatically. Or you can put it in an extension to UISplitViewController, like phatmann did.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionColinEView Question on Stackoverflow
Solution 1 - Objective CphatmannView Answer on Stackoverflow
Solution 2 - Objective CVinay JainView Answer on Stackoverflow
Solution 3 - Objective CjithinroyView Answer on Stackoverflow
Solution 4 - Objective CImanou PetitView Answer on Stackoverflow
Solution 5 - Objective CAndy GView Answer on Stackoverflow
Solution 6 - Objective CTimView Answer on Stackoverflow
Solution 7 - Objective CdarenView Answer on Stackoverflow
Solution 8 - Objective CAlexander KhitevView Answer on Stackoverflow
Solution 9 - Objective Ccoder1087View Answer on Stackoverflow
Solution 10 - Objective Cuser3723199View Answer on Stackoverflow