iOS 13 отключить изменения Dark Mode - программирование
Подтвердить что ты не робот

iOS 13 отключить изменения Dark Mode

Мое приложение не подготовлено для Dark Mode, и я не собираюсь работать над ним сегодня.

Есть ли способ отключить изменения темного режима для моего приложения?

4b9b3361

Ответ 1

Во-первых, запись Apple, связанная с выходом из темного режима. Содержание по этой ссылке написано для Xcode 11 & iOS 13:

Этот раздел относится к использованию Xcode 11


Если вы хотите отказаться от ВСЕХ заявлений

Подход № 1

Используйте следующий ключ в вашем файле info.plist :

UIUserInterfaceStyle

И присвойте ему значение Light.

XML для назначения UIUserInterfaceStyle:

<key>UIUserInterfaceStyle</key>
<string>Light</string>

Подход № 2

Вы можете установить overrideUserInterfaceStyle для переменной app window.

В зависимости от того, как был создан ваш проект, это может быть в файле AppDelegate или SceneDelegate.

if #available(iOS 13.0, *) {
    window?.overrideUserInterfaceStyle = .light
}


Если вы хотите отказаться от своего UIViewController на индивидуальной основе

override func viewDidLoad() {
    super.viewDidLoad()
    // overrideUserInterfaceStyle is available with iOS 13
    if #available(iOS 13.0, *) {
        // Always adopt a light interface style.
        overrideUserInterfaceStyle = .light
    }
}

Документация Apple для overrideUserInterfaceStyle

Как приведенный выше код будет выглядеть в Xcode 11:

enter image description here

Этот раздел относится к использованию Xcode 10.x


Если вы используете Xcode 11 для представления, вы можете спокойно игнорировать все, что находится ниже этой строки.

Поскольку соответствующий API не существует в iOS 12, вы будете получать ошибки при попытке использовать значения, указанные выше:

Для настройки overrideUserInterfaceStyle в вашем UIViewController

enter image description here

Если вы хотите отказаться от своего UIViewController на индивидуальной основе

Это можно сделать в Xcode 10, протестировав версию компилятора и версию iOS:

#if compiler(>=5.1)
if #available(iOS 13.0, *) {
    // Always adopt a light interface style.
    overrideUserInterfaceStyle = .light
}
#endif

Если вы хотите отказаться от ВСЕХ заявлений

Вы можете изменить приведенный выше фрагмент для работы со всем приложением для Xcode 10, добавив следующий код в файл AppDelegate.

#if compiler(>=5.1)
if #available(iOS 13.0, *) {
    // Always adopt a light interface style.
    window?.overrideUserInterfaceStyle = .light
}
#endif

Однако настройка plist завершится ошибкой при использовании Xcode версии 10.x:

enter image description here

Благодарим @Aron Nelson, @Raimundas Sakalauskas, @NSLeader и rmaddy за улучшение ответа с помощью своих отзывов.

Ответ 2

Это может показаться подходом Objective-C, если я не ошибаюсь. Но это не всегда работает. Кажется, это всегда самая последняя вещь, которую ты можешь назвать.

- (void)viewDidLoad;
{
    [super viewDidLoad];
    [self setOverrideUserInterfaceStyle:UIUserInterfaceStyleLight];
}

Ответ 3

На самом деле я только что написал некоторый код, который позволит вам глобально отказаться от темного режима в коде, не прибегая к использованию каждого отдельного контроллера viw в вашем приложении. Вероятно, это может быть улучшено, чтобы отказаться от класса за классом, управляя списком классов. Я хочу, чтобы мои пользователи увидели, нравится ли им интерфейс темного режима для моего приложения, и если им это не нравится, они могут его отключить. Это позволит им продолжать использовать темный режим для остальных приложений.

Выбор пользователя хороший (Гм, глядя на тебя, Apple, вот как ты должен был это реализовать).

Итак, как это работает, так это то, что это просто категория UIViewController. Когда он загружается, он заменяет собственный метод viewDidLoad на тот, который будет проверять глобальный флаг, чтобы увидеть, отключен ли темный режим для всех или нет.

Поскольку он запускается при загрузке UIViewController, он должен автоматически запуститься и отключить темный режим по умолчанию. Если это не то, что вам нужно, тогда вам нужно рано или поздно войти туда и установить флаг, или просто установить флаг по умолчанию.

Я еще ничего не написал, чтобы пользователь мог включить или выключить флаг. Так что это в основном пример кода. Если мы хотим, чтобы пользователь взаимодействовал с этим, все контроллеры представления должны будут перезагрузиться. Я не знаю, как сделать это от руки, но, вероятно, отправка какого-то уведомления поможет. Так что сейчас это глобальное включение/выключение для темного режима будет работать только при запуске или перезапуске приложения.

