Aula de dimensionamento para os modos retrato e paisagem do iPad

97

Eu basicamente quero ter minhas subvisualizações posicionadas de maneira diferente dependendo da orientação do iPad (Retrato ou Paisagem) usando classes de dimensionamento introduzidas no xcode 6. Eu encontrei vários tutoriais explicando como diferentes classes de dimensionamento estão disponíveis para Iphones em retrato e paisagem no IB mas, no entanto, parece não haver nenhum que cubra modos individuais de paisagem ou retrato para o iPad no IB. Alguém pode ajudar?

neelIVP
fonte
Parece não haver uma solução não programática, que seria necessária para a tela padrão
SwiftArchitect

Respostas:

174

Parece que a intenção da Apple é tratar as duas orientações do iPad como iguais - mas, como vários de nós estamos descobrindo, há razões de design muito legítimas para querer variar o layout da IU para iPad Retrato vs. iPad Paisagem.

Infelizmente, o sistema operacional atual não parece fornecer suporte para essa distinção ... o que significa que estamos de volta à manipulação de restrições de layout automático no código ou soluções alternativas para alcançar o que deveríamos idealmente conseguir gratuitamente usando Adaptive UI .

Não é uma solução elegante.

Não há uma maneira de aproveitar a magia que a Apple já incorporou ao IB e ao UIKit para usar uma classe de tamanho de nossa escolha para uma determinada orientação?

~

Ao pensar sobre o problema de forma mais genérica, percebi que 'classes de tamanho' são simplesmente maneiras de lidar com vários layouts armazenados em IB, para que possam ser chamados conforme necessário no tempo de execução.

Na verdade, uma 'classe de tamanho' é apenas um par de valores enum. De UIInterface.h:

typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
    UIUserInterfaceSizeClassUnspecified = 0,
    UIUserInterfaceSizeClassCompact     = 1,
    UIUserInterfaceSizeClassRegular     = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);

Portanto, independentemente de como a Apple decidiu nomear essas variações diferentes, fundamentalmente, eles são apenas um par de números inteiros usados ​​como uma espécie de identificador único, para distinguir um layout de outro, armazenado em IB.

Agora, supondo que criemos um layout alternativo (usando uma classe de tamanho não utilizada) em IB - digamos, para iPad Portrait ... há uma maneira de fazer com que o dispositivo use nossa escolha de classe de tamanho (layout de IU) conforme necessário no tempo de execução ?

Depois de tentar várias abordagens diferentes (menos elegantes) para o problema, suspeitei que poderia haver uma maneira de substituir a classe de tamanho padrão programaticamente. E há (em UIViewController.h):

// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);

Assim, se você pode empacotar sua hierarquia de controlador de visão como um controlador de visão 'filho', e adicioná-lo a um controlador de visão pai de nível superior ... então você pode sobrescrever condicionalmente o filho fazendo-o pensar que é uma classe de tamanho diferente do padrão do sistema operacional.

Aqui está um exemplo de implementação que faz isso, no controlador de visualização 'pai':

@interface RDTraitCollectionOverrideViewController : UIViewController {
    BOOL _willTransitionToPortrait;
    UITraitCollection *_traitCollection_CompactRegular;
    UITraitCollection *_traitCollection_AnyAny;
}
@end

@implementation RDTraitCollectionOverrideViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setUpReferenceSizeClasses];
}

- (void)setUpReferenceSizeClasses {
    UITraitCollection *traitCollection_hCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    UITraitCollection *traitCollection_vRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
    _traitCollection_CompactRegular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hCompact, traitCollection_vRegular]];

    UITraitCollection *traitCollection_hAny = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified];
    UITraitCollection *traitCollection_vAny = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassUnspecified];
    _traitCollection_AnyAny = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hAny, traitCollection_vAny]];
}

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width;
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]
    _willTransitionToPortrait = size.height > size.width;
}

