FluentDarkModeKit/Sources/DarkModeCore/DMTraitCollection.m

318 строки
11 KiB
Objective-C

//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
#import "DMEnvironmentConfiguration.h"
#import "DMTraitCollection+DarkModeKitSwizzling.h"
#import "UIView+DarkModeKitSwizzling.h"
#import "UIImage+DarkModeKitSwizzling.h"
@import ObjectiveC;
@interface NSObject (DMTraitEnvironment)
+ (void)swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange API_AVAILABLE(ios(13.0));
@end
@implementation NSObject (DMTraitEnvironment)
+ (void)swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange {
[self swizzleTraitCollectionDidChangeToDMTraitCollectionDidChangeWithBlock:nil];
}
+ (void)swizzleTraitCollectionDidChangeToDMTraitCollectionDidChangeWithBlock:(void (^)(id<UITraitEnvironment>, UITraitCollection *))block API_AVAILABLE(ios(13.0)) {
// Only swizzling classes that conforms to both UITraitEnvironment & DMTraitEnvironment
if (!class_conformsToProtocol(self, @protocol(UITraitEnvironment)) || !class_conformsToProtocol(self, @protocol(DMTraitEnvironment))) {
return;
}
SEL selector = @selector(traitCollectionDidChange:);
Method method = class_getInstanceMethod(self, selector);
if (!method)
NSAssert(NO, @"Method not found for [%@ traitCollectionDidChange:]", NSStringFromClass(self));
IMP imp = method_getImplementation(method);
class_replaceMethod(self, selector, imp_implementationWithBlock(^(id<UITraitEnvironment> self, UITraitCollection *previousTraitCollection) {
// Call previous implementation
((void (*)(NSObject *, SEL, UITraitCollection *))imp)(self, selector, previousTraitCollection);
// Call DMTraitEnvironment method
[(id <DMTraitEnvironment>)self dmTraitCollectionDidChange:previousTraitCollection == nil ? nil : [DMTraitCollection traitCollectionWithUITraitCollection:previousTraitCollection]];
// Call custom block
if (block) {
block(self, previousTraitCollection);
}
}), method_getTypeEncoding(method));
}
@end
@implementation DMTraitCollection
static DMTraitCollection *_overrideTraitCollection = nil; // This is set manually in setOverrideTraitCollection:animated
static void (^_userInterfaceStyleChangeHandler)(DMTraitCollection *, BOOL) = nil;
static void (^_themeChangeHandler)(void) = nil;
static BOOL _isObservingNewWindowAddNotification = NO;
+ (DMTraitCollection *)currentTraitCollection {
if (@available(iOS 13.0, *)) {
return [DMTraitCollection traitCollectionWithUITraitCollection:UITraitCollection.currentTraitCollection];
}
return [self overrideTraitCollection];
}
+ (DMTraitCollection *)overrideTraitCollection {
if (!_overrideTraitCollection) {
// Provide unspecified at first
_overrideTraitCollection = [DMTraitCollection traitCollectionWithUserInterfaceStyle:DMUserInterfaceStyleUnspecified];
}
return _overrideTraitCollection;
}
+ (DMTraitCollection *)currentSystemTraitCollection API_AVAILABLE(ios(13.0)) {
return [DMTraitCollection traitCollectionWithUITraitCollection:UIScreen.mainScreen.traitCollection];
}
+ (void)setOverrideTraitCollection:(DMTraitCollection *)overrideTraitCollection animated:(BOOL)animated {
_overrideTraitCollection = overrideTraitCollection;
[self syncImmediatelyAnimated:animated];
}
+ (void)updateUIWithViews:(NSArray<UIView *> *)views viewControllers:(NSArray<UIViewController *> *)viewControllers traitCollection:(DMTraitCollection *)traitCollection animated:(BOOL)animated {
NSMutableArray<UIView *> *snapshotViews = nil;
if (animated) {
// Create snapshot views to ease the transition
snapshotViews = [NSMutableArray array];
[views enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
if (view.isHidden) // Skip hidden views
return;
UIView *snapshotView = [view snapshotViewAfterScreenUpdates:NO];
if (snapshotView) {
[view addSubview:snapshotView];
[snapshotViews addObject:snapshotView];
}
}];
[viewControllers enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull vc, NSUInteger idx, BOOL * _Nonnull stop) {
if (!vc.isViewLoaded || vc.view.isHidden) // Skip view controllers that are not loaded and hidden views
return;
UIView *snapshotView = [vc.view snapshotViewAfterScreenUpdates:NO];
if (snapshotView) {
[vc.view addSubview:snapshotView];
[snapshotViews addObject:snapshotView];
}
}];
}
[views enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
if (@available(iOS 13.0, *)) {
// Let the system propogate the change
view.overrideUserInterfaceStyle = traitCollection.uiTraitCollection.userInterfaceStyle;
}
else {
// Propogate the change to subviews
[view dmTraitCollectionDidChange:nil];
}
}];
[viewControllers enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull vc, NSUInteger idx, BOOL * _Nonnull stop) {
if (@available(iOS 13.0, *)) {
// Let the system propogate the change
vc.overrideUserInterfaceStyle = traitCollection.uiTraitCollection.userInterfaceStyle;
}
else {
// Propogate the change to subviews
[vc dmTraitCollectionDidChange:nil];
}
}];
if (animated) {
[UIViewPropertyAnimator runningPropertyAnimatorWithDuration:0.25 delay:0 options:0 animations:^{
[snapshotViews enumerateObjectsUsingBlock:^(UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
view.alpha = 0;
}];
} completion:^(UIViewAnimatingPosition finalPosition) {
[snapshotViews enumerateObjectsUsingBlock:^(UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
[view removeFromSuperview];
}];
}];
}
}
+ (DMTraitCollection *)traitCollectionWithUserInterfaceStyle:(DMUserInterfaceStyle)userInterfaceStyle {
DMTraitCollection *traitCollection = [[DMTraitCollection alloc] init];
traitCollection->_userInterfaceStyle = userInterfaceStyle;
return traitCollection;
}
+ (DMTraitCollection *)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection {
DMUserInterfaceStyle style = DMUserInterfaceStyleUnspecified;
switch (traitCollection.userInterfaceStyle) {
case UIUserInterfaceStyleLight:
style = DMUserInterfaceStyleLight;
break;
case UIUserInterfaceStyleDark:
style = DMUserInterfaceStyleDark;
break;
case UIUserInterfaceStyleUnspecified:
default:
style = DMUserInterfaceStyleUnspecified;
break;
}
return [self traitCollectionWithUserInterfaceStyle:style];
}
- (UITraitCollection *)uiTraitCollection {
UIUserInterfaceStyle style = UIUserInterfaceStyleUnspecified;
switch (_userInterfaceStyle) {
case DMUserInterfaceStyleLight:
style = UIUserInterfaceStyleLight;
break;
case DMUserInterfaceStyleDark:
style = UIUserInterfaceStyleDark;
break;
case DMUserInterfaceStyleUnspecified:
default:
style = UIUserInterfaceStyleUnspecified;
break;
}
return [UITraitCollection traitCollectionWithUserInterfaceStyle:style];
}
- (instancetype)init {
self = [super init];
if (self) {
_userInterfaceStyle = DMUserInterfaceStyleUnspecified;
}
return self;
}
// MARK: - Observer Registration
+ (void)registerWithApplication:(UIApplication *)application syncImmediately:(BOOL)syncImmediately animated:(BOOL)animated {
__weak UIApplication *weakApp = application;
__weak typeof(self) weakSelf = self;
_userInterfaceStyleChangeHandler = ^(DMTraitCollection *traitCollection, BOOL animated) {
__strong UIApplication *strongApp = weakApp;
if (!strongApp)
return;
[weakSelf updateUIWithViews:strongApp.windows viewControllers:nil traitCollection:traitCollection animated:animated];
if (_themeChangeHandler)
_themeChangeHandler();
};
[self observeNewWindowNotificationIfNeeded];
if (syncImmediately)
[self syncImmediatelyAnimated:animated];
}
+ (void)registerWithViewController:(UIViewController *)viewController syncImmediately:(BOOL)syncImmediately animated:(BOOL)animated {
__weak UIViewController *weakVc = viewController;
__weak typeof(self) weakSelf = self;
_userInterfaceStyleChangeHandler = ^(DMTraitCollection *traitCollection, BOOL animated) {
__strong UIViewController *strongVc = weakVc;
if (!strongVc)
return;
[weakSelf updateUIWithViews:nil viewControllers:[NSArray arrayWithObject:strongVc] traitCollection:traitCollection animated:animated];
if (_themeChangeHandler)
_themeChangeHandler();
};
if (syncImmediately)
[self syncImmediatelyAnimated:animated];
}
+ (void)observeNewWindowNotificationIfNeeded {
if (_isObservingNewWindowAddNotification)
return;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidBecomeVisible:) name:UIWindowDidBecomeVisibleNotification object:nil];
}
+ (void)windowDidBecomeVisible:(NSNotification *)notification {
NSObject *object = [notification object];
if ([object isKindOfClass:[UIWindow class]])
[self updateUIWithViews:@[(UIWindow *)object] viewControllers:nil traitCollection:[self overrideTraitCollection] animated:NO];
}
+ (void)syncImmediatelyAnimated:(BOOL)animated {
if (_userInterfaceStyleChangeHandler)
_userInterfaceStyleChangeHandler([self overrideTraitCollection], animated);
}
+ (void)unregister {
_userInterfaceStyleChangeHandler = nil;
if (_isObservingNewWindowAddNotification) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIWindowDidBecomeVisibleNotification object:nil];
_isObservingNewWindowAddNotification = NO;
}
}
// MARK: - Swizzling
+ (void)swizzleUIScreenTraitCollectionDidChange API_AVAILABLE(ios(13.0)) {
static dispatch_once_t onceToken;
__weak typeof(self) weakSelf = self;
dispatch_once(&onceToken, ^{
[UIScreen swizzleTraitCollectionDidChangeToDMTraitCollectionDidChangeWithBlock:^(id<UITraitEnvironment> object, UITraitCollection *previousTraitCollection) {
if ([DMTraitCollection overrideTraitCollection].userInterfaceStyle != DMUserInterfaceStyleUnspecified) {
// User has specified explicit dark mode or light mode
return;
}
[weakSelf syncImmediatelyAnimated:YES];
}];
});
}
@end
@implementation DMTraitCollection (DarkModeKitSwizzling)
+ (void)setupEnvironmentWithConfiguration:(DMEnvironmentConfiguration *)configuration {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 13.0, *)) {
[DMTraitCollection swizzleUIScreenTraitCollectionDidChange];
[UIView swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange];
[UIViewController swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange];
if (!configuration.useImageAsset)
[UIImage dm_swizzleIsEqual];
}
else {
[UIView dm_swizzleSetTintColor];
[UIView dm_swizzleSetBackgroundColor];
[UIImage dm_swizzleIsEqual];
}
_themeChangeHandler = configuration.themeChangeHandler;
});
}
@end
@interface UIScreen (DMTraitEnvironment) <DMTraitEnvironment>
@end
@implementation UIScreen (DMTraitEnvironment)
- (DMTraitCollection *)dmTraitCollection {
if (@available(iOS 13.0, *)) {
return [DMTraitCollection traitCollectionWithUITraitCollection:self.traitCollection];
}
return DMTraitCollection.overrideTraitCollection;
}
- (void)dmTraitCollectionDidChange:(DMTraitCollection *)previousTraitCollection {}
@end