En realidad, acabo de escribir un código que le permitirá optar globalmente por el modo oscuro en el código sin tener que usar putz con cada controlador viw de su aplicación. Esto probablemente se puede refinar para optar por una clase por clase administrando una lista de clases. Para mí, lo que quiero es que mis usuarios vean si les gusta la interfaz de modo oscuro para mi aplicación, y si no les gusta, pueden desactivarla. Esto les permitirá continuar usando el modo oscuro para el resto de sus aplicaciones.
La elección del usuario es buena (ejem, mirándote Apple, así es como deberías haberlo implementado).
Entonces, cómo funciona esto es que es solo una categoría de UIViewController. Cuando se carga, reemplaza el método viewDidLoad nativo con uno que verificará un indicador global para ver si el modo oscuro está desactivado para todo o no.
Debido a que se activa al cargar UIViewController, debería iniciarse automáticamente y deshabilitar el modo oscuro de forma predeterminada. Si esto no es lo que quieres, entonces debes llegar temprano a algún lugar y configurar la bandera, o simplemente configurar la bandera predeterminada.
Todavía no he escrito nada para responder al usuario que activa o desactiva la bandera. Entonces este es básicamente un código de ejemplo. Si queremos que el usuario interactúe con esto, todos los controladores de vista deberán volver a cargarse. No sé cómo hacer eso de forma espontánea, pero probablemente enviar alguna notificación será suficiente. Así que en este momento, este encendido / apagado global para el modo oscuro solo funcionará al iniciar o reiniciar la aplicación.
Ahora, no es suficiente tratar de desactivar el modo oscuro en cada MFING viewController en su gran aplicación. Si está utilizando activos de color, está completamente deshuesado. Por más de 10 años hemos entendido que los objetos inmutables son inmutables. Los colores que obtiene del catálogo de activos de color dicen que son UIColor pero son colores dinámicos (mutables) y cambiarán debajo de usted a medida que el sistema cambie del modo oscuro al claro. Se supone que eso es una característica. Pero, por supuesto, no hay una palanca maestra para pedirle a estas cosas que dejen de hacer este cambio (por lo que sé en este momento, tal vez alguien pueda mejorar esto).
Entonces la solución está en dos partes:
una categoría pública en UIViewController que ofrece algunos métodos de utilidad y conveniencia ... por ejemplo, no creo que Apple haya pensado en el hecho de que algunos de nosotros mezclamos código web en nuestras aplicaciones. Como tal, tenemos hojas de estilo que deben alternarse según el modo oscuro o claro. Por lo tanto, necesita construir algún tipo de objeto de hoja de estilo dinámico (lo que sería bueno) o simplemente preguntar cuál es el estado actual (malo pero fácil).
esta categoría cuando se carga reemplazará el método viewDidLoad de la clase UIViewController e interceptará llamadas. No sé si eso rompe las reglas de la tienda de aplicaciones. Si lo hace, probablemente hay otras formas de evitarlo, pero puede considerarlo una prueba de concepto. Por ejemplo, puede crear una subclase de todos los tipos de controladores de vista principales y hacer que todos sus propios controladores de vista hereden de ellos, y luego puede usar la idea de categoría DarkMode y llamarla para forzar la exclusión voluntaria de todos sus controladores de vista. Es más feo pero no va a romper ninguna regla. Prefiero usar el tiempo de ejecución porque para eso se hizo el tiempo de ejecución. Entonces, en mi versión, solo agrega la categoría, establece una variable global en la categoría para saber si desea o no bloquear el modo oscuro, y lo hará.
Todavía no está fuera de peligro, como se mencionó, el otro problema es que UIColor básicamente hace lo que quiera. Entonces, incluso si sus controladores de vista están bloqueando el modo oscuro, UIColor no sabe dónde o cómo lo está utilizando, por lo que no puede adaptarse. Como resultado, puede buscarlo correctamente, pero luego volverá sobre usted en algún momento en el futuro. Tal vez pronto tal vez más tarde. Entonces, la forma de evitarlo es asignándolo dos veces usando un CGColor y convirtiéndolo en un color estático. Esto significa que si su usuario regresa y vuelve a habilitar el modo oscuro en su página de configuración (la idea aquí es hacer que esto funcione para que el usuario tenga control sobre su aplicación más allá del resto del sistema), todos esos colores estáticos necesita ser reemplazado Hasta ahora, esto queda para que alguien más lo resuelva. La manera más fácil de hacerlo es establecer un valor predeterminado que optando por salir del modo oscuro, divida entre cero para bloquear la aplicación, ya que no puede salir de ella y dígale al usuario que simplemente la reinicie. Eso probablemente también viola las pautas de la tienda de aplicaciones, pero es una idea.
La categoría UIColor no necesita exponerse, solo funciona llamando a colorNamed: ... si no le dijo a la clase DarkMode ViewController que bloquee el modo oscuro, funcionará perfectamente como se esperaba. Intentar hacer algo elegante en lugar del código estándar de sphaghetti de Apple, lo que significa que tendrá que modificar la mayor parte de su aplicación si desea optar programáticamente por el modo oscuro o alternarlo. Ahora no sé si hay una mejor manera de alterar programáticamente Info.plist para desactivar el modo oscuro según sea necesario. Hasta donde tengo entendido, esa es una característica de tiempo de compilación y después de eso estás deshuesado.
Así que aquí está el código que necesitas. Debe aparecer y usar el único método para establecer el estilo de la interfaz de usuario o establecer el valor predeterminado en el código. Usted es libre de usar, modificar, hacer lo que quiera con esto para cualquier propósito y no se otorga ninguna garantía y no sé si pasará la tienda de aplicaciones. Mejoras muy bienvenidas.
Advertencia justa No uso ARC ni ningún otro método de retención.
////// 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
Hay un conjunto de funciones de utilidad que esto usa para realizar el intercambio de métodos. Archivo separado. Sin embargo, esto es algo estándar y puedes encontrar código similar en cualquier 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;
}
}
Estoy copiando y pegando esto de un par de archivos ya que q-runtime.h es mi biblioteca reutilizable y esto es solo una parte de ella. Si algo no se compila, hágamelo saber.
UIUserInterfaceStyle
aLight
en su info.plist. Ver developer.apple.com/library/archive/documentation/General/…