Classe de dimensionnement pour les modes portrait et paysage de l'iPad

97

Je souhaite essentiellement que mes sous-vues soient positionnées différemment en fonction de l'orientation de l'iPad (Portrait ou Paysage) à l'aide des classes de dimensionnement introduites dans xcode 6. J'ai trouvé de nombreux tutoriels expliquant comment différentes classes de dimensionnement sont disponibles pour les Iphones en portrait et paysage sur l'IB mais cependant, il semble n'y en avoir aucun qui couvre les modes paysage ou portrait individuels pour l'iPad sur IB. Quelqu'un peut-il aider?

neelIVP
la source
Il ne semble pas y avoir de solution non programmatique, ce qui serait nécessaire pour l'écran par défaut
SwiftArchitect

Réponses:

174

Il semble que l'intention d'Apple soit de traiter les deux orientations de l'iPad de la même manière - mais comme un certain nombre d'entre nous le constatent, il existe des raisons de conception très légitimes de vouloir modifier la disposition de l'interface utilisateur pour iPad Portrait par rapport à iPad Landscape.

Malheureusement, le système d'exploitation actuel ne semble pas prendre en charge cette distinction ... ce qui signifie que nous sommes de retour à la manipulation des contraintes de mise en page automatique dans le code ou des solutions de contournement similaires pour atteindre ce que nous devrions idéalement pouvoir obtenir gratuitement en utilisant l'interface utilisateur adaptative. .

Pas une solution élégante.

N'existe-t-il pas un moyen d'exploiter la magie qu'Apple a déjà intégrée à IB et UIKit pour utiliser une classe de taille de notre choix pour une orientation donnée?

~

En réfléchissant au problème de manière plus générique, j'ai réalisé que les «classes de taille» sont simplement des moyens de traiter plusieurs mises en page stockées dans IB, afin qu'elles puissent être appelées au besoin au moment de l'exécution.

En fait, une «classe de taille» n'est en réalité qu'une paire de valeurs d'énumération. Depuis UIInterface.h:

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

Ainsi, indépendamment de ce qu'Apple a décidé de nommer ces différentes variantes, il ne s'agit fondamentalement que d'une paire d'entiers utilisés comme identifiant unique, pour distinguer une mise en page d'une autre, stockée dans IB.

Maintenant, en supposant que nous créons une mise en page alternative (en utilisant une classe de taille inutilisée) dans IB - par exemple, pour iPad Portrait ... existe-t-il un moyen pour que l'appareil utilise notre choix de classe de taille (disposition de l'interface utilisateur) au besoin au moment de l'exécution ?

Après avoir essayé plusieurs approches différentes (moins élégantes) du problème, j'ai soupçonné qu'il pourrait y avoir un moyen de remplacer la classe de taille par défaut par programme. Et il y a (dans 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);

Ainsi, si vous pouvez empaqueter la hiérarchie de votre contrôleur de vue en tant que contrôleur de vue `` enfant '', et l'ajouter à un contrôleur de vue parent de niveau supérieur ... alors vous pouvez conditionnellement remplacer l'enfant en pensant qu'il s'agit d'une classe de taille différente de la classe par défaut. du système d'exploitation.

Voici un exemple d'implémentation qui fait cela, dans le contrôleur de vue 'parent':

@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

En guise de démonstration rapide pour voir si cela fonctionnait, j'ai ajouté des étiquettes personnalisées spécifiquement aux versions `` Regular / Regular '' et `` Compact / Regular '' de la disposition du contrôleur enfant dans IB:

entrez la description de l'image ici entrez la description de l'image ici

Et voici à quoi cela ressemble en cours d'exécution, lorsque l'iPad est dans les deux orientations: entrez la description de l'image ici entrez la description de l'image ici

Voila! Configurations de classe de taille personnalisée au moment de l'exécution.

Espérons qu'Apple rendra cela inutile dans la prochaine version du système d'exploitation. En attendant, cela peut être une approche plus élégante et évolutive que de jouer par programme avec les contraintes de mise en page automatique ou de faire d'autres manipulations dans le code.

~

EDIT (6/4/15): Veuillez garder à l'esprit que l'exemple de code ci-dessus est essentiellement une preuve de concept pour démontrer la technique. N'hésitez pas à vous adapter au besoin à votre propre application spécifique.

~

EDIT (24/7/15): Il est gratifiant que l'explication ci-dessus semble aider à démystifier le problème. Bien que je ne l'ai pas testé, le code de mohamede1945 [ci-dessous] ressemble à une optimisation utile à des fins pratiques. N'hésitez pas à le tester et dites-nous ce que vous en pensez. (Par souci d'exhaustivité, je laisserai l'exemple de code ci-dessus tel quel.)

RonDiamond
la source
1
Super message @RonDiamond!
amergin le
3
Apple adopte une approche similaire pour redéfinir la classe de taille de largeur dans Safari, vous pouvez donc être assuré qu'il s'agit d'une approche plus ou moins prise en charge. Vous ne devriez pas vraiment avoir besoin des ivars supplémentaires; UITraitCollectionest suffisamment optimisé et overrideTraitCollectionForChildViewControllerest appelé assez rarement pour ne pas poser de problème de vérifier la largeur et de le créer ensuite.
zwaldowski
1
@zwaldowski Merci. L'exemple de code ici est juste pour illustrer la technique et peut évidemment être optimisé de différentes manières selon vos besoins. (En règle générale, avec un objet utilisé à plusieurs reprises [par exemple, chaque fois que l'appareil change d'orientation], je ne pense pas que ce soit une mauvaise idée de s'accrocher à un objet; mais comme vous le faites remarquer, la différence de performance ici peut être minime.)
RonDiamond
1
@Rashmi: Comme je l'ai mentionné dans l'explication, peu importe le choix que vous choisissez - vous mappez simplement la ou les mises en page à une paire de valeurs d'énumération. Ainsi, vous pouvez vraiment utiliser la "classe de taille" qui vous convient. Je m'assurerais simplement qu'il ne soit pas en conflit avec une autre classe de taille (légitime) qui peut être héritée quelque part par défaut.
RonDiamond
1
Quant au contrôleur de vue enfant, je ne pense pas que ce soit important. Vous devriez pouvoir l'instancier par programme ou à partir d'une pointe, à condition qu'il soit correctement ajouté en tant que contrôleur enfant au conteneur.
RonDiamond
41

Pour résumer la très longue réponse de RonDiamond. Tout ce que vous avez à faire est dans votre contrôleur de vue racine.

Objectif c

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

Rapide:

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

Ensuite, dans Storyborad, utilisez une largeur compacte pour Portrait et une largeur régulière pour Paysage.

mohamede1945
la source
Je me demande comment cela va fonctionner avec les nouveaux modes d'écran partagé ... une façon de le savoir!
mm2001
UITraitCollection est uniquement pour iOS 8.0+
mohamede1945
Cela ne fonctionne pas pour moi J'ai une vue avec deux vues de contenu qui doivent être affichées dans un emplacement différent en fonction de l'orientation. J'ai créé toutes les classes de taille nécessaires, tout fonctionne pour iPhone. J'ai également essayé d'utiliser votre code pour séparer l'orientation de l'iPad. Cependant, il agira simplement bizarrement sur les vues. Cela ne change rien comme c'est censé le faire. Avez-vous une idée de ce qui pourrait se passer?
NoSixties
1
Cela a fonctionné pour moi lorsque j'ai dépassé - (UITraitCollection *)traitCollectionau lieu de overrideTraitCollectionForChildViewController. Les contraintes doivent également correspondre aux collections de traits, donc wC (hAny).
H. de Jonge
1
@Apollo je le ferais mais cela ne répondrait pas à la question réelle. Jetez un œil ici pour un exemple d'utilisation de setOverrideTraitCollection d' Apple github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
malhal
5

L'iPad a le trait de taille «régulière» pour les dimensions horizontales et verticales, ne faisant aucune distinction entre portrait et paysage.

Ces traits de taille peuvent être remplacés dans votre UIViewControllercode de sous-classe personnalisé , via la méthode traitCollection, par exemple:

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

Cela donne à l'iPad les mêmes caractéristiques de taille que l'iPhone 7 Plus. Notez que les autres modèles d'iPhone ont généralement le trait de `` largeur compacte '' (plutôt que de largeur régulière) quelle que soit l'orientation.

Imiter l'iPhone 7 Plus de cette manière permet à ce modèle d'être utilisé comme substitut de l'iPad dans Interface Builder de Xcode, qui n'est pas au courant des personnalisations dans le code.

Sachez que Split View sur l'iPad peut utiliser des caractéristiques de taille différentes du fonctionnement normal en plein écran.

Cette réponse est basée sur l'approche adoptée dans ce billet de blog , avec quelques améliorations.

Mise à jour 2019-01-02: mise à jour pour corriger la barre d'état cachée intermittente dans le paysage iPad et le piétinement potentiel des (plus récents) traits dans UITraitCollection. Notez également que la documentation Apple recommande en fait de ne pas remplacer traitCollection, donc à l'avenir, il peut se révéler des problèmes avec cette technique.

jedwidz
la source
Apple précise que la traitCollectionpropriété doit être en lecture seule: developer.apple.com/documentation/uikit/uitraitenvironment/…
cleverbit
4

La réponse longue et utile de RonDiamond est un bon début pour comprendre les principes, mais le code qui a fonctionné pour moi (iOS 8+) est basé sur une méthode de substitution (UITraitCollection *)traitCollection

Donc, ajoutez des contraintes dans InterfaceBuilder avec des variations pour Width - Compact, par exemple pour la propriété de contrainte Installed. Donc Largeur - Tout sera valable pour le paysage, Largeur - Compact pour Portrait.

Pour changer les contraintes dans le code en fonction de la taille actuelle du contrôleur de vue, ajoutez simplement ce qui suit dans votre 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
la source
0

Dans quelle mesure votre mode paysage sera-t-il différent de votre mode portrait? Si c'est très différent, il peut être judicieux de créer un autre contrôleur de vue et de le charger lorsque l'appareil est en mode paysage

Par exemple

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
MendyK
la source
Oui, c'est une option. Mais je ne pense pas que ce soit le plus optimal. Mon point est que s'il existe une option pour utiliser différents traits de classe de taille pour les modes portrait et paysage pour l'iphone dans ios8, alors pourquoi pas la même chose pour l'iPad?
neelIVP
Avant Xcode 6, nous pouvons utiliser différents storyboard pour différentes orientations. Ce n'est pas très efficace si la plupart des contrôleurs de vue sont identiques. Mais c'est très pratique pour différentes mises en page. Dans Xcode 6, il n'y a aucun moyen de le faire. Peut-être que créer un contrôleur de vue différent pour une orientation différente est la seule solution.
Bagusflyer
2
Charger un viewController différent à chaque fois semble très inefficace, surtout s'il se passe quelque chose à l'écran. Il est préférable d'utiliser la même vue et de manipuler la contrainte de mise en page automatique ou la position des éléments dans le code. Ou pour utiliser le hack mentionné ci-dessus, jusqu'à ce qu'Apple résout ce problème.
Pahnev
0

Version Swift 5. Ça fonctionne bien.

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
la source
-3

Code Swift 3.0 pour la solution @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
la source