En fait, je viens d'écrire du code qui vous permettra de désactiver globalement le mode sombre dans le code sans avoir à mettre en place tous les contrôleurs viw de votre application. Cela peut probablement être affiné pour se retirer classe par classe en gérant une liste de classes. Pour moi, ce que je veux, c'est que mes utilisateurs voient s'ils aiment l'interface en mode sombre de mon application, et s'ils ne l'aiment pas, ils peuvent la désactiver. Cela leur permettra de continuer à utiliser le mode sombre pour le reste de leurs applications.
Le choix de l'utilisateur est bon (Ahem, en te regardant Apple, c'est comme ça que tu aurais dû l'implémenter).
Donc, comment cela fonctionne, c'est que c'est juste une catégorie de UIViewController. Lorsqu'il se charge, il remplace la méthode native viewDidLoad par une qui vérifiera un indicateur global pour voir si le mode sombre est désactivé pour tout ou non.
Parce qu'il est déclenché lors du chargement de UIViewController, il devrait démarrer automatiquement et désactiver le mode sombre par défaut. Si ce n'est pas ce que vous voulez, vous devez y arriver tôt et définir le drapeau, ou bien simplement définir le drapeau par défaut.
Je n'ai encore rien écrit pour répondre à l'utilisateur qui active ou désactive le drapeau. Il s'agit donc essentiellement d'un exemple de code. Si nous voulons que l'utilisateur interagisse avec cela, tous les contrôleurs de vue devront être rechargés. Je ne sais pas comment faire cela à la volée mais probablement envoyer une notification va faire l'affaire. Donc, pour le moment, cette activation / désactivation globale du mode sombre ne fonctionnera qu'au démarrage ou au redémarrage de l'application.
Maintenant, il ne suffit plus d'essayer de désactiver le mode sombre dans chaque viewController MFING de votre énorme application. Si vous utilisez des actifs de couleur, vous êtes complètement désossé. Depuis plus de 10 ans, nous comprenons que les objets immuables sont immuables. Les couleurs que vous obtenez dans le catalogue des ressources de couleur disent qu'elles sont UIColor mais ce sont des couleurs dynamiques (modifiables) et changeront sous vous lorsque le système passera du mode sombre au mode clair. C'est censé être une fonctionnalité. Mais bien sûr, il n'y a pas de bascule principale pour demander à ces choses de cesser de faire ce changement (à ma connaissance, peut-être que quelqu'un peut améliorer cela).
La solution est donc en deux parties:
une catégorie publique sur UIViewController qui donne des méthodes d'utilité et de commodité ... par exemple, je ne pense pas qu'Apple ait pensé au fait que certains d'entre nous mélangent du code Web dans nos applications. En tant que tel, nous avons des feuilles de style qui doivent être basculées en fonction du mode sombre ou clair. Ainsi, vous devez soit créer une sorte d'objet de feuille de style dynamique (ce qui serait bien), soit simplement demander quel est l'état actuel (mauvais mais facile).
cette catégorie lors du chargement remplacera la méthode viewDidLoad de la classe UIViewController et interceptera les appels. Je ne sais pas si cela enfreint les règles de l'App Store. Si c'est le cas, il existe probablement d'autres moyens de contourner cela, mais vous pouvez le considérer comme une preuve de concept. Vous pouvez par exemple créer une sous-classe de tous les principaux types de contrôleurs de vue et faire hériter tous vos propres contrôleurs de vue de ceux-ci, puis vous pouvez utiliser l'idée de catégorie DarkMode et l'appeler pour forcer la désactivation de tous vos contrôleurs de vue. C'est plus laid mais ça ne va enfreindre aucune règle. Je préfère utiliser le runtime car c'est pour cela que le runtime a été conçu. Donc, dans ma version, vous ajoutez simplement la catégorie, vous définissez une variable globale sur la catégorie pour savoir si vous souhaitez ou non qu'elle bloque le mode sombre, et elle le fera.
Vous n'êtes pas encore sorti du bois, comme mentionné, l'autre problème est que UIColor fait essentiellement ce qu'il veut. Donc, même si vos contrôleurs de vue bloquent le mode sombre, UIColor ne sait pas où ni comment vous l'utilisez, donc ne peut pas s'adapter. En conséquence, vous pouvez le récupérer correctement, mais cela reviendra sur vous à un moment donné dans le futur. Peut-être bientôt peut-être plus tard. Donc, la solution consiste à l'allouer deux fois à l'aide d'un CGColor et à le transformer en une couleur statique. Cela signifie que si votre utilisateur revient en arrière et réactive le mode sombre sur votre page de paramètres (l'idée est de faire en sorte que cela fonctionne afin que l'utilisateur ait le contrôle sur votre application au-delà du reste du système), toutes ces couleurs statiques besoin de remplacement. Jusqu'à présent, cela reste à résoudre par quelqu'un d'autre. Le moyen le plus simple de le faire est de faire un défaut que vous ' en désactivant le mode sombre, divisez par zéro pour planter l'application car vous ne pouvez pas la quitter et dire à l'utilisateur de simplement la redémarrer. Cela viole probablement également les directives de l'App Store, mais c'est une idée.
La catégorie UIColor n'a pas besoin d'être exposée, elle fonctionne simplement en appelant colorNamed: ... si vous n'avez pas dit à la classe DarkMode ViewController de bloquer le mode sombre, cela fonctionnera parfaitement comme prévu. Essayer de créer quelque chose d'élégant au lieu du code standard des sphaghetti aux pommes, ce qui signifie que vous devrez modifier la plupart de votre application si vous souhaitez désactiver par programme le mode sombre ou le basculer. Maintenant, je ne sais pas s'il existe un meilleur moyen de modifier par programmation l'Info.plist pour désactiver le mode sombre si nécessaire. Pour autant que je sache, c'est une fonction de compilation et après cela, vous êtes désossé.
Voici donc le code dont vous avez besoin. Doit être déposé et utiliser la seule méthode pour définir le style d'interface utilisateur ou définir la valeur par défaut dans le code. Vous êtes libre d'utiliser, de modifier, de faire tout ce que vous voulez avec cela pour n'importe quel but et aucune garantie n'est donnée et je ne sais pas si cela passera sur l'App Store. Améliorations très bienvenues.
Juste avertissement Je n'utilise pas ARC ou toute autre méthode de prise en main.
////// H file
#import <UIKit/UIKit.h>
@interface UIViewController(DarkMode)
// if you want to globally opt out of dark mode you call these before any view controllers load
// at the moment they will only take effect for future loaded view controllers, rather than currently
// loaded view controllers
// we are doing it like this so you don't have to fill your code with @availables() when you include this
typedef enum {
QOverrideUserInterfaceStyleUnspecified,
QOverrideUserInterfaceStyleLight,
QOverrideUserInterfaceStyleDark,
} QOverrideUserInterfaceStyle;
// the opposite condition is light interface mode
+ (void)setOverrideUserInterfaceMode:(QOverrideUserInterfaceStyle)override;
+ (QOverrideUserInterfaceStyle)overrideUserInterfaceMode;
// utility methods
// this will tell you if any particular view controller is operating in dark mode
- (BOOL)isUsingDarkInterfaceStyle;
// this will tell you if any particular view controller is operating in light mode mode
- (BOOL)isUsingLightInterfaceStyle;
// this is called automatically during all view controller loads to enforce a single style
- (void)tryToOverrideUserInterfaceStyle;
@end
////// M file
//
// QDarkMode.m
#import "UIViewController+DarkMode.h"
#import "q-runtime.h"
@implementation UIViewController(DarkMode)
typedef void (*void_method_imp_t) (id self, SEL cmd);
static void_method_imp_t _nativeViewDidLoad = NULL;
// we can't @available here because we're not in a method context
static long _override = -1;
+ (void)load;
{
#define DEFAULT_UI_STYLE UIUserInterfaceStyleLight
// we won't mess around with anything that is not iOS 13 dark mode capable
if (@available(iOS 13,*)) {
// default setting is to override into light style
_override = DEFAULT_UI_STYLE;
/*
This doesn't work...
NSUserDefaults *d = NSUserDefaults.standardUserDefaults;
[d setObject:@"Light" forKey:@"UIUserInterfaceStyle"];
id uiStyle = [d objectForKey:@"UIUserInterfaceStyle"];
NSLog(@"%@",uiStyle);
*/
if (!_nativeViewDidLoad) {
Class targetClass = UIViewController.class;
SEL targetSelector = @selector(viewDidLoad);
SEL replacementSelector = @selector(_overrideModeViewDidLoad);
_nativeViewDidLoad = (void_method_imp_t)QMethodImplementationForSEL(targetClass,targetSelector);
QInstanceMethodOverrideFromClass(targetClass, targetSelector, targetClass, replacementSelector);
}
}
}
// we do it like this because it's not going to be set often, and it will be tested often
// so we can cache the value that we want to hand to the OS
+ (void)setOverrideUserInterfaceMode:(QOverrideUserInterfaceStyle)style;
{
if (@available(iOS 13,*)){
switch(style) {
case QOverrideUserInterfaceStyleLight: {
_override = UIUserInterfaceStyleLight;
} break;
case QOverrideUserInterfaceStyleDark: {
_override = UIUserInterfaceStyleDark;
} break;
default:
/* FALLTHROUGH - more modes can go here*/
case QOverrideUserInterfaceStyleUnspecified: {
_override = UIUserInterfaceStyleUnspecified;
} break;
}
}
}
+ (QOverrideUserInterfaceStyle)overrideUserInterfaceMode;
{
if (@available(iOS 13,*)){
switch(_override) {
case UIUserInterfaceStyleLight: {
return QOverrideUserInterfaceStyleLight;
} break;
case UIUserInterfaceStyleDark: {
return QOverrideUserInterfaceStyleDark;
} break;
default:
/* FALLTHROUGH */
case UIUserInterfaceStyleUnspecified: {
return QOverrideUserInterfaceStyleUnspecified;
} break;
}
} else {
// we can't override anything below iOS 12
return QOverrideUserInterfaceStyleUnspecified;
}
}
- (BOOL)isUsingDarkInterfaceStyle;
{
if (@available(iOS 13,*)) {
if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark){
return YES;
}
}
return NO;
}
- (BOOL)isUsingLightInterfaceStyle;
{
if (@available(iOS 13,*)) {
if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight){
return YES;
}
// if it's unspecified we should probably assume light mode, esp. iOS 12
}
return YES;
}
- (void)tryToOverrideUserInterfaceStyle;
{
// we have to check again or the compile will bitch
if (@available(iOS 13,*)) {
[self setOverrideUserInterfaceStyle:(UIUserInterfaceStyle)_override];
}
}
// this method will be called via the viewDidLoad chain as we will patch it into the
// UIViewController class
- (void)_overrideModeViewDidLoad;
{
if (_nativeViewDidLoad) {
_nativeViewDidLoad(self,@selector(viewDidLoad));
}
[self tryToOverrideUserInterfaceStyle];
}
@end
// keep this in the same file, hidden away as it needs to switch on the global ... yeah global variables, I know, but viewDidLoad and colorNamed: are going to get called a ton and already it's adding some inefficiency to an already inefficient system ... you can change if you want to make it a class variable.
// this is necessary because UIColor will also check the current trait collection when using asset catalogs
// so we need to repair colorNamed: and possibly other methods
@interface UIColor(DarkMode)
@end
@implementation UIColor (DarkMode)
typedef UIColor *(*color_method_imp_t) (id self, SEL cmd, NSString *name);
static color_method_imp_t _nativeColorNamed = NULL;
+ (void)load;
{
// we won't mess around with anything that is not iOS 13 dark mode capable
if (@available(iOS 13,*)) {
// default setting is to override into light style
if (!_nativeColorNamed) {
// we need to call it once to force the color assets to load
Class targetClass = UIColor.class;
SEL targetSelector = @selector(colorNamed:);
SEL replacementSelector = @selector(_overrideColorNamed:);
_nativeColorNamed = (color_method_imp_t)QClassMethodImplementationForSEL(targetClass,targetSelector);
QClassMethodOverrideFromClass(targetClass, targetSelector, targetClass, replacementSelector);
}
}
}
// basically the colors you get
// out of colorNamed: are dynamic colors... as the system traits change underneath you, the UIColor object you
// have will also change since we can't force override the system traits all we can do is force the UIColor
// that's requested to be allocated out of the trait collection, and then stripped of the dynamic info
// unfortunately that means that all colors throughout the app will be static and that is either a bug or
// a good thing since they won't respond to the system going in and out of dark mode
+ (UIColor *)_overrideColorNamed:(NSString *)string;
{
UIColor *value = nil;
if (@available(iOS 13,*)) {
value = _nativeColorNamed(self,@selector(colorNamed:),string);
if (_override != UIUserInterfaceStyleUnspecified) {
// the value we have is a dynamic color... we need to resolve against a chosen trait collection
UITraitCollection *tc = [UITraitCollection traitCollectionWithUserInterfaceStyle:_override];
value = [value resolvedColorWithTraitCollection:tc];
}
} else {
// this is unreachable code since the method won't get patched in below iOS 13, so this
// is left blank on purpose
}
return value;
}
@end
Il existe un ensemble de fonctions utilitaires que cela utilise pour effectuer un échange de méthode. Fichier séparé. Il s'agit cependant de choses standard et vous pouvez trouver du code similaire n'importe où.
// q-runtime.h
#import <Foundation/Foundation.h>
#import <objc/message.h>
#import <stdatomic.h>
// returns the method implementation for the selector
extern IMP
QMethodImplementationForSEL(Class aClass, SEL aSelector);
// as above but gets class method
extern IMP
QClassMethodImplementationForSEL(Class aClass, SEL aSelector);
extern BOOL
QClassMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector);
extern BOOL
QInstanceMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector);
// q-runtime.m
static BOOL
_QMethodOverride(Class targetClass, SEL targetSelector, Method original, Method replacement)
{
BOOL flag = NO;
IMP imp = method_getImplementation(replacement);
// we need something to work with
if (replacement) {
// if something was sitting on the SEL already
if (original) {
flag = method_setImplementation(original, imp) ? YES : NO;
// if we're swapping, use this
//method_exchangeImplementations(om, rm);
} else {
// not sure this works with class methods...
// if it's not there we want to add it
flag = YES;
const char *types = method_getTypeEncoding(replacement);
class_addMethod(targetClass,targetSelector,imp,types);
XLog_FB(red,black,@"Not sure this works...");
}
}
return flag;
}
BOOL
QInstanceMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector)
{
BOOL flag = NO;
if (targetClass && replacementClass) {
Method om = class_getInstanceMethod(targetClass,targetSelector);
Method rm = class_getInstanceMethod(replacementClass,replacementSelector);
flag = _QMethodOverride(targetClass,targetSelector,om,rm);
}
return flag;
}
BOOL
QClassMethodOverrideFromClass(Class targetClass, SEL targetSelector,
Class replacementClass, SEL replacementSelector)
{
BOOL flag = NO;
if (targetClass && replacementClass) {
Method om = class_getClassMethod(targetClass,targetSelector);
Method rm = class_getClassMethod(replacementClass,replacementSelector);
flag = _QMethodOverride(targetClass,targetSelector,om,rm);
}
return flag;
}
IMP
QMethodImplementationForSEL(Class aClass, SEL aSelector)
{
Method method = class_getInstanceMethod(aClass,aSelector);
if (method) {
return method_getImplementation(method);
} else {
return NULL;
}
}
IMP
QClassMethodImplementationForSEL(Class aClass, SEL aSelector)
{
Method method = class_getClassMethod(aClass,aSelector);
if (method) {
return method_getImplementation(method);
} else {
return NULL;
}
}
Je copie et colle ceci à partir de quelques fichiers depuis que q-runtime.h est ma bibliothèque réutilisable et ceci n'en est qu'une partie. Si quelque chose ne se compile pas, faites le moi savoir.
UIUserInterfaceStyle
surLight
dans votre Info.Plist. Voir developer.apple.com/library/archive/documentation/General/…