Si vous utilisez un iPhone ou un iPad, vous avez peut-être remarqué un nombre inhabituel de mises à jour depuis la sortie d’iOS 6. Bien sûr, il y a l’adaptation urgente à l’écran étiré de l’iPhone 5, mais il y a une autre raison : Apple a modifié la gestion de la rotation, sans trop se soucier de la compatibilité des applications existantes…
L’idée générale derrière cette évolution part plutôt d’un bon sentiment : la méthode précédente induisait une confusion entre rotation et mise en page (layout). Par exemple, sur un iPad, il est banal d’utiliser une subView organisée de façon identique pour la “detail view” quelle que soit la rotation.
Si le viewController pilotant la subView est interrogé classiquement via
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
il peut se produire des situations ambiguës où cette confusion pose problème. Pour éviter ça, Apple a rendue “deprecated” cette méthode et le framework a tout simplement cessé de l’appeler (de ce fait, certaines applications ont tout simplement cessé de pivoter !). À la place, l’application s’appuie sur les orientations indiquées dans le fichier Info.plist. Autant de code en moins à écrire. Ou bien, avec une nouvelle API, elle interroge seulement le viewController “racine”, mais plus les autres. C’est maintenant le rootViewController qui décide pour ses “enfants”.
Cette nouvelle méthode est évidemment beaucoup trop simple. Un nouveau problème apparaît lorsque l’application doit gérer sélectivement la rotation selon la view courante, accepter la rotation sur une view et la refuser sur une autre. Pour préserver la souplesse nécessaire, Apple a introduit plusieurs nouvelles méthodes dans UIViewController :
- (BOOL)shouldAutorotate;
- (NSUInteger)supportedInterfaceOrientations;
Il faut d’abord prévenir le framework que l’application les utilise. La solution retenue pour ça est assez étrange : au lieu d’une méthode de délégué qu’on aurait pu s’attendre à trouver (mais un UIViewController n’a pas de délégué par défaut…), c’est un userDefault qu’il faut créer :
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Register for new API rotation calls
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"UIApplicationSupportedInterfaceOrientationsIsEnabled"];
...
}
Deuxième contrainte à respecter : la fenêtre doit impérativement être dotée d’un viewController “root”. C’était facultatif jusqu’ici (mais on voyait depuis iOS 5 passer un warning dans la console en son absence), c’est devenu obligatoire en iOS 6, sinon il est impossible d’exploiter cette nouvelle API. En outre, la bonne syntaxe doit être utilisée pour installer le viewController et sa view dans la fenêtre :
self.window.rootViewController = viewController;
et surtout pas [window addSubView:viewController.view];
qui ne fonctionne pas correctement puisque la property rootViewController n’est pas sollicitée. Attention avec les projets anciens créés avec les vieux templates…
Ensuite, il faut implémenter shouldAutorotate
et supportedInterfaceOrientations
dans le rootViewController lui-même. Pas de problème si c’est une sous-classe. Si c’est un UINavigationController, il faut le sous-classer aussi, idem pour un UITabBarController utilisé en rootViewController. C’est assez étrange d’avoir à sous-classer ces vénérables objets juste pour ça, mais apparemment c’est la bonne solution.
Comme indiqué plus haut, le framework appelle seulement ces méthodes dans le rootViewController, et non dans les viewControllers “enfants”. La décision effective de rotation sera l’intersection entre ce qu’il y a dans Info.plist et ce que retourne supportedInterfaceOrientations
. Il y a peut-être une solution plus simple qui m’a échappé pour l’instant, en tout cas voici une approche qui fonctionne pour autoriser sélectivement la rotation. Implémenter ainsi les deux méthodes dans la sous-classe de UINavigationController :
- (NSUInteger)supportedInterfaceOrientations
{
UIViewController *lastVC = [self.viewControllers lastObject];
if ([lastVC respondsToSelector:@selector(supportedInterfaceOrientations)]) {
return [lastVC supportedInterfaceOrientations];
}
return UIInterfaceOrientationMaskAllButUpsideDown;
}
- (BOOL)shouldAutorotate
{
UIViewController *lastVC = [self.viewControllers lastObject];
if ([lastVC respondsToSelector:@selector(shouldAutorotate)]) {
return [lastVC shouldAutorotate];
}
return YES;
}
On peut adapter ça à un UITabBarController “root” en lui indiquant le “chemin” du viewController à interroger (ça peut être plus compliqué, car un tabBarItem peut contenir à son tour un navigationController, donc il faudra adapter l’approche au cas par cas). Ensuite il ne reste plus qu’à implémenter ces méthodes dans les viewControllers qui doivent se comporter différemment de la règle générale, et y retourner les valeurs appropriées, par exemple return NO;
pour refuser la rotation, ou une valeur différente de masque pour l’autoriser sélectivement.
Bien sûr, on conserve en parallèle l’API précédente pour préserver la compatibilité avec iOS 5 encore quelques temps…
Ceci n’est peut-être pas la solution optimale. La doc est assez elliptique pour le moment et laisse largement place à l’interprétation. N’hésitez pas à commenter si vous trouvez une solution plus simple !