Classe di dimensionamento per iPad in modalità verticale e orizzontale


97

Fondamentalmente voglio che le mie sottoview siano posizionate in modo diverso a seconda dell'orientamento dell'iPad (verticale o orizzontale) utilizzando le classi di dimensionamento introdotte in xcode 6. Ho trovato numerosi tutorial che spiegano come sono disponibili diverse classi di dimensionamento per iPhone in verticale e orizzontale su IB ma tuttavia non sembra esserci nessuno che copra le singole modalità orizzontale o verticale per l'iPad su IB. Qualcuno può aiutare?


Non sembra esserci una soluzione non programmatica, che sarebbe necessaria per la schermata predefinita
SwiftArchitect

Risposte:


174

Sembra che sia l'intenzione di Apple di trattare entrambi gli orientamenti dell'iPad allo stesso modo, ma come molti di noi stanno scoprendo, ci sono motivi di progettazione molto legittimi per voler variare il layout dell'interfaccia utente per iPad Portrait rispetto a iPad Landscape.

Sfortunatamente, l'attuale sistema operativo non sembra fornire supporto per questa distinzione ... il che significa che siamo tornati a manipolare i vincoli di layout automatico nel codice o soluzioni alternative simili per ottenere ciò che dovremmo idealmente essere in grado di ottenere gratuitamente utilizzando l'interfaccia utente adattiva .

Non una soluzione elegante.

Non c'è un modo per sfruttare la magia che Apple ha già integrato in IB e UIKit per utilizzare una classe di dimensioni a nostra scelta per un determinato orientamento?

~

Considerando il problema in modo più generico, mi sono reso conto che le "classi di dimensioni" sono semplicemente modi per indirizzare più layout memorizzati in IB, in modo che possano essere richiamati secondo necessità in fase di esecuzione.

In effetti, una "classe di dimensioni" è in realtà solo una coppia di valori enum. Da UIInterface.h:

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

Quindi, indipendentemente da ciò che Apple ha deciso di denominare queste diverse varianti, fondamentalmente, sono solo una coppia di numeri interi usati come identificatori univoci, per distinguere un layout da un altro, archiviati in IB.

