Version d'iOS et rétro-compatibilité - Bonnes pratiques

LeChatNoirLeChatNoir Membre, Modérateur
octobre 2013 modifié dans API UIKit #1

Ma future version sera iOS6 et 7 compatible (en principe).


Je dois donc tester la version iOS en cours.


 


J'ai fait une macro pour me simplifier la vie.


 


 


#define iOS7AtLeast floor([[[UIDevice currentDevice] systemVersion] doubleValue]<7)?1:0


 

et je teste simplement :

 

if (iOS7AtLeast) ...

 

Est ce a good practice ?

 

Thxs :-)

 

«1

Réponses

  • En toute logique tu n'as pas à  tester la version d'iOS, en aucune manière.


     


    Ce que tu dois tester c'est la présence de fonctionnalité. En jouant avec du NSClassFromString ou avec les API de capacité de certains framework par exemple.


  • AliGatorAliGator Membre, Modérateur
    +1 avec Yoann


    Relire de toute urgence le SDK Compatibility Guide dans la doc Apple.
  • LeChatNoirLeChatNoir Membre, Modérateur

    Et ben...


     



    • To check the version of iOS at run time, use code like this:.




      NSString *osVersion = [[UIDevice currentDevice] systemVersion];




    Bon, après, je peux prendre une classe présente uniquement dans iOS7 et tester son existence mais bon...


  • AliGatorAliGator Membre, Modérateur

    Heu ça c'est la solution vraiment dernier recours. A n'utiliser que si vraiment toutes les autres solutions listés dans le SDK Compatibility Guide ne peuvent pas être appliquées. Mais avant tu as les respondsToSelector, les if([Machin class]), etc.


     


    Mais le principe reste toujours le même : ne pas tester la version de l'OS, mais tester, quand tu veux utiliser une fonctionnalité, si elle est disponible ou pas. Faut pas prendre une classe au hasard présente que dans iOS7 non plus, ça ne fait que déporter le problème, c'est pas la solution non plus. Faut tester... ce que tu veux tester.


     


    Si tu veux tester si ton objet roule, il ne faut pas tester si c'est une voiture, mais si ça roule. Car un camion aussi ça roule, et un vélo aussi. Si tu veux tester si ton objet sait démarrer, pareil. Si les dernières versions de voitures sont électriques, et que tu veux savoir si ta voiture est électrique ou pas, tu ne testes pas la version de ta voiture est supérieure à  N, tu testes si elle est électrique ou pas. Car peut-être que demain elles ne seront plus électriques mais fonctionneront à  eau et ton test version>N ne collera plus.


     


     


    Si tu veux utiliser une méthode qui n'est que dispo dans iOS7, tu testes la présence de cette méthode avant de l'appeler, c'est pas plus compliqué que ça.


  • LeChatNoirLeChatNoir Membre, Modérateur

    Ben si, c'est un peu plus compliqué que ça en fait...


     


    Car sous iOS7, je n'ai pas le même design... Donc par exemple, si je suis en iOS7, ma navigationBar n'est pas tintée, alors qu'en iOS6, oui...


     


    Donc dans ce cas, je fais comment ?


  • AliGatorAliGator Membre, Modérateur
    Ah ben voilà  fallait le dire tout de suite !

    Ca dépend, certes dans ce cas précis si tu veux changer de design selon la version de l'OS, pourquoi pas ta macro alors (sauf qu'elle est mal écrite, parenthèse fermante du floor mal placée + opérateur ternaire inutile car équivalent à  un "if YES then YES else NO")

    Après, faut quand même vérifier que c'est pas pour d'autres raisons que tu changes tel ou tel élément d'interface. Pour bien tester la raison première du changement et pas une conséquence induite. Par exemple si tu veux changer la couleur de ta statusBar sous iOS7 parce que sous iOS7 ton ViewController passe en dessous de la statusBar et que tu veux qu'elle soit donc plus pâle, alors il faut peut-être plutôt faire un test pour savoir "est-ce que mon VC passe en dessous de ma statusBar ou pas" (tester le frame.origin.y de ton viewController.view ?)... Ca dépend de tes besoins, mais l'idée est d'éviter autant que possible de tester une conséquence de notre besoin plutôt que notre besoin réel (même si parfois on ne réalise pas que ce qu'on teste n'est pas la condition vraiment racine/sous-jacente, c'est étonnant comme on peut avoir la tête dans le guidon sur ce genre de conditions parfois)

    C'est comme quand tu veux tester si tu es sur l'iPhone 5 avec un écran 4" ou sur un iPhone 4S ou inférieur, pour pouvoir dans ce cas mettre une rangée de boutons en plus par exemple. Y'en a qui testent pour ça le modèle d'iPhone. Ou si la hauteur de leur window est 568. Alors que si tu veux pouvoir mettre cette rangée de boutons en plus, c'est plus logique de tester si tu as la place de les mettre, donc si la hauteur de ton ViewController est >= la hauteur nécesaire pour rajouter cette rangée de boutons. Si tu as assez de place, alors tu la mets, sinon non. Comme ça le jour où l'iPhone 12 avec son écran de 6" va sortir, ça marchera toujours, même si la hauteur ne vaut plus exactement 568. Et s'il y a un iPhone qui sort un écran de 3.8", de hauteur un peu plus petit que 568 mais quand même juste assez grand pour ta rangée de boutons supplémentaires, tu l'auras quand même.

    La pour ton cas c'est pareil, faut bien être sûr que tu testes la vrai raison et pas une conséquence.
  • LeChatNoirLeChatNoir Membre, Modérateur

    rooo là  là ... Tu mets le doigt sur un truc qui fache la...


    Justement, j'ai des pb avec mon UINavigationController... Sous iOS6, ma vue est dessous (je veux dire bien calée en dessous), sous iOS7, ma vue passe effectivement en dessous... 


     


    J'ai bien compris qu'elles étaient transparentes par défaut donc c'est un comportement normal mais le pb, c'est que la transparence n'y est pas... (on touche peut être au NDA mais bon, faut bien se bouger un peu vu les délais impartis...).


     


    Bref, je retiens ton idée.


    Je l'avais appliquée pour une police qui existe sous iOS7 mais pas iOS6. Je teste donc simplement si la police existe ou pas.


     


    Je vais essayer de gérer la partie navigationBar avec cette histoire de position (quand j'aurai compris :-))

  • LeChatNoirLeChatNoir Membre, Modérateur

    Après moult tergiversations, pour traiter ce genre de pb (design différent selon version), j'ai fait cette macro, directement inspirée du " iOS 7 UI Transition Guide" :


     


    #define prior_iOS7  floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1


     

    Si ça peut aider.
  • muqaddarmuqaddar Administrateur

    Je viens de regarder le SDK Compatibility Guide.


     


    Ma question n'est pas pour tester la présence d'une classe, mais le changement de type d'un argument de la méthode, sachant que la méthode est la même.


     


    Je pense notamment à  setLineBreakMode qui prend UILineBreakMode jusqu'à  iOS 5, puis NSLineBreakMode maintenant ?! Apple veut uniformiser OS X et iOS ?


  • AliGatorAliGator Membre, Modérateur
    septembre 2013 modifié #11

    Cela est surtout suite au fait que maintenant UIKit supporte la gestion des NSAttributedString (via UILabel par exemple) et qu'il n'est plus nécessaire de passer par CoreText pour ça (adieu OHAttributedLabel).


     


    Et dans ce cas cela ne dépend nullement du Runtime, et donc de la version de iOS sur laquelle ton application va tourner. Donc ce n'est pas la version d'iOS qu'il faut tester.


     


    Ce qu'il faut tester, c'est la version du SDK. A partir d'un certain SDK, les headers indiquent que c'est un autre type pour cette propriété, donc si tu compiles avec un SDK récent tu vas utiliser NSLineBreakMode. Si tu compiles avec un SDK plus ancien UILineBreakMode sera de mise pour coller aux headers qui ne connaissent pas NSLineBreakMode.


     


    La version minimum supportée par ton application, c'est défini par le Deployment Target. Mais la version maximum que ton application est assurée de supporter c'est la version du SDK (normal, tu ne peux pas utiliser des API de iOS7 si tu compiles avec le SDK iOS6 par exemple). Donc c'est un "#if __IPHONE_OS_VERSION_MAX_ALLOWED < 60000" qu'il faut utiliser.


  • muqaddarmuqaddar Administrateur
    septembre 2013 modifié #12

    OK, je crois avoir compris.


     


    Et du coup, je m'aperçois que je dois quand-même utiliser une nouvelle méthode qui a remplacé l'ancienne, dont je dois donc tester la présence, et aussi ta macro pour taire les warnings !


     


    But: compatibilité iOS 5, iOS 6, iOS 7.


     


    Vois-tu une hérésie dans ce double test ?



      if ([NSString instancesRespondToSelector:@selector(drawWithRect:options:attributes:context:)])
      {
        NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
        [style setAlignment:NSTextAlignmentCenter];
        [string drawInRect:rect withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIFont fontWithName:@HelveticaNeue-Light size:20], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]];
      }
      else
      {
    #if __IPHONE_OS_VERSION_MAX_ALLOWED < 70000
        [string drawInRect:rect
                  withFont:[UIFont fontWithName:@HelveticaNeue-Light size:20]
             lineBreakMode:UILineBreakModeWordWrap
                 alignment:UITextAlignmentCenter];
    #endif
      }
  • AliGatorAliGator Membre, Modérateur

    En effet ça me parait pas déconnant


  • muqaddarmuqaddar Administrateur
    septembre 2013 modifié #14


    En effet ça me parait pas déconnant




     


    C'est du Ali ça ?


    T'as ton neveux sur tes genoux ?  8--)  :-*


  • AliGatorAliGator Membre, Modérateur
    Presque.
  • muqaddarmuqaddar Administrateur

    Ce code ne marche pas pour une appli compilée iOS 7 DSK qui doit tourner sur iOS 6.x:


     




      if ([NSString instancesRespondToSelector:@selector(drawWithRect:options:attributes:context:)])
      {
        NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
        [style setAlignment:NSTextAlignmentCenter];
        [string drawInRect:rect withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIFont fontWithName:@HelveticaNeue-Light size:20], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]];
      }
      else
      {
    #if __IPHONE_OS_VERSION_MAX_ALLOWED < 70000
        [string drawInRect:rect
                  withFont:[UIFont fontWithName:@HelveticaNeue-Light size:20]
             lineBreakMode:UILineBreakModeWordWrap
                 alignment:UITextAlignmentCenter];
    #endif
      }

    1) En effet, __IPHONE_OS_VERSION_MAX_ALLOWED < 70000 indique au compilateur de ne pas compiler, donc le else est vide.


    Comment alors taire les warnings sur UILineBreakModeWordWrap ?


    Je ne comprends pas ce que je dois faire.


     


    2) Autre question: quand une méthode est deprecated, on peut quand-même l'utiliser. Jusqu'à  quand reste-t-elle deprecated en général ? Jusqu'à  l'OS suivant, puis ils la retirent complètement ?


  • AliGatorAliGator Membre, Modérateur
    __IPHONE_OS_VERSION_MIN_REQUIRED correspond au Deployment Target, donc à  la version min d'iOS supportée par ton appli
    __IPHONE_OS_VERSION_MAX_REQUIRED correspond à  la version de ton Dominique Strauss-Kann avec laquelle tu compiles.

    Là  si tu compiles ton code avec le iOS 7 DSK le "else" va être vide comme tu l'as si bien dit. Du coup c'est normal que sous iOS6 ça ne fasse plus rien.
    Et si tu compilais ton code avec Xcode4 et l'ancien iOS6 DSK, le "else" aurait le code, mais le code ne compilerait pas car à  l'inverse le "drawInRect:withAttributes:" que tu utilises dans ton "if" n'existe que depuis iOS7 donc n'est déclaré dans Dominique que depuis sa version 7. Donc même si tu fais ton tests sur respondsToSelector, la méthode n'existant même pas dans les headers de Dominique 6.0 le compilateur de Xcode4 va gueuler.


    Ne pas confondre version sous laquelle ton code tourne, version du SDK, Dominique Strauss-Kann et SDK.
  • muqaddarmuqaddar Administrateur
    octobre 2013 modifié #18

    J'ai bien compris ce que tu dis.


    Mais je ne vois toujours pas comment taire les warnings dont je parle. Le SDK 7 bronche.


     


     





    Ne pas confondre version sous laquelle ton code tourne, version du SDK, Dominique Strauss-Kann et SDK.




     


    héhé, celui-là  il est partout et nique même mon code !


  • AliGatorAliGator Membre, Modérateur
    Donc en gros ce qu'il faut que tu fasses c'est :

    #Si je compile avec le SDK 7
    {
    // Donc la méthode "drawInRect:withAttributes:" est déclarée qqpart dans les headers du SDK, et le compilo va connaà®tre cette méthode et sa signature
    Si au runtime cette méthode est disponible
    {
    // Donc l'utilisateur est sur iOS7, en gros
    Alors utilise-la
    }
    Sinon
    {
    Utilise une autre méthode qui existait sous iOS6
    }
    }
    #Sinon
    }
    // Tu es sur un ancien SDK (genre iOS6 SDK) donc le compilo lui-même ne connaà®t même pas drawInRect:withAttributes
    Utilise la méthode qui existait sous iOS6
    }

    Soit :
    #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
    if ([NSString instancesRespondToSelector:@selector(drawInRect:withAttributes:)])
    {
    // On a compilé avec le SDK 7, et l'utilisateur tourne sous iOS7, donc OK
    /* Code qui utilise drawInRect:withAttributes: */
    }
    else
    #endif
    {
    // On a pas compilé avec le SDK7 mais un SDK plus ancien (donc on est en dehors du #if...#endif)
    // Ou bien on a compilé avec le SDK7 (donc le code entre le #if...#endif a bien été compilé)
    // mais l'utilisateur tourne encore sous iOS6 et pas iOS7 (donc on est passé dans le "else")
    /* Code qui utilise l'ancienne méthode, drawInRect:withFont:lineBreakMode:alignment: */
    }
  • muqaddarmuqaddar Administrateur
    octobre 2013 modifié #20

    OK.


     


    Par contre, je m'aperçois que dans le else je dois utiliser NSLineBreakByWordWrapping et non UILineBreakModeWordWrap.


    Tu me diras, c'est normal, c'est le SDK 7 qui compile et il le connaà®t.


     


    Et moi je te dis, OK, mais comment ça arrive à  ne pas planter sous iOS 6 qui est sensé ne connaà®tre que UILineBreakModeWordWrap ?



    #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
      if ([NSString instancesRespondToSelector:@selector(drawWithRect:options:attributes:context:)])
      {
        NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
        [style setAlignment:NSTextAlignmentCenter];
        [string drawInRect:rect withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIFont fontWithName:@HelveticaNeue-Light size:20], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]];
      }
      else
    #endif
      {
        [string drawInRect:rect withFont:[UIFont fontWithName:@HelveticaNeue-Light size:20] lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentCenter];
      }

  • AliGatorAliGator Membre, Modérateur
    Parce que NSLineBreakMode et UILineBreakMode ne sont que des enums (autrement dit, des entiers, une fois le code compilé). Et puis NSLineBreakMode existe déjà  depuis le SDK 6 donc bon...

    Par contre ton code a une incohérence. Tu testes l'existence du @selector "drawWithRect:options:attributes:context:" mais c'est pour appeler "drawInRect:withAttributes:" et non pas la méthode dont tu as testé le @selector ;)
  • muqaddarmuqaddar Administrateur


    Parce que NSLineBreakMode et UILineBreakMode ne sont que des enums (autrement dit, des entiers, une fois le code compilé). Et puis NSLineBreakMode existe déjà  depuis le SDK 6 donc bon...




     


    Ah ok, je comprends. 


     


     




    Par contre ton code a une incohérence. Tu testes l'existence du @selector "drawWithRect:options:attributes:context:" mais c'est pour appeler "drawInRect:withAttributes:" et non pas la méthode dont tu as testé le @selector ;)




     


    Bien vu, ça m'a échappé ! Merci !

  • muqaddarmuqaddar Administrateur
    octobre 2013 modifié #23

    Bon, même sur iOS 6, je rentre dans le if:



    if ([NSString instancesRespondsToSelector:@selector(drawInRect:withAttributes:)])
    {
    }

    ::)  :p


     


    Je suppose que ce n'est pas le bon test, pourtant je veux tester la présence de cette méthode d'instance de NSString.


     


    J'avoue que j'en perds mon latin.


  • AliGatorAliGator Membre, Modérateur
    Merde si ça se trouve c'est une méthode qui existait depuis iOS6 (mais privée), et n'a fait que sortir de l'ombre avec iOS7 :-/
  • muqaddarmuqaddar Administrateur
    octobre 2013 modifié #25


    Merde si ça se trouve c'est une méthode qui existait depuis iOS6 (mais privée), et n'a fait que sortir de l'ombre avec iOS7 :-/




     


    Ouais mais du coup il rentre dans le if, veut exécuter la méthode et paf, ça plante.


  • AliGatorAliGator Membre, Modérateur
    Mouais, chelou... si ça **plante** quand, une fois rentré dans le "if", il essaye d'exécuter [string drawInRect:rect withAttributes:...] et que le plantage c'est une exception "unrecognized selector drawInRect:withAttributes: sent to instance <NSString 0x....>" alors c'est que la méthode n'existe vraiment pas (même pas dans une API privée) et du coup ça n'a pas de sens que ton "if" soit passé...

    C'est quoi le plantage exact que tu as ?
  • muqaddarmuqaddar Administrateur
    octobre 2013 modifié #27

    Le plantage ne semble pas être sur la méthode mais sur son contenu passé en argument.


     


    -[__NSDictionaryI font]: unrecognized selector sent to instance 0xb1ef390


     


    Le code exécuté pour rappel:



        [string drawInRect:rect withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIFont fontWithName:@HelveticaNeue-Light size:20], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]];
  • AliGatorAliGator Membre, Modérateur
    Ah bizarre...

    Du coup ça doit être un cas particulier où la méthode existait dans iOS6... mais était privée et prenait autre chose qu'un NSDictionary comme argument ?!
  • muqaddarmuqaddar Administrateur


    Ah bizarre...


    Du coup ça doit être un cas particulier où la méthode existait dans iOS6... mais était privée et prenait autre chose qu'un NSDictionary comme argument ?!




     


    C'est ce que je me suis dit, mais ça en devient vraiment étrange.

  • muqaddarmuqaddar Administrateur

    T'es sûr que mon code de test est bon ?


     


     


    if ([NSString instancesRespondToSelector:@selector(drawInRect:withAttributes:)])


     


    parce que j'y rentre aussi sous iOS 5...


  • AliGatorAliGator Membre, Modérateur
    octobre 2013 modifié #31
    Je confirme que sur iOS6 et sur iOS7 si je demande la signature de la méthode au runtime il ne me répond pas la même chose:
    NSMethodSignature* sign = [NSString instanceMethodSignatureForSelector:@selector(drawInRect:withAttributes:)];
    NSUInteger nbArgs = [sign numberOfArguments];
    for(NSUInteger i=0;i<nbArgs;++i)
    {
    NSLog(@Argument #%d type: %s, i, [sign getArgumentTypeAtIndex:i]);
    }
    NSLog(@Return type: %s, [sign methodReturnType]);
    Sous iOS6 :


    Argument #1 type: :
    Argument #2 type: {CGRect={CGPoint=ff}{CGSize=ff}}

    Return type: {CGSize=ff}


    Sous iOS7 :


    Argument #1 type: :
    Argument #2 type: {CGRect={CGPoint=ff}{CGSize=ff}}

    Return type: v

    (Pour la référence des Type Encodings, voir la doc Apple ici)

    Donc déjà  première analyse : la méthode existe bien sous iOS6, même si elle n'est pas publique. Elle devait faire partie des API privée.
    Et deuxième analyse : la méthode privée du temps de iOS6 n'a pas la même signature que la méthode publique apparue avec iOS7. Ce qui tend vers ma théorie qu'ils ont certainement changé les arguments de cette méthode avant de la passer publique.
    • Evidemment les 2 premiers arguments (#0 et #1) sont de type "id" ("@) et SEL" (":"), puisque ce sont les paramètres cachés "self" et "_cmd" de toute méthode Objective-C.
    • Ensuite vient un CGRect dans les 2 cas, normal c'est l'argument de "drawInRect:".
    • Puis vient un objet (id) pour l'argument "withAttributes:", bon on ne pourra pas savoir précisément ce que c'est (tous les objects Cocoa sont représentés par le type encoding "@ / le type id" générique dans les signatures au final) et si c'est un NSDictionary qui était déjà  attendu sous iOS6 comme ça l'est officiellement pour iOS7
    • Par contre on voit bien que le "Return type" est "void" ("v") sous iOS7, ce qui est tout à  fait ce que dit la doc... mais qu'il était de type CGSize ("CGSize{ff}" autrement dit une struct qui s'appelle CGSize et contient 2 champs de type float, oui oui c'est donc bien le CGSize qu'on connait tous) sous iOS6. Donc quand c'état encore une API privée, cette méthode retournait un CGSize, sans doute la taille prise par le dessin. Dommage du coup qu'ils ne retournent plus rien quand ils l'ont passée publique sous iOS7, mais bon c'est comme ça.
    Conclusion bah tu viens de tomber dans un cas un peu sioux d'une API dispo que sous iOS7 mais pour laquelle il existait une méthode privée du même nom mais avec signature différente sous iOS6... Pas de bol :/
Connectez-vous ou Inscrivez-vous pour répondre.