-(UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController {
    UITraitCollection *traitCollectionForOverride = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny;
    return traitCollectionForOverride;
}
@end

Como uma demonstração rápida para ver se funcionava, adicionei rótulos personalizados especificamente às versões 'Regular / Regular' e 'Compacto / Regular' do layout do controlador filho em IB:

insira a descrição da imagem aqui insira a descrição da imagem aqui

E é assim que funciona, quando o iPad está nas duas orientações: insira a descrição da imagem aqui insira a descrição da imagem aqui

Voila! Configurações de classe de tamanho personalizado em tempo de execução.

Esperançosamente, a Apple tornará isso desnecessário na próxima versão do sistema operacional. Enquanto isso, essa pode ser uma abordagem mais elegante e escalonável do que mexer programaticamente com restrições de layout automático ou fazer outras manipulações no código.

~

EDIT (6/4/15): Por favor, tenha em mente que o código de exemplo acima é essencialmente uma prova de conceito para demonstrar a técnica. Sinta-se à vontade para adaptar conforme necessário para sua aplicação específica.

~

EDIT (24/07/15): É gratificante que a explicação acima pareça ajudar a desmistificar o problema. Embora eu não tenha testado, o código de mohamede1945 [abaixo] parece uma otimização útil para fins práticos. Sinta-se à vontade para testar e nos dar sua opinião. (Para fins de integridade, deixarei o código de exemplo acima como está.)

RonDiamond
fonte
1
Excelente postagem @RonDiamond!
amergin
3
A Apple adota uma abordagem semelhante para redefinir a classe de tamanho de largura no Safari, então você pode ter certeza de que esta é uma abordagem mais ou menos compatível. Você realmente não deveria precisar dos ivars extras; UITraitCollectioné otimizado o suficiente e overrideTraitCollectionForChildViewControllerraramente é chamado o suficiente para não ser um problema fazer a verificação de largura e criá-lo.
zwaldowski
1
@zwaldowski Obrigado. O código de amostra aqui é apenas para demonstrar a técnica e, obviamente, pode ser otimizado de várias maneiras, conforme desejado. (Como regra geral, com um objeto sendo usado repetidamente [por exemplo, sempre que o dispositivo muda de orientação], não acho que seja uma má ideia segurar um objeto; mas como você apontou, a diferença de desempenho aqui pode ser mínimo.)
RonDiamond
1
@Rashmi: Como aludi na explicação, não importa qual você escolha - você está apenas mapeando o (s) layout (s) para um par de valores enum. Portanto, você pode realmente usar qualquer "classe de tamanho" que faça sentido para você. Eu apenas me certificaria de que não haja conflito com alguma outra classe de tamanho (legítima) que pode ser herdada em algum lugar como padrão.
RonDiamond
1
Quanto ao controlador de visualização filho, não acho que isso importe. Você deve ser capaz de instanciá-lo programaticamente ou a partir de um bico, desde que seja adicionado corretamente como um controlador filho ao contêiner.
RonDiamond
41

Como um resumo da longa resposta de RonDiamond. Tudo que você precisa fazer é no seu controlador de visualização raiz.

Objective-c

- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
    if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    } else {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
    }
}

Rápido:

override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection! {
        if view.bounds.width < view.bounds.height {
            return UITraitCollection(horizontalSizeClass: .Compact)
        } else {
            return UITraitCollection(horizontalSizeClass: .Regular)
        }
    }

Em seguida, no storyborad, use largura compacta para Retrato e largura normal para Paisagem.

Mohamede1945
fonte
Estou me perguntando como isso vai funcionar com os novos modos de tela dividida ... uma maneira de descobrir!
mm2001
UITraitCollection é apenas para iOS 8.0+
mohamede1945
Não funciona para mim. Tenho uma visualização com duas visualizações de conteúdo, que precisam ser mostradas em um local diferente, dependendo da orientação. Criei todas as classes de tamanho necessárias, tudo funciona para o iPhone. Tentei usar seu código para separar a orientação do iPad também. No entanto, ele apenas parecerá estranho nas visualizações. Não muda como deveria. Alguma pista do que pode estar acontecendo?
NoSessties
1
Isso funcionou para mim quando eu - (UITraitCollection *)traitCollectionsubstituí em vez de overrideTraitCollectionForChildViewController. Além disso, as restrições devem corresponder às coleções de características, portanto, wC (hAny).
H. de Jonge
1
@Apollo eu faria, mas não responderia à pergunta real. Dê uma olhada aqui para ver um exemplo da Apple de como usar setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
malhal
5

O iPad tem o traço de tamanho 'regular' para dimensões horizontais e verticais, sem distinção entre retrato e paisagem.

Essas características de tamanho podem ser substituídas em seu UIViewControllercódigo de subclasse personalizado , por meio do método traitCollection, por exemplo:

- (UITraitCollection *)traitCollection {
    // Distinguish portrait and landscape size traits for iPad, similar to iPhone 7 Plus.
    // Be aware that `traitCollection` documentation advises against overriding it.
    UITraitCollection *superTraits = [super traitCollection];
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalRegular]];

        if ([superTraits containsTraitsInCollection:regular]) {
            if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
                // iPad in portrait orientation
                UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalCompact, verticalRegular]];
            } else {
                // iPad in landscape orientation
                UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalRegular, verticalCompact]];
            }
        }
    }
    return superTraits;
}

