[iOS8] Gestion rotation iOS8

NiClouNiClou Membre
septembre 2014 modifié dans API UIKit #1

Hello, 

Je rebondis sur le sujet suivant: http://forum.cocoacafe.fr/topic/12885-uiwebview-rotation-et-ios-8/


qui n'était pas sans fondements.


 


Au chapitre: Unified Storyboards for Universal Apps de la page https://developer.apple.com/library/prerelease/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS8.html


 



 


 


The size classes for iPhones differ based on the kind of device and its orientation. In portrait, the screen has a compact size class horizontally and a regular size class vertically. This corresponds to the common usage paradigm of scrolling vertically for more information. When iPhones are in landscape, their size classes vary. Most iPhones have a compact size class both horizontally and vertically, as shown in Figure 3 and Figure 4. The iPhone 6 Plus has a screen large enough to support regular width in landscape mode, as shown in Figure 5.

 


J'aimerais savoir ce que cela implique et si c'est cela qui a foiré mon code existant et fonctionnel de la 4.3 à  7.1.2


 


Comment adapter mon code pour iOS8 et les versions antérieurs?


Pour faire simple, comment gérer les rotations sur iOS8?


Merci d'avance.


«1

Réponses

  • Pourrais-tu faire un exemple minimal qui foire ?


  • Oui je l'ai mis içi: http://forum.cocoacafe.fr/topic/12885-uiwebview-rotation-et-ios-8/


    A savoir: Il n'y a pas que des webviews.


    Comment ça fonctionne:


    J'ai des templates dans des fichiers xib.

    Lorsque je veux afficher une de ces vues je load la vue d'un fichier xib que j'instancie dans le code.

    Donc c'est moi qui gère la transformation et rotation. (En fonction des cas de figure je peux chager un autre template)


  • Juste une idée comme ça, essaye de voir si les contraintes générées sont les mêmes sont ios 7 & 8...


  • NiClouNiClou Membre
    septembre 2014 modifié #5

    Mon projet est rétro compatible jusqu'a la 4.3 d'iOS. J'utilise donc des xib sans auto layout.


    Quand je charge des templates je fais un loadNibNamed, ça n'est vraiment qu'un template, il n'y a pas d'outlet ou autre chose du genre.


     


    Tu parles bien de ça n'est ce pas? ^^'


  • colas_colas_ Membre
    septembre 2014 modifié #6

    Je crois que même si tu n'utilises pas autolayout, le système derrière génère des contraintes (à  vérifier).


     


    Donc, tu prends la view qui ne marche pas et tu fais :





    - (void)printConstraints:(UIView *)view
    {
    NSLog(@Pour %@ : \n %@",view, [view constraints]) ;
    }


    [self printConstraints:view] ;
    [self printConstraints:view.superview] ;
    [self printConstraints:view.superview.superview] ;


    Il s'agit de voir si c'est la même chose pour ios7 et ios8 (laissons de côté ios 4.3 dans un premier temps)


  • sur iOS8:



    Pour <UIView: 0x16d461f0; frame = (0 0; 568 50); autoresize = RM+BM; layer = <CALayer: 0x16d56ea0>> :
    (
    )

    Pour (null) :
    (null)
    Pour (null) :
    (null)

    iOS7:

     



    Pour <UIView: 0x170167440; frame = (0 0; 568 50); autoresize = RM+BM; layer = <CALayer: 0x17003dca0>> :
    (
    )

    Pour (null) :
    (null)

    Pour (null) :
    (null)

    Pourtant sur iOS7 elle s'affiche bien en bas et en haut sur iOS8 :/


  • ça me fait penser aux problèmes de coordonnées sur osx (où l'origine des ordonnées (y) est en bas et l'axe est vers le haut...)


     


    Est-ce que les frames sont les mêmes quand tu les loggues ?


  • A priori oui, je continue mes tests :/

    Je ne m'en sors pas --'


  • LeChatNoirLeChatNoir Membre, Modérateur

    ouep, je commence à  m'y frotter et je crois comprendre :


     


    => que UIScreen renvoie plus la meme chose donc faut utiliser windows


    => que les coordonnées sont plus inversées...


     


    Mais je tâtonne encore là ....

  • Imagine un instant : avec ios 8, Apple décide de revenir au système de coordonné inversé, comme sur osx.... l'horreur absolue !


     


    Une piste ? Remplacer CGRect finalRect = [[[UIApplication sharedApplication] keyWindow] frame]; par du UIScreen ?


     


    Bon courage !

  • CéroceCéroce Membre, Modérateur


     


    Imagine un instant : avec ios 8, Apple décide de revenir au système de coordonné inversé, comme sur osx.... l'horreur absolue !


     




    Mais il n'est pas inversé sur OS X, c'est sur iOS qu'il l'est! (Mais en pratique, un repère qui pointe vers le bas est plus pratique).

  • AliGatorAliGator Membre, Modérateur
    septembre 2014 modifié #13
    Sous iOS7, la UIWindow à  toujours eu son (0,0) dans le coin qui se trouve en haut à  gauche de votre téléphone quand il est en mode portrait (le coin pas loin des boutons physiques de volume)

    Cette origine de repère n'est pas affecté lorsque l'on change d'orientation : la UIWindow ne tourne pas. Ainsi si vous tournez votre iPhone vers la gauche pour le passer en mode landscapeLeft, l'origine de la UIWindow sera toujours au même endroit physique (qui sera donc maintenant en bas à  gauche vu que vous avez tourné votre téléphone), toujours dans le coin proche des boutons de volume.


    Par contre si votre application supporte la rotation d'interface, la view du rootViewController de la window va se voir appliquer un CGAffineTransform pour changer son système de coordonnés et l'adopter en conséquence de la rotation de sorte que quand vous passez votre iPhone en orientation landscape left, l'origine de window.rootViewController.view lui se trouve toujours en haut à  gauche contrairement à  l'origine de la window.


    Ce principe de garder la UIWindow inchangée et d'appliquer la CGAffineTransform de rotation à  la rootViewController.view existe depuis quasiment le début. C'est pour ça que la window.frame.size.width vaut toujours 320 (sur un iPhone 4-4S-5-5S) mais que, ce qui vaut 320 sur la rootViewController.view, c'est sa width en portrait mais sa height en paysage.


    ---


    Mais tout cela pose un problème avec les ViewControllerTransitions apparus avec iOS7, ces effets qu'on peut personnaliser pour faire la transition entre 2 VC.

    Ils fonctionnent sur le fait que iOS crée automatiquement une vue intermédiaire pendant la transition entre le VC sur lequel on va faire une transition et son VC parent. Sauf que si on fait une transition sur le rootViewController il insére donc cette vue temporaire... entre le rootViewController.view et la window. Et du coup en mode paysage, autant le rootViewController.view a une transform d'appliquée pour prendre en compte la rotation du device, autant la vue intermédiaire temporaire créé entre elle et la window n'a pas de transform d'appliquée dessus !

    Bilan : les 2 vues ne sont pas dans le même repère et n'ont pas la même origine, et pour faire tous les calculs pour gérer la custom transition, c'est une grosse galère à  coup de maths et d'aspirine.
    (Alors que si la transition est appliquée à  un autre VC que le rootVC (mais à  un de ses fils) là  pas de soucis, les vues de ces derniers n'ont pas de CGAffineTransform qui leur est appliquée puisque c'est la vue racine qui porte déjà  cette transform, et du coup le problème n'apparaà®t pas)




    Tout ça pour dire que j'ai pas testé sous iOS8 en détail mais ça ne m'étonnerait pas tant que ça qu'ils aient changé de système et qu'ils appliquent maintenant la transform à  la window directement plutôt qu'au rootViewController.view histoire de faire disparaà®tre la prise de tête de ce cas particulier d'une Custom Transition sur le rootVC et qu'ainsi la rootViewController.view et sa vue intermédiaire insérée pendant la transition soient dans le même système de coordonnées, évitant de faire du rootViewController un cas particulier pour les Custom Transitions.


    Ce qui expliquerait les différences que vous semblez avoir remarqué entre iOS7 et iOS8, et ferait que la bounds.size.width de la window serait maintenant aussi "inversé" en mode paysage, passant maintenant à  une hauteur de 320 et non une largeur quand l'écran est horizontal.


    Par contre le UIScreen lui ne bouge pas et garde les mêmes dimensions (correspondant aux dimensions physique de l'écran) et n'est pas affecté par les rotations pour un sou (puisque ses dimensions sont des caractéristiques physiques et pas liées au système de coordonnées comme le sont la window.bounds ou le rootViewController.bounds)
  • LeChatNoirLeChatNoir Membre, Modérateur
    septembre 2014 modifié #14

    quand je dis : les coordonnées sont plus inversées, c'est juste que quand on rotationne, avant iOS8, width devient height et vice-versa alors qu'en iOS8, non.


     


    Enfin je crois... Pas encore assez avancé sur le sujet...


     


    (bon, le croco explique ça mieux que moi :))


  • LeChatNoirLeChatNoir Membre, Modérateur

    Voilà  comment j'ai résolu le truc :


     


    Partout où j'utilise des :



    CGRect screenFrame=[[UIScreen mainScreen] applicationFrame];

    Je remplace par :



    CGRect screenFrame=self.window.bounds;


    Et ensuite :



    BOOL ignoreOrientation = [[NSProcessInfo processInfo] respondsToSelector:@selector(operatingSystemVersion)];

    if((orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight)&&!ignoreOrientation) {
    monAncienCodeQuiGéraitLaBasculeWidthHeight...
    }


    Ca semble bien fonctionner...


  • AliGatorAliGator Membre, Modérateur

    La solution n'est pas de remplacer une mauvaise pratique par une autre mauvaise pratique, mais de faire un code plus propre :


     


     - qui ne dépend pas du operatingSystemVersion (c'est quoi ce test à  la noix ?! Quel rapport entre le fait que la classe NSProcessInfo réponde au sélecteur "operatingSystemVersion" et le fait que tu ignores l'orientation ou pas ?! Illisible car on ne voit pas le rapport, et inmaintenable (c'est aussi mal qu'un Magic Number !))


     - Utiliser ce qu'il y a déjà  disponible (AutoLayout ou LayoutConstraints pour adapter l'interface, les méthodes qui vont bien pour gérer l'orientation, l'application de la CGAffineTransform de ta vue à  ton CGRect pour faire automatiquement la bonne transformation inverse, ou encore utiliser [UIView convertRect:toView:] pour convertir ton CGRect dans les coordonnées d'une vue ou d'une autre, etc...


     


    Bref, éviter de faire des bidouilles du style "si la méthode operatingSystemVersion existe alors inverser width et height" mais plutôt "convertir mon CGRect dans tel système de coordonnées".


     


    C'est un peu comme cet enfer du dev web où à  une époque plein de scripts testaient le "User-Agent" du browser pour exécuter un code plutôt qu'un autre, car ils se disaient "je sais que telle méthode existe sur IE et telle autre sur WebKit". Au lieu de tester l'existence  même de la méthode avant de l'appeler, les gens se basaient sur des critères extérieurs dépendants ! Pourquoi faire compliqué et pas très sûr/stable quand on peut faire simple, maintenable et sûr ?


  • LeChatNoirLeChatNoir Membre, Modérateur

    pas taper...


    :o


  • LeChatNoirLeChatNoir Membre, Modérateur
    septembre 2014 modifié #18

    En fait, le problème des applications comme la mienne qui existe depuis 4 ans maintenant, c'est qu'on se traine tout un historique...


    Mon affichage des informations détaillées d'un site date d'une époque ou les layout n'existaient pas...


     


    Du coup, faudrait que je reparte de 0. Et quand il s'agit de repartir de 0, pleins de questions se posent et on part dans un truc qui potentiellement peut devenir très long...


    Alors que là , un tweak pourri et ça repart pour 1 an ;)


     


    Mais je vais essayer d'utiliser autolayout... Et vous dirai.


  • LeChatNoirLeChatNoir Membre, Modérateur
    septembre 2014 modifié #19

    Bon bon. J'ai viré mon code crade et passe en Autolayout...


     


    Ca commence plutôt pas mal car après qques réglages de contraintes (bien plus simples maintenant), j'obtiens un truc qui semble tenir la route.


     


    Par contre, les vues de premier niveau, je les ajoute par code car parfois il faut les mettre, parfois pas. Exemple, parfois, je dois afficher des photos, parfois j'en ai pas, etc...


     


    Du coup, là  où je définissais leur frame, je vais devoir ajouter les contraintes par code ?


    Ou y a t il une astuce pour gérer des vues "optionnelles" ?


     


    Sachant que pour que tout se "goupille" bien, j'avais surchargé layoutSubviews pour qu'elles se mettent les unes en dessous des autres...


     


    Merci :)


  • AliGatorAliGator Membre, Modérateur
    Tu as deux solutions :

    1) soit tu les mets dans ton XIB avec les contraintes qu'il faut, mais tu les mets à  hidden=YES et tu les rends visible quand tu les veut

    2) soit tu continues de les rajouter par code comme tu faisais avant et dans ce cas en effet il va falloir rajouter les contraintes par code (au lieu de surcharger layoutSubviews et changer leur frame). Je t'invite à  utiliser le "Visual format" por créer tes contraintes par code, ces plus simple et visuel et moins verbeux.


    En l'occurrence si tes vues doivent être les unes en dessous des autres c'est assez simple, à  chaque fois tu rajoutes une contrainte d'espace=0 entre le bas de la vue précédente et le haut de la vue (l'équation donnerait symboliquement : vueCourante.top = 1 * vuePrecedente.bottom + 0)
  • LeChatNoirLeChatNoir Membre, Modérateur

    Dis Ali, j'ai lu dans un autre post qu'il y avait possibilité de "tester" dans IB sans build&run dans le simu... Désolé mais je ne trouve pas cette fonctionnalité :(



  • AliGatorAliGator Membre, Modérateur
    septembre 2014 modifié #22
    Affiche l'assistant dans ton Xcode et dans le menu en haut de l'assistant où tu choisis le fichier à  afficher, choisis "Preview"

    assistant_preview_2x.png


    À noter que ça existe depuis Xcode 5 (ce n'est pas une nouveauté de Xcode 6 et iOS8)


    Une fois que le Preview Assistant est affiché, tu peux voir un aperçu de ton XIB ou Storyboard donc, mais tu peux aussi cliquer sur le petit bouton "+" en bas à  gauche du Preview Assistant pour rajouter d'autres aperçus pour d'autres modèles d'iPhones/iPad, ou pour le même modèle (l'intérêt d'avoir 2 aperçus pour le même modèle étant que tu peux ensuite en faire tourner un en paysage dans l'aperçu et garder l'autre en portrait)
  • Sinon, LeChatNoir, une petite catégorie qui pourrait être utile



    + (CGSize)currentScreenSize_cbd_
    {
    return [UIApplication screenSizeForOrientation_cbd_:[UIApplication sharedApplication].statusBarOrientation];
    }

    + (CGSize)screenSizeForOrientation_cbd_:(UIInterfaceOrientation)orientation
    {
    CGSize size = [UIScreen mainScreen].bounds.size;
    UIApplication *application = [UIApplication sharedApplication];
    if (UIInterfaceOrientationIsLandscape(orientation))
    {
    size = CGSizeMake(size.height, size.width);
    }
    if (application.statusBarHidden == NO)
    {
    size.height -= MIN(application.statusBarFrame.size.width, application.statusBarFrame.size.height);
    }
    return size;
    }
  • LeChatNoirLeChatNoir Membre, Modérateur

    Merci Ali :)


    Merci colas :)


     


    Mais je suis reparti de 0. Mon code datait de 2011 quand même... Il est temps de faire un peu de ménage et de revoir tout ça.


     


    Alors je pars sur du full autolayout :)


  • AliGatorAliGator Membre, Modérateur
    septembre 2014 modifié #25
    Juste pour apporter de l'eau au moulin et vous aider à  y voir plus clair :

    1) La [UIScreen mainScreen].size est toujours la même quelle que soit l'orientation

    2) Sur iOS7, en mode portrait :
    • la Window a la même frame et les même bounds qu'en paysage (window.frame.size = window.bounds.size = 768 de large par 1024 de haut sur un iPad)
    • le RootViewController.view a la même frame que les bounds de la Window (normal) mais a une CGAffineTransform correspondant à  une rotation. Ce qui fait que ses bounds, eux, sont inversés.
    • Exemple sur iPad en Landscape, RootVC.view.frame.size = 768x1024 et RootVC.view.bounds.size = 1024x768 (échange width/height dû à  l'application de la transform, donc, puisque par définition les bounds correspondent aux coordonnées dans le référenciel interne à  la vue donc avec la transform d'appliquée contrairement à  la frame)
    3) Sur iOS8, en mode portrait, ce n'est plus une CGAffineTransform qui est appliqué sur le contenu de la window.rootViewController.view, mais directement la frame de la window qui est changée (la window est redimentionnée). Ainsi window.frame.size = 1024x768 dès le départ, et il n'y a plus de transform d'appliquée, donc ses bounds aussi sont 1024x768, et la frame et les bounds de la rootVC.view aussi.


    Table résumant tout ça :
    iOS 7                                           iOS 8
    { {
    Window = { Window = {
    frame = "{{0, 0}, {768, 1024}}"; frame = "{{0, 0}, {1024, 768}}";
    transform = "[1, 0, 0, 1, 0, 0]"; transform = "[1, 0, 0, 1, 0, 0]";
    bounds = "{{0, 0}, {768, 1024}}"; bounds = "{{0, 0}, {1024, 768}}";
    }; };
    RootVC = { RootVC = {
    frame = "{{0, 0}, {768, 1024}}"; frame = "{{0, 0}, {1024, 768}}";
    transform = "[0, -1, 1, 0, 0, 0]"; transform = "[1, 0, 0, 1, 0, 0]";
    bounds = "{{0, 0}, {1024, 768}}"; bounds = "{{0, 0}, {1024, 768}}";
    }; };
    } }
  • Je suis confronté à  un problème un peu similaire quand je récupère la size d'un ViewController. 


    Lorsque ce VC est le rootVC, la size retournée n'est pas la même, à  cause des transform.


     


    Du coup, j'ai écrit cette méthode qui renvoie toujours la bonne size. 



    - (CGSize)sizeAlwaysGood
    {
    /*
    cf.
    http://forum.cocoacafe.fr/topic/12890-ios8-gestion-rotation-ios8/
    */

    CGSize size = CGSizeApplyAffineTransform(self.presenterView.frame.size,
    self.presenterView.transform) ;

    size = CGSizeMake(ABS(size.width),
    ABS(size.height)) ;

    return size ;
    }
  • AliGatorAliGator Membre, Modérateur
    Pourquoi ne pas se baser sur bound.size (qui inclus déjà  la transform) plutôt que prendre frame.size et appliquer la transform toi même ?!
  • Joanna CarterJoanna Carter Membre, Modérateur
    octobre 2014 modifié #28
    Si tu as les transforms de rotation pour iOS 7, c'est plus facile de les enlever. Ils ne sont plus necessaires car le Window, dès iOS 8, se garde en mêmes proportions que l'orientation ; soit 1024 x 768 en paysage, 768 x 1024 en portrait.


    Dès iOS 8 le rotation n'est qu'un changement de la largeur et de l'hauteur, l'idée de rotation des vues n'existe plus.
  • muqaddarmuqaddar Administrateur
    octobre 2014 modifié #29

    Je relance ce sujet, car je pense que j'ai un problème d'affichage d'un custom Transition View Controller du à  la différence de traitement dont parle Ali entre iOS 7 et iOS 8.


     


    Voilà  le code qui marche très bien sur iOS 8:



    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
      UIView *fromView = [[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey] view];
      UIView *toView = [[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey] view];
      UIView *inView = [transitionContext containerView];
      
      float width = 0.0;
      if DEVICE_IPAD width = TRANSITION_WIDTH_IPAD;
      if DEVICE_IPHONE width = TRANSITION_WIDTH_IPHONE;
      
      CGFloat damping = 0.5;
      NSTimeInterval duration = [self transitionDuration:transitionContext];
      
      [toView setUserInteractionEnabled: true];
      [fromView setUserInteractionEnabled: true];

      if (self.isPresented == NO)
      {
        DLog(@Presenting View Controller);
        
        fromView.alpha = 0.6;
        
        // manage frame
        toView.frame = CGRectMake(fromView.frame.size.width, 0, width, fromView.frame.size.height);
        [inView addSubview:toView];
      }
      else
      {
        DLog(@Hiding View Controller);
        
        toView.alpha = 1.0;
      }
      
      // launch animation
      [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:damping initialSpringVelocity:10.0 options:0 animations:^{
        if (self.isPresented == NO)
        {
          toView.transform = CGAffineTransformMakeTranslation(-width, 0);
        }
        else
        {
          fromView.transform = CGAffineTransformMakeTranslation(width, 0);
        }
      }
      completion:^(BOOL finished) {
        [transitionContext completeTransition:finished]; // vital
      }];

    }

    Mon problème est que sur iOS 7, en mode portrait, la vue affichée n'est pas du tout au bon endroit.


    fromView.frame affiche effectivement la même frame de base (1024,768) mais une transform en plus sur iOS 7, et pas sous iOS 8.


     


    Comment rendre ce code compatible avec iOS 7 le plus proprement possible ? 


     


    EDIT:


     


    Au moment où j'affiche la transition:


     


    fromView sous iOS 7:


     



    Printing description of fromView:


    <UIView: 0x7fdf73e3f770; frame = (0 0; 768 1024); transform = [0, 1, -1, 0, 0, 0]; alpha = 0.6; autoresize = RM+BM; layer = <CALayer: 0x7fdf73e3f830>>


     



     


    fromView sous iOS 8:


     



    Printing description of fromView:


    <UIView: 0x7fb842fc8f90; frame = (0 0; 768 1024); alpha = 0.6; autoresize = W+H; layer = <CALayer: 0x7fb842fc9060>>



  • Joanna CarterJoanna Carter Membre, Modérateur


    Comment rendre ce code compatible avec iOS 7 le plus proprement possible ? 




     


    Avec beaucoup de code conditionnel ?  :(  :*

  • AliGatorAliGator Membre, Modérateur
    octobre 2014 modifié #31
    En appliquant a ta fromFrame la transform inverse de la vue parente (comme sous iOS8 ca sera la CGAffineTransformIdentity ça changera rien mais sous iOS7 ca devrait annuler l'effet de la transform de la rootViewController.view)

    CGRectApplyAffineTransform(fromFrame, CGAffineTransformInvert(rootViewController.view.transform))

    (Pas testé)
Connectez-vous ou Inscrivez-vous pour répondre.