UISplitViewController em retrato no iPhone mostra o detalhe VC em vez de mestre

177

Estou usando um Universal Storyboard no Xcode 6, segmentando o iOS 7 e superior. Eu implementei um UISplitViewControllerque agora é suportado nativamente no iPhone executando o iOS 8 e o Xcode o portará automaticamente para o iOS 7. Ele está funcionando muito bem, exceto quando você inicia o aplicativo no iPhone no retrato executando o iOS 8, a exibição de detalhes da exibição dividida O controlador é exibido quando eu esperava ver o controlador da visualização principal. Eu acreditava que isso era um bug no iOS 8, porque quando você executa o aplicativo no iOS 7, ele mostra corretamente o controlador de exibição principal. Mas o iOS 8 agora é GM e isso ainda está ocorrendo. Como posso configurá-lo para que, quando o controlador de exibição dividida for recolhido (apenas um controlador de exibição exibido na tela), quando o controlador de exibição dividido for exibido, ele mostre o controlador de exibição principal e não os detalhes?

Eu criei esse controlador de exibição dividida no Interface Builder. O controlador de exibição dividida é o primeiro controlador de exibição dentro de um controlador de barra de guias. O VC mestre e o detalhe são controladores de navegação com controladores de exibição de tabela incorporados.

Jordan H
fonte

Respostas:

238

Oh cara, isso estava me causando dor de cabeça por alguns dias e não conseguia descobrir como fazer isso. A pior parte foi que a criação de um novo projeto iOS do Xcode com o modelo de detalhes mestre funcionou perfeitamente. Felizmente, no final, esse pequeno fato foi como eu encontrei a solução.

Existem alguns posts que eu achei que sugerem que a solução é implementar o novo primaryViewControllerForCollapsingSplitViewController:método UISplitViewControllerDelegate. Eu tentei isso sem sucesso. O que a Apple faz no modelo de detalhes mestres que parece funcionar é implementar o novo splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:método de delegação ( respire fundo para dizer tudo isso) (novamente UISplitViewControllerDelegate). De acordo com os documentos , este método:

Solicita ao delegado que ajuste o controlador de exibição primário e incorpore o controlador de exibição secundário na interface recolhida.

Leia a parte da discussão desse método para obter detalhes mais específicos.

A maneira como a Apple lida com isso é:

- (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;

    }
}

Essa implementação basicamente faz o seguinte:

  1. Se secondaryViewControlleré o que estamos esperando (a UINavigationController) e está mostrando o que estamos esperando (a DetailViewController- seu controlador de exibição), mas não tem modelo ( detailItem), então " Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded."
  2. Caso contrário, retorne " NOpara permitir que o controlador de exibição dividido tente incorporar o conteúdo do controlador de exibição secundário na interface recolhida"

Os resultados são os seguintes para o iPhone em retrato (iniciando em retrato ou girando para retrato - ou classe de tamanho mais compacta):

  1. Se sua visão estiver correta
    • e tem um modelo, mostra o controlador de exibição de detalhes
    • mas não tem modelo, mostre o controlador de visualização principal
  2. Se sua visão não estiver correta
    • mostre o controlador da vista principal

Claro como lama.

Mark S
fonte
8
Resposta fantástica! Simplesmente subclassei UISplitViewControllere sempre voltei YESdesse método, depois mudei a classe de exibição dividida no Storyboard, pois sempre quero mostrar o mestre no iPhone em retrato. :)
Jordan H
2
Quero que meu controlador de exibição principal fique oculto se o "iPhone" estiver no modo "Retrato" porque tenho uma configuração padrão do controlador de exibição de detalhes. Como eu posso fazer isso. Meu mestre e detalhes são do tipo VC. Especificamente, meus detalhes são MMDrawerController. Por favor ajude
Harshit Gupta
3
Tentei a sugestão de subclasse de Joey, UISplitViewControllermas achei que isso não funcionava: splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:nunca foi chamado. Em vez disso, copiei o modelo da Apple e o coloquei no AppDelagate. Isso exigiu algumas alterações na criação do UISplitViewController application didFinishLaunchingWithOptions:também (onde eu também copiei o modelo da Apple).
Nick
7
O comentário de @ joey funciona com a configuração self.delegate = self; no viewdidload! E adicionando <UISplitViewControllerDelegate> no arquivo .h Obrigado!
fellowworldcitizen
2
Esta parece ser a resposta certa para mim, pois estou tendo exatamente o mesmo problema. No entanto, por algum motivo, meu splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:nunca é chamado. Parece que o delegado está sendo definido corretamente como o applicationDidFinishLaunchingWithOptions:método do delegado do meu aplicativo . Alguém já viu esse problema e NÃO teve essa solução funcionando?
Tim Dean
60

