UISplitViewController en portrait sur iPhone montre le détail VC au lieu du maître

177

J'utilise un Universal Storyboard dans Xcode 6, ciblant iOS 7 et supérieur. J'ai implémenté un UISplitViewControllerqui est maintenant pris en charge nativement sur iPhone sous iOS 8, et Xcode le rétroportera automatiquement pour iOS 7. Cela fonctionne très bien, sauf lorsque vous lancez l'application sur iPhone en mode portrait sous iOS 8, la vue détaillée de la vue partagée Le contrôleur s'affiche lorsque je m'attendais à voir pour la première fois le contrôleur de vue maître. Je pensais qu'il s'agissait d'un bogue avec iOS 8 car lorsque vous exécutez l'application sur iOS 7, elle affiche correctement le contrôleur de vue maître. Mais iOS 8 est maintenant GM et cela se produit toujours. Comment puis-je le configurer de sorte que lorsque le contrôleur de vue fractionnée va être réduit (un seul contrôleur de vue affiché à l'écran), lorsque le contrôleur de vue fractionnée est affiché, il montre le contrôleur de vue principale et non le détail?

J'ai créé ce contrôleur de vue fractionnée dans Interface Builder. Le contrôleur de vue fractionnée est le premier contrôleur de vue dans un contrôleur de barre d'onglets. Le maître et les VC de détail sont des contrôleurs de navigation avec des contrôleurs de vue de table intégrés à l'intérieur.

Jordanie H
la source

Réponses:

238

Oh mec, cela me causait un mal de tête pendant quelques jours et je ne savais pas comment faire cela. Le pire, c'est que la création d'un nouveau projet Xcode iOS avec le modèle maître-détail fonctionnait très bien. Heureusement, à la fin, ce petit fait a été la façon dont j'ai trouvé la solution.

Certains articles que j'ai trouvés suggèrent que la solution consiste à implémenter la nouvelle primaryViewControllerForCollapsingSplitViewController:méthode UISplitViewControllerDelegate. J'ai essayé cela en vain. Ce qu'Apple fait dans le modèle maître-détail qui semble fonctionner, c'est implémenter la nouvelle splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:méthode de délégué (prenez une grande respiration pour dire tout cela) (encore une fois UISplitViewControllerDelegate). Selon la documentation , cette méthode:

Demande au délégué d'ajuster le contrôleur de vue principal et d'incorporer le contrôleur de vue secondaire dans l'interface réduite.

Assurez-vous de lire la partie discussion de cette méthode pour plus de détails.

La façon dont Apple gère cela est:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController
collapseSecondaryViewController:(UIViewController *)secondaryViewController
  ontoPrimaryViewController:(UIViewController *)primaryViewController {

    if ([secondaryViewController isKindOfClass:[UINavigationController class]]
        && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]]
        && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)) {

        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;

    } else {

        return NO;

    }
}

Cette implémentation fait essentiellement ce qui suit:

  1. Si secondaryViewControllerc'est ce à quoi nous nous attendons (a UINavigationController), et cela montre ce que nous attendons (un DetailViewController- votre contrôleur de vue), mais n'a pas de modèle ( detailItem), alors " Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded."
  2. Sinon, retournez " NOpour laisser le contrôleur de vue fractionnée essayer d'incorporer le contenu du contrôleur de vue secondaire dans l'interface réduite"

Les résultats sont les suivants pour l'iPhone en portrait (en commençant en portrait ou en tournant en portrait - ou plus précisément en classe de taille compacte):

  1. Si votre vue est correcte
    • et a un modèle, montre le contrôleur de vue de détail
    • mais n'a pas de modèle, affichez le contrôleur de vue maître
  2. Si votre vue n'est pas correcte
    • afficher le contrôleur de vue maître

Clair comme de la boue.

Des marques
la source
8
Réponse fantastique! J'ai simplement sous UISplitViewController- classé et reviens toujours YESde cette méthode, puis j'ai simplement changé la classe de vue fractionnée dans Storyboard, car je veux toujours montrer le maître sur iPhone en portrait. :)
Jordan H
2
Je veux que mon contrôleur de vue principale soit masqué si «iPhone» est en mode «Portrait» car j'ai une configuration de contrôleur de vue détaillée par défaut. Comment puis je faire ça. Mon maître et mon détail sont tous deux du type VC. Plus précisément, mon détail est MMDrawerController. S'il vous plaît aider
Harshit Gupta
3
J'ai essayé la suggestion de Joey de sous-classer, UISplitViewControllermais j'ai trouvé que cela ne fonctionnait pas: splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:n'a jamais été appelé. Au lieu de cela, j'ai copié le modèle d'Apple et l'ai mis dans l'AppDelagate. Cela a nécessité quelques modifications pour créer également UISplitViewController application didFinishLaunchingWithOptions:(où j'ai également copié le modèle d'Apple).
Nick
7
Le commentaire de @ joey fonctionne avec le réglage self.delegate = self; dans la vuedidload! Et en ajoutant <UISplitViewControllerDelegate> dans le fichier .h Thankyou!
fellowworldcitizen
2
Cela me semble être la bonne réponse, car j'ai exactement le même problème. Cependant, pour une raison quelconque, je splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:ne suis jamais appelé. Il semble que le délégué est correctement défini la applicationDidFinishLaunchingWithOptions:méthode de mon délégué d'application . Est-ce que quelqu'un d'autre a vu ce problème et n'a PAS eu cette solution?
Tim Dean
60

