Właściwie właśnie napisałem kod, który pozwoli ci globalnie zrezygnować z trybu ciemnego w kodzie bez konieczności instalowania każdego kontrolera viw w twojej aplikacji. Prawdopodobnie można to poprawić, rezygnując z poszczególnych klas, zarządzając listą klas. Dla mnie chcę, aby użytkownicy zobaczyli, czy podoba im się interfejs trybu ciemnego dla mojej aplikacji, a jeśli im się to nie podoba, mogą go wyłączyć. Pozwoli im to kontynuować korzystanie z trybu ciemnego dla pozostałych aplikacji.
Wybór użytkownika jest dobry (Ahem, patrząc na ciebie Apple, tak powinieneś był go wdrożyć).
Jak to działa, to tylko kategoria UIViewController. Podczas ładowania zastępuje natywną metodę viewDidLoad taką, która sprawdzi flagę globalną, aby sprawdzić, czy tryb ciemny jest wyłączony dla wszystkich, czy nie.
Ponieważ jest on uruchamiany podczas ładowania UIViewController, powinien automatycznie uruchomić się i domyślnie wyłączyć tryb ciemny. Jeśli to nie jest to, czego potrzebujesz, musisz wcześniej gdzieś tam wejść i ustawić flagę, albo po prostu ustawić flagę domyślną.
Nie napisałem jeszcze nic, by odpowiedzieć na to, że użytkownik włącza lub wyłącza flagę. Jest to w zasadzie przykładowy kod. Jeśli chcemy, aby użytkownik z tym współdziałał, wszystkie kontrolery widoku będą musiały się ponownie załadować. Nie wiem, jak to zrobić odręcznie, ale prawdopodobnie wysłanie jakiegoś powiadomienia załatwi sprawę. Tak więc teraz to globalne włączanie / wyłączanie dla trybu ciemnego będzie działać tylko podczas uruchamiania lub ponownego uruchamiania aplikacji.
Teraz nie wystarczy spróbować wyłączyć tryb ciemny w każdym widoku kontrolera MFING w swojej ogromnej aplikacji. Jeśli używasz zasobów kolorów, jesteś całkowicie bez kości. Od ponad 10 lat rozumiemy niezmienne obiekty jako niezmienne. Kolory, które otrzymujesz z katalogu zasobów kolorów, mówią, że są to UIColor, ale są to kolory dynamiczne (zmienne) i będą się zmieniać pod tobą, gdy system zmieni się z trybu ciemnego na jasny. To ma być funkcja. Ale oczywiście nie ma przełącznika głównego, który prosi te rzeczy o zaprzestanie wprowadzania tej zmiany (o ile wiem teraz, może ktoś może to poprawić).
Rozwiązanie składa się z dwóch części:
kategoria publiczna na UIViewController, która daje pewne metody użyteczności i wygody ... na przykład nie sądzę, że Apple pomyślał o tym, że niektórzy z nas mieszają kod internetowy z naszymi aplikacjami. Jako takie mamy arkusze stylów, które należy przełączać w oparciu o tryb ciemny lub jasny. Dlatego musisz albo zbudować jakiś dynamiczny obiekt arkusza stylów (co byłoby dobre), albo po prostu zapytać, jaki jest obecny stan (zły, ale łatwy).
ta kategoria podczas ładowania zastąpi metodę viewDidLoad klasy UIViewController i przechwytuje połączenia. Nie wiem, czy to łamie zasady sklepu z aplikacjami. Jeśli tak, prawdopodobnie istnieją inne sposoby, ale można to uznać za dowód koncepcji. Możesz na przykład utworzyć jedną podklasę wszystkich głównych typów kontrolerów widoku i sprawić, że wszystkie własne kontrolery widoku odziedziczą po nich, a następnie możesz użyć idei kategorii DarkMode i wywołać ją, aby wymusić rezygnację ze wszystkich kontrolerów widoku. Jest brzydszy, ale nie złamie żadnych zasad. Wolę używać środowiska wykonawczego, ponieważ właśnie do tego zostało stworzone środowisko wykonawcze. Więc w mojej wersji po prostu dodajesz kategorię, ustawiasz zmienną globalną na kategorię, czy chcesz, aby blokowała tryb ciemny, i zrobi to.
Jak już wspomniano, jeszcze nie wyszedłeś z lasu, innym problemem jest to, że UIColor zasadniczo robi wszystko, do diabła, czego chce. Więc nawet jeśli kontrolery widoku blokują tryb ciemny, UIColor nie wie, gdzie i jak go używasz, więc nie można go dostosować. W rezultacie możesz go poprawnie pobrać, ale w pewnym momencie wróci do ciebie. Może wkrótce może później. Tak więc można to zrobić, przydzielając go dwukrotnie za pomocą CGColor i zmieniając go w kolor statyczny. Oznacza to, że jeśli użytkownik wróci i ponownie włączy tryb ciemny na stronie ustawień (chodzi o to, aby działał tak, aby użytkownik miał kontrolę nad aplikacją ponad resztą systemu), wszystkie te statyczne kolory wymagają wymiany. Jak dotąd pozostaje to do rozwiązania przez kogoś innego. Najłatwiejszym sposobem na zrobienie tego jest ustawienie domyślnej wartości „ wyłączając tryb ciemny, podziel przez zero, aby zawiesić aplikację, ponieważ nie możesz jej zamknąć i powiedzieć użytkownikowi, aby po prostu zrestartował. To prawdopodobnie narusza również wytyczne sklepu z aplikacjami, ale to pomysł.
Kategoria UIColor nie musi być ujawniana, działa po prostu wywołując colorNamed: ... jeśli nie powiedziałeś klasie DarkMode ViewController, aby blokowała tryb ciemny, będzie działała idealnie zgodnie z oczekiwaniami. Próba zrobienia czegoś eleganckiego zamiast standardowego kodu sphaghetti z jabłkiem, co oznacza, że będziesz musiał zmodyfikować większość aplikacji, jeśli chcesz programowo zrezygnować z trybu ciemnego lub go przełączyć. Teraz nie wiem, czy istnieje lepszy sposób na programową zmianę Info.plist, aby w razie potrzeby wyłączyć tryb ciemny. O ile dobrze rozumiem, jest to funkcja czasu kompilacji, a następnie jesteś bez kości.
Oto kod, którego potrzebujesz. Powinieneś wpaść i po prostu użyć jednej metody, aby ustawić styl interfejsu użytkownika lub ustawić wartość domyślną w kodzie. Możesz dowolnie używać, modyfikować, robić, co chcesz z tym do dowolnego celu i nie udziela się gwarancji, a ja nie wiem, czy przejdzie do sklepu z aplikacjami. Ulepszenia bardzo mile widziane.
Uczciwe ostrzeżenie Nie używam ARC ani innych metod trzymania w ręku.
////// 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
Istnieje zestaw funkcji narzędziowych, których używa do wykonywania wymiany metod. Oddzielny plik. Jest to jednak standard i podobny kod można znaleźć w dowolnym miejscu.
// 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;
}
}
Kopiuję i wklejam to z kilku plików, ponieważ q-runtime.h jest moją biblioteką wielokrotnego użytku i to tylko część tego. Jeśli coś się nie kompiluje, daj mi znać.
UIUserInterfaceStyle
naLight
w swojej Info.Plist. Zobacz developer.apple.com/library/archive/documentation/General/…