Aqui está a resposta aceita em Swift. Basta criar essa subclasse e atribuí-la ao seu splitViewController no seu 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
fonte
3
Ótimo, isso ajuda muito. Mas um novo problema surgiu. O botão que me leva ao mestre agora desaparece (nunca aparece). Como faço para recuperá-la? EDIT: Não importa, me imaginei :-). ? Para outros usuários: adicione no detailView: self.navigationItem.leftBarButtonItem = self.splitViewController .displayModeButtonItem () self.navigationItem.leftItemsSupplementBackButton = true
Tom Tallak Solbu
3
Agora em Swift seja o que for #func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
Dan Rosenstark 23/04/19
2
Parece que esse método delegado só está sendo chamado quando o tamanho da classe é compacto. Ele está sendo chamado no iPhone, mas não no retrato do iPad, o que significa que não resolve o problema, pois o retrato do iPad também está no modo recolhido. Testado com iOS 12.1
Daniel
21

Versão rápida da resposta correta de Mark S

Conforme fornecido pelo modelo Master-Detail da 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
}

Esclarecimento

(O que Mark S disse foi um pouco confuso)

Esse método delegado é chamado splitViewController: collapseSecondaryViewController: ontoPrimaryViewController:, porque é o que faz. Ao mudar para um tamanho de largura mais compacto (por exemplo, ao girar o telefone de paisagem para retrato), ele precisa recolher o controlador de exibição dividida em apenas um deles.

Esta função retorna um booleano para decidir se deve recolher o detalhe e mostrar o mestre ou não.

Portanto, no nosso caso, decidiremos com base se houve um detalhe selecionado ou não. Como sabemos se nossos detalhes foram selecionados? Se seguirmos o modelo Master-Detail da Apple, o controlador de exibição de detalhes deve ter uma variável opcional com as informações detalhadas, portanto, se for nulo (.

É isso aí.

NiñoScript
fonte
Apenas para esclarecer por que voltei da edição do @ sschale. Esse código é uma citação de Apple's Master-Detail template, não se destina a ser ótimo ou conciso, apenas factual. :)
NiñoScript
10

A partir da documentação , você precisa usar um representante para dizer para UISplitViewController não incorporar a exibição de detalhes na "interface recolhida" (por exemplo, o "modo Retrato" no seu caso). No Swift 4, o método delegado a ser implementado para isso foi renomeado:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    return true
}
oli
fonte
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
fonte
9

Meu aplicativo foi escrito no Swift 2.xe pode funcionar bem. Depois de convertê-lo no Swift 3.0 (usando o conversor XCode), ele começa a mostrar os detalhes primeiro, em vez de mestre no modo retrato. O problema é que o nome da função splitViewController não é alterado para corresponder ao novo UISplitViewControllerDelegate.

Depois de alterar o nome dessa função manualmente, meu aplicativo agora pode funcionar corretamente:

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
fonte
Estou com o mesmo problema que você, mas não entendo sua solução. Não vejo nenhuma alteração no código que você postou aqui. Você poderia ser mais específico. Graças
bibscy
Muitos métodos são renomeados.
Dave
A resposta de Tony é a sintaxe Swift 3 a @ resposta de NiñoScript (que é escrito para versões anteriores Swift)
Hellojeffy
2
para SWIFT 3, não se esqueça de colocar self.delegate = selfno viewDidLoadmétodo.
Fer
7

Se você não tiver valores padrão para mostrar no controlador de exibição de detalhes, basta excluir as segue padrão entre o SplitViewController e seu UIViewController de detalhes no storyboard. Isso fará com que ele sempre entre primeiro no Master View Controller.

O efeito colateral disso é que, em vez de ver duas vistas em paisagem, você verá uma vista em tamanho real no SplitViewController até que Mostrar detalhe do segmento no controlador de vista principal seja acionado.

Hao-Cher Hong
fonte
bom truque. Meu aplicativo está apenas no modo retrato e posso fazer isso.
Peacemoon
Isso é verdade, exceto na orientação Paisagem, você verá a parte direita da visualização vazia, possivelmente preenchida em cinza.
vedrano
4

Para todas as pessoas que não conseguiram encontrar a seção sexta-feira do cs193p:

No Swift 3.1.1, criar uma subclasse de UISplitViewController e implementar um dos métodos delegados funcionou para mim como um encanto:

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
} }

Meu storyboard

Bartosz Kunat
fonte
Como o @olito apontou, no Swift 4 a sintaxe para isso foi alterada para: # public func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool
Robuske
3

Na minha opinião, você deve resolver esse problema de forma mais genérica. Você pode subclassificar o UISplitViewController e implementar um protocolo nos controladores de exibição incorporados.

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
}

Exemplo de implementação no UITableViewController:

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

Espero que ajude. Então você pode reutilizar essa classe e só precisa implementar um protocolo.

Maik639
fonte
O método delegado nunca é chamado!
K_Mohit
não é chamado no iPad e no iPhone 6/7/8 Plus. Esse é o seu problema? Dê uma olhada em: stackoverflow.com/questions/29767614/…
Maik639
2

Apenas remova o DetailViewController dos controladores SplitView quando precisar iniciar a partir do 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
fonte
2

Isso funcionou para mim no iOS-11 e 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
fonte
2

A função é renomeada em novas versões do Swift, portanto, este código funciona no 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
fonte
0

Solução 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
fonte
0

Basta definir a preferredDisplayModepropriedade de UISplitViewControllercomo.allVisible

class MySplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.preferredDisplayMode = .allVisible
    }

}
Arash Etemad
fonte