Voici la réponse acceptée dans Swift. Créez simplement cette sous-classe et attribuez-la à votre splitViewController dans votre storyboard.

//GlobalSplitViewController.swift

import UIKit

class GlobalSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

  override func viewDidLoad() {
    super.viewDidLoad()

    self.delegate = self
  }

  func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool{
    return true
  }

}
Clifton Labrum
la source
3
Super, cela aide beaucoup. Mais un nouveau problème s'est posé. Le bouton arrière qui m'amène au maître disparaît maintenant (ne s'affiche jamais). Comment est-ce que je le récupère? EDIT: Qu'à cela ne tienne, je me suis dit :-). Pour les autres utilisateurs: ajoutez ceci dans le detailView: self.navigationItem.leftBarButtonItem = self.splitViewController? .DisplayModeButtonItem () self.navigationItem.leftItemsSupplementBackButton = true
Tom Tallak Solbu
3
Maintenant dans Swift, peu importe ce que c'estfunc splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
Dan Rosenstark
2
Il semble que cette méthode déléguée ne soit appelée que lorsque la taille de la classe est compacte. Il est appelé sur iPhone, mais pas sur iPad portrait, ce qui signifie qu'il ne résout pas le problème, car le portrait iPad est également en mode réduit. Testé avec iOS 12.1
Daniel
21

Version Swift de la bonne réponse de Mark S

Tel que fourni par le modèle Master-Detail d'Apple.

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
}

Clarification

(Ce que Mark S a dit était légèrement déroutant)

Cette méthode déléguée est appelée splitViewController: collapseSecondaryViewController: ontoPrimaryViewController: , car c'est ce qu'elle fait. Lors du passage à une taille de largeur plus compacte (par exemple lors de la rotation du téléphone du paysage au portrait), il doit réduire le contrôleur de vue fractionnée en un seul d'entre eux.

Cette fonction renvoie un booléen pour décider si elle doit réduire le détail et afficher le maître ou non.

Donc, dans notre cas, nous déciderons en fonction de la sélection ou non d'un détail. Comment savons-nous si notre détail est sélectionné? Si nous suivons le modèle Master-Detail d'Apple, le contrôleur de vue détaillée devrait avoir une variable facultative contenant les informations détaillées, donc si c'est nul (.Aucun), il n'y a encore rien de sélectionné et nous devrions afficher le maître afin que l'utilisateur puisse sélectionner quelque chose.

C'est tout.

NiñoScript
la source
Juste pour clarifier pourquoi j'ai abandonné l'édition de @ sschale. Ce code est une citation de Apple's Master-Detail template, il n'est pas destiné à être génial ou concis, mais simplement factuel. :)
NiñoScript
10

Dans la documentation , vous devez utiliser un délégué pour dire au de UISplitViewController ne pas incorporer la vue détaillée dans "l'interface réduite" (c'est-à-dire le "mode Portrait" dans votre cas). Dans Swift 4, la méthode déléguée à implémenter pour cela a été renommée:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    return true
}
Oli
la source
9
   #import <UIKit/UIKit.h>

    @interface SplitProductView : UISplitViewController<UISplitViewControllerDelegate>




    @end

.m:

#import "SplitProductView.h"
#import "PriceDetailTableView.h"

@interface SplitProductView ()

@end

@implementation SplitProductView

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.delegate = self;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
collapseSecondaryViewController:(UIViewController *)secondaryViewController
  ontoPrimaryViewController:(UIViewController *)primaryViewController {

    if ([secondaryViewController isKindOfClass:[UINavigationController class]]
        && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[PriceDetailTableView class]]

        //&& ([(PriceDetailTableView *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)

        ) {

        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;

    } else {

        return NO;

    }
}
@end
Gank
la source
9

Mon application a été écrite en Swift 2.x et pourrait bien fonctionner. Après l'avoir converti en Swift 3.0 (à l'aide du convertisseur XCode), il commence à afficher les détails au lieu du maître en mode portrait. Le problème est que le nom de la fonction splitViewController n'est pas modifié pour correspondre au nouveau UISplitViewControllerDelegate.

Après avoir modifié le nom de cette fonction manuellement, mon application peut désormais fonctionner correctement:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
    guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
    if topAsDetailController.game == nil {
        // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return true
    }
    return false
}
Tony
la source
J'ai le même problème que vous, mais je ne comprends pas votre solution. Je ne vois aucun changement dans le code que vous avez publié ici. Pourriez-vous être plus précis. Merci
bibscy
De nombreuses méthodes sont légèrement renommées.
Dave
La réponse de Tony est la syntaxe Swift 3 à la réponse de @ NiñoScript (qui est écrite pour les versions précédentes de Swift)
Hellojeffy
2
pour 3 rapide, ne pas oublier de mettre self.delegate = selfsur la viewDidLoadméthode.
Fer
7