Теперь недостаточно просто отключить темный режим в каждом отдельном контроллере MFING в вашем огромном приложении. Если вы используете цветовые активы, вы полностью очищены от костей. Мы в течение 10+ лет понимали, что неизменяемые объекты неизменны. Цвета, которые вы получаете из каталога цветовых активов, говорят, что это UIColor, но они являются динамическими (изменяемыми) цветами и будут меняться под вами, когда система переключается с темного на светлый режим. Это должно быть особенность. Но, конечно, нет главного переключателя, который бы попросил эти вещи прекратить вносить эти изменения (насколько я знаю сейчас, возможно, кто-то может улучшить это).

Итак, решение состоит из двух частей:

  1. публичная категория на UIViewController, которая предоставляет некоторые полезные и удобные методы... например, я не думаю, что Apple думала о том, что некоторые из нас смешивают веб-код с нашими приложениями. Таким образом, у нас есть таблицы стилей, которые нужно переключать на основе темного или светлого режима. Таким образом, вам нужно либо создать какой-то динамический объект таблицы стилей (что было бы хорошо), либо просто спросить, каково текущее состояние (плохо, но легко).

  2. эта категория при загрузке заменит метод viewDidLoad класса UIViewController и перехватывает вызовы. Я не знаю, нарушает ли это правила магазина приложений. Если это произойдет, есть и другие способы обойти это, но вы можете считать это доказательством концепции. Например, вы можете создать один подкласс из всех основных типов контроллеров представлений и сделать так, чтобы все ваши собственные контроллеры представлений наследовали от них, а затем вы можете использовать идею категории DarkMode и вызывать ее, чтобы заставить отказаться от всех ваших контроллеров представлений. Это уродливее, но не нарушает никаких правил. Я предпочитаю использовать среду выполнения, потому что это то, что было сделано для выполнения среды выполнения. Так что в моей версии вы просто добавляете категорию, вы устанавливаете глобальную переменную для категории, хотите ли вы, чтобы она блокировала темный режим, и она это сделает.

  3. Вы еще не вышли из леса, как уже упоминалось, другая проблема заключается в том, что UIColor делает все, что захочет. Так что даже если ваши контроллеры вида блокируют темный режим, UIColor не знает, где и как вы его используете, поэтому не может адаптироваться. В результате вы можете получить его правильно, но потом он вернется к вам в какой-то момент в будущем. Возможно, скоро, может быть, позже. Так что обойти это можно, выделив его дважды с помощью CGColor и превратив его в статический цвет. Это означает, что если ваш пользователь вернется и снова включит темный режим на странице настроек (идея заключается в том, чтобы заставить эту работу работать так, чтобы пользователь мог контролировать ваше приложение над остальной частью системы), все эти статические цвета нужна замена. Пока это остается решать кому-то другому. Самый простой способ сделать это - установить значение по умолчанию, которое вы выбираете из темного режима, разделить на ноль, чтобы завершить работу приложения, поскольку вы не можете выйти из него, и попросить пользователя просто перезапустить его. Это, вероятно, нарушает правила магазина приложений, но это идея.

Категория UIColor не нуждается в показе, она просто работает, вызывая colorNamed:... если вы не указали классу DarkMode ViewController блокировать темный режим, он будет работать прекрасно, как и ожидалось. Попытка сделать что-то элегантное вместо стандартного кода яблочного сфагетти, что будет означать, что вам придется изменить большую часть своего приложения, если вы хотите программно отказаться от темного режима или переключить его. Теперь я не знаю, есть ли лучший способ программно изменить Info.plist, чтобы отключить темный режим по мере необходимости. Насколько я понимаю, это особенность времени компиляции, и после этого вы костей.

Итак, вот код, который вам нужен. Должен быть вставлен и просто использовать один метод, чтобы установить стиль пользовательского интерфейса или установить по умолчанию в коде. Вы можете свободно использовать, изменять, делать с этим все, что вы хотите, для каких-либо целей, и никаких гарантий не дается, и я не знаю, пройдет ли это в магазине приложений. Улучшения приветствуются.

Справедливое предупреждение, я не использую 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 does not 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 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 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 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 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

Существует набор служебных функций, которые используются для замены метода. Отдельный файл. Это стандартный материал, и вы можете найти подобный код в любом месте.

// 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 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 - это моя библиотека многократного использования, и это только ее часть. Если что-то не компилируется, дайте мне знать.