Ora, supponendo di creare un layout alternativo (utilizzando una classe di dimensioni inutilizzata) in IB, ad esempio per iPad Portrait ... esiste un modo per fare in modo che il dispositivo utilizzi la nostra classe di dimensioni (layout dell'interfaccia utente) secondo necessità in fase di esecuzione ?

Dopo aver provato diversi approcci (meno eleganti) al problema, ho sospettato che potesse esserci un modo per sovrascrivere la classe di dimensione predefinita a livello di programmazione. E c'è (in 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);

Pertanto, se puoi impacchettare la gerarchia del controller di visualizzazione come controller di visualizzazione `` figlio '' e aggiungerlo a un controller di visualizzazione genitore di primo livello ... allora puoi ignorare condizionalmente il figlio facendogli pensare che sia una classe di dimensioni diversa da quella predefinita dal sistema operativo.

Ecco un'implementazione di esempio che esegue questa operazione, nel controller di visualizzazione "padre":

@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

Come rapida demo per vedere se funzionava, ho aggiunto etichette personalizzate in modo specifico alle versioni "Regular / Regular" e "Compact / Regular" del layout del controller figlio in IB:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Ed ecco come appare in esecuzione, quando l'iPad è in entrambi gli orientamenti: inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Ecco! Configurazioni di classi di dimensioni personalizzate in fase di esecuzione.

Si spera che Apple lo renderà superfluo nella prossima versione del sistema operativo. Nel frattempo, questo potrebbe essere un approccio più elegante e scalabile rispetto a modificare a livello di codice i vincoli di layout automatico o eseguire altre manipolazioni nel codice.

~

EDIT (6/4/15): tieni presente che il codice di esempio sopra è essenzialmente una prova di concetto per dimostrare la tecnica. Sentiti libero di adattarti secondo necessità per la tua specifica applicazione.

~

EDIT (7/24/15): È gratificante che la spiegazione di cui sopra sembra aiutare a demistificare il problema. Anche se non l'ho testato, il codice di mohamede1945 [sotto] sembra un'ottimizzazione utile per scopi pratici. Sentiti libero di provarlo e facci sapere cosa ne pensi. (Per motivi di completezza, lascerò il codice di esempio sopra così com'è.)


1
Ottimo post @RonDiamond!
amergin

3
Apple adotta un approccio simile per ridefinire la classe delle dimensioni della larghezza in Safari, quindi puoi essere certo che si tratta di un approccio più o meno supportato. Non dovresti davvero aver bisogno degli ivar extra; UITraitCollectionè sufficientemente ottimizzato e overrideTraitCollectionForChildViewControllerviene chiamato abbastanza raramente da non dover essere un problema eseguire il controllo della larghezza e crearlo successivamente.
zwaldowski

1
@zwaldowski Grazie. Il codice di esempio qui è solo per dimostrare la tecnica e può ovviamente essere ottimizzato in vari modi come desiderato. (Come regola generale, con un oggetto utilizzato più volte [ad esempio, ogni volta che il dispositivo cambia orientamento], non penso sia una cattiva idea trattenere un oggetto; ma come fai notare, la differenza di prestazioni qui può essere minimo.)
RonDiamond

1
@ Rashmi: Come ho accennato nella spiegazione, alla fine non importa quale scegli: stai semplicemente mappando i layout su una coppia di valori enum. Quindi puoi davvero usare qualsiasi "classe di taglia" abbia senso per te. Mi assicurerei solo che non sia in conflitto con qualche altra classe di dimensioni (legittima) che potrebbe essere ereditata da qualche parte come impostazione predefinita.
RonDiamond

1
Per quanto riguarda il controller della vista bambino, non credo sia importante. Dovresti essere in grado di crearne un'istanza a livello di codice o da un pennino, purché sia ​​aggiunto correttamente come controller figlio al contenitore.
RonDiamond

41

In sintesi alla lunghissima risposta di RonDiamond. Tutto quello che devi fare è nel tuo controller di visualizzazione root.

Obiettivo-c

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

Swift:

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

Quindi in storyborad usa la larghezza compatta per il ritratto e la larghezza normale per il paesaggio.


Mi chiedo come andrà a finire con le nuove modalità schermo diviso ... un modo per scoprirlo!
mm2001

UITraitCollection è solo per iOS 8.0+
mohamede1945

Non funziona per me Ho una vista con due visualizzazioni di contenuto che devono essere mostrate in una posizione diversa a seconda dell'orientamento. Ho creato tutte le classi di dimensioni necessarie, tutto funziona per iPhone. Ho provato a utilizzare il tuo codice per separare anche l'orientamento dell'iPad. Tuttavia agirà in modo strano sulle opinioni. Non lo cambia come dovrebbe. Qualche idea su cosa potrebbe succedere?
NoSixties

1
Questo ha funzionato per me quando ho scavalcato - (UITraitCollection *)traitCollectioninvece di overrideTraitCollectionForChildViewController. Anche i vincoli dovrebbero corrispondere alle traitcollections, quindi wC (hAny).
H. de Jonge

1
@ Apollo lo farei ma non risponderebbe alla domanda vera e propria. Dai un'occhiata qui per l'esempio di Apple su come utilizzare setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…
malhal

5

L'iPad ha il tratto di dimensione "regolare" sia per le dimensioni orizzontali che per quelle verticali, senza distinzione tra ritratto e paesaggio.

Questi tratti di dimensione possono essere sovrascritti nel UIViewControllercodice della sottoclasse personalizzata , tramite il metodo traitCollection, ad esempio:

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

Ciò conferisce all'iPad le stesse caratteristiche di dimensione dell'iPhone 7 Plus. Nota che altri modelli di iPhone hanno generalmente il tratto "larghezza compatta" (piuttosto che larghezza regolare) indipendentemente dall'orientamento.

Imitare l'iPhone 7 Plus in questo modo consente a quel modello di essere utilizzato come sostituto per l'iPad in Interface Builder di Xcode, che non è a conoscenza delle personalizzazioni nel codice.

Tieni presente che Split View sull'iPad potrebbe utilizzare tratti di dimensioni diverse rispetto al normale funzionamento a schermo intero.

Questa risposta si basa sull'approccio adottato in questo post del blog , con alcuni miglioramenti.

Aggiornamento 2019-01-02: aggiornato per correggere la barra di stato nascosta intermittente nel panorama dell'iPad e il potenziale calpestio dei tratti (più recenti) in UITraitCollection. Ho anche notato che la documentazione Apple in realtà sconsiglia l'override traitCollection, quindi in futuro potrebbero verificarsi problemi con questa tecnica.


Apple specifica che la traitCollectionproprietà è di sola lettura: developer.apple.com/documentation/uikit/uitraitenvironment/…
cleverbit

4

La lunga e utile risposta di RonDiamond è un buon inizio per comprendere i principi, tuttavia il codice che ha funzionato per me (iOS 8+) si basa sul metodo di override (UITraitCollection *)traitCollection

Quindi, aggiungi vincoli in InterfaceBuilder con variazioni per Larghezza - Compatto, ad esempio per la proprietà del vincolo Installato. Quindi Larghezza - Qualsiasi sarà valida per il paesaggio, Larghezza - Compatta per il ritratto.

Per cambiare i vincoli nel codice in base alla dimensione del controller di visualizzazione corrente, aggiungi semplicemente quanto segue nella tua 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]];
    }
}

0

Quanto sarà diversa la tua modalità orizzontale rispetto alla modalità verticale? Se è molto diverso, potrebbe essere una buona idea creare un altro controller di visualizzazione e caricarlo quando il dispositivo è in orizzontale

Per esempio

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

Sì, questa è un'opzione. Ma non penso che sia il più ottimale. Il mio punto è che se esiste un'opzione per utilizzare tratti di classe di dimensionamento diversi per le modalità verticale e orizzontale per l'iPhone in ios8, perché non lo stesso per l'iPad?
neelIVP

Prima di Xcode 6, possiamo usare diversi storyboard per un diverso orientamento. Non è molto efficiente se la maggior parte dei controller di visualizzazione è la stessa. Ma è molto conveniente per diversi layout. In Xcode 6 non è possibile farlo. Forse la creazione di un controller di visualizzazione diverso per un orientamento diverso è l'unica soluzione.
Bagusflyer

2
Il caricamento di un viewController diverso ogni volta sembra molto inefficiente, soprattutto se c'è qualcosa che accade sullo schermo. È molto meglio usare la stessa vista e manipolare il vincolo di layout automatico o la posizione degli elementi nel codice. O per usare l'hack menzionato sopra, fino a quando Apple non risolverà questo problema.
Pahnev

0

Versione Swift 5. Funziona bene.

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

-3

Codice Swift 3.0 per la soluzione @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
}}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.