Si vous n'avez pas de valeurs par défaut à afficher dans le contrôleur de vue détaillée, vous pouvez simplement supprimer la transition par défaut entre SplitViewController et votre UIViewController de détail dans le storyboard. Cela le fera toujours entrer dans Master View Controller en premier.

L'effet secondaire de ceci est qu'au lieu de voir deux vues en mode paysage, vous verrez une vue en taille réelle dans SplitViewController jusqu'à ce que Show Detail Segue dans le contrôleur de vue maître soit déclenché.

Hao-Cher Hong
la source
bon truc. Mon application est uniquement en mode portrait et je peux le faire.
Peacemoon
Ceci est vrai sauf en orientation Paysage, vous verrez une partie droite vide de la vue éventuellement remplie de gris.
vedrano
4

Pour toutes les personnes qui n'ont pas pu trouver la section vendredi de cs193p:

Dans Swift 3.1.1, créer une sous-classe de UISplitViewController et implémenter l'une de ses méthodes de délégué a fonctionné pour moi comme un charme:

class MainSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

override func viewDidLoad() {
    super.viewDidLoad()
    self.delegate = self
}

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
    return true
} }

Mon storyboard

Bartosz Kunat
la source
Comme @olito l'a souligné, dans Swift 4, la syntaxe pour cela a changé en: public func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool
Robuske
3

À mon avis, vous devriez résoudre ce problème plus générique. Vous pouvez sous-classer UISplitViewController et implémenter un protocole dans les contrôleurs de vue intégrés.

class MasterShowingSplitViewController: UISplitViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension MasterShowingSplitViewController: UISplitViewControllerDelegate {
    func splitViewController(splitViewController: UISplitViewController,
                             collapseSecondaryViewController secondaryViewController: UIViewController,
                             ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
        guard let masterNavigationController = primaryViewController as? UINavigationController,
                  master = masterNavigationController.topViewController as? SplitViewControllerCollapseProtocol else {
            return true
        }
        return master.shouldShowMasterOnCollapse()

    }
}

protocol SplitViewControllerCollapseProtocol {
    func shouldShowMasterOnCollapse() -> Bool
}

Exemple d'implémentation dans UITableViewController:

extension SettingsTableViewController: SplitViewControllerCollapseProtocol {
    func shouldShowMasterOnCollapse() -> Bool {
        return tableView.indexPathForSelectedRow == nil
    }
}

J'espère que ça aide. Vous pouvez donc réutiliser cette classe et avoir juste besoin d'implémenter un protocole.

Maik639
la source
La méthode déléguée n'est jamais appelée!
K_Mohit
il ne s'appelle pas sur l'iPad et l'iPhone 6/7/8 Plus. Est-ce votre problème? Jetez un œil à: stackoverflow.com/questions/29767614/…
Maik639
2

Supprimez simplement DetailViewController des contrôleurs SplitView lorsque vous en avez besoin pour démarrer à partir de Master.

UISplitViewController *splitViewController = (UISplitViewController *)[self.storyboard instantiateViewControllerWithIdentifier:@"SETTINGS"];
splitViewController.delegate = self;
[self.navigationController presentViewController:splitViewController animated:YES completion:nil];
if (IPHONE) {
    NSMutableArray * cntlrs = [splitViewController.viewControllers mutableCopy];
    [cntlrs removeLastObject];
    splitViewController.viewControllers = cntlrs;
}
Borys Shcherbyna
la source
2

Cela a fonctionné pour moi sur iOS-11 et Swift 4:

//Following code in application didFinishLaunching (inside Application Delegate)
guard let splitViewController = window?.rootViewController as? UISplitViewController,
            let masterNavVC = splitViewController.viewControllers.first as? UINavigationController,
            let masterVC = masterNavVC.topViewController as? MasterViewController
else { fatalError() }
splitViewController.delegate = masterVC

//Following code in MasterViewController class
extension MasterViewController:UISplitViewControllerDelegate {
    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        return true
    }
}
Vishal Chaudhry
la source
2

La fonction est renommée dans les nouvelles versions de Swift, donc ce code fonctionne sur Swift 4:

import UIKit

class GlobalSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
    }

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        return true
    }

}
Saeed Ir
la source
0

Solution Xamarin / C #

public partial class MainSplitViewController : UISplitViewController
{
    public MainSplitViewController(IntPtr handle) : base(handle)
    {
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        Delegate = new MainSplitViewControllerDelegate();
    }
}

public class MainSplitViewControllerDelegate : UISplitViewControllerDelegate
{
    public override bool CollapseSecondViewController(UISplitViewController splitViewController, UIViewController secondaryViewController, UIViewController primaryViewController)
    {
        return true;
    }
}
Mark Moeykens
la source
0

Définissez simplement la preferredDisplayModepropriété de UISplitViewControllersur.allVisible

class MySplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.preferredDisplayMode = .allVisible
    }

}
Arash Etemad
la source