Na verdade, acabei de escrever um código que permitirá que você desative globalmente o modo escuro no código sem precisar digitar todos os controladores viw em seu aplicativo. Provavelmente, isso pode ser refinado para optar por excluir classe por classe, gerenciando uma lista de classes. Para mim, o que eu quero é que meus usuários vejam se eles gostam da interface de modo escuro do meu aplicativo e, se não gostam, podem desativá-lo. Isso permitirá que eles continuem usando o modo escuro para o restante de seus aplicativos.
A escolha do usuário é boa (Ahem, olhando para você Apple, é assim que você deveria ter implementado).
Então, como isso funciona é que é apenas uma categoria do UIViewController. Quando carrega, substitui o método viewDidLoad nativo por um que verificará um sinalizador global para ver se o modo escuro está desativado para tudo ou não.
Como é acionado no carregamento do UIViewController, ele deve iniciar automaticamente e desativar o modo escuro por padrão. Se isso não é o que você deseja, é necessário chegar lá cedo e definir o sinalizador, ou apenas definir o sinalizador padrão.
Ainda não escrevi nada para responder ao usuário que liga ou desliga a bandeira. Portanto, este é basicamente um código de exemplo. Se quisermos que o usuário interaja com isso, todos os controladores de exibição precisarão ser recarregados. Não sei como fazer isso de imediato, mas provavelmente enviar alguma notificação será suficiente. Então, neste momento, esse global ativado / desativado para o modo escuro só funcionará na inicialização ou reinicialização do aplicativo.
Agora, não é apenas o suficiente tentar desativar o modo escuro em todos os controladores de exibição MFING em seu enorme aplicativo. Se você estiver usando materiais coloridos, estará completamente desossado. Por mais de 10 anos, entendemos que objetos imutáveis são imutáveis. As cores que você obtém do catálogo de ativos de cores dizem que são UIColor, mas são cores dinâmicas (mutáveis) e serão alteradas embaixo de você conforme o sistema muda do modo escuro para o claro. Isso deveria ser um recurso. Mas é claro que não há um mestre para pedir que essas coisas parem de fazer essa alteração (até onde eu sei agora, talvez alguém possa melhorar isso).
Portanto, a solução está dividida em duas partes:
uma categoria pública no UIViewController que fornece alguns métodos de utilidade e conveniência ... por exemplo, não acho que a apple tenha pensado no fato de que alguns de nós misturam código da web em nossos aplicativos. Como tal, temos folhas de estilo que precisam ser alternadas com base no modo escuro ou claro. Portanto, você precisa criar algum tipo de objeto de folha de estilo dinâmico (o que seria bom) ou apenas perguntar qual é o estado atual (ruim, mas fácil).
essa categoria ao carregar substituirá o método viewDidLoad da classe UIViewController e interceptará as chamadas. Não sei se isso viola as regras da loja de aplicativos. Nesse caso, provavelmente existem outras maneiras de contornar isso, mas você pode considerá-lo uma prova de conceito. Você pode, por exemplo, criar uma subclasse de todos os tipos principais de controladores de exibição e herdar todos os seus próprios controladores de exibição e, em seguida, você pode usar a ideia da categoria DarkMode e entrar nela para forçar a desativação de todos os controladores de exibição. É mais feio, mas não vai quebrar nenhuma regra. Eu prefiro usar o tempo de execução porque é para isso que o tempo de execução foi feito. Então, na minha versão, você apenas adiciona a categoria, define uma variável global para a categoria, quer queira ou não bloquear o modo escuro, e ele fará isso.
Você ainda não saiu da floresta, como mencionado, o outro problema é que o UIColor está basicamente fazendo o que quiser. Portanto, mesmo que seus controladores de tela estejam bloqueando o modo escuro, o UIColor não sabe onde ou como você o está usando, por isso não pode se adaptar. Como resultado, você pode buscá-lo corretamente, mas, em algum momento, ele reverterá em você. Talvez em breve, talvez mais tarde. Portanto, a solução é alocá-lo duas vezes usando um CGColor e transformá-lo em uma cor estática. Isso significa que, se o usuário voltar e reativar o modo escuro na sua página de configurações (a idéia aqui é fazer com que isso funcione para que o usuário tenha controle sobre seu aplicativo além do resto do sistema), todas essas cores estáticas precisa substituir. Até agora, isso resta para outra pessoa resolver. A maneira mais fácil de fazer isso é tornar um padrão que você Ao sair do modo escuro, divida por zero para travar o aplicativo, pois você não pode sair dele e peça ao usuário que o reinicie. Provavelmente isso também viola as diretrizes da loja de aplicativos, mas é uma ideia.
A categoria UIColor não precisa ser exposta, apenas funciona chamando colorNamed: ... se você não disse à classe DarkMode ViewController para bloquear o modo escuro, ela funcionará perfeitamente como esperado. Tentando criar algo elegante em vez do código padrão de sphaghetti da maçã, o que significa que você precisará modificar a maior parte do seu aplicativo se desejar desativar programaticamente o modo escuro ou alterná-lo. Agora não sei se existe uma maneira melhor de alterar programaticamente o Info.plist para desativar o modo escuro conforme necessário. No meu entender, esse é um recurso de tempo de compilação e, depois disso, você é desossado.
Então, aqui está o código que você precisa. Deve aparecer e usar apenas um método para definir o estilo da interface do usuário ou o padrão no código. Você é livre para usar, modificar, fazer o que quiser com isso para qualquer finalidade e não há garantia e não sei se ele passará na loja de aplicativos. Melhorias muito bem-vindas.
Aviso justo Eu não uso o ARC ou qualquer outro método de retenção manual.
////// 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
Há um conjunto de funções utilitárias que ele usa para fazer a troca de métodos. Arquivo separado. Porém, isso é padrão e você pode encontrar código semelhante em qualquer lugar.
// 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;
}
}
Estou copiando e colando isso em alguns arquivos desde que o q-runtime.h é minha biblioteca reutilizável e isso é apenas parte dela. Se algo não compilar, me avise.
UIUserInterfaceStyle
comoLight
na sua Info.Plist. Veja developer.apple.com/library/archive/documentation/General/…