- (BOOL)prefersStatusBarHidden {
    // Override to negate this documented special case, and avoid erratic hiding of status bar in conjunction with `traitCollection` override:
    // For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.
    return NO;
}

Isso dá ao iPad as mesmas características de tamanho do iPhone 7 Plus. Observe que outros modelos de iPhone geralmente têm a característica de 'largura compacta' (em vez de largura regular), independentemente da orientação.

Imitar o iPhone 7 Plus dessa forma permite que esse modelo seja usado como um substituto para o iPad no Interface Builder do Xcode, que não reconhece personalizações no código.

Esteja ciente de que a visualização dividida no iPad pode usar características de tamanho diferentes da operação normal em tela cheia.

Essa resposta é baseada na abordagem adotada nesta postagem do blog , com algumas melhorias.

Atualização 02/01/2019: Atualizado para corrigir a barra de status oculta intermitente no cenário do iPad e possível atropelamento de características (mais recentes) UITraitCollection. Observe também que a documentação da Apple realmente não recomenda a substituição traitCollection, portanto, no futuro, pode haver problemas com essa técnica.

Jedwidz
fonte
A Apple especifica que a traitCollectionpropriedade seja somente leitura: developer.apple.com/documentation/uikit/uitraitenvironment/…
cleverbit
4

A resposta longa e útil de RonDiamond é um bom começo para compreender os princípios, no entanto, o código que funcionou para mim (iOS 8+) é baseado no método de substituição (UITraitCollection *)traitCollection

Portanto, adicione restrições no InterfaceBuilder com variações para Largura - Compacto, por exemplo, para a propriedade da restrição Instalada. Portanto, largura - qualquer será válido para paisagem, largura - compacto para retrato.

Para alterar as restrições no código com base no tamanho do controlador de visualização atual, basta adicionar o seguinte em sua classe UIViewController:

- (UITraitCollection *)traitCollection
{
    UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];

    if (self.view.bounds.size.width < self.view.bounds.size.height) {
        // wCompact, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact],
                  verticalRegular]];
    } else {
        // wRegular, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular],
                  verticalRegular]];
    }
}
Jadamec
fonte
0

Quão diferente será o seu modo paisagem do que o modo retrato? Se for muito diferente, pode ser uma boa ideia criar outro controlador de visualização e carregá-lo quando o dispositivo estiver no modo paisagem

Por exemplo

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
MendyK
fonte
Sim, esta é uma opção. Mas eu não acho que seja o melhor. Meu ponto é que, se há uma opção de usar diferentes características de classe de dimensionamento para os modos retrato e paisagem para o iphone no ios8, por que não o mesmo para o iPad?
neelIVP
Antes do Xcode 6, podemos usar diferentes storyboards para diferentes orientações. Isso não é muito eficiente se a maioria dos controladores de visualização forem iguais. Mas é muito conveniente para diferentes layouts. No Xcode 6, não há como fazer isso. Talvez a criação de controlador de visualização diferente para orientação diferente seja a única solução.
Bagusflyer 01 de
2
Carregar viewController diferente a cada vez parece muito ineficiente, especialmente se algo estiver acontecendo na tela. É muito melhor usar a mesma visualização e manipular a restrição de layout automático ou a posição dos elementos no código. Ou usar o hack mencionado acima, até que a apple resolva esse problema.
Pahnev
0

Versão 5 do Swift. Funciona bem.

override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
    if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
        let collections = [UITraitCollection(horizontalSizeClass: .regular),
                           UITraitCollection(verticalSizeClass: .compact)]
        return UITraitCollection(traitsFrom: collections)
    }
    return super.overrideTraitCollection(forChild: childViewController)
}
Li Jin
fonte
-3

Código Swift 3.0 para solução @RonDiamond

class Test : UIViewController {


var _willTransitionToPortrait: Bool?
var _traitCollection_CompactRegular: UITraitCollection?
var _traitCollection_AnyAny: UITraitCollection?

func viewDidLoad() {
    super.viewDidLoad()
    self.upReferenceSizeClasses = null
}

func setUpReferenceSizeClasses() {
    var traitCollection_hCompact: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassCompact)
    var traitCollection_vRegular: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassRegular)
    _traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact,traitCollection_vRegular])
    var traitCollection_hAny: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassUnspecified)
    var traitCollection_vAny: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassUnspecified)
    _traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny,traitCollection_vAny])
}

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}

func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    _willTransitionToPortrait = size.height > size.width
}

func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection {
    var traitCollectionForOverride: UITraitCollection = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny
    return traitCollectionForOverride
}}
Zeeshan
fonte