Bridge iOS 13 traitCollection and color (#78)

* Use system dynamic color on iOS 13+
* Swizzling for DMTraitCollection
* Remove override of dm_updateDynamicColors and dm_updateDynamicImages
* Update naming and return UITraitCollection.current directly
* Allow using view controller to observe change
This commit is contained in:
Levin Li 2020-06-20 11:11:26 +08:00 коммит произвёл GitHub
Родитель 13e0d9db28
Коммит 7a20bcf603
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
30 изменённых файлов: 569 добавлений и 194 удалений

Просмотреть файл

@ -48,7 +48,6 @@
8CDA629F2366DAA9004895B5 /* UILabel+DarkModeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDA62852366DAA9004895B5 /* UILabel+DarkModeKit.swift */; };
8CDA62A02366DAA9004895B5 /* UISlider+DarkModeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDA62862366DAA9004895B5 /* UISlider+DarkModeKit.swift */; };
8CDA62A12366DAA9004895B5 /* UITextField+DarkModeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDA62872366DAA9004895B5 /* UITextField+DarkModeKit.swift */; };
8CDA62A22366DAA9004895B5 /* UIApplication+DarkModeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDA62882366DAA9004895B5 /* UIApplication+DarkModeKit.swift */; };
8CDA62A32366DAA9004895B5 /* DMDynamicImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CDA62892366DAA9004895B5 /* DMDynamicImage.m */; };
8CDA62A42366DAA9004895B5 /* DarkModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDA628A2366DAA9004895B5 /* DarkModeManager.swift */; };
8CDA62A52366DAA9004895B5 /* DMDynamicColor.h in Headers */ = {isa = PBXBuildFile; fileRef = 8CDA628B2366DAA9004895B5 /* DMDynamicColor.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -144,7 +143,6 @@
8CDA62852366DAA9004895B5 /* UILabel+DarkModeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+DarkModeKit.swift"; sourceTree = "<group>"; };
8CDA62862366DAA9004895B5 /* UISlider+DarkModeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UISlider+DarkModeKit.swift"; sourceTree = "<group>"; };
8CDA62872366DAA9004895B5 /* UITextField+DarkModeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+DarkModeKit.swift"; sourceTree = "<group>"; };
8CDA62882366DAA9004895B5 /* UIApplication+DarkModeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+DarkModeKit.swift"; sourceTree = "<group>"; };
8CDA62892366DAA9004895B5 /* DMDynamicImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DMDynamicImage.m; sourceTree = "<group>"; };
8CDA628A2366DAA9004895B5 /* DarkModeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkModeManager.swift; sourceTree = "<group>"; };
8CDA628B2366DAA9004895B5 /* DMDynamicColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DMDynamicColor.h; sourceTree = "<group>"; };
@ -321,7 +319,6 @@
8CDA62852366DAA9004895B5 /* UILabel+DarkModeKit.swift */,
8CDA62862366DAA9004895B5 /* UISlider+DarkModeKit.swift */,
8CDA62872366DAA9004895B5 /* UITextField+DarkModeKit.swift */,
8CDA62882366DAA9004895B5 /* UIApplication+DarkModeKit.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -537,7 +534,6 @@
8CE066D0239E5586002CE16D /* UIImage+DarkModeKit.m in Sources */,
8CE066CF239E5582002CE16D /* UIColor+DarkModeKit.m in Sources */,
8CDA62A02366DAA9004895B5 /* UISlider+DarkModeKit.swift in Sources */,
8CDA62A22366DAA9004895B5 /* UIApplication+DarkModeKit.swift in Sources */,
8CDA628F2366DAA9004895B5 /* DMDynamicColor.m in Sources */,
8CDA62A42366DAA9004895B5 /* DarkModeManager.swift in Sources */,
8CDA62982366DAA9004895B5 /* UITextView+DarkModeKit.swift in Sources */,

Просмотреть файл

@ -18,8 +18,8 @@ NS_SWIFT_NAME(DynamicColor)
@property (nonatomic, readonly) UIColor *lightColor;
@property (nonatomic, readonly) UIColor *darkColor;
- (instancetype)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
- (instancetype)initWithDynamicProvider:(UIColor * (^)(DMTraitCollection *traitCollection))dynamicProvider;
+ (UIColor *)colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(DMTraitCollection *traitCollection))dynamicProvider;
@end

Просмотреть файл

@ -16,13 +16,6 @@
@implementation DMDynamicColorProxy
// TODO: We need a more generic initializer.
- (instancetype)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
return [self initWithDynamicProvider:^(DMTraitCollection *traitCollection){
return traitCollection.userInterfaceStyle == DMUserInterfaceStyleDark ? darkColor : lightColor;
}];
}
- (instancetype)initWithDynamicProvider:(UIColor * (^)(DMTraitCollection *traitCollection))dynamicProvider {
self.dynamicProvider = dynamicProvider;
return self;
@ -39,7 +32,7 @@
// MARK: UIColor
- (UIColor *)colorWithAlphaComponent:(CGFloat)alpha {
return [[DMDynamicColor alloc] initWithDynamicProvider:^UIColor *(DMTraitCollection *traitCollection) {
return [DMDynamicColor colorWithDynamicProvider:^UIColor *(DMTraitCollection *traitCollection) {
return [self.dynamicProvider(traitCollection) colorWithAlphaComponent:alpha];
}];
}
@ -99,11 +92,13 @@
@implementation DMDynamicColor
- (UIColor *)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
return (DMDynamicColor *)[[DMDynamicColorProxy alloc] initWithLightColor:lightColor darkColor:darkColor];
+ (UIColor *)colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
return [self colorWithDynamicProvider:^(DMTraitCollection *traitCollection){
return traitCollection.userInterfaceStyle == DMUserInterfaceStyleDark ? darkColor : lightColor;
}];
}
- (instancetype)initWithDynamicProvider:(UIColor * _Nonnull (^)(DMTraitCollection * _Nonnull))dynamicProvider {
+ (UIColor *)colorWithDynamicProvider:(UIColor * _Nonnull (^)(DMTraitCollection * _Nonnull))dynamicProvider {
return (DMDynamicColor *)[[DMDynamicColorProxy alloc] initWithDynamicProvider:dynamicProvider];
}

Просмотреть файл

@ -3,7 +3,9 @@
// Licensed under the MIT License.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class UITraitCollection;
NS_ASSUME_NONNULL_BEGIN
@ -15,14 +17,28 @@ typedef NS_ENUM(NSInteger, DMUserInterfaceStyle) {
@interface DMTraitCollection : NSObject
@property (class, nonatomic, strong) DMTraitCollection *currentTraitCollection;
@property (class, nonatomic, readonly) DMTraitCollection *currentTraitCollection;
@property (class, nonatomic, readonly) DMTraitCollection *overrideTraitCollection;
+ (DMTraitCollection *)traitCollectionWithUserInterfaceStyle:(DMUserInterfaceStyle)userInterfaceStyle;
+ (DMTraitCollection *)traitCollectionWithUITraitCollection:(UITraitCollection *)traitCollection API_AVAILABLE(ios(13.0));
@property (nonatomic, readonly) DMUserInterfaceStyle userInterfaceStyle;
@property (nonatomic, readonly) UITraitCollection *uiTraitCollection API_AVAILABLE(ios(13.0));
- (instancetype)init NS_DESIGNATED_INITIALIZER;
+ (void)setOverrideTraitCollection:(DMTraitCollection *)overrideTraitCollection animated:(BOOL)animated;
// MARK: - Observer Registration
+ (void)registerWithApplication:(UIApplication *)application syncImmediately:(BOOL)syncImmediately animated:(BOOL)animated;
+ (void)registerWithViewController:(UIViewController *)viewController syncImmediately:(BOOL)syncImmediately animated:(BOOL)animated;
+ (void)unregister;
// MARK: - Swizzling
// TODO: move swizzling to private header
+ (void)swizzleUIScreenTraitCollectionDidChange API_AVAILABLE(ios(13.0));
@end
#pragma mark - DMTraitEnvironment
@ -33,4 +49,10 @@ typedef NS_ENUM(NSInteger, DMUserInterfaceStyle) {
@end
@interface NSObject (DMTraitEnvironment)
+ (void)swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange API_AVAILABLE(ios(13.0));
@end
NS_ASSUME_NONNULL_END

Просмотреть файл

@ -4,17 +4,131 @@
//
#import "DMTraitCollection.h"
#import "UIView+DarkModeKit.h"
@import ObjectiveC;
@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 *_currentTraitCollection = nil;
static DMTraitCollection *_overrideTraitCollection = nil; // This is set manually in setCurrentTraitCollection:animated
static void (^_userInterfaceStyleChangeHandler)(DMTraitCollection *, BOOL) = nil;
+ (DMTraitCollection *)currentTraitCollection {
return _currentTraitCollection;
if (@available(iOS 13.0, *)) {
return [DMTraitCollection traitCollectionWithUITraitCollection:UITraitCollection.currentTraitCollection];
}
return [self overrideTraitCollection];
}
+ (void)setCurrentTraitCollection:(DMTraitCollection *)currentTraitCollection {
_currentTraitCollection = currentTraitCollection;
+ (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 *)currentTraitCollection animated:(BOOL)animated {
_overrideTraitCollection = currentTraitCollection;
[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) {
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)
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 {
@ -23,6 +137,40 @@ static DMTraitCollection *_currentTraitCollection = nil;
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) {
@ -31,4 +179,69 @@ static DMTraitCollection *_currentTraitCollection = nil;
return self;
}
// MARK: - Observer Registration
+ (void)registerWithApplication:(UIApplication *)application syncImmediately:(BOOL)syncImmediately animated:(BOOL)animated {
__weak UIApplication *weakApp = application;
_userInterfaceStyleChangeHandler = ^(DMTraitCollection *traitCollection, BOOL animated) {
__strong UIApplication *strongApp = weakApp;
if (!strongApp)
return;
[self updateUIWithViews:strongApp.windows viewControllers:nil traitCollection:traitCollection animated:animated];
};
if (syncImmediately)
[self syncImmediatelyAnimated:animated];
}
+ (void)registerWithViewController:(UIViewController *)viewController syncImmediately:(BOOL)syncImmediately animated:(BOOL)animated {
__weak UIViewController *weakVc = viewController;
_userInterfaceStyleChangeHandler = ^(DMTraitCollection *traitCollection, BOOL animated) {
__strong UIViewController *strongVc = weakVc;
if (!strongVc)
return;
[self updateUIWithViews:nil viewControllers:[NSArray arrayWithObject:strongVc] traitCollection:traitCollection animated:animated];
};
if (syncImmediately)
[self syncImmediatelyAnimated:animated];
}
+ (void)syncImmediatelyAnimated:(BOOL)animated {
if (_userInterfaceStyleChangeHandler)
_userInterfaceStyleChangeHandler([self overrideTraitCollection], animated);
}
+ (void)unregister {
_userInterfaceStyleChangeHandler = nil;
}
// MARK: - Swizzling
+ (void)swizzleUIScreenTraitCollectionDidChange {
static dispatch_once_t onceToken;
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;
}
[self syncImmediatelyAnimated:YES];
}];
});
}
@end
@interface UIScreen (DMTraitEnvironment) <DMTraitEnvironment>
- (void)dmTraitCollectionDidChange:(DMTraitCollection *)previousTraitCollection;
@end
@implementation UIScreen (DMTraitEnvironment)
- (void)dmTraitCollectionDidChange:(DMTraitCollection *)previousTraitCollection {}
@end

Просмотреть файл

@ -9,7 +9,12 @@
@implementation UIColor (DarkModeKit)
+ (UIColor *)dm_colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
return (UIColor *)[[DMDynamicColor alloc] initWithLightColor:lightColor darkColor:darkColor];
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
return traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? darkColor : lightColor;
}];
}
return [DMDynamicColor colorWithLightColor:lightColor darkColor:darkColor];
}
+ (UIColor *)dm_namespace:(DMNamespace)namespace
@ -19,7 +24,12 @@
}
+ (UIColor *)dm_colorWithDynamicProvider:(UIColor *(^)(DMTraitCollection *))dynamicProvider {
return (UIColor *)[[DMDynamicColor alloc] initWithDynamicProvider:dynamicProvider];
if (@available(iOS 13.0, *)) {
return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
return dynamicProvider([DMTraitCollection traitCollectionWithUITraitCollection:traitCollection]);
}];
}
return [DMDynamicColor colorWithDynamicProvider:dynamicProvider];
}
+ (UIColor *)dm_namespace:(DMNamespace)namespace dynamicProvider:(UIColor *(^)(DMTraitCollection *))dynamicProvider {
@ -27,9 +37,11 @@
}
- (UIColor *)dm_resolvedColorWithTraitCollection:(DMTraitCollection *)traitCollection {
// Here we just need to take care of UIColor that is not DMDynamicColor
// since DMDynamicColor methods are all forwarded, simply return self
// before we need to bridge iOS 13's color mechanism
if (@available(iOS 13.0, *)) {
// Here we just need to take care of UIColor that is not DMDynamicColor
// since DMDynamicColor methods are all forwarded
return [self resolvedColorWithTraitCollection:traitCollection.uiTraitCollection];
}
return self;
}

Просмотреть файл

@ -4,16 +4,23 @@
//
#import <UIKit/UIKit.h>
#ifdef SWIFT_PACKAGE
#import "DMTraitCollection.h"
#else
#import <FluentDarkModeKit/DMTraitCollection.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class DMDynamicColor;
@interface UIView (DarkModeKit)
@interface UIView (DarkModeKit) <DMTraitEnvironment>
+ (void)dm_swizzleSetBackgroundColor;
+ (void)dm_swizzleSetTintColor;
@property (nonatomic, copy, nullable) DMDynamicColor *dm_dynamicBackgroundColor;
- (void)dm_updateDynamicColors API_DEPRECATED("dm_updateDynamicColors is deprecated and will not be called on iOS 13.0, use dmTraitCollectionDidChange: instead", ios(11.0, 13.0));;
- (void)dm_updateDynamicImages API_DEPRECATED("dm_updateDynamicImages is deprecated and will not be called on iOS 13.0, use dmTraitCollectionDidChange: instead", ios(11.0, 13.0));;
@end

Просмотреть файл

@ -31,6 +31,27 @@
});
}
+ (void)dm_swizzleSetTintColor {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selector = @selector(setTintColor:);
Method method = class_getInstanceMethod(self, selector);
if (!method)
NSAssert(NO, @"Method not found for [UIView setTintdColor:]");
IMP imp = method_getImplementation(method);
class_replaceMethod(self, selector, imp_implementationWithBlock(^(UIView *self, UIColor *tintColor) {
if ([tintColor isKindOfClass:[DMDynamicColor class]]) {
self.dm_dynamicTintColor = (DMDynamicColor *)tintColor;
}
else {
self.dm_dynamicTintColor = nil;
}
((void (*)(UIView *, SEL, UIColor *))imp)(self, selector, tintColor);
}), method_getTypeEncoding(method));
});
}
- (DMDynamicColor *)dm_dynamicBackgroundColor {
return objc_getAssociatedObject(self, _cmd);
}
@ -42,4 +63,44 @@
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (DMDynamicColor *)dm_dynamicTintColor {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setDm_dynamicTintColor:(DMDynamicColor *)dm_dynamicTintColor {
objc_setAssociatedObject(self,
@selector(dm_dynamicTintColor),
dm_dynamicTintColor,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (void)dmTraitCollectionDidChange:(DMTraitCollection *)previousTraitCollection {
if (@available(iOS 13.0, *)) {
return;
}
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
[view dmTraitCollectionDidChange:previousTraitCollection];
}];
[self setNeedsLayout];
[self setNeedsDisplay];
[self dm_updateDynamicColors];
[self dm_updateDynamicImages];
}
- (void)dm_updateDynamicColors {
UIColor *backgroundColor = [self dm_dynamicBackgroundColor];
if (backgroundColor) {
[self setBackgroundColor:backgroundColor];
}
UIColor *tintColor = [self dm_dynamicTintColor];
if (tintColor) {
[self setTintColor:tintColor];
}
}
- (void)dm_updateDynamicImages {
// For subclasses to override.
}
@end

Просмотреть файл

@ -9,29 +9,49 @@ import UIKit
#endif
public final class DarkModeManager: NSObject {
public static func setup() {
// Colors
UIView.swizzleWillMoveToWindowOnce
UIView.dm_swizzleSetBackgroundColor()
UIView.swizzleSetTintColorOnce
UITextField.swizzleTextFieldWillMoveToWindowOnce
UILabel.swizzleDidMoveToWindowOnce
private static var swizzlingConfigured = false
// Images
UIImage.dm_swizzleIsEqual()
UIImageView.swizzleSetImageOnce
UIImageView.swizzleInitImageOnce
UITabBarItem.swizzleSetImageOnce
UITabBarItem.swizzleSetSelectedImageOnce
public class func register(with application: UIApplication, syncImmediately: Bool = false, animated: Bool = false) {
commonSetup()
DMTraitCollection.register(with: application, syncImmediately: syncImmediately, animated: animated)
}
/// Update application's appearance based on current theme (This method is for main app.)
///
/// - Parameters:
/// - application: The application needs to update.
/// - animated: Use animation or not.
@objc public static func updateAppearance(for application: UIApplication, animated: Bool) {
application.updateAppearance(with: application.windows, animated: animated)
public class func register(with viewController: UIViewController, syncImmediately: Bool = false, animated: Bool = false) {
commonSetup()
DMTraitCollection.register(with: viewController, syncImmediately: syncImmediately, animated: animated)
}
public class func unregister() {
DMTraitCollection.unregister()
}
private class func commonSetup() {
guard !swizzlingConfigured else {
return
}
if #available(iOS 13.0, *) {
DMTraitCollection.swizzleUIScreenTraitCollectionDidChange()
UIView.swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange()
UIViewController.swizzleTraitCollectionDidChangeToDMTraitCollectionDidChange()
}
else {
// Colors
UIView.swizzleWillMoveToWindowOnce
UIView.dm_swizzleSetBackgroundColor()
UIView.dm_swizzleSetTintColor()
UITextField.swizzleTextFieldWillMoveToWindowOnce
UILabel.swizzleDidMoveToWindowOnce
// Images
UIImage.dm_swizzleIsEqual()
UIImageView.swizzleSetImageOnce
UIImageView.swizzleInitImageOnce
UITabBarItem.swizzleSetImageOnce
UITabBarItem.swizzleSetSelectedImageOnce
}
swizzlingConfigured = true
}
// MARK: - Internal
@ -40,38 +60,3 @@ public final class DarkModeManager: NSObject {
return "Method swizzling for theme failed! Class: \(cls), Selector: \(selector)"
}
}
// MARK: -
extension DMTraitEnvironment {
/// Trigger `themeDidChange()`.
///
/// - Parameters:
/// - views: Views visiable by user, will be snapshoted if use animation.
/// - animated: Use animation or not.
fileprivate func updateAppearance(with views: [UIView], animated: Bool) {
assert(Thread.isMainThread)
if animated {
var snapshotViews: [UIView] = []
views.forEach { view in
guard let snapshotView = view.snapshotView(afterScreenUpdates: false) else {
return
}
view.addSubview(snapshotView)
snapshotViews.append(snapshotView)
}
dmTraitCollectionDidChange(nil)
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.25, delay: 0, options: [], animations: {
snapshotViews.forEach { $0.alpha = 0 }
}) { _ in
snapshotViews.forEach { $0.removeFromSuperview() }
}
}
else {
dmTraitCollectionDidChange(nil)
}
}
}

Просмотреть файл

@ -1,10 +0,0 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
extension UIApplication: DMTraitEnvironment {
open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
windows.forEach { $0.dmTraitCollectionDidChange(previousTraitCollection) }
}
}

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UIButton {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
[UIControl.State.normal, .highlighted, .disabled, .selected, .focused].forEach { state in
if let color = titleColor(for: state)?.copy() as? DynamicColor {

Просмотреть файл

@ -51,8 +51,12 @@ extension UIImageView {
} as @convention(block) (UIImageView, UIImage?) -> UIImageView), method_getTypeEncoding(method))
}()
override func dm_updateDynamicImages() {
super.dm_updateDynamicImages()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
if let dynamicImage = dm_dynamicImage {
image = dynamicImage

Просмотреть файл

@ -34,6 +34,10 @@ extension UILabel {
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
guard #available(iOS 12.0, *) else {
// Fix for iOS 11.x
updateDynamicColorInAttributedText()

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UINavigationBar {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicBarTintColor = barTintColor?.copy() as? DynamicColor {
barTintColor = dynamicBarTintColor

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UIPageControl {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicPageIndicatorTintColor = pageIndicatorTintColor?.copy() as? DynamicColor {
pageIndicatorTintColor = dynamicPageIndicatorTintColor

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UIProgressView {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicProgressTintColor = progressTintColor?.copy() as? DynamicColor {
progressTintColor = dynamicProgressTintColor

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UIScrollView {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
indicatorStyle = {
if DMTraitCollection.current.userInterfaceStyle == .dark {

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UISlider {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicMinimumTrackTintColor = minimumTrackTintColor?.copy() as? DynamicColor {
minimumTrackTintColor = dynamicMinimumTrackTintColor

Просмотреть файл

@ -6,11 +6,12 @@
extension UITabBar {
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
items?.forEach { $0.dmTraitCollectionDidChange(previousTraitCollection) }
}
override func dm_updateDynamicImages() {
super.dm_updateDynamicImages()
if #available(iOS 13.0, *) {
return
}
items?.forEach { $0.dmTraitCollectionDidChange(previousTraitCollection) }
items?.forEach { $0._updateDynamicImages() }
}
}

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UITableView {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicSectionIndexColor = sectionIndexColor?.copy() as? DynamicColor {
sectionIndexColor = dynamicSectionIndexColor

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UITextField {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicTextColor = textColor?.copy() as? DynamicColor {
textColor = dynamicTextColor
@ -36,10 +42,7 @@ extension UITextField {
class_replaceMethod(UITextField.self, selector, imp_implementationWithBlock({ (self: UITextField, window: UIWindow?) -> Void in
let oldIMP = unsafeBitCast(imp, to: (@convention(c) (UITextField, Selector, UIWindow?) -> Void).self)
oldIMP(self, selector, window)
if window != nil {
self.dm_updateDynamicColors()
self.dm_updateDynamicImages()
}
self.dmTraitCollectionDidChange(nil)
} as @convention(block) (UITextField, UIWindow?) -> Void), method_getTypeEncoding(method))
}()
}

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UITextView {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
keyboardAppearance = {
if DMTraitCollection.current.userInterfaceStyle == .dark {

Просмотреть файл

@ -4,8 +4,14 @@
//
extension UIToolbar {
override func dm_updateDynamicColors() {
super.dm_updateDynamicColors()
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
dm_updateDynamicColors()
if let dynamicBarTintColor = barTintColor?.copy() as? DynamicColor {
barTintColor = dynamicBarTintColor

Просмотреть файл

@ -3,29 +3,6 @@
// Licensed under the MIT License.
//
extension UIView: DMTraitEnvironment {
open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
subviews.forEach { $0.dmTraitCollectionDidChange(previousTraitCollection) }
setNeedsLayout()
setNeedsDisplay()
dm_updateDynamicColors()
dm_updateDynamicImages()
}
@objc func dm_updateDynamicColors() {
if let dynamicBackgroundColor = dm_dynamicBackgroundColor {
backgroundColor = dynamicBackgroundColor
}
if let dynamicTintColor = dm_dynamicTintColor {
tintColor = dynamicTintColor
}
}
@objc func dm_updateDynamicImages() {
// For subclasses to override.
}
}
extension UIView {
static let swizzleWillMoveToWindowOnce: Void = {
let selector = #selector(willMove(toWindow:))
@ -45,29 +22,3 @@ extension UIView {
} as @convention(block) (UIView, UIWindow?) -> Void), method_getTypeEncoding(method))
}()
}
extension UIView {
private struct Constants {
static var dynamicTintColorKey = "dynamicTintColorKey"
}
static let swizzleSetTintColorOnce: Void = {
let selector = #selector(setter: tintColor)
guard let method = class_getInstanceMethod(UIView.self, selector) else {
assertionFailure(DarkModeManager.messageForSwizzlingFailed(class: UIView.self, selector: selector))
return
}
let imp = method_getImplementation(method)
class_replaceMethod(UIView.self, selector, imp_implementationWithBlock({ (self: UIView, tintColor: UIColor) -> Void in
self.dm_dynamicTintColor = tintColor as? DynamicColor
let oldIMP = unsafeBitCast(imp, to: (@convention(c) (UIView, Selector, UIColor) -> Void).self)
oldIMP(self, selector, tintColor)
} as @convention(block) (UIView, UIColor) -> Void), method_getTypeEncoding(method))
}()
private var dm_dynamicTintColor: DynamicColor? {
get { return objc_getAssociatedObject(self, &Constants.dynamicTintColorKey) as? DynamicColor }
set { objc_setAssociatedObject(self, &Constants.dynamicTintColorKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) }
}
}

Просмотреть файл

@ -5,6 +5,10 @@
extension UIViewController: DMTraitEnvironment {
open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
if #available(iOS 13.0, *) {
return
}
setNeedsStatusBarAppearanceUpdate()
presentedViewController?.dmTraitCollectionDidChange(previousTraitCollection)
children.forEach { $0.dmTraitCollectionDidChange(previousTraitCollection) }

Просмотреть файл

@ -6,6 +6,11 @@
extension UIWindow {
override open func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?) {
super.dmTraitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
return
}
rootViewController?.dmTraitCollectionDidChange(previousTraitCollection)
}
}

Просмотреть файл

@ -14,7 +14,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
DarkModeManager.setup()
DarkModeManager.register(with: application)
window = UIWindow()
window?.rootViewController = {

Просмотреть файл

@ -18,12 +18,44 @@ class ViewController: UIViewController {
}
@objc private func refresh() {
if DMTraitCollection.current.userInterfaceStyle == .dark {
DMTraitCollection.current = DMTraitCollection(userInterfaceStyle: .light)
// Loop throught the available styles
DMTraitCollection.setOverride(DMTraitCollection(userInterfaceStyle: DMTraitCollection.override.userInterfaceStyle.next), animated: true)
showUserSetInterfaceStyle()
}
private func showUserSetInterfaceStyle() {
let alert = UIAlertController(title: DMTraitCollection.override.userInterfaceStyle.description, message: nil, preferredStyle: .alert)
if alert.popoverPresentationController != nil {
alert.popoverPresentationController?.sourceRect = .zero
alert.popoverPresentationController?.sourceView = view
}
else {
DMTraitCollection.current = DMTraitCollection(userInterfaceStyle: .dark)
present(alert, animated: true, completion: nil)
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}
}
}
private extension DMUserInterfaceStyle {
var description: String {
switch self {
case .dark:
return "dark"
case .light:
return "light"
default:
return "unspecified"
}
}
var next: DMUserInterfaceStyle {
switch self {
case .light:
return .dark
case .dark:
return .unspecified
default:
return .light
}
DarkModeManager.updateAppearance(for: .shared, animated: true)
}
}

Просмотреть файл

@ -9,18 +9,20 @@ import XCTest
final class DarkModeKitTests: XCTestCase {
func testSetBackgroundColorSwizzling() {
UIWindow.appearance().backgroundColor = .white
DarkModeManager.setup()
DarkModeManager.register(with: UIApplication.shared)
_ = UIWindow()
}
func testColorInitializer() {
let color = UIColor(.dm, light: .white, dark: .black)
DMTraitCollection.current = DMTraitCollection(userInterfaceStyle: .light)
XCTAssertEqual(color.rgba, UIColor.white.rgba)
perform(with: .light) {
XCTAssertEqual(color.rgba, UIColor.white.rgba)
}
DMTraitCollection.current = DMTraitCollection(userInterfaceStyle: .dark)
XCTAssertEqual(color.rgba, UIColor.black.rgba)
perform(with: .dark) {
XCTAssertEqual(color.rgba, UIColor.black.rgba)
}
}
func testImageInitializer() {
@ -34,11 +36,13 @@ final class DarkModeKitTests: XCTestCase {
$0.userInterfaceStyle == .dark ? UIColor.black : UIColor.white
}
DMTraitCollection.current = DMTraitCollection(userInterfaceStyle: .light)
XCTAssertEqual(color.rgba, UIColor.white.rgba)
perform(with: .light) {
XCTAssertEqual(color.rgba, UIColor.white.rgba)
}
DMTraitCollection.current = DMTraitCollection(userInterfaceStyle: .dark)
XCTAssertEqual(color.rgba, UIColor.black.rgba)
perform(with: .dark) {
XCTAssertEqual(color.rgba, UIColor.black.rgba)
}
// Test color fetched from specific trait collections
XCTAssertEqual(color.resolvedColor(.dm, with: DMTraitCollection(userInterfaceStyle: .dark)).rgba, UIColor.black.rgba)
@ -51,7 +55,12 @@ final class DarkModeKitTests: XCTestCase {
let view = UIView()
view.backgroundColor = color
view.tintColor = color
XCTAssertFalse(view.backgroundColor === color)
if #available(iOS 13.0, *) {
XCTAssertTrue(view.backgroundColor === color)
}
else {
XCTAssertFalse(view.backgroundColor === color)
}
XCTAssertTrue(view.tintColor === color)
// UIView subclasses
@ -110,7 +119,12 @@ final class DarkModeKitTests: XCTestCase {
label.shadowColor = color
label.highlightedTextColor = color
XCTAssertTrue(label.textColor === color)
XCTAssertFalse(label.shadowColor === color)
if #available(iOS 13.0, *) {
XCTAssertTrue(label.shadowColor === color)
}
else {
XCTAssertFalse(label.shadowColor === color)
}
XCTAssertTrue(label.highlightedTextColor === color)
let navigationBar = UINavigationBar()
@ -155,6 +169,23 @@ final class DarkModeKitTests: XCTestCase {
XCTAssertTrue(toolbar.barTintColor === color)
}
}
func perform(with userInterfaceStyle: DMUserInterfaceStyle, expression: () -> Void) {
if #available(iOS 13.0, *) {
// On iOS 13, we use the system wide one, while in unit tests there is
// no actual views, use UITraitCollection.performAsCurrent to simulate
// theme change
DMTraitCollection(userInterfaceStyle: userInterfaceStyle).uiTraitCollection.performAsCurrent {
expression()
}
}
else {
let saved = DMTraitCollection.current
DMTraitCollection.setOverride(DMTraitCollection(userInterfaceStyle: userInterfaceStyle), animated: false)
expression()
DMTraitCollection.setOverride(saved, animated: false)
}
}
}
extension UIColor {

Просмотреть файл

@ -35,21 +35,25 @@ final class DarkModeKitUITests: XCTestCase {
func _test(_ className: String) {
let app = XCUIApplication()
let refreshButton = app.navigationBars["FluentDarkModeKitExample.MainView"].buttons["Refresh"]
refreshButton.tap()
refreshButton.tap() // light mode
refreshButton.tap() // dark mode
let uiviewStaticText = app.tables.staticTexts[className]
uiviewStaticText.tap()
sleep(1)
let screenshot1 = app.screenshot()
app.navigationBars["FluentDarkModeKitExample.\(className)VC"].buttons["Back"].tap()
refreshButton.tap()
refreshButton.tap() // unspecified
refreshButton.tap() // light mode
uiviewStaticText.tap()
let tabBarsQuery = app.tabBars
tabBarsQuery.children(matching: .button).element(boundBy: 1).tap()
app.navigationBars["FluentDarkModeKitExample.View"].buttons["Refresh"].tap()
tabBarsQuery.children(matching: .button).element(boundBy: 0).tap()
tabBarsQuery.children(matching: .button).element(boundBy: 0).tap() // dark mode
let screenshot2 = app.screenshot()