51

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?

phatmann
  • 18,161
  • 7
  • 61
  • 51
ColinE
  • 68,894
  • 15
  • 164
  • 232

10 Answers10

69

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.

Community
  • 1
  • 1
phatmann
  • 18,161
  • 7
  • 61
  • 51
  • Nice solution. I've just tested this and it works as expected, animating out the popover in iPad portrait mode. The only slight side effect is this also hides the master view when in iPhone 6+ Landscape mode (but not on iPads) - another joyous way in which the iPhone 6+ is neither a phone nor a tablet! – pjh68 Jan 11 '15 at 21:35
  • 1
    I ended up implementing a a device specific bypass for iPhone 6 Plus - see http://stackoverflow.com/questions/25780283/ios-how-to-detect-iphone-6-plus-iphone-6-iphone-5-by-macro for easy way of detecting this. – pjh68 Jan 11 '15 at 21:45
  • 1
    Hey very nice answer, it works, but I have another issue. It animates also the change of the detail view controller. Is this happening to you too? Thanks – Sanandrea Feb 20 '15 at 21:08
  • Nevermind, I called the toggle method in viewDidLoad in DetailViewController thanks – Sanandrea Feb 20 '15 at 21:16
  • 1
    This looks terrible on iOS9 iPhone 6s plus - there is a underlying gray outlined view that ends up animating slower than the primary view. Works fine on an iPad – David H Dec 08 '15 at 16:27
  • You are right @DavidH, I see the same gray outlined view while closing the primary with this work around (it doesn't happen when it's dismissed by a tap onto the secondary). This gray border seems to be a view underneath the view of the primary vc which for some reasons doesn't follow the rest while animating. Have you been able to find a solution for that? Thanks – ggould75 Jun 19 '16 at 21:30
  • @ggould75 I did get a solution. Post a question I'll answer it with the code. – David H Jun 20 '16 at 11:13
  • Thanks @DavidH, I posted my question here: http://stackoverflow.com/questions/37939151/weird-underlying-gray-outlined-view-trying-to-dismiss-programmatically-the-maste – ggould75 Jun 21 '16 at 08:24
  • when adopting this solution the details loads with a weird animation. not working for me. – zumzum Aug 22 '18 at 03:49
  • I added this so it doesn't do this unnecessarily: splitViewController?.displayMode == .primaryOverlay – TruMan1 May 26 '19 at 19:59
13

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.

Aviel Gross
  • 9,770
  • 3
  • 52
  • 62
Vinay Jain
  • 1,653
  • 20
  • 28
  • This does not work at all. The master view stays on screen after a row is selected, and does not appear side-by-side in landscape mode. – phatmann Dec 10 '14 at 09:42
  • @phatmann it does work, I tried to add this code in my didSelectRowAtIndexPath: -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden; } – Vinay Jain Dec 10 '14 at 10:12
  • 1
    If I put the above code in `prepareForSegue`, the master view does indeed disappear. I also had to call `preferredDisplayMode = .Automatic` right afterwards so landscape operation is correct. The problem is that the master view does not smoothly slide away, making this technique less than ideal. On the other hand, no one has provided a better answer yet, so thank you! – phatmann Dec 10 '14 at 10:34
  • 3
    @phatmann animations can be added using `[UIView beginAnimations]` – Vinay Jain Dec 10 '14 at 11:17
  • This answer helped me build some code that allows the desired behavior: [see my answer below](http://stackoverflow.com/a/30128233/1966109). – Imanou Petit May 14 '15 at 11:26
13

The code below hides the master view with animation

UIView.animateWithDuration(0.5) { () -> Void in
            self.splitViewController?.preferredDisplayMode = .PrimaryHidden
        }
jithinroy
  • 1,885
  • 1
  • 16
  • 23
10

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)
}
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
5

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)
}
Andy G
  • 129
  • 2
  • 4
2

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.

Tim
  • 1,108
  • 13
  • 25
1

try

let svc = self.splitViewController
svc.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
daren
  • 51
  • 5
0

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)
    }
}
Alexander Khitev
  • 6,417
  • 13
  • 59
  • 115
0

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];
coder1087
  • 115
  • 1
  • 2
  • 9
0

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.