iOS 13禁用黑暗模式更改

时间:2019-06-11 14:42:35

标签: ios ios13 ios-darkmode

我的应用程序未为黑暗模式做好准备,我今天不打算对其进行处理。

是否可以禁用我的应用的暗模式更改?

2 个答案:

答案 0 :(得分:22)

Here is where Apple discusses opting out of light or dark mode:

下面的代码将退出特定的视图控制器:

override func viewDidLoad() {
    super.viewDidLoad()

    // Always adopt a light interface style.    
    overrideUserInterfaceStyle = .light
}

或者您可以使用following key in your info.plist文件选择退出整个应用程序:

UIUserInterfaceStyle

并为其分配值Light

答案 1 :(得分:0)

实际上,我只是编写了一些代码,这些代码使您可以在代码中全局选择退出暗模式,而不必在应用程序中使用每个viw控制器。可以通过管理班级列表来完善此方法以逐班退出。对我来说,我要让用户查看他们是否喜欢我的应用程序的暗模式界面,如果他们不喜欢它,则可以将其关闭。这将使他们能够在其余应用程序中继续使用暗模式。

用户选择是好的(哎呀,看着苹果,这是应该实现的方式。)

所以这是如何工作的只是UIViewController的一个类别。加载时,它将替换本机的viewDidLoad方法,该方法将检查全局标志以查看是否对所有功能都禁用了暗模式。

由于它是在加载UIViewController时触发的,因此默认情况下应自动启动并禁用暗模式。如果这不是您想要的,那么您需要早点到达那里并设置标志,否则只需设置默认标志即可。

我还没有写任何东西来回应用户打开或关闭该标志。因此,这基本上是示例代码。如果我们希望用户与此交互,则所有视图控制器都需要重新加载。我不知道如何立即执行此操作,但是可能会发送一些通知来解决问题。因此,目前,暗模式下的全局打开/关闭仅在启动或重新启动应用程序时起作用。

现在,在大型应用程序的每个MFING viewController中尝试关闭暗模式还不够。如果您正在使用色彩资产,那么您将完全陷入困境。我们十多年以来一直将不可变对象理解为不可变的。从颜色资产目录中获得的颜色表示它们是UIColor,但它们是动态(可变)颜色,并且随着系统从暗到亮模式的变化,它们在您的下方也会发生变化。那应该是一个功能。但是,当然没有主开关要求这些事情停止进行更改(据我所知,也许有人可以对此进行改进)。

因此,解决方案分为两部分:

  1. UIViewController上的公共类别,提供了一些实用和便捷的方法...例如,我不认为苹果公司曾考虑过我们中的一些人将Web代码混入我们的应用程序这一事实。因此,我们需要基于暗或亮模式切换样式表。因此,您要么需要构建某种动态样式表对象(这会很好),要么仅询问当前状态是什么(不好但很容易)。

  2. 此类别在加载时将替换UIViewController类的viewDidLoad方法并拦截调用。我不知道这是否违反了应用商店的规则。如果是这样,可能还有其他方法可以解决,但您可以认为这是概念证明。例如,您可以使所有主要视图控制器类型的一个子类,并使您自己的所有视图控制器都继承自这些视图控制器,然后可以使用DarkMode类别提示并对其进行调用以强制选择退出所有视图控制器。这很丑陋,但不会违反任何规则。我更喜欢使用运行时,因为这是运行时要做的。因此,在我的版本中,您只需添加类别,就可以在类别上设置一个全局变量,以决定是否要阻止暗模式,

  3. 如上所述,您还没有走出困境,另一个问题是UIColor基本上可以完成所需的工作。因此,即使您的视图控制器阻止了黑暗模式,UIColor也不知道您在哪里或如何使用它,因此无法适应。结果,您可以正确地获取它,但是将来它将在某个时候恢复您的状态。也许很快,也许以后。因此,解决方法是使用CGColor对其分配两次,然后将其转换为静态颜色。这意味着,如果您的用户返回并在设置页面上重新启用暗模式(此处的目的是使此项工作正常进行,以便用户可以在系统的其余部分之上控制您的应用),所有这些静态颜色需要更换。到目前为止,这还有待其他人解决。最简单的方法是将您退出黑暗模式的默认值设为默认值,除以零会使应用程序崩溃,因为您无法退出该应用程序并告诉用户仅重新启动它。这可能也违反了应用商店指南,但这是一个主意。

不需要公开UIColor类别,它只需要调用colorNamed即可:...如果您不告诉DarkMode ViewController类阻止黑暗模式,它将按预期完美地工作。尝试使某些东西变得优雅,而不是标准的苹果花粉代码,这意味着如果要以编程方式选择退出暗模式或切换它,则必须修改大部分应用程序。现在,我不知道是否有更好的方法以编程方式更改Info.plist以根据需要关闭暗模式。据我所知,这是一个编译时功能,在此之后您便会陷入困境。

这是您需要的代码。应该插入,仅使用一种方法来设置UI样式或在代码中设置默认值。您可以出于任何目的随意使用,修改,做任何您想做的事情,并且不提供任何担保,而且我不知道它是否会通过应用程序商店。改进非常受欢迎。

公平警告,我不使用ARC或任何其他手持方法。

////// 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;
// this is necessary because trying to allocate a trait collection directly based on user interfaca
// style will return the "any" color out of the color assets, which is obviously what we don't want
static UIViewController *_petViewController;
+ (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);
            // sadly wasted space, but if you don't care about space you can make it on the fly
            _petViewController = [UIViewController new];
        }
    }
}


// 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,*)) {
        if (_override == UIUserInterfaceStyleUnspecified) {
            // nothing to do, we got it via the normal way because the bundle needs to load
            value = _nativeColorNamed(self,@selector(colorNamed:),string);
        } else {
            [_petViewController setOverrideUserInterfaceStyle:_override];
            UITraitCollection *tc = _petViewController.traitCollection;
            // below is the way that would be nice to do it, but doesn't work
            //[UITraitCollection traitCollectionWithUserInterfaceStyle:_override];
            value = [UIColor colorNamed:string inBundle:nil compatibleWithTraitCollection:tc];
            // the value we have is a dynamic color... this obviously breaks EVERYTHING we know about
            // UIKit programming, where the object is basically a mutable object posing as an immutable
            // one ... so we need to force it to become immutable and we can do that by making a new color
            // out of the CGColor ... otherwise as the app continues to run, the color we hand out here
            // is going to revert to whatever mode the operating system is in even when we specified dark
            // or light to begin with
            value = [UIColor colorWithCGColor:value.CGColor];
        }
    } 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

有一组实用程序函数可用于进行方法交换。单独的文件。不过,这是标准的东西,您可以在任何地方找到类似的代码。

// 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;
    }
}

我正在将其复制并粘贴到几个文件中,因为q-runtime.h是我的可重用库,而这只是其中的一部分。如果无法编译,请告诉我。