Files
featherVoice/TUIKit/TUICore/TUIThemeManager.m

526 lines
18 KiB
Mathematica
Raw Normal View History

2025-08-08 10:49:36 +08:00
//
// TUIThemeManager.m
// TUICore
//
// Created by harvy on 2022/1/5.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIThemeManager.h"
#import "UIColor+TUIHexColor.h"
@interface TUIDarkThemeRootVC : UIViewController
@end
@implementation TUIDarkThemeRootVC
- (BOOL)shouldAutorotate {
return NO;
}
@end
@interface TUIDarkWindow : UIWindow
@property(nonatomic, readonly, class) TUIDarkWindow *sharedInstance;
@property(nonatomic, strong) UIWindow *previousKeyWindow;
@end
@implementation TUIDarkWindow
+ (void)load {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(windowDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
}
+ (void)windowDidBecomeActive {
UIWindow *darkWindow = [self sharedInstance];
if (@available(iOS 13.0, *)) {
UIScene *scene = UIApplication.sharedApplication.connectedScenes.anyObject;
if (scene) {
darkWindow.windowScene = (UIWindowScene *)scene;
}
}
[darkWindow setRootViewController:[TUIDarkThemeRootVC new]];
darkWindow.hidden = NO;
[NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)becomeKeyWindow {
_previousKeyWindow = [self appKeyWindow];
[super becomeKeyWindow];
}
- (void)resignKeyWindow {
[super resignKeyWindow];
[_previousKeyWindow makeKeyWindow];
_previousKeyWindow = nil;
}
- (UIWindow *)appKeyWindow {
UIWindow *keywindow = UIApplication.sharedApplication.keyWindow;
if (keywindow == nil) {
if (@available(iOS 13.0, *)) {
for (UIWindowScene *scene in UIApplication.sharedApplication.connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive) {
UIWindow *tmpWindow = nil;
if (@available(iOS 15.0, *)) {
tmpWindow = scene.keyWindow;
}
if (tmpWindow == nil) {
for (UIWindow *window in scene.windows) {
if (window.windowLevel == UIWindowLevelNormal && window.hidden == NO &&
CGRectEqualToRect(window.bounds, UIScreen.mainScreen.bounds)) {
tmpWindow = window;
break;
}
}
}
}
}
}
}
if (keywindow == nil) {
for (UIWindow *window in UIApplication.sharedApplication.windows) {
if (window.windowLevel == UIWindowLevelNormal && window.hidden == NO && CGRectEqualToRect(window.bounds, UIScreen.mainScreen.bounds)) {
keywindow = window;
break;
}
}
}
return keywindow;
}
+ (instancetype)sharedInstance {
static TUIDarkWindow *shareWindow = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareWindow = [[self alloc] init];
shareWindow.frame = UIScreen.mainScreen.bounds;
shareWindow.userInteractionEnabled = YES;
shareWindow.windowLevel = UIWindowLevelNormal - 1;
shareWindow.hidden = YES;
shareWindow.opaque = NO;
shareWindow.backgroundColor = [UIColor clearColor];
shareWindow.layer.backgroundColor = [UIColor clearColor].CGColor;
});
return shareWindow;
}
- (void)setOverrideUserInterfaceStyle:(UIUserInterfaceStyle)overrideUserInterfaceStyle {
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 13.0, *)) {
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[NSNotificationCenter.defaultCenter postNotificationName:TUIDidApplyingThemeChangedNotfication object:nil];
if ([TUIThemeManager.shareManager respondsToSelector:@selector(allListenerExcuteonApplyThemeMethod:module:)]) {
[TUIThemeManager.shareManager performSelector:@selector(allListenerExcuteonApplyThemeMethod:module:) withObject:nil withObject:nil];
}
}
}
}
@end
@implementation TUITheme
+ (UIColor *)dynamicColor:(NSString *)colorKey module:(TUIThemeModule)module defaultColor:(NSString *)hex {
TUITheme *theme = TUICurrentTheme(module);
TUITheme *darkTheme = TUIDarkTheme(module);
if (theme) {
return [theme dynamicColor:colorKey defaultColor:hex];
} else {
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(UITraitCollection *_Nonnull traitCollection) {
switch (traitCollection.userInterfaceStyle) {
case UIUserInterfaceStyleDark:
if(darkTheme){
return [darkTheme dynamicColor:colorKey defaultColor:hex];
}
case UIUserInterfaceStyleLight:
case UIUserInterfaceStyleUnspecified:
default:
return [UIColor tui_colorWithHex:hex];
}
}];
} else {
return [UIColor tui_colorWithHex:hex];
}
}
}
- (UIColor *)dynamicColor:(NSString *)colorKey defaultColor:(NSString *)hex {
UIColor *color = nil;
NSString *colorHex = [self.manifest objectForKey:colorKey];
if (colorHex && [colorHex isKindOfClass:NSString.class]) {
color = [UIColor tui_colorWithHex:colorHex];
}
if (color == nil) {
color = [UIColor tui_colorWithHex:hex];
}
return color;
}
+ (UIImage *)dynamicImage:(NSString *)imageKey module:(TUIThemeModule)module defaultImage:(UIImage *)image {
TUITheme *theme = TUICurrentTheme(module);
TUITheme *darkTheme = TUIDarkTheme(module);
if (theme) {
return [theme dynamicImage:imageKey defaultImage:image];
} else {
UIImage *lightImage = image;
UIImage *darkImage = [darkTheme dynamicImage:imageKey defaultImage:image];
return [self imageWithImageLight:lightImage dark:darkImage];
}
}
- (UIImage *)dynamicImage:(NSString *)imageKey defaultImage:(UIImage *)image {
UIImage *dynamic = nil;
NSString *imageName = [self.manifest objectForKey:imageKey];
if ([imageName isKindOfClass:NSString.class]) {
imageName = [self.resourcePath stringByAppendingPathComponent:imageName];
dynamic = [UIImage imageWithContentsOfFile:imageName];
}
if (dynamic == nil) {
dynamic = image;
}
return dynamic;
}
+ (UIImage *)imageWithImageLight:(UIImage *)lightImage dark:(UIImage *)darkImage {
if (@available(iOS 13.0, *)) {
UITraitCollection *const scaleTraitCollection = [UITraitCollection currentTraitCollection];
UITraitCollection *const darkUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
UITraitCollection *const darkScaledTraitCollection =
[UITraitCollection traitCollectionWithTraitsFromCollections:@[ scaleTraitCollection, darkUnscaledTraitCollection ]];
UIImage *image = [lightImage
imageWithConfiguration:[lightImage.configuration
configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]];
darkImage = [darkImage
imageWithConfiguration:[darkImage.configuration
configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]];
[image.imageAsset registerImage:darkImage withTraitCollection:darkScaledTraitCollection];
return image;
} else {
return lightImage;
}
}
@end
@interface TUIThemeManager ()
/**
* The theme resource path of each module, module: theme path
*/
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSString *> *themeResourcePathCache;
/**
* The theme currently used by module, module: theme
*/
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, TUITheme *> *currentThemeCache;
/**
* The dark theme for each module, if any
*/
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, TUITheme *> *darkThemeCache;
@property(nonatomic, strong) NSHashTable *listeners;
- (void)allListenerExcuteonApplyThemeMethod:(TUITheme *)theme module:(TUIThemeModule)module;
@end
@implementation TUIThemeManager
#ifdef TUIThreadSafe
dispatch_queue_t _queue;
dispatch_queue_t _read_write_queue;
#endif
static id gShareInstance;
+ (instancetype)shareManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gShareInstance = [[self alloc] init];
});
return gShareInstance;
}
- (instancetype)init {
if (self = [super init]) {
#ifdef TUIThreadSafe
_queue = dispatch_queue_create("theme_manager_queue", DISPATCH_QUEUE_SERIAL);
_read_write_queue = dispatch_queue_create("read_write_secure_queue", DISPATCH_QUEUE_CONCURRENT);
#endif
_themeResourcePathCache = [NSMutableDictionary dictionary];
_currentThemeCache = [NSMutableDictionary dictionary];
_darkThemeCache = [NSMutableDictionary dictionary];
_listeners = [NSHashTable weakObjectsHashTable];
}
return self;
}
- (void)addListener:(id<TUIThemeManagerListener>)listener {
#ifdef TUIThreadSafe
dispatch_async(_queue, ^{
#endif
if (![self.listeners containsObject:listener]) {
[self.listeners addObject:listener];
}
#ifdef TUIThreadSafe
});
#endif
}
- (void)removeListener:(id<TUIThemeManagerListener>)listener {
#ifdef TUIThreadSafe
dispatch_async(_queue, ^{
#endif
if ([self.listeners containsObject:listener]) {
[self.listeners removeObject:listener];
}
#ifdef TUIThreadSafe
});
#endif
}
- (void)registerThemeResourcePath:(NSString *)path darkThemeID:(NSString *)darkThemeID forModule:(TUIThemeModule)module {
if (path.length == 0) {
return;
}
#ifdef TUIThreadSafe
dispatch_async(_queue, ^{
#endif
[self.themeResourcePathCache setObject:path forKey:@(module)];
TUITheme *theme = [self loadTheme:darkThemeID module:module];
if (theme) {
[self setDarkTheme:theme forModule:module];
}
#ifdef TUIThreadSafe
});
#endif
}
- (void)registerThemeResourcePath:(NSString *)path forModule:(TUIThemeModule)module {
[self registerThemeResourcePath:path darkThemeID:@"dark" forModule:module];
}
- (TUITheme *)currentThemeForModule:(TUIThemeModule)module {
__block TUITheme *theme = nil;
#ifdef TUIThreadSafe
dispatch_sync(_read_write_queue, ^{
#endif
theme = [self.currentThemeCache objectForKey:@(module)];
#ifdef TUIThreadSafe
});
#endif
return theme;
}
- (void)setCurrentTheme:(TUITheme *)theme forModule:(TUIThemeModule)module {
#ifdef TUIThreadSafe
dispatch_barrier_async(_read_write_queue, ^{
#endif
if ([self.currentThemeCache.allKeys containsObject:@(module)]) {
[self.currentThemeCache removeObjectForKey:@(module)];
}
if (theme) {
[self.currentThemeCache setObject:theme forKey:@(module)];
}
#ifdef TUIThreadSafe
});
#endif
}
- (TUITheme *)darkThemeForModule:(TUIThemeModule)module {
__block TUITheme *theme = nil;
#ifdef TUIThreadSafe
dispatch_sync(_read_write_queue, ^{
#endif
if ([self.darkThemeCache.allKeys containsObject:@(module)]) {
theme = [self.darkThemeCache objectForKey:@(module)];
}
#ifdef TUIThreadSafe
});
#endif
return theme;
}
- (void)setDarkTheme:(TUITheme *)theme forModule:(TUIThemeModule)module {
#ifdef TUIThreadSafe
dispatch_barrier_async(_read_write_queue, ^{
#endif
if ([self.darkThemeCache.allKeys containsObject:@(module)]) {
[self.darkThemeCache removeObjectForKey:@(module)];
}
if (theme) {
[self.darkThemeCache setObject:theme forKey:@(module)];
}
#ifdef TUIThreadSafe
});
#endif
}
- (void)applyTheme:(NSString *)themeID forModule:(TUIThemeModule)module {
if (themeID.length == 0) {
NSLog(@"[theme][applyTheme] invalid themeID, module:%zd", module);
return;
}
#ifdef TUIThreadSafe
dispatch_async(_queue, ^{
#endif
BOOL isAll = NO;
NSMutableArray *allKeys = [NSMutableArray arrayWithArray:self.themeResourcePathCache.allKeys];
if (module == TUIThemeModuleAll || ((module & TUIThemeModuleAll) == TUIThemeModuleAll)) {
isAll = YES;
}
if (isAll) {
for (NSNumber *moduleObject in allKeys) {
TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
[self doApplyTheme:themeID forSingleModule:tmpModue];
}
} else {
for (NSNumber *moduleObject in allKeys) {
TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
if ((module & tmpModue) == tmpModue) {
[self doApplyTheme:themeID forSingleModule:tmpModue];
}
}
}
#ifdef TUIThreadSafe
});
#endif
}
- (void)unApplyThemeForModule:(TUIThemeModule)module {
#ifdef TUIThreadSafe
dispatch_async(_queue, ^{
#endif
BOOL isAll = NO;
NSMutableArray *allKeys = [NSMutableArray arrayWithArray:self.themeResourcePathCache.allKeys];
if (module == TUIThemeModuleAll || ((module & TUIThemeModuleAll) == TUIThemeModuleAll)) {
isAll = YES;
}
if (isAll) {
for (NSNumber *moduleObject in allKeys) {
TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
[self setCurrentTheme:nil forModule:tmpModue];
}
[NSNotificationCenter.defaultCenter postNotificationName:TUIDidApplyingThemeChangedNotfication object:nil userInfo:nil];
} else {
for (NSNumber *moduleObject in allKeys) {
TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
if ((module & tmpModue) == tmpModue) {
[self setCurrentTheme:nil forModule:tmpModue];
}
}
}
#ifdef TUIThreadSafe
});
#endif
}
#pragma mark - Not thread safe
- (void)doApplyTheme:(NSString *)themeID forSingleModule:(TUIThemeModule)module {
TUITheme *theme = [self loadTheme:themeID module:module];
if (theme == nil) {
return;
}
[self setCurrentTheme:theme forModule:module];
[self notifyApplyTheme:theme module:module];
}
- (TUITheme *)loadTheme:(NSString *)themeID module:(TUIThemeModule)module {
NSString *themeResourcePath = [self themeResourcePathForModule:module];
if (themeResourcePath.length == 0) {
NSLog(@"[theme][applyTheme] theme resurce path not set, themeID:%@, module:%zd", themeID, module);
return nil;
}
themeResourcePath = [themeResourcePath stringByAppendingPathComponent:themeID];
{
BOOL isDirectory = NO;
BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:themeResourcePath isDirectory:&isDirectory];
if (!exist || !isDirectory) {
NSLog(@"[theme][applyTheme] invalid theme resurce, themeID:%@, module:%zd", themeID, module);
return nil;
}
}
NSString *manifestPath = [themeResourcePath stringByAppendingPathComponent:@"manifest.plist"];
{
BOOL isDirectory = NO;
BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:manifestPath isDirectory:&isDirectory];
if (!exist || isDirectory) {
NSLog(@"[theme][applyTheme] invalid manifest, themeID:%@, module:%zd", themeID, module);
return nil;
}
}
NSString *resourcePath = [themeResourcePath stringByAppendingPathComponent:@"resource"];
{
BOOL isDirectory = NO;
BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:resourcePath isDirectory:&isDirectory];
if (!exist || !isDirectory) {
NSLog(@"[theme][applyTheme] invalid resurce, themeID:%@, module:%zd", themeID, module);
}
}
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:manifestPath];
if (dict == nil) {
NSLog(@"[theme][applyTheme] manifest is null");
return nil;
}
TUITheme *theme = [[TUITheme alloc] init];
theme.themeID = themeID;
theme.module = module;
theme.themeDesc = [NSString stringWithFormat:@"theme_%@_%zd", themeID, module];
theme.resourcePath = resourcePath;
theme.manifest = dict;
return theme;
}
- (void)notifyApplyTheme:(TUITheme *)theme module:(TUIThemeModule)module {
if (theme == nil) {
return;
}
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf notifyApplyTheme:theme module:module];
});
return;
}
[self allListenerExcuteonApplyThemeMethod:theme module:module];
NSDictionary *userInfo = @{TUIDidApplyingThemeChangedNotficationModuleKey : @(module), TUIDidApplyingThemeChangedNotficationThemeKey : theme};
[NSNotificationCenter.defaultCenter postNotificationName:TUIDidApplyingThemeChangedNotfication object:nil userInfo:userInfo];
}
- (void)allListenerExcuteonApplyThemeMethod:(TUITheme *)theme module:(TUIThemeModule)module {
for (id<TUIThemeManagerListener> listener in self.listeners) {
if ([listener respondsToSelector:@selector(onApplyTheme:module:)]) {
[listener onApplyTheme:theme module:module];
}
}
}
- (NSString *)themeResourcePathForModule:(TUIThemeModule)module {
if ([self.themeResourcePathCache.allKeys containsObject:@(module)]) {
return [self.themeResourcePathCache objectForKey:@(module)];
}
return @"";
}
@end