TUIThemeManager.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. //
  2. // TUIThemeManager.m
  3. // TUICore
  4. //
  5. // Created by harvy on 2022/1/5.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. #import "TUIThemeManager.h"
  9. #import "UIColor+TUIHexColor.h"
  10. @interface TUIDarkThemeRootVC : UIViewController
  11. @end
  12. @implementation TUIDarkThemeRootVC
  13. - (BOOL)shouldAutorotate {
  14. return NO;
  15. }
  16. @end
  17. @interface TUIDarkWindow : UIWindow
  18. @property(nonatomic, readonly, class) TUIDarkWindow *sharedInstance;
  19. @property(nonatomic, strong) UIWindow *previousKeyWindow;
  20. @end
  21. @implementation TUIDarkWindow
  22. + (void)load {
  23. [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(windowDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
  24. }
  25. + (void)windowDidBecomeActive {
  26. UIWindow *darkWindow = [self sharedInstance];
  27. if (@available(iOS 13.0, *)) {
  28. UIScene *scene = UIApplication.sharedApplication.connectedScenes.anyObject;
  29. if (scene) {
  30. darkWindow.windowScene = (UIWindowScene *)scene;
  31. }
  32. }
  33. [darkWindow setRootViewController:[TUIDarkThemeRootVC new]];
  34. darkWindow.hidden = NO;
  35. [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
  36. }
  37. - (void)becomeKeyWindow {
  38. _previousKeyWindow = [self appKeyWindow];
  39. [super becomeKeyWindow];
  40. }
  41. - (void)resignKeyWindow {
  42. [super resignKeyWindow];
  43. [_previousKeyWindow makeKeyWindow];
  44. _previousKeyWindow = nil;
  45. }
  46. - (UIWindow *)appKeyWindow {
  47. UIWindow *keywindow = UIApplication.sharedApplication.keyWindow;
  48. if (keywindow == nil) {
  49. if (@available(iOS 13.0, *)) {
  50. for (UIWindowScene *scene in UIApplication.sharedApplication.connectedScenes) {
  51. if (scene.activationState == UISceneActivationStateForegroundActive) {
  52. UIWindow *tmpWindow = nil;
  53. if (@available(iOS 15.0, *)) {
  54. tmpWindow = scene.keyWindow;
  55. }
  56. if (tmpWindow == nil) {
  57. for (UIWindow *window in scene.windows) {
  58. if (window.windowLevel == UIWindowLevelNormal && window.hidden == NO &&
  59. CGRectEqualToRect(window.bounds, UIScreen.mainScreen.bounds)) {
  60. tmpWindow = window;
  61. break;
  62. }
  63. }
  64. }
  65. }
  66. }
  67. }
  68. }
  69. if (keywindow == nil) {
  70. for (UIWindow *window in UIApplication.sharedApplication.windows) {
  71. if (window.windowLevel == UIWindowLevelNormal && window.hidden == NO && CGRectEqualToRect(window.bounds, UIScreen.mainScreen.bounds)) {
  72. keywindow = window;
  73. break;
  74. }
  75. }
  76. }
  77. return keywindow;
  78. }
  79. + (instancetype)sharedInstance {
  80. static TUIDarkWindow *shareWindow = nil;
  81. static dispatch_once_t onceToken;
  82. dispatch_once(&onceToken, ^{
  83. shareWindow = [[self alloc] init];
  84. shareWindow.frame = UIScreen.mainScreen.bounds;
  85. shareWindow.userInteractionEnabled = YES;
  86. shareWindow.windowLevel = UIWindowLevelNormal - 1;
  87. shareWindow.hidden = YES;
  88. shareWindow.opaque = NO;
  89. shareWindow.backgroundColor = [UIColor clearColor];
  90. shareWindow.layer.backgroundColor = [UIColor clearColor].CGColor;
  91. });
  92. return shareWindow;
  93. }
  94. - (void)setOverrideUserInterfaceStyle:(UIUserInterfaceStyle)overrideUserInterfaceStyle {
  95. }
  96. - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
  97. [super traitCollectionDidChange:previousTraitCollection];
  98. if (@available(iOS 13.0, *)) {
  99. if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
  100. [NSNotificationCenter.defaultCenter postNotificationName:TUIDidApplyingThemeChangedNotfication object:nil];
  101. if ([TUIThemeManager.shareManager respondsToSelector:@selector(allListenerExcuteonApplyThemeMethod:module:)]) {
  102. [TUIThemeManager.shareManager performSelector:@selector(allListenerExcuteonApplyThemeMethod:module:) withObject:nil withObject:nil];
  103. }
  104. }
  105. }
  106. }
  107. @end
  108. @implementation TUITheme
  109. + (UIColor *)dynamicColor:(NSString *)colorKey module:(TUIThemeModule)module defaultColor:(NSString *)hex {
  110. TUITheme *theme = TUICurrentTheme(module);
  111. TUITheme *darkTheme = TUIDarkTheme(module);
  112. if (theme) {
  113. return [theme dynamicColor:colorKey defaultColor:hex];
  114. } else {
  115. if (@available(iOS 13.0, *)) {
  116. return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(UITraitCollection *_Nonnull traitCollection) {
  117. switch (traitCollection.userInterfaceStyle) {
  118. case UIUserInterfaceStyleDark:
  119. if(darkTheme){
  120. return [darkTheme dynamicColor:colorKey defaultColor:hex];
  121. }
  122. case UIUserInterfaceStyleLight:
  123. case UIUserInterfaceStyleUnspecified:
  124. default:
  125. return [UIColor tui_colorWithHex:hex];
  126. }
  127. }];
  128. } else {
  129. return [UIColor tui_colorWithHex:hex];
  130. }
  131. }
  132. }
  133. - (UIColor *)dynamicColor:(NSString *)colorKey defaultColor:(NSString *)hex {
  134. UIColor *color = nil;
  135. NSString *colorHex = [self.manifest objectForKey:colorKey];
  136. if (colorHex && [colorHex isKindOfClass:NSString.class]) {
  137. color = [UIColor tui_colorWithHex:colorHex];
  138. }
  139. if (color == nil) {
  140. color = [UIColor tui_colorWithHex:hex];
  141. }
  142. return color;
  143. }
  144. + (UIImage *)dynamicImage:(NSString *)imageKey module:(TUIThemeModule)module defaultImage:(UIImage *)image {
  145. TUITheme *theme = TUICurrentTheme(module);
  146. TUITheme *darkTheme = TUIDarkTheme(module);
  147. if (theme) {
  148. return [theme dynamicImage:imageKey defaultImage:image];
  149. } else {
  150. UIImage *lightImage = image;
  151. UIImage *darkImage = [darkTheme dynamicImage:imageKey defaultImage:image];
  152. return [self imageWithImageLight:lightImage dark:darkImage];
  153. }
  154. }
  155. - (UIImage *)dynamicImage:(NSString *)imageKey defaultImage:(UIImage *)image {
  156. UIImage *dynamic = nil;
  157. NSString *imageName = [self.manifest objectForKey:imageKey];
  158. if ([imageName isKindOfClass:NSString.class]) {
  159. imageName = [self.resourcePath stringByAppendingPathComponent:imageName];
  160. dynamic = [UIImage imageWithContentsOfFile:imageName];
  161. }
  162. if (dynamic == nil) {
  163. dynamic = image;
  164. }
  165. return dynamic;
  166. }
  167. + (UIImage *)imageWithImageLight:(UIImage *)lightImage dark:(UIImage *)darkImage {
  168. if (@available(iOS 13.0, *)) {
  169. UITraitCollection *const scaleTraitCollection = [UITraitCollection currentTraitCollection];
  170. UITraitCollection *const darkUnscaledTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
  171. UITraitCollection *const darkScaledTraitCollection =
  172. [UITraitCollection traitCollectionWithTraitsFromCollections:@[ scaleTraitCollection, darkUnscaledTraitCollection ]];
  173. UIImage *image = [lightImage
  174. imageWithConfiguration:[lightImage.configuration
  175. configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]];
  176. darkImage = [darkImage
  177. imageWithConfiguration:[darkImage.configuration
  178. configurationWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]];
  179. [image.imageAsset registerImage:darkImage withTraitCollection:darkScaledTraitCollection];
  180. return image;
  181. } else {
  182. return lightImage;
  183. }
  184. }
  185. @end
  186. @interface TUIThemeManager ()
  187. /**
  188. * The theme resource path of each module, module: theme path
  189. */
  190. @property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSString *> *themeResourcePathCache;
  191. /**
  192. * The theme currently used by module, module: theme
  193. */
  194. @property(nonatomic, strong) NSMutableDictionary<NSNumber *, TUITheme *> *currentThemeCache;
  195. /**
  196. * The dark theme for each module, if any
  197. */
  198. @property(nonatomic, strong) NSMutableDictionary<NSNumber *, TUITheme *> *darkThemeCache;
  199. @property(nonatomic, strong) NSHashTable *listeners;
  200. - (void)allListenerExcuteonApplyThemeMethod:(TUITheme *)theme module:(TUIThemeModule)module;
  201. @end
  202. @implementation TUIThemeManager
  203. #ifdef TUIThreadSafe
  204. dispatch_queue_t _queue;
  205. dispatch_queue_t _read_write_queue;
  206. #endif
  207. static id gShareInstance;
  208. + (instancetype)shareManager {
  209. static dispatch_once_t onceToken;
  210. dispatch_once(&onceToken, ^{
  211. gShareInstance = [[self alloc] init];
  212. });
  213. return gShareInstance;
  214. }
  215. - (instancetype)init {
  216. if (self = [super init]) {
  217. #ifdef TUIThreadSafe
  218. _queue = dispatch_queue_create("theme_manager_queue", DISPATCH_QUEUE_SERIAL);
  219. _read_write_queue = dispatch_queue_create("read_write_secure_queue", DISPATCH_QUEUE_CONCURRENT);
  220. #endif
  221. _themeResourcePathCache = [NSMutableDictionary dictionary];
  222. _currentThemeCache = [NSMutableDictionary dictionary];
  223. _darkThemeCache = [NSMutableDictionary dictionary];
  224. _listeners = [NSHashTable weakObjectsHashTable];
  225. }
  226. return self;
  227. }
  228. - (void)addListener:(id<TUIThemeManagerListener>)listener {
  229. #ifdef TUIThreadSafe
  230. dispatch_async(_queue, ^{
  231. #endif
  232. if (![self.listeners containsObject:listener]) {
  233. [self.listeners addObject:listener];
  234. }
  235. #ifdef TUIThreadSafe
  236. });
  237. #endif
  238. }
  239. - (void)removeListener:(id<TUIThemeManagerListener>)listener {
  240. #ifdef TUIThreadSafe
  241. dispatch_async(_queue, ^{
  242. #endif
  243. if ([self.listeners containsObject:listener]) {
  244. [self.listeners removeObject:listener];
  245. }
  246. #ifdef TUIThreadSafe
  247. });
  248. #endif
  249. }
  250. - (void)registerThemeResourcePath:(NSString *)path darkThemeID:(NSString *)darkThemeID forModule:(TUIThemeModule)module {
  251. if (path.length == 0) {
  252. return;
  253. }
  254. #ifdef TUIThreadSafe
  255. dispatch_async(_queue, ^{
  256. #endif
  257. [self.themeResourcePathCache setObject:path forKey:@(module)];
  258. TUITheme *theme = [self loadTheme:darkThemeID module:module];
  259. if (theme) {
  260. [self setDarkTheme:theme forModule:module];
  261. }
  262. #ifdef TUIThreadSafe
  263. });
  264. #endif
  265. }
  266. - (void)registerThemeResourcePath:(NSString *)path forModule:(TUIThemeModule)module {
  267. [self registerThemeResourcePath:path darkThemeID:@"dark" forModule:module];
  268. }
  269. - (TUITheme *)currentThemeForModule:(TUIThemeModule)module {
  270. __block TUITheme *theme = nil;
  271. #ifdef TUIThreadSafe
  272. dispatch_sync(_read_write_queue, ^{
  273. #endif
  274. theme = [self.currentThemeCache objectForKey:@(module)];
  275. #ifdef TUIThreadSafe
  276. });
  277. #endif
  278. return theme;
  279. }
  280. - (void)setCurrentTheme:(TUITheme *)theme forModule:(TUIThemeModule)module {
  281. #ifdef TUIThreadSafe
  282. dispatch_barrier_async(_read_write_queue, ^{
  283. #endif
  284. if ([self.currentThemeCache.allKeys containsObject:@(module)]) {
  285. [self.currentThemeCache removeObjectForKey:@(module)];
  286. }
  287. if (theme) {
  288. [self.currentThemeCache setObject:theme forKey:@(module)];
  289. }
  290. #ifdef TUIThreadSafe
  291. });
  292. #endif
  293. }
  294. - (TUITheme *)darkThemeForModule:(TUIThemeModule)module {
  295. __block TUITheme *theme = nil;
  296. #ifdef TUIThreadSafe
  297. dispatch_sync(_read_write_queue, ^{
  298. #endif
  299. if ([self.darkThemeCache.allKeys containsObject:@(module)]) {
  300. theme = [self.darkThemeCache objectForKey:@(module)];
  301. }
  302. #ifdef TUIThreadSafe
  303. });
  304. #endif
  305. return theme;
  306. }
  307. - (void)setDarkTheme:(TUITheme *)theme forModule:(TUIThemeModule)module {
  308. #ifdef TUIThreadSafe
  309. dispatch_barrier_async(_read_write_queue, ^{
  310. #endif
  311. if ([self.darkThemeCache.allKeys containsObject:@(module)]) {
  312. [self.darkThemeCache removeObjectForKey:@(module)];
  313. }
  314. if (theme) {
  315. [self.darkThemeCache setObject:theme forKey:@(module)];
  316. }
  317. #ifdef TUIThreadSafe
  318. });
  319. #endif
  320. }
  321. - (void)applyTheme:(NSString *)themeID forModule:(TUIThemeModule)module {
  322. if (themeID.length == 0) {
  323. NSLog(@"[theme][applyTheme] invalid themeID, module:%zd", module);
  324. return;
  325. }
  326. #ifdef TUIThreadSafe
  327. dispatch_async(_queue, ^{
  328. #endif
  329. BOOL isAll = NO;
  330. NSMutableArray *allKeys = [NSMutableArray arrayWithArray:self.themeResourcePathCache.allKeys];
  331. if (module == TUIThemeModuleAll || ((module & TUIThemeModuleAll) == TUIThemeModuleAll)) {
  332. isAll = YES;
  333. }
  334. if (isAll) {
  335. for (NSNumber *moduleObject in allKeys) {
  336. TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
  337. [self doApplyTheme:themeID forSingleModule:tmpModue];
  338. }
  339. } else {
  340. for (NSNumber *moduleObject in allKeys) {
  341. TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
  342. if ((module & tmpModue) == tmpModue) {
  343. [self doApplyTheme:themeID forSingleModule:tmpModue];
  344. }
  345. }
  346. }
  347. #ifdef TUIThreadSafe
  348. });
  349. #endif
  350. }
  351. - (void)unApplyThemeForModule:(TUIThemeModule)module {
  352. #ifdef TUIThreadSafe
  353. dispatch_async(_queue, ^{
  354. #endif
  355. BOOL isAll = NO;
  356. NSMutableArray *allKeys = [NSMutableArray arrayWithArray:self.themeResourcePathCache.allKeys];
  357. if (module == TUIThemeModuleAll || ((module & TUIThemeModuleAll) == TUIThemeModuleAll)) {
  358. isAll = YES;
  359. }
  360. if (isAll) {
  361. for (NSNumber *moduleObject in allKeys) {
  362. TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
  363. [self setCurrentTheme:nil forModule:tmpModue];
  364. }
  365. [NSNotificationCenter.defaultCenter postNotificationName:TUIDidApplyingThemeChangedNotfication object:nil userInfo:nil];
  366. } else {
  367. for (NSNumber *moduleObject in allKeys) {
  368. TUIThemeModule tmpModue = (TUIThemeModule)[moduleObject integerValue];
  369. if ((module & tmpModue) == tmpModue) {
  370. [self setCurrentTheme:nil forModule:tmpModue];
  371. }
  372. }
  373. }
  374. #ifdef TUIThreadSafe
  375. });
  376. #endif
  377. }
  378. #pragma mark - Not thread safe
  379. - (void)doApplyTheme:(NSString *)themeID forSingleModule:(TUIThemeModule)module {
  380. TUITheme *theme = [self loadTheme:themeID module:module];
  381. if (theme == nil) {
  382. return;
  383. }
  384. [self setCurrentTheme:theme forModule:module];
  385. [self notifyApplyTheme:theme module:module];
  386. }
  387. - (TUITheme *)loadTheme:(NSString *)themeID module:(TUIThemeModule)module {
  388. NSString *themeResourcePath = [self themeResourcePathForModule:module];
  389. if (themeResourcePath.length == 0) {
  390. NSLog(@"[theme][applyTheme] theme resurce path not set, themeID:%@, module:%zd", themeID, module);
  391. return nil;
  392. }
  393. themeResourcePath = [themeResourcePath stringByAppendingPathComponent:themeID];
  394. {
  395. BOOL isDirectory = NO;
  396. BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:themeResourcePath isDirectory:&isDirectory];
  397. if (!exist || !isDirectory) {
  398. NSLog(@"[theme][applyTheme] invalid theme resurce, themeID:%@, module:%zd", themeID, module);
  399. return nil;
  400. }
  401. }
  402. NSString *manifestPath = [themeResourcePath stringByAppendingPathComponent:@"manifest.plist"];
  403. {
  404. BOOL isDirectory = NO;
  405. BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:manifestPath isDirectory:&isDirectory];
  406. if (!exist || isDirectory) {
  407. NSLog(@"[theme][applyTheme] invalid manifest, themeID:%@, module:%zd", themeID, module);
  408. return nil;
  409. }
  410. }
  411. NSString *resourcePath = [themeResourcePath stringByAppendingPathComponent:@"resource"];
  412. {
  413. BOOL isDirectory = NO;
  414. BOOL exist = [NSFileManager.defaultManager fileExistsAtPath:resourcePath isDirectory:&isDirectory];
  415. if (!exist || !isDirectory) {
  416. NSLog(@"[theme][applyTheme] invalid resurce, themeID:%@, module:%zd", themeID, module);
  417. }
  418. }
  419. NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:manifestPath];
  420. if (dict == nil) {
  421. NSLog(@"[theme][applyTheme] manifest is null");
  422. return nil;
  423. }
  424. TUITheme *theme = [[TUITheme alloc] init];
  425. theme.themeID = themeID;
  426. theme.module = module;
  427. theme.themeDesc = [NSString stringWithFormat:@"theme_%@_%zd", themeID, module];
  428. theme.resourcePath = resourcePath;
  429. theme.manifest = dict;
  430. return theme;
  431. }
  432. - (void)notifyApplyTheme:(TUITheme *)theme module:(TUIThemeModule)module {
  433. if (theme == nil) {
  434. return;
  435. }
  436. if (![NSThread isMainThread]) {
  437. __weak typeof(self) weakSelf = self;
  438. dispatch_async(dispatch_get_main_queue(), ^{
  439. [weakSelf notifyApplyTheme:theme module:module];
  440. });
  441. return;
  442. }
  443. [self allListenerExcuteonApplyThemeMethod:theme module:module];
  444. NSDictionary *userInfo = @{TUIDidApplyingThemeChangedNotficationModuleKey : @(module), TUIDidApplyingThemeChangedNotficationThemeKey : theme};
  445. [NSNotificationCenter.defaultCenter postNotificationName:TUIDidApplyingThemeChangedNotfication object:nil userInfo:userInfo];
  446. }
  447. - (void)allListenerExcuteonApplyThemeMethod:(TUITheme *)theme module:(TUIThemeModule)module {
  448. for (id<TUIThemeManagerListener> listener in self.listeners) {
  449. if ([listener respondsToSelector:@selector(onApplyTheme:module:)]) {
  450. [listener onApplyTheme:theme module:module];
  451. }
  452. }
  453. }
  454. - (NSString *)themeResourcePathForModule:(TUIThemeModule)module {
  455. if ([self.themeResourcePathCache.allKeys containsObject:@(module)]) {
  456. return [self.themeResourcePathCache objectForKey:@(module)];
  457. }
  458. return @"";
  459. }
  460. @end