Hiding the master view controller with UISplitViewController in iOS8
Asked Answered
D

10

52

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?

Deferral answered 2/12, 2014 at 6:27 Comment(0)
Z
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.

Z answered 10/12, 2014 at 11:18 Comment(10)
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!Roguery
I ended up implementing a a device specific bypass for iPhone 6 Plus - see #25780783 for easy way of detecting this.Roguery
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? ThanksAbscissa
Nevermind, I called the toggle method in viewDidLoad in DetailViewController thanksAbscissa
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 iPadStewartstewed
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? ThanksBread
@Bread I did get a solution. Post a question I'll answer it with the code.Stewartstewed
Thanks @DavidH, I posted my question here: #37939651Bread
when adopting this solution the details loads with a weird animation. not working for me.Hartz
I added this so it doesn't do this unnecessarily: splitViewController?.displayMode == .primaryOverlayAnnabal
K
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.

Katabolism answered 2/12, 2014 at 11:6 Comment(5)
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.Z
@Z it does work, I tried to add this code in my didSelectRowAtIndexPath: -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryHidden; }Katabolism
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!Z
@Z animations can be added using [UIView beginAnimations]Katabolism
This answer helped me build some code that allows the desired behavior: see my answer below.Gaselier
F
13

The code below hides the master view with animation

UIView.animateWithDuration(0.5) { () -> Void in
            self.splitViewController?.preferredDisplayMode = .PrimaryHidden
        }
Finis answered 29/12, 2015 at 1:15 Comment(1)
This should be the accepted answer, doesn't require an extension of UISplitViewControllerSteradian
G
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)
}
Gaselier answered 8/5, 2015 at 16:5 Comment(0)
B
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)
}
Bowel answered 25/2, 2019 at 14:49 Comment(0)
S
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.

Suspend answered 8/1, 2016 at 17:42 Comment(0)
L
1

try

let svc = self.splitViewController
svc.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
Lingerie answered 28/2, 2016 at 16:35 Comment(0)
N
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)
    }
}
Noisemaker answered 10/7, 2015 at 17:32 Comment(0)
S
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];
Sita answered 6/1, 2016 at 20:0 Comment(0)
O
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.

Olivero answered 13/5, 2020 at 3:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.