web-dev-qa-db-fra.com

Classe de dimensionnement pour les modes portrait et paysage iPad

Je souhaite fondamentalement 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 téléphones en mode portrait et paysage sur l'IB. cependant, aucun ne semble couvrir les modes paysage ou portrait individuels pour iPad sur IB. Quelqu'un peut-il aider?

97
neelIVP

Il semble que l'intention d'Apple soit de considérer les deux orientations de l'iPad comme identiques, mais comme nombre d'entre nous le constatons, il existe des raisons très légitimes de vouloir modifier la disposition de l'interface utilisateur pour iPad Portrait ou Paysage pour iPad.

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

Pas une solution élégante.

N'y a-t-il pas moyen de tirer parti de la magie déjà intégrée par Apple dans IB et UIKit pour utiliser une classe de taille de notre choix pour une orientation donnée?

~

En réfléchissant plus généralement au problème, je me suis rendu compte que les "classes de taille" sont simplement des moyens de traiter de multiples présentations stockées dans IB, de sorte qu'elles puissent être appelées en fonction des besoins au moment de l'exécution.

En fait, une "classe de taille" n'est en réalité qu'une paire de valeurs enum. De UIInterface.h:

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

Donc, peu importe ce que Apple a décidé de nommer ces différentes variations, fondamentalement, il ne s'agit que d'une paire d'entiers utilisé comme identifiant unique de sortes pour distinguer une mise en page, stocké dans IB.

Supposons maintenant que nous créons une mise en page alternative (en utilisant une classe de taille non utilisée) dans IB - par exemple, pour iPad Portrait ... existe-t-il un moyen de faire utiliser le périphérique à notre choix de la classe de taille (disposition de l'interface utilisateur) selon les besoins au moment de l'exécution?

Après avoir essayé plusieurs approches différentes (moins élégantes) du problème, je me suis dit 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 conditionner votre hiérarchie de 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 ..., vous pouvez forcer conditionnellement l'enfant à penser qu'il s'agit d'une classe de taille différente de celle par défaut. de l'OS.

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

Pour vous montrer rapidement si cela a fonctionné, j'ai ajouté des étiquettes personnalisées spécifiquement aux versions "Normal/Normal" et "Compact/Normal" de la disposition du contrôleur enfant dans IB:

enter image description hereenter image description here

Et voici à quoi ça ressemble en cours d'exécution, lorsque l'iPad est dans les deux orientations: enter image description hereenter image description here

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

Espérons que Apple rendra cela inutile dans la prochaine version du système d'exploitation. Entre-temps, cela pourrait être une approche plus élégante et évolutive que de jouer par programmation avec des contraintes de mise en page automatique ou d'effectuer d'autres manipulations dans le code .

~

EDIT (6/4/15): N'oubliez pas 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 application spécifique.

~

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

174
RonDiamond

En résumé à la très longue réponse de RonDiamond. Tout ce que vous devez 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 la largeur compacte pour Portrait et la largeur normale pour Paysage.

41
mohamede1945

L'iPad a le trait de taille "normal" pour les dimensions horizontales et verticales, sans distinction entre portrait et paysage.

Ces traits de taille peuvent être remplacés dans votre code de sous-classe UIViewController 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 confère à 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 "largeur compacte" (plutôt que la largeur régulière) quelle que soit leur orientation.

En imitant l'iPhone 7 Plus de cette manière, ce modèle peut être utilisé en remplacement de l'iPad dans Interface Builder de Xcode, qui n'est pas au courant des personnalisations apportées au code.

Sachez que la vue fractionnée sur l'iPad peut utiliser des traits de taille différents de ceux utilisés en mode plein écran.

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

Mise à jour 2019-01-02: Mise à jour pour corriger la barre d'état masquée par intermittence dans l'environnement iPad, et piétination potentielle de traits (plus récents) dans UITraitCollection. Il a également été noté que Apple recommande en fait de ne pas remplacer traitCollection, de sorte qu'à l'avenir, cette technique pourrait poser des problèmes.

5
jedwidz

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

Ajoutez donc des contraintes dans InterfaceBuilder avec des variantes pour Width - Compact, par exemple pour la propriété de contrainte Installed. So Width - Any sera valable pour paysage, Width - Compact for Portrait.

Pour permuter 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]];
    }
}
4
Jadamec

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 le périphérique est en mode paysage.

Par exemple

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
0
MendyK