├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Document
├── Principle.md
├── 原理.md
└── 迁移到系统API指南.md
├── Example
├── Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── jiaxin.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── Example.xcworkspace
│ └── contents.xcworkspacedata
└── Example
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── catBlack.imageset
│ │ ├── Contents.json
│ │ └── catBlack.png
│ └── catWhite.imageset
│ │ ├── Contents.json
│ │ └── catWhite.png
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── DynamicSourceManager.swift
│ ├── Images
│ ├── catBlack.png
│ └── catWhite.png
│ ├── Info.plist
│ ├── ListViewController.swift
│ ├── StaticSource.plist
│ ├── StaticSourceManager.swift
│ └── ViewController.swift
├── GIF
├── JXTheme.png
└── preview.gif
├── JXTheme.podspec
├── JXTheme.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── JXTheme.xcscheme
└── xcuserdata
│ └── jiaxin.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── JXTheme
├── Info.plist
└── JXTheme.h
├── LICENSE
├── Package.swift
├── README-CN.md
├── README.md
└── Sources
├── Extensions.swift
├── ThemeDefines.swift
└── ThemeManager.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 | IDEWorkspaceChecks.plist
70 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Document/Principle.md:
--------------------------------------------------------------------------------
1 | # Principle
2 |
3 | In order to achieve the theme switching, the following five problems are mainly solved:
4 | ## 1. How to elegantly set theme properties
5 | By extending the namespace property `theme` to the control, similar to the `snp` of `SnapKit` and the `kf` of `Kingfisher`, you can concentrate the properties that support the theme modification to the `theme` property. This is more elegant than directly extending the property 'theme_backgroundColor` to the control.
6 | The core code is as follows:
7 | ```Swift
8 | view.theme.backgroundColor = ThemeProvider({ (style) in
9 | If style == .dark {
10 | Return .white
11 | }else {
12 | Return .black
13 | }
14 | })
15 | ```
16 |
17 | ## 2. How to configure the corresponding value according to the incoming style
18 | Reference the iOS13 system API `UIColor(dynamicProvider: UIColor>)`. Customize the `ThemeProvider` structure, the initializer is `init(_ provider: @escaping ThemePropertyProvider)`. The passed argument `ThemePropertyProvider` is a closure defined as: `typealias ThemePropertyProvider = (ThemeStyle) -> T`. This allows for maximum customization of different controls and different attribute configurations.
19 | The core code refers to the first step sample code.
20 |
21 | ## 3. How to save the theme properties configuration closure
22 | Add the `Associated object` property `providers` to the control to store `ThemeProvider`.
23 | The core code is as follows:
24 | ```Swift
25 | Public extension ThemeWrapper where Base: UIView {
26 | Var backgroundColor: ThemeProvider? {
27 | Set(new) {
28 | If new != nil {
29 | Let baseItem = self.base
30 | Let config: ThemeCustomizationClosure = {[weak baseItem] (style) in
31 | baseItem?.backgroundColor = new?.provider(style)
32 | }
33 | / / Stored in the extended properties provider
34 | Var newProvider = new
35 | newProvider?.config = config
36 | Self.base.providers["UIView.backgroundColor"] = newProvider
37 | ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
38 | }else {
39 | self.base.configs.removeValue(forKey: "UIView.backgroundColor")
40 | }
41 | }
42 | Get { return self.base.providers["UIView.backgroundColor"] as? ThemeProvider }
43 | }
44 | }
45 | ```
46 |
47 | ## 4. How to track controls that support theme properties
48 | In order to switch to the theme, notify the control that supports the theme property configuration. By tracking the target control when setting the theme properties.
49 | The core code is the code in step 3:
50 | ```Swift
51 | ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
52 | ```
53 |
54 | ## 5. How to switch the theme and call the closure of theme property
55 | The theme is switched by `ThemeManager.changeTheme(to: style)`, and the method internally calls the `ThemeProvider.provider` theme property in the `providers` of the tracked control to configure the closure.
56 | The core code is as follows:
57 | ```Swift
58 | Public func changeTheme(to style: ThemeStyle) {
59 | currentThemeStyle = style
60 | self.trackedHashTable.allObjects.forEach { (object) in
61 | If let view = object as? UIView {
62 | view.providers.values.forEach { self.resolveProvider($0) }
63 | }
64 | }
65 | }
66 | Private func resolveProvider(_ object: Any) {
67 | //castdown generic
68 | If let provider = object as? ThemeProvider {
69 | Provider.config?(currentThemeStyle)
70 | }else ...
71 | }
72 | ```
73 |
--------------------------------------------------------------------------------
/Document/原理.md:
--------------------------------------------------------------------------------
1 | # 原理
2 |
3 | 为了实现主题切换,主要解决以下五个问题:
4 | ## 1.如何优雅的设置主题属性
5 | 通过给控件扩展命名空间属性`theme`,类似于`SnapKit`的`snp`、`Kingfisher`的`kf`,这样可以将支持主题修改的属性,集中到`theme`属性。这样比直接给控件扩展属性`theme_backgroundColor`更加优雅。
6 | 核心代码如下:
7 | ```Swift
8 | view.theme.backgroundColor = ThemeProvider({ (style) in
9 | if style == .dark {
10 | return .white
11 | }else {
12 | return .black
13 | }
14 | })
15 | ```
16 |
17 | ## 2.如何根据传入的style配置对应的值
18 | 借鉴iOS13系统API`UIColor(dynamicProvider: UIColor>)`。自定义`ThemeProvider`结构体,初始化器为`init(_ provider: @escaping ThemePropertyProvider)`。传入的参数`ThemePropertyProvider`是一个闭包,定义为:`typealias ThemePropertyProvider = (ThemeStyle) -> T`。这样就可以针对不同的控件,不同的属性配置,实现最大化的自定义。
19 | 核心代码参考第一步示例代码。
20 |
21 | ## 3.如何保存主题属性配置闭包
22 | 对控件添加`Associated object`属性`providers`存储`ThemeProvider`。
23 | 核心代码如下:
24 | ```Swift
25 | public extension ThemeWrapper where Base: UIView {
26 | var backgroundColor: ThemeProvider? {
27 | set(new) {
28 | if new != nil {
29 | let baseItem = self.base
30 | let config: ThemeCustomizationClosure = {[weak baseItem] (style) in
31 | baseItem?.backgroundColor = new?.provider(style)
32 | }
33 | //存储在扩展属性providers里面
34 | var newProvider = new
35 | newProvider?.config = config
36 | self.base.providers["UIView.backgroundColor"] = newProvider
37 | ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
38 | }else {
39 | self.base.configs.removeValue(forKey: "UIView.backgroundColor")
40 | }
41 | }
42 | get { return self.base.providers["UIView.backgroundColor"] as? ThemeProvider }
43 | }
44 | }
45 | ```
46 |
47 | ## 4.如何记录支持主题属性的控件
48 | 为了在主题切换的时候,通知到支持主题属性配置的控件。通过在设置主题属性时,就记录目标控件。
49 | 核心代码就是第3步里面的这句代码:
50 | ```Swift
51 | ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
52 | ```
53 |
54 | ## 5.如何切换主题并调用主题属性配置闭包
55 | 通过`ThemeManager.changeTheme(to: style)`完成主题切换,方法内部再调用被追踪的控件的`providers`里面的`ThemeProvider.provider`主题属性配置闭包。
56 | 核心代码如下:
57 | ```Swift
58 | public func changeTheme(to style: ThemeStyle) {
59 | currentThemeStyle = style
60 | self.trackedHashTable.allObjects.forEach { (object) in
61 | if let view = object as? UIView {
62 | view.providers.values.forEach { self.resolveProvider($0) }
63 | }
64 | }
65 | }
66 | private func resolveProvider(_ object: Any) {
67 | //castdown泛型
68 | if let provider = object as? ThemeProvider {
69 | provider.config?(currentThemeStyle)
70 | }else ...
71 | }
72 | ```
73 |
--------------------------------------------------------------------------------
/Document/迁移到系统API指南.md:
--------------------------------------------------------------------------------
1 | # 迁移到系统API指南
2 |
3 | 当你的应用最低支持iOS13时,恭喜你可以按照如下指南,迁移到系统方案。该方案仅提供一个思路,实际执行时请灵活变通。
4 |
5 | ## UIColor迁移指南
6 |
7 | 系统API示例:
8 | ```Swift
9 | view.backgroundColor = UIColor(dynamicProvider: { (trait) -> UIColor in
10 | if trait.userInterfaceStyle == .dark {
11 | return .white
12 | }else {
13 | return .black
14 | }
15 | })
16 | ```
17 | JXTheme代码示例:
18 | ```Swift
19 | view.theme.backgroundColor = ThemeProvider({ (style) in
20 | if style == .dark {
21 | return .white
22 | }else {
23 | return .black
24 | }
25 | })
26 | ```
27 |
28 | ### 第一步
29 |
30 | 全局搜索`theme.backgroundColor`然后替换为`backgroundColor`。
31 |
32 | ### 第二步
33 |
34 | 全局搜索`ThemeProvider({ (style)`然后替换为`IColor(dynamicProvider: { (trait) -> UIColor`。
35 |
36 | ### 第三步
37 |
38 | 全局搜索`if style == .dark`然后替换为`if trait.userInterfaceStyle == .dark`
39 |
40 | ## UIImage迁移指南
41 |
42 | 删除JXTheme配置代码:
43 | ```Swift
44 | imageView.theme.image = ThemeProvider({ (style) in
45 | if style == .dark {
46 | return UIImage(named: "catWhite")!
47 | }else {
48 | return UIImage(named: "catBlack")!
49 | }
50 | })
51 | ```
52 | 然后用XCode11版本里的Asset里面按照要求配置图片。
53 |
54 |
55 | ## 其他属性配置
56 |
57 | 其他支持主题属性配置的代码,迁移到方法`func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)`里面。
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1906719422D868DF00D8AEDD /* StaticSource.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1906719322D868DF00D8AEDD /* StaticSource.plist */; };
11 | 1906719622D86A2E00D8AEDD /* StaticSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1906719522D86A2E00D8AEDD /* StaticSourceManager.swift */; };
12 | 1906719822D8712900D8AEDD /* DynamicSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1906719722D8712900D8AEDD /* DynamicSourceManager.swift */; };
13 | 1961100822D592FB000ABC4E /* JXTheme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1961100722D592FB000ABC4E /* JXTheme.framework */; };
14 | 1961100922D592FB000ABC4E /* JXTheme.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1961100722D592FB000ABC4E /* JXTheme.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
15 | 19879EBE22D58AA800767E2E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19879EBD22D58AA800767E2E /* AppDelegate.swift */; };
16 | 19879EC022D58AA800767E2E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19879EBF22D58AA800767E2E /* ViewController.swift */; };
17 | 19879EC322D58AA800767E2E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 19879EC122D58AA800767E2E /* Main.storyboard */; };
18 | 19879EC522D58AAD00767E2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19879EC422D58AAD00767E2E /* Assets.xcassets */; };
19 | 19879EC822D58AAD00767E2E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 19879EC622D58AAD00767E2E /* LaunchScreen.storyboard */; };
20 | 19D4BBE822DD752A00ACC1A4 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D4BBE722DD752A00ACC1A4 /* ListViewController.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXCopyFilesBuildPhase section */
24 | 1961100A22D592FB000ABC4E /* Embed Frameworks */ = {
25 | isa = PBXCopyFilesBuildPhase;
26 | buildActionMask = 2147483647;
27 | dstPath = "";
28 | dstSubfolderSpec = 10;
29 | files = (
30 | 1961100922D592FB000ABC4E /* JXTheme.framework in Embed Frameworks */,
31 | );
32 | name = "Embed Frameworks";
33 | runOnlyForDeploymentPostprocessing = 0;
34 | };
35 | /* End PBXCopyFilesBuildPhase section */
36 |
37 | /* Begin PBXFileReference section */
38 | 1906719322D868DF00D8AEDD /* StaticSource.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = StaticSource.plist; sourceTree = ""; };
39 | 1906719522D86A2E00D8AEDD /* StaticSourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticSourceManager.swift; sourceTree = ""; };
40 | 1906719722D8712900D8AEDD /* DynamicSourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicSourceManager.swift; sourceTree = ""; };
41 | 1961100722D592FB000ABC4E /* JXTheme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = JXTheme.framework; sourceTree = BUILT_PRODUCTS_DIR; };
42 | 19879EBA22D58AA800767E2E /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
43 | 19879EBD22D58AA800767E2E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
44 | 19879EBF22D58AA800767E2E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
45 | 19879EC222D58AA800767E2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
46 | 19879EC422D58AAD00767E2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
47 | 19879EC722D58AAD00767E2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
48 | 19879EC922D58AAD00767E2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
49 | 19D4BBE722DD752A00ACC1A4 /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; };
50 | /* End PBXFileReference section */
51 |
52 | /* Begin PBXFrameworksBuildPhase section */
53 | 19879EB722D58AA800767E2E /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | 1961100822D592FB000ABC4E /* JXTheme.framework in Frameworks */,
58 | );
59 | runOnlyForDeploymentPostprocessing = 0;
60 | };
61 | /* End PBXFrameworksBuildPhase section */
62 |
63 | /* Begin PBXGroup section */
64 | 1961100622D592FB000ABC4E /* Frameworks */ = {
65 | isa = PBXGroup;
66 | children = (
67 | 1961100722D592FB000ABC4E /* JXTheme.framework */,
68 | );
69 | name = Frameworks;
70 | sourceTree = "";
71 | };
72 | 19879EB122D58AA800767E2E = {
73 | isa = PBXGroup;
74 | children = (
75 | 19879EBC22D58AA800767E2E /* Example */,
76 | 19879EBB22D58AA800767E2E /* Products */,
77 | 1961100622D592FB000ABC4E /* Frameworks */,
78 | );
79 | sourceTree = "";
80 | };
81 | 19879EBB22D58AA800767E2E /* Products */ = {
82 | isa = PBXGroup;
83 | children = (
84 | 19879EBA22D58AA800767E2E /* Example.app */,
85 | );
86 | name = Products;
87 | sourceTree = "";
88 | };
89 | 19879EBC22D58AA800767E2E /* Example */ = {
90 | isa = PBXGroup;
91 | children = (
92 | 19879EBD22D58AA800767E2E /* AppDelegate.swift */,
93 | 19879EBF22D58AA800767E2E /* ViewController.swift */,
94 | 19D4BBE722DD752A00ACC1A4 /* ListViewController.swift */,
95 | 1906719522D86A2E00D8AEDD /* StaticSourceManager.swift */,
96 | 1906719722D8712900D8AEDD /* DynamicSourceManager.swift */,
97 | 19879EC122D58AA800767E2E /* Main.storyboard */,
98 | 19879EC422D58AAD00767E2E /* Assets.xcassets */,
99 | 19879EC622D58AAD00767E2E /* LaunchScreen.storyboard */,
100 | 19879EC922D58AAD00767E2E /* Info.plist */,
101 | 1906719322D868DF00D8AEDD /* StaticSource.plist */,
102 | );
103 | path = Example;
104 | sourceTree = "";
105 | };
106 | /* End PBXGroup section */
107 |
108 | /* Begin PBXNativeTarget section */
109 | 19879EB922D58AA800767E2E /* Example */ = {
110 | isa = PBXNativeTarget;
111 | buildConfigurationList = 19879ECC22D58AAD00767E2E /* Build configuration list for PBXNativeTarget "Example" */;
112 | buildPhases = (
113 | 19879EB622D58AA800767E2E /* Sources */,
114 | 19879EB722D58AA800767E2E /* Frameworks */,
115 | 19879EB822D58AA800767E2E /* Resources */,
116 | 1961100A22D592FB000ABC4E /* Embed Frameworks */,
117 | );
118 | buildRules = (
119 | );
120 | dependencies = (
121 | );
122 | name = Example;
123 | productName = Example;
124 | productReference = 19879EBA22D58AA800767E2E /* Example.app */;
125 | productType = "com.apple.product-type.application";
126 | };
127 | /* End PBXNativeTarget section */
128 |
129 | /* Begin PBXProject section */
130 | 19879EB222D58AA800767E2E /* Project object */ = {
131 | isa = PBXProject;
132 | attributes = {
133 | LastSwiftUpdateCheck = 1020;
134 | LastUpgradeCheck = 1020;
135 | ORGANIZATIONNAME = jiaxin;
136 | TargetAttributes = {
137 | 19879EB922D58AA800767E2E = {
138 | CreatedOnToolsVersion = 10.2.1;
139 | };
140 | };
141 | };
142 | buildConfigurationList = 19879EB522D58AA800767E2E /* Build configuration list for PBXProject "Example" */;
143 | compatibilityVersion = "Xcode 9.3";
144 | developmentRegion = en;
145 | hasScannedForEncodings = 0;
146 | knownRegions = (
147 | en,
148 | Base,
149 | );
150 | mainGroup = 19879EB122D58AA800767E2E;
151 | productRefGroup = 19879EBB22D58AA800767E2E /* Products */;
152 | projectDirPath = "";
153 | projectRoot = "";
154 | targets = (
155 | 19879EB922D58AA800767E2E /* Example */,
156 | );
157 | };
158 | /* End PBXProject section */
159 |
160 | /* Begin PBXResourcesBuildPhase section */
161 | 19879EB822D58AA800767E2E /* Resources */ = {
162 | isa = PBXResourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | 19879EC822D58AAD00767E2E /* LaunchScreen.storyboard in Resources */,
166 | 1906719422D868DF00D8AEDD /* StaticSource.plist in Resources */,
167 | 19879EC522D58AAD00767E2E /* Assets.xcassets in Resources */,
168 | 19879EC322D58AA800767E2E /* Main.storyboard in Resources */,
169 | );
170 | runOnlyForDeploymentPostprocessing = 0;
171 | };
172 | /* End PBXResourcesBuildPhase section */
173 |
174 | /* Begin PBXSourcesBuildPhase section */
175 | 19879EB622D58AA800767E2E /* Sources */ = {
176 | isa = PBXSourcesBuildPhase;
177 | buildActionMask = 2147483647;
178 | files = (
179 | 19879EC022D58AA800767E2E /* ViewController.swift in Sources */,
180 | 1906719622D86A2E00D8AEDD /* StaticSourceManager.swift in Sources */,
181 | 1906719822D8712900D8AEDD /* DynamicSourceManager.swift in Sources */,
182 | 19D4BBE822DD752A00ACC1A4 /* ListViewController.swift in Sources */,
183 | 19879EBE22D58AA800767E2E /* AppDelegate.swift in Sources */,
184 | );
185 | runOnlyForDeploymentPostprocessing = 0;
186 | };
187 | /* End PBXSourcesBuildPhase section */
188 |
189 | /* Begin PBXVariantGroup section */
190 | 19879EC122D58AA800767E2E /* Main.storyboard */ = {
191 | isa = PBXVariantGroup;
192 | children = (
193 | 19879EC222D58AA800767E2E /* Base */,
194 | );
195 | name = Main.storyboard;
196 | sourceTree = "";
197 | };
198 | 19879EC622D58AAD00767E2E /* LaunchScreen.storyboard */ = {
199 | isa = PBXVariantGroup;
200 | children = (
201 | 19879EC722D58AAD00767E2E /* Base */,
202 | );
203 | name = LaunchScreen.storyboard;
204 | sourceTree = "";
205 | };
206 | /* End PBXVariantGroup section */
207 |
208 | /* Begin XCBuildConfiguration section */
209 | 19879ECA22D58AAD00767E2E /* Debug */ = {
210 | isa = XCBuildConfiguration;
211 | buildSettings = {
212 | ALWAYS_SEARCH_USER_PATHS = NO;
213 | CLANG_ANALYZER_NONNULL = YES;
214 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
215 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
216 | CLANG_CXX_LIBRARY = "libc++";
217 | CLANG_ENABLE_MODULES = YES;
218 | CLANG_ENABLE_OBJC_ARC = YES;
219 | CLANG_ENABLE_OBJC_WEAK = YES;
220 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
221 | CLANG_WARN_BOOL_CONVERSION = YES;
222 | CLANG_WARN_COMMA = YES;
223 | CLANG_WARN_CONSTANT_CONVERSION = YES;
224 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
226 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
227 | CLANG_WARN_EMPTY_BODY = YES;
228 | CLANG_WARN_ENUM_CONVERSION = YES;
229 | CLANG_WARN_INFINITE_RECURSION = YES;
230 | CLANG_WARN_INT_CONVERSION = YES;
231 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
232 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
233 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
234 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
235 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
236 | CLANG_WARN_STRICT_PROTOTYPES = YES;
237 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
238 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
239 | CLANG_WARN_UNREACHABLE_CODE = YES;
240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
241 | CODE_SIGN_IDENTITY = "iPhone Developer";
242 | COPY_PHASE_STRIP = NO;
243 | DEBUG_INFORMATION_FORMAT = dwarf;
244 | ENABLE_STRICT_OBJC_MSGSEND = YES;
245 | ENABLE_TESTABILITY = YES;
246 | GCC_C_LANGUAGE_STANDARD = gnu11;
247 | GCC_DYNAMIC_NO_PIC = NO;
248 | GCC_NO_COMMON_BLOCKS = YES;
249 | GCC_OPTIMIZATION_LEVEL = 0;
250 | GCC_PREPROCESSOR_DEFINITIONS = (
251 | "DEBUG=1",
252 | "$(inherited)",
253 | );
254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
256 | GCC_WARN_UNDECLARED_SELECTOR = YES;
257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
258 | GCC_WARN_UNUSED_FUNCTION = YES;
259 | GCC_WARN_UNUSED_VARIABLE = YES;
260 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
261 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
262 | MTL_FAST_MATH = YES;
263 | ONLY_ACTIVE_ARCH = YES;
264 | SDKROOT = iphoneos;
265 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
266 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
267 | };
268 | name = Debug;
269 | };
270 | 19879ECB22D58AAD00767E2E /* Release */ = {
271 | isa = XCBuildConfiguration;
272 | buildSettings = {
273 | ALWAYS_SEARCH_USER_PATHS = NO;
274 | CLANG_ANALYZER_NONNULL = YES;
275 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
276 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
277 | CLANG_CXX_LIBRARY = "libc++";
278 | CLANG_ENABLE_MODULES = YES;
279 | CLANG_ENABLE_OBJC_ARC = YES;
280 | CLANG_ENABLE_OBJC_WEAK = YES;
281 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
282 | CLANG_WARN_BOOL_CONVERSION = YES;
283 | CLANG_WARN_COMMA = YES;
284 | CLANG_WARN_CONSTANT_CONVERSION = YES;
285 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
286 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
287 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
288 | CLANG_WARN_EMPTY_BODY = YES;
289 | CLANG_WARN_ENUM_CONVERSION = YES;
290 | CLANG_WARN_INFINITE_RECURSION = YES;
291 | CLANG_WARN_INT_CONVERSION = YES;
292 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
293 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
294 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
295 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
296 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
297 | CLANG_WARN_STRICT_PROTOTYPES = YES;
298 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
299 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
300 | CLANG_WARN_UNREACHABLE_CODE = YES;
301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
302 | CODE_SIGN_IDENTITY = "iPhone Developer";
303 | COPY_PHASE_STRIP = NO;
304 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
305 | ENABLE_NS_ASSERTIONS = NO;
306 | ENABLE_STRICT_OBJC_MSGSEND = YES;
307 | GCC_C_LANGUAGE_STANDARD = gnu11;
308 | GCC_NO_COMMON_BLOCKS = YES;
309 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
310 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
311 | GCC_WARN_UNDECLARED_SELECTOR = YES;
312 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
313 | GCC_WARN_UNUSED_FUNCTION = YES;
314 | GCC_WARN_UNUSED_VARIABLE = YES;
315 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
316 | MTL_ENABLE_DEBUG_INFO = NO;
317 | MTL_FAST_MATH = YES;
318 | SDKROOT = iphoneos;
319 | SWIFT_COMPILATION_MODE = wholemodule;
320 | SWIFT_OPTIMIZATION_LEVEL = "-O";
321 | VALIDATE_PRODUCT = YES;
322 | };
323 | name = Release;
324 | };
325 | 19879ECD22D58AAD00767E2E /* Debug */ = {
326 | isa = XCBuildConfiguration;
327 | buildSettings = {
328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
329 | CODE_SIGN_STYLE = Automatic;
330 | INFOPLIST_FILE = Example/Info.plist;
331 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
332 | LD_RUNPATH_SEARCH_PATHS = (
333 | "$(inherited)",
334 | "@executable_path/Frameworks",
335 | );
336 | PRODUCT_BUNDLE_IDENTIFIER = jiaxin.Example;
337 | PRODUCT_NAME = "$(TARGET_NAME)";
338 | SWIFT_VERSION = 5.0;
339 | TARGETED_DEVICE_FAMILY = "1,2";
340 | };
341 | name = Debug;
342 | };
343 | 19879ECE22D58AAD00767E2E /* Release */ = {
344 | isa = XCBuildConfiguration;
345 | buildSettings = {
346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
347 | CODE_SIGN_STYLE = Automatic;
348 | INFOPLIST_FILE = Example/Info.plist;
349 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
350 | LD_RUNPATH_SEARCH_PATHS = (
351 | "$(inherited)",
352 | "@executable_path/Frameworks",
353 | );
354 | PRODUCT_BUNDLE_IDENTIFIER = jiaxin.Example;
355 | PRODUCT_NAME = "$(TARGET_NAME)";
356 | SWIFT_VERSION = 5.0;
357 | TARGETED_DEVICE_FAMILY = "1,2";
358 | };
359 | name = Release;
360 | };
361 | /* End XCBuildConfiguration section */
362 |
363 | /* Begin XCConfigurationList section */
364 | 19879EB522D58AA800767E2E /* Build configuration list for PBXProject "Example" */ = {
365 | isa = XCConfigurationList;
366 | buildConfigurations = (
367 | 19879ECA22D58AAD00767E2E /* Debug */,
368 | 19879ECB22D58AAD00767E2E /* Release */,
369 | );
370 | defaultConfigurationIsVisible = 0;
371 | defaultConfigurationName = Release;
372 | };
373 | 19879ECC22D58AAD00767E2E /* Build configuration list for PBXNativeTarget "Example" */ = {
374 | isa = XCConfigurationList;
375 | buildConfigurations = (
376 | 19879ECD22D58AAD00767E2E /* Debug */,
377 | 19879ECE22D58AAD00767E2E /* Release */,
378 | );
379 | defaultConfigurationIsVisible = 0;
380 | defaultConfigurationName = Release;
381 | };
382 | /* End XCConfigurationList section */
383 | };
384 | rootObject = 19879EB222D58AA800767E2E /* Project object */;
385 | }
386 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Example.xcscheme
8 |
9 | orderHint
10 | 1
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 19879EB922D58AA800767E2E
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/Example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/7/10.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/catBlack.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "catBlack.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/catBlack.imageset/catBlack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXTheme/bd557355a4d2b339a9b9a5f8252865c95e621524/Example/Example/Assets.xcassets/catBlack.imageset/catBlack.png
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/catWhite.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "catWhite.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/catWhite.imageset/catWhite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXTheme/bd557355a4d2b339a9b9a5f8252865c95e621524/Example/Example/Assets.xcassets/catWhite.imageset/catWhite.png
--------------------------------------------------------------------------------
/Example/Example/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/Example/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
64 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
92 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
120 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
266 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
354 |
363 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
414 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
--------------------------------------------------------------------------------
/Example/Example/DynamicSourceManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DynamicSourceManager.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/7/12.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import JXTheme
11 |
12 | class DynamicSourceManager {
13 | static let shared = DynamicSourceManager()
14 | var configs: [String: [String: String]]?
15 | var themes: [String]?
16 |
17 | init() {
18 | let jsonString = "{ \"light_new\": {\"normal\" : \"#555555\", \"mainTitle\" : \"#000000\", \"subTitle\" : \"#333333\" }, \"dark_new\": {\"normal\" : \"#FFFFFF\", \"mainTitle\" : \"#FFFFFF\", \"subTitle\" : \"#FFFFFF\" }}"
19 | let jsonData = jsonString.data(using: .utf8)
20 | configs = (try? JSONSerialization.jsonObject(with: jsonData!, options: [])) as? [String: [String: String]]
21 | if configs != nil {
22 | themes = Array.init(configs!.keys)
23 | }
24 | }
25 |
26 | func textColor(style: ThemeStyle, level: TextColorLevel) -> UIColor {
27 | if let hex = configs?[style.rawValue]?[level.rawValue] {
28 | return hexStringToUIColor(hex: hex)
29 | }else {
30 | //可以根据需求配置默认色
31 | return UIColor.gray
32 | }
33 | }
34 |
35 | func hexStringToUIColor (hex:String) -> UIColor {
36 | var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
37 | if (cString.hasPrefix("#")) {
38 | cString.remove(at: cString.startIndex)
39 | }
40 | if ((cString.count) != 6) {
41 | return UIColor.gray
42 | }
43 | var rgbValue:UInt32 = 0
44 | Scanner(string: cString).scanHexInt32(&rgbValue)
45 | return UIColor(
46 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
47 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
48 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
49 | alpha: CGFloat(1.0)
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Example/Example/Images/catBlack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXTheme/bd557355a4d2b339a9b9a5f8252865c95e621524/Example/Example/Images/catBlack.png
--------------------------------------------------------------------------------
/Example/Example/Images/catWhite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXTheme/bd557355a4d2b339a9b9a5f8252865c95e621524/Example/Example/Images/catWhite.png
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Example/Example/ListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/7/16.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import JXTheme
11 |
12 | class ListViewController: UITableViewController {
13 |
14 | override init(style: UITableView.Style) {
15 | super.init(style: style)
16 |
17 | tableView.register(ListCell.self, forCellReuseIdentifier: "cell")
18 | navigationController?.navigationBar.theme.barStyle = ThemeProvider({ (style) -> UIBarStyle in
19 | if style == .dark {
20 | return .black
21 | }else {
22 | return .default
23 | }
24 | })
25 | tableView.theme.separatorColor = ThemeProvider({ (style) -> UIColor in
26 | if style == .dark {
27 | return .white
28 | }else {
29 | return .black
30 | }
31 | })
32 | tableView.theme.backgroundColor = ThemeProvider({ (style) -> UIColor in
33 | if style == .dark {
34 | return .black
35 | }else {
36 | return .white
37 | }
38 | })
39 | tableView.theme.indicatorStyle = ThemeProvider({ (style) -> UIScrollView.IndicatorStyle in
40 | if style == .dark {
41 | return .white
42 | }else {
43 | return .black
44 | }
45 | })
46 | refreshToggleButton()
47 | }
48 |
49 | required init?(coder aDecoder: NSCoder) {
50 | fatalError("init(coder:) has not been implemented")
51 | }
52 |
53 | func refreshToggleButton() {
54 | let barButtonItem = UIBarButtonItem(title: ThemeManager.shared.currentThemeStyle.rawValue, style: .plain, target: self, action: #selector(toggleThemeStyle))
55 | barButtonItem.theme.tintColor = ThemeProvider({ (style) -> UIColor in
56 | if style == .dark {
57 | return .white
58 | }else {
59 | return .black
60 | }
61 | })
62 | navigationItem.rightBarButtonItem = barButtonItem
63 | }
64 |
65 | @objc func toggleThemeStyle() {
66 | if ThemeManager.shared.currentThemeStyle == .dark {
67 | ThemeManager.shared.changeTheme(to: .light)
68 | }else {
69 | ThemeManager.shared.changeTheme(to: .dark)
70 | }
71 | refreshToggleButton()
72 | }
73 |
74 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
75 | return 100
76 | }
77 |
78 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
79 | return 100
80 | }
81 |
82 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
83 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ListCell
84 | cell.indexLabel.text = "index:\(indexPath.row)"
85 | return cell
86 | }
87 | }
88 |
89 | class ListCell: UITableViewCell {
90 | let nickLabel: UILabel
91 | let genderLabel: UILabel
92 | let indexLabel: UILabel
93 |
94 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
95 | nickLabel = UILabel()
96 | genderLabel = UILabel()
97 | indexLabel = UILabel()
98 | super.init(style: style, reuseIdentifier: reuseIdentifier)
99 |
100 | contentView.theme.backgroundColor = ThemeProvider({[weak self] (style) in
101 | if self?.isSelected == true && self?.selectedBackgroundView != nil {
102 | //①有背景选中视图且被选中状态,返回UIColor.clear
103 | return UIColor.clear
104 | }
105 | if style == .dark {
106 | return .black
107 | }else {
108 | return .white
109 | }
110 | })
111 |
112 | selectedBackgroundView = UIView()
113 | selectedBackgroundView?.theme.backgroundColor = ThemeProvider({ (style) in
114 | if style == .dark {
115 | return .red
116 | }else {
117 | return .green
118 | }
119 | })
120 |
121 | nickLabel.theme.textColor = ThemeProvider({ (style) in
122 | if style == .dark {
123 | return .white
124 | }else {
125 | return .black
126 | }
127 | })
128 | nickLabel.font = UIFont.systemFont(ofSize: 18)
129 | nickLabel.text = "这是昵称"
130 | contentView.addSubview(nickLabel)
131 |
132 | genderLabel.theme.textColor = ThemeProvider({ (style) in
133 | if style == .dark {
134 | return .white
135 | }else {
136 | return .gray
137 | }
138 | })
139 | genderLabel.font = UIFont.systemFont(ofSize: 15)
140 | genderLabel.text = "这是性别"
141 | contentView.addSubview(genderLabel)
142 |
143 | indexLabel.theme.textColor = ThemeProvider({ (style) in
144 | if style == .dark {
145 | return .white
146 | }else {
147 | return .lightGray
148 | }
149 | })
150 | indexLabel.font = UIFont.systemFont(ofSize: 13)
151 | indexLabel.text = "这是index"
152 | contentView.addSubview(indexLabel)
153 | }
154 |
155 | required init?(coder aDecoder: NSCoder) {
156 | fatalError("init(coder:) has not been implemented")
157 | }
158 |
159 | override func setSelected(_ selected: Bool, animated: Bool) {
160 | super.setSelected(selected, animated: animated)
161 |
162 | if !selected && selectedBackgroundView != nil {
163 | //因为上面①处在选中状态设置为clear,所以在未选中时,就需要刷新backgroundColor
164 | contentView.theme.backgroundColor?.refresh()
165 | }
166 | }
167 |
168 | override func layoutSubviews() {
169 | super.layoutSubviews()
170 |
171 | nickLabel.frame = CGRect(x: 20, y: 25, width: 100, height: 25)
172 | genderLabel.frame = CGRect(x: 20, y: 50 + 10, width: 100, height: 20)
173 | indexLabel.frame = CGRect(x: 150, y: 40, width: 100, height: 20)
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/Example/Example/StaticSource.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | unspecified
6 |
7 | subTitle
8 | #333333
9 | mainTitle
10 | #000000
11 | normal
12 | #555555
13 |
14 | light
15 |
16 | subTitle
17 | #333333
18 | mainTitle
19 | #000000
20 | normal
21 | #555555
22 |
23 | dark
24 |
25 | subTitle
26 | #FFFFFF
27 | mainTitle
28 | #FFFFFF
29 | normal
30 | #FFFFFF
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Example/Example/StaticSourceManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StaticSourceManager.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/7/12.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import JXTheme
12 |
13 | class StaticSourceManager {
14 | static let shared = StaticSourceManager()
15 | let configs: [String: [String: String]]
16 |
17 | init() {
18 | let plistPath = Bundle.main.path(forResource: "StaticSource", ofType: "plist")
19 | if plistPath == nil {
20 | configs = [String: [String: String]]()
21 | }else {
22 | let sources = NSDictionary(contentsOfFile: plistPath!) as? [String: [String: String]]
23 | if sources == nil {
24 | configs = [String: [String: String]]()
25 | }else {
26 | configs = sources!
27 | }
28 | }
29 | }
30 |
31 | func textColor(style: ThemeStyle, level: TextColorLevel) -> UIColor {
32 | if let hex = configs[style.rawValue]?[level.rawValue] {
33 | return hexStringToUIColor(hex: hex)
34 | }else {
35 | //可以根据需求配置默认色.
36 | return UIColor.gray
37 | }
38 | }
39 |
40 | func hexStringToUIColor (hex:String) -> UIColor {
41 | var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
42 | if (cString.hasPrefix("#")) {
43 | cString.remove(at: cString.startIndex)
44 | }
45 | if ((cString.count) != 6) {
46 | return UIColor.gray
47 | }
48 | var rgbValue:UInt32 = 0
49 | Scanner(string: cString).scanHexInt32(&rgbValue)
50 | return UIColor(
51 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
52 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
53 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
54 | alpha: CGFloat(1.0)
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Example/Example/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Example
4 | //
5 | // Created by jiaxin on 2019/7/10.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import JXTheme
11 |
12 | class ViewController: UITableViewController {
13 | @IBOutlet weak var themeView: UIView!
14 | @IBOutlet weak var themeLabel: UILabel!
15 | @IBOutlet weak var themeButton: UIButton!
16 | @IBOutlet weak var themeTextField: UITextField!
17 | @IBOutlet weak var themeTextView: UITextView!
18 | @IBOutlet weak var themeImageView: UIImageView!
19 | @IBOutlet weak var themeLayerContainerView: UIView!
20 | lazy var themeLayer: CALayer = { CALayer() }()
21 | @IBOutlet weak var customThemeStyleLabel: UILabel!
22 | @IBOutlet weak var attributedLabel: UILabel!
23 | @IBOutlet weak var statusLabel: UILabel!
24 | @IBOutlet weak var statusSwitch: UISwitch!
25 | @IBOutlet weak var overrideThemeStyleParentView: UIView!
26 | @IBOutlet weak var overrideThemeStyleSubview: UIView!
27 | @IBOutlet weak var overrideThemeStyleLabel: UILabel!
28 | @IBOutlet var cellTitleLabels: [UILabel]!
29 | @IBOutlet var cells: [UITableViewCell]!
30 | @IBOutlet weak var shadowColorLabel: UILabel!
31 |
32 |
33 | override func viewDidLoad() {
34 | super.viewDidLoad()
35 |
36 | NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange(notification:)), name: Notification.Name.JXThemeDidChange, object: nil)
37 | refreshToggleButton()
38 | // tableView.theme.backgroundColor = ThemeProvider({ (style) in
39 | // if style == .dark {
40 | // return .black
41 | // }else {
42 | // return .white
43 | // }
44 | // })
45 | //ThemeProvider根据项目的ThemeStyle自定义初始化器
46 | tableView.theme.backgroundColor = ThemeProvider(light: UIColor.white, dark: UIColor.white)
47 |
48 | //自定义属性shadowColor
49 | shadowColorLabel.shadowOffset = CGSize(width: 0, height: 2)
50 | shadowColorLabel.theme.shadowColor = ThemeProvider({ style in
51 | if style == .dark {
52 | return .red
53 | }else {
54 | return .green
55 | }
56 | })
57 |
58 | themeView.theme.backgroundColor = ThemeProvider({ (style) in
59 | if style == .dark {
60 | return .white
61 | }else {
62 | return .black
63 | }
64 | })
65 | //UIView customization
66 | themeView.theme.customization = ThemeProvider({[weak self] style in
67 | if style == .dark {
68 | self?.themeView.bounds = CGRect(x: 0, y: 0, width: 30, height: 30)
69 | }else {
70 | self?.themeView.bounds = CGRect(x: 0, y: 0, width: 80, height: 80)
71 | }
72 | })
73 |
74 | themeLabel.theme.backgroundColor = ThemeProvider({ (style) in
75 | if style == .dark {
76 | return .black
77 | }else {
78 | return .white
79 | }
80 | })
81 | themeLabel.theme.textColor = ThemeProvider({ (style) in
82 | if style == .dark {
83 | return .white
84 | }else {
85 | return .black
86 | }
87 | })
88 | //配置逻辑封装示例
89 | // themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
90 | // themeLabel.theme.textColor = dynamicPlistTextColor(.subTitle)
91 | // themeLabel.theme.textColor = dynamicJSONTextColor(.subTitle)
92 |
93 | themeButton.theme.backgroundColor = ThemeProvider({ (style) in
94 | if style == .dark {
95 | return .black
96 | }else {
97 | return .white
98 | }
99 | })
100 | themeButton.theme.setTitleColor(ThemeProvider({ (style) in
101 | if style == .dark {
102 | return .white
103 | }else {
104 | return .black
105 | }
106 | }), for: .normal)
107 | themeButton.theme.setTitleColor(ThemeProvider({ (style) in
108 | if style == .dark {
109 | return .yellow
110 | }else {
111 | return .orange
112 | }
113 | }), for: .selected)
114 |
115 | attributedLabel.theme.backgroundColor = ThemeProvider({ (style) in
116 | if style == .dark {
117 | return .black
118 | }else {
119 | return .white
120 | }
121 | })
122 | attributedLabel.theme.attributedText = ThemeProvider({ (style) in
123 | if style == .dark {
124 | let attributedText = NSMutableAttributedString(string: "这是attributedText主题测试文本", attributes: [.foregroundColor : UIColor.white, .font : UIFont.systemFont(ofSize: 15)])
125 | attributedText.addAttribute(.foregroundColor, value: UIColor.red, range: NSRange(location: 2, length: 14))
126 | return attributedText
127 | }else {
128 | let attributedText = NSMutableAttributedString(string: "这是attributedText主题测试文本", attributes: [.foregroundColor : UIColor.black, .font : UIFont.systemFont(ofSize: 15)])
129 | attributedText.addAttribute(.foregroundColor, value: UIColor.blue, range: NSRange(location: 2, length: 14))
130 | return attributedText
131 | }
132 | })
133 |
134 | themeTextField.theme.backgroundColor = ThemeProvider({ (style) in
135 | if style == .dark {
136 | return .black
137 | }else {
138 | return .white
139 | }
140 | })
141 | themeTextField.theme.textColor = ThemeProvider({ (style) in
142 | if style == .dark {
143 | return .white
144 | }else {
145 | return .black
146 | }
147 | })
148 |
149 | themeTextView.theme.backgroundColor = ThemeProvider({ (style) in
150 | if style == .dark {
151 | return .black
152 | }else {
153 | return .white
154 | }
155 | })
156 | themeTextView.theme.textColor = ThemeProvider({ (style) in
157 | if style == .dark {
158 | return .white
159 | }else {
160 | return .black
161 | }
162 | })
163 |
164 | themeImageView.theme.backgroundColor = ThemeProvider({ (style) in
165 | if style == .dark {
166 | return .black
167 | }else {
168 | return .white
169 | }
170 | })
171 | //图片本地静态配置(load image from bundle)
172 | themeImageView.theme.image = ThemeProvider({ (style) in
173 | if style == .dark {
174 | return UIImage(named: "catWhite")!
175 | }else {
176 | return UIImage(named: "catBlack")!
177 | }
178 | })
179 | //图片动态下载配置(load image from server)
180 | /*
181 | themeImageView.theme.customization = ThemeProvider({[weak self] style in
182 | // let url = switch sytle choose image url
183 | // self?.themeImageView.image = download image with url by other third library like Kingfisher
184 | })
185 | */
186 |
187 | themeLayerContainerView.layer.addSublayer(themeLayer)
188 | themeLayer.theme.backgroundColor = ThemeProvider({ (style) in
189 | if style == .dark {
190 | return .white
191 | }else {
192 | return .black
193 | }
194 | })
195 | //CALayer customization
196 | themeLayer.theme.customization = ThemeProvider({ [weak self] style in
197 | if style == .dark {
198 | self?.themeLayer.bounds = CGRect(x: 0, y: 0, width: 30, height: 30)
199 | }else {
200 | self?.themeLayer.bounds = CGRect(x: 0, y: 0, width: 80, height: 80)
201 | }
202 | })
203 |
204 | //可以下面这行注释掉,看看运行效果(Try comment below code.)
205 | overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
206 | overrideThemeStyleParentView.theme.backgroundColor = ThemeProvider({ (style) in
207 | if style == .dark {
208 | return .red
209 | }else {
210 | return .green
211 | }
212 | })
213 | overrideThemeStyleSubview.theme.backgroundColor = ThemeProvider({ (style) in
214 | if style == .dark {
215 | return .green
216 | }else {
217 | return .blue
218 | }
219 | })
220 | overrideThemeStyleLabel.theme.backgroundColor = ThemeProvider({ (style) in
221 | if style == .dark {
222 | return .black
223 | }else {
224 | return .white
225 | }
226 | })
227 | overrideThemeStyleLabel.theme.textColor = ThemeProvider({ (style) in
228 | if style == .dark {
229 | return .white
230 | }else {
231 | return .black
232 | }
233 | })
234 |
235 | statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
236 | if self?.statusSwitch.isOn == true {
237 | if style == .dark {
238 | return .red
239 | }else {
240 | return .green
241 | }
242 | }else {
243 | if style == .dark {
244 | return .white
245 | }else {
246 | return .black
247 | }
248 | }
249 | })
250 |
251 | //ThemeStyle customization
252 | customThemeStyleLabel.theme.backgroundColor = ThemeProvider({ (style) in
253 | if style == .dark {
254 | return .black
255 | }else if style == .pink {
256 | return UIColor(red: 255.0/255, green: 192.0/255, blue: 203.0/255, alpha: 1)
257 | }else {
258 | return .white
259 | }
260 | })
261 | customThemeStyleLabel.theme.textColor = ThemeProvider({ (style) in
262 | if style == .dark {
263 | return .white
264 | }else if style == .pink {
265 | return .white
266 | }else {
267 | return .black
268 | }
269 | })
270 |
271 | tableView.theme.separatorColor = ThemeProvider({ (style) in
272 | if style == .dark {
273 | return .white
274 | }else {
275 | return .black
276 | }
277 | })
278 | navigationController?.navigationBar.isTranslucent = false
279 | navigationController?.navigationBar.theme.barStyle = ThemeProvider({ (style) in
280 | if style == .dark {
281 | return .black
282 | }else {
283 | return .default
284 | }
285 | })
286 | }
287 |
288 | override func viewDidLayoutSubviews() {
289 | super.viewDidLayoutSubviews()
290 |
291 | themeLayer.frame = themeLayerContainerView.bounds
292 | }
293 |
294 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
295 | if indexPath.row == 11 {
296 | navigationController?.pushViewController(ListViewController(style: .plain), animated: true)
297 | }
298 | }
299 |
300 | override func viewWillAppear(_ animated: Bool) {
301 | super.viewWillAppear(animated)
302 |
303 | if ThemeManager.shared.currentThemeStyle == .dark {
304 | cells.forEach { $0.contentView.backgroundColor = .black }
305 | cellTitleLabels.forEach { $0.textColor = .white }
306 | }else {
307 | cells.forEach { $0.contentView.backgroundColor = .white }
308 | cellTitleLabels.forEach { $0.textColor = .black }
309 | }
310 | }
311 |
312 | // The notification of theme did change
313 | @objc func themeDidChange(notification: Notification) {
314 | let newStyle = notification.userInfo?["style"] as! ThemeStyle
315 | if newStyle == .dark {
316 | cells.forEach { $0.contentView.backgroundColor = .black }
317 | cellTitleLabels.forEach { $0.textColor = .white }
318 | }else {
319 | cells.forEach { $0.contentView.backgroundColor = .white }
320 | cellTitleLabels.forEach { $0.textColor = .black }
321 | }
322 | }
323 |
324 | func refreshToggleButton() {
325 | let barButtonItem = UIBarButtonItem(title: ThemeManager.shared.currentThemeStyle.rawValue, style: .plain, target: self, action: #selector(toggleThemeStyle))
326 | barButtonItem.theme.tintColor = ThemeProvider({ (style) in
327 | if style == .dark {
328 | return .white
329 | }else {
330 | return .black
331 | }
332 | })
333 | navigationItem.rightBarButtonItem = barButtonItem
334 | }
335 |
336 | @objc func toggleThemeStyle() {
337 | if ThemeManager.shared.currentThemeStyle == .dark {
338 | ThemeManager.shared.changeTheme(to: .light)
339 | }else {
340 | ThemeManager.shared.changeTheme(to: .dark)
341 | }
342 | refreshToggleButton()
343 | }
344 |
345 | @IBAction func togglePinkButtonClicked(_ sender: UIButton) {
346 | ThemeManager.shared.changeTheme(to: .pink)
347 | refreshToggleButton()
348 | }
349 |
350 | @IBAction func statusSwitchDidClicked(_ sender: UISwitch) {
351 | //当statusLabel的状态发生变化时,调用目标主题属性的refresh方法,触发主题色更新
352 | statusLabel.theme.textColor?.refresh()
353 | //如果你的状态控件有许多支持状态的主题属性,你也可以使用下面的方法,触发所有的主题属性刷新
354 | // statusLabel.theme.refresh()
355 | }
356 | }
357 |
358 | //自定义ThemeStyle示例
359 | extension ThemeStyle {
360 | static let light = ThemeStyle(rawValue: "light")
361 | static let dark = ThemeStyle(rawValue: "dark")
362 | static let pink = ThemeStyle(rawValue: "pink")
363 | }
364 |
365 | //业务自己封装配置逻辑
366 | enum TextColorLevel: String {
367 | case normal
368 | case mainTitle
369 | case subTitle
370 | }
371 |
372 | func dynamicTextColor(_ level: TextColorLevel) -> ThemeProvider {
373 | switch level {
374 | case .normal:
375 | return ThemeProvider({ (style) in
376 | if style == .dark {
377 | return UIColor.white
378 | }else {
379 | return UIColor.gray
380 | }
381 | })
382 | case .mainTitle:
383 | return ThemeProvider({ (style) in
384 | if style == .dark {
385 | return UIColor.white
386 | }else {
387 | return UIColor.black
388 | }
389 | })
390 | case .subTitle:
391 | return ThemeProvider({ (style) in
392 | if style == .dark {
393 | return UIColor.white
394 | }else {
395 | return UIColor.lightGray
396 | }
397 | })
398 | }
399 | }
400 |
401 | //静态plist使用示例
402 | func dynamicPlistTextColor(_ level: TextColorLevel) -> ThemeProvider {
403 | return ThemeProvider({ (style) in
404 | return StaticSourceManager.shared.textColor(style: style, level: level)
405 | })
406 | }
407 |
408 | //动态json使用示例
409 | func dynamicJSONTextColor(_ level: TextColorLevel) -> ThemeProvider {
410 | return ThemeProvider({ (style) in
411 | return DynamicSourceManager.shared.textColor(style: style, level: level)
412 | })
413 | }
414 |
415 | extension ThemeProvider {
416 | //根据项目支持的ThemeStyle调整
417 | init(light: T, dark: T) {
418 | self.init { style in
419 | switch style {
420 | case .light: return light
421 | case .dark: return dark
422 | default: return light
423 | }
424 | }
425 | }
426 | }
427 |
428 | //自定义添加ThemeProperty,目前仅支持UIView、CALayer、UIBarItem及其它们的子类
429 | extension ThemeWrapper where Base: UILabel {
430 | var shadowColor: ThemeProvider? {
431 | set(new) {
432 | let baseItem = self.base
433 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.shadowColor", provider: new) {[weak baseItem] (style) in
434 | baseItem?.shadowColor = new?.provider(style)
435 | }
436 | }
437 | get { return ThemeTool.getThemeProvider(target: self.base, with: "UILabel.shadowColor") as? ThemeProvider }
438 | }
439 | }
440 |
--------------------------------------------------------------------------------
/GIF/JXTheme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXTheme/bd557355a4d2b339a9b9a5f8252865c95e621524/GIF/JXTheme.png
--------------------------------------------------------------------------------
/GIF/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pujiaxin33/JXTheme/bd557355a4d2b339a9b9a5f8252865c95e621524/GIF/preview.gif
--------------------------------------------------------------------------------
/JXTheme.podspec:
--------------------------------------------------------------------------------
1 |
2 | Pod::Spec.new do |s|
3 | s.name = "JXTheme"
4 | s.version = "0.0.7"
5 | s.summary = "A powerful and lightweight and customization theme/skin library for iOS 9+ in swift. 主题、换肤、暗黑模式"
6 | s.homepage = "https://github.com/pujiaxin33/JXTheme"
7 | s.license = "MIT"
8 | s.author = { "pujiaxin33" => "317437084@qq.com" }
9 | s.platform = :ios, "9.0"
10 | s.swift_version = "5.0"
11 | s.source = { :git => "https://github.com/pujiaxin33/JXTheme.git", :tag => "#{s.version}" }
12 | s.framework = "UIKit"
13 | s.source_files = "Sources", "Sources/*.{swift}"
14 | s.requires_arc = true
15 | end
16 |
--------------------------------------------------------------------------------
/JXTheme.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1906719222D5E2FE00D8AEDD /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1906719122D5E2FE00D8AEDD /* Extensions.swift */; };
11 | 19879EA622D5862A00767E2E /* JXTheme.h in Headers */ = {isa = PBXBuildFile; fileRef = 19879EA422D5862A00767E2E /* JXTheme.h */; settings = {ATTRIBUTES = (Public, ); }; };
12 | 19879EAE22D5868000767E2E /* ThemeDefines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19879EAD22D5868000767E2E /* ThemeDefines.swift */; };
13 | 19879EB022D5870A00767E2E /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19879EAF22D5870A00767E2E /* ThemeManager.swift */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | 1906719122D5E2FE00D8AEDD /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; };
18 | 19879EA122D5862A00767E2E /* JXTheme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JXTheme.framework; sourceTree = BUILT_PRODUCTS_DIR; };
19 | 19879EA422D5862A00767E2E /* JXTheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JXTheme.h; sourceTree = ""; };
20 | 19879EA522D5862A00767E2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
21 | 19879EAD22D5868000767E2E /* ThemeDefines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeDefines.swift; sourceTree = ""; };
22 | 19879EAF22D5870A00767E2E /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; };
23 | /* End PBXFileReference section */
24 |
25 | /* Begin PBXFrameworksBuildPhase section */
26 | 19879E9E22D5862A00767E2E /* Frameworks */ = {
27 | isa = PBXFrameworksBuildPhase;
28 | buildActionMask = 2147483647;
29 | files = (
30 | );
31 | runOnlyForDeploymentPostprocessing = 0;
32 | };
33 | /* End PBXFrameworksBuildPhase section */
34 |
35 | /* Begin PBXGroup section */
36 | 19879E9722D5862A00767E2E = {
37 | isa = PBXGroup;
38 | children = (
39 | 19879EA322D5862A00767E2E /* JXTheme */,
40 | 19879EA222D5862A00767E2E /* Products */,
41 | );
42 | sourceTree = "";
43 | };
44 | 19879EA222D5862A00767E2E /* Products */ = {
45 | isa = PBXGroup;
46 | children = (
47 | 19879EA122D5862A00767E2E /* JXTheme.framework */,
48 | );
49 | name = Products;
50 | sourceTree = "";
51 | };
52 | 19879EA322D5862A00767E2E /* JXTheme */ = {
53 | isa = PBXGroup;
54 | children = (
55 | 19879EAC22D5864E00767E2E /* Sources */,
56 | 19879EA422D5862A00767E2E /* JXTheme.h */,
57 | 19879EA522D5862A00767E2E /* Info.plist */,
58 | );
59 | path = JXTheme;
60 | sourceTree = "";
61 | };
62 | 19879EAC22D5864E00767E2E /* Sources */ = {
63 | isa = PBXGroup;
64 | children = (
65 | 19879EAD22D5868000767E2E /* ThemeDefines.swift */,
66 | 1906719122D5E2FE00D8AEDD /* Extensions.swift */,
67 | 19879EAF22D5870A00767E2E /* ThemeManager.swift */,
68 | );
69 | path = Sources;
70 | sourceTree = SOURCE_ROOT;
71 | };
72 | /* End PBXGroup section */
73 |
74 | /* Begin PBXHeadersBuildPhase section */
75 | 19879E9C22D5862A00767E2E /* Headers */ = {
76 | isa = PBXHeadersBuildPhase;
77 | buildActionMask = 2147483647;
78 | files = (
79 | 19879EA622D5862A00767E2E /* JXTheme.h in Headers */,
80 | );
81 | runOnlyForDeploymentPostprocessing = 0;
82 | };
83 | /* End PBXHeadersBuildPhase section */
84 |
85 | /* Begin PBXNativeTarget section */
86 | 19879EA022D5862A00767E2E /* JXTheme */ = {
87 | isa = PBXNativeTarget;
88 | buildConfigurationList = 19879EA922D5862A00767E2E /* Build configuration list for PBXNativeTarget "JXTheme" */;
89 | buildPhases = (
90 | 19879E9C22D5862A00767E2E /* Headers */,
91 | 19879E9D22D5862A00767E2E /* Sources */,
92 | 19879E9E22D5862A00767E2E /* Frameworks */,
93 | 19879E9F22D5862A00767E2E /* Resources */,
94 | );
95 | buildRules = (
96 | );
97 | dependencies = (
98 | );
99 | name = JXTheme;
100 | productName = JXTheme;
101 | productReference = 19879EA122D5862A00767E2E /* JXTheme.framework */;
102 | productType = "com.apple.product-type.framework";
103 | };
104 | /* End PBXNativeTarget section */
105 |
106 | /* Begin PBXProject section */
107 | 19879E9822D5862A00767E2E /* Project object */ = {
108 | isa = PBXProject;
109 | attributes = {
110 | LastUpgradeCheck = 1020;
111 | ORGANIZATIONNAME = jiaxin;
112 | TargetAttributes = {
113 | 19879EA022D5862A00767E2E = {
114 | CreatedOnToolsVersion = 10.2.1;
115 | LastSwiftMigration = 1020;
116 | };
117 | };
118 | };
119 | buildConfigurationList = 19879E9B22D5862A00767E2E /* Build configuration list for PBXProject "JXTheme" */;
120 | compatibilityVersion = "Xcode 9.3";
121 | developmentRegion = en;
122 | hasScannedForEncodings = 0;
123 | knownRegions = (
124 | en,
125 | );
126 | mainGroup = 19879E9722D5862A00767E2E;
127 | productRefGroup = 19879EA222D5862A00767E2E /* Products */;
128 | projectDirPath = "";
129 | projectRoot = "";
130 | targets = (
131 | 19879EA022D5862A00767E2E /* JXTheme */,
132 | );
133 | };
134 | /* End PBXProject section */
135 |
136 | /* Begin PBXResourcesBuildPhase section */
137 | 19879E9F22D5862A00767E2E /* Resources */ = {
138 | isa = PBXResourcesBuildPhase;
139 | buildActionMask = 2147483647;
140 | files = (
141 | );
142 | runOnlyForDeploymentPostprocessing = 0;
143 | };
144 | /* End PBXResourcesBuildPhase section */
145 |
146 | /* Begin PBXSourcesBuildPhase section */
147 | 19879E9D22D5862A00767E2E /* Sources */ = {
148 | isa = PBXSourcesBuildPhase;
149 | buildActionMask = 2147483647;
150 | files = (
151 | 19879EAE22D5868000767E2E /* ThemeDefines.swift in Sources */,
152 | 1906719222D5E2FE00D8AEDD /* Extensions.swift in Sources */,
153 | 19879EB022D5870A00767E2E /* ThemeManager.swift in Sources */,
154 | );
155 | runOnlyForDeploymentPostprocessing = 0;
156 | };
157 | /* End PBXSourcesBuildPhase section */
158 |
159 | /* Begin XCBuildConfiguration section */
160 | 19879EA722D5862A00767E2E /* Debug */ = {
161 | isa = XCBuildConfiguration;
162 | buildSettings = {
163 | ALWAYS_SEARCH_USER_PATHS = NO;
164 | CLANG_ANALYZER_NONNULL = YES;
165 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
167 | CLANG_CXX_LIBRARY = "libc++";
168 | CLANG_ENABLE_MODULES = YES;
169 | CLANG_ENABLE_OBJC_ARC = YES;
170 | CLANG_ENABLE_OBJC_WEAK = YES;
171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
172 | CLANG_WARN_BOOL_CONVERSION = YES;
173 | CLANG_WARN_COMMA = YES;
174 | CLANG_WARN_CONSTANT_CONVERSION = YES;
175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
178 | CLANG_WARN_EMPTY_BODY = YES;
179 | CLANG_WARN_ENUM_CONVERSION = YES;
180 | CLANG_WARN_INFINITE_RECURSION = YES;
181 | CLANG_WARN_INT_CONVERSION = YES;
182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
186 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
187 | CLANG_WARN_STRICT_PROTOTYPES = YES;
188 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
189 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
190 | CLANG_WARN_UNREACHABLE_CODE = YES;
191 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
192 | CODE_SIGN_IDENTITY = "iPhone Developer";
193 | COPY_PHASE_STRIP = NO;
194 | CURRENT_PROJECT_VERSION = 1;
195 | DEBUG_INFORMATION_FORMAT = dwarf;
196 | ENABLE_STRICT_OBJC_MSGSEND = YES;
197 | ENABLE_TESTABILITY = YES;
198 | GCC_C_LANGUAGE_STANDARD = gnu11;
199 | GCC_DYNAMIC_NO_PIC = NO;
200 | GCC_NO_COMMON_BLOCKS = YES;
201 | GCC_OPTIMIZATION_LEVEL = 0;
202 | GCC_PREPROCESSOR_DEFINITIONS = (
203 | "DEBUG=1",
204 | "$(inherited)",
205 | );
206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
208 | GCC_WARN_UNDECLARED_SELECTOR = YES;
209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
210 | GCC_WARN_UNUSED_FUNCTION = YES;
211 | GCC_WARN_UNUSED_VARIABLE = YES;
212 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
213 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
214 | MTL_FAST_MATH = YES;
215 | ONLY_ACTIVE_ARCH = YES;
216 | SDKROOT = iphoneos;
217 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
218 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
219 | VERSIONING_SYSTEM = "apple-generic";
220 | VERSION_INFO_PREFIX = "";
221 | };
222 | name = Debug;
223 | };
224 | 19879EA822D5862A00767E2E /* Release */ = {
225 | isa = XCBuildConfiguration;
226 | buildSettings = {
227 | ALWAYS_SEARCH_USER_PATHS = NO;
228 | CLANG_ANALYZER_NONNULL = YES;
229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
231 | CLANG_CXX_LIBRARY = "libc++";
232 | CLANG_ENABLE_MODULES = YES;
233 | CLANG_ENABLE_OBJC_ARC = YES;
234 | CLANG_ENABLE_OBJC_WEAK = YES;
235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
236 | CLANG_WARN_BOOL_CONVERSION = YES;
237 | CLANG_WARN_COMMA = YES;
238 | CLANG_WARN_CONSTANT_CONVERSION = YES;
239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
242 | CLANG_WARN_EMPTY_BODY = YES;
243 | CLANG_WARN_ENUM_CONVERSION = YES;
244 | CLANG_WARN_INFINITE_RECURSION = YES;
245 | CLANG_WARN_INT_CONVERSION = YES;
246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
251 | CLANG_WARN_STRICT_PROTOTYPES = YES;
252 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
254 | CLANG_WARN_UNREACHABLE_CODE = YES;
255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
256 | CODE_SIGN_IDENTITY = "iPhone Developer";
257 | COPY_PHASE_STRIP = NO;
258 | CURRENT_PROJECT_VERSION = 1;
259 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
260 | ENABLE_NS_ASSERTIONS = NO;
261 | ENABLE_STRICT_OBJC_MSGSEND = YES;
262 | GCC_C_LANGUAGE_STANDARD = gnu11;
263 | GCC_NO_COMMON_BLOCKS = YES;
264 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
265 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
266 | GCC_WARN_UNDECLARED_SELECTOR = YES;
267 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
268 | GCC_WARN_UNUSED_FUNCTION = YES;
269 | GCC_WARN_UNUSED_VARIABLE = YES;
270 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
271 | MTL_ENABLE_DEBUG_INFO = NO;
272 | MTL_FAST_MATH = YES;
273 | SDKROOT = iphoneos;
274 | SWIFT_COMPILATION_MODE = wholemodule;
275 | SWIFT_OPTIMIZATION_LEVEL = "-O";
276 | VALIDATE_PRODUCT = YES;
277 | VERSIONING_SYSTEM = "apple-generic";
278 | VERSION_INFO_PREFIX = "";
279 | };
280 | name = Release;
281 | };
282 | 19879EAA22D5862A00767E2E /* Debug */ = {
283 | isa = XCBuildConfiguration;
284 | buildSettings = {
285 | CLANG_ENABLE_MODULES = YES;
286 | CODE_SIGN_IDENTITY = "";
287 | CODE_SIGN_STYLE = Automatic;
288 | DEFINES_MODULE = YES;
289 | DYLIB_COMPATIBILITY_VERSION = 1;
290 | DYLIB_CURRENT_VERSION = 1;
291 | DYLIB_INSTALL_NAME_BASE = "@rpath";
292 | INFOPLIST_FILE = JXTheme/Info.plist;
293 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
294 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
295 | LD_RUNPATH_SEARCH_PATHS = (
296 | "$(inherited)",
297 | "@executable_path/Frameworks",
298 | "@loader_path/Frameworks",
299 | );
300 | PRODUCT_BUNDLE_IDENTIFIER = jiaxin.JXTheme;
301 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
302 | SKIP_INSTALL = YES;
303 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
304 | SWIFT_VERSION = 5.0;
305 | TARGETED_DEVICE_FAMILY = "1,2";
306 | };
307 | name = Debug;
308 | };
309 | 19879EAB22D5862A00767E2E /* Release */ = {
310 | isa = XCBuildConfiguration;
311 | buildSettings = {
312 | CLANG_ENABLE_MODULES = YES;
313 | CODE_SIGN_IDENTITY = "";
314 | CODE_SIGN_STYLE = Automatic;
315 | DEFINES_MODULE = YES;
316 | DYLIB_COMPATIBILITY_VERSION = 1;
317 | DYLIB_CURRENT_VERSION = 1;
318 | DYLIB_INSTALL_NAME_BASE = "@rpath";
319 | INFOPLIST_FILE = JXTheme/Info.plist;
320 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
321 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
322 | LD_RUNPATH_SEARCH_PATHS = (
323 | "$(inherited)",
324 | "@executable_path/Frameworks",
325 | "@loader_path/Frameworks",
326 | );
327 | PRODUCT_BUNDLE_IDENTIFIER = jiaxin.JXTheme;
328 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
329 | SKIP_INSTALL = YES;
330 | SWIFT_VERSION = 5.0;
331 | TARGETED_DEVICE_FAMILY = "1,2";
332 | };
333 | name = Release;
334 | };
335 | /* End XCBuildConfiguration section */
336 |
337 | /* Begin XCConfigurationList section */
338 | 19879E9B22D5862A00767E2E /* Build configuration list for PBXProject "JXTheme" */ = {
339 | isa = XCConfigurationList;
340 | buildConfigurations = (
341 | 19879EA722D5862A00767E2E /* Debug */,
342 | 19879EA822D5862A00767E2E /* Release */,
343 | );
344 | defaultConfigurationIsVisible = 0;
345 | defaultConfigurationName = Release;
346 | };
347 | 19879EA922D5862A00767E2E /* Build configuration list for PBXNativeTarget "JXTheme" */ = {
348 | isa = XCConfigurationList;
349 | buildConfigurations = (
350 | 19879EAA22D5862A00767E2E /* Debug */,
351 | 19879EAB22D5862A00767E2E /* Release */,
352 | );
353 | defaultConfigurationIsVisible = 0;
354 | defaultConfigurationName = Release;
355 | };
356 | /* End XCConfigurationList section */
357 | };
358 | rootObject = 19879E9822D5862A00767E2E /* Project object */;
359 | }
360 |
--------------------------------------------------------------------------------
/JXTheme.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/JXTheme.xcodeproj/xcshareddata/xcschemes/JXTheme.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/JXTheme.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | JXTheme.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/JXTheme/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/JXTheme/JXTheme.h:
--------------------------------------------------------------------------------
1 | //
2 | // JXTheme.h
3 | // JXTheme
4 | //
5 | // Created by jiaxin on 2019/7/10.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for JXTheme.
12 | FOUNDATION_EXPORT double JXThemeVersionNumber;
13 |
14 | //! Project version string for JXTheme.
15 | FOUNDATION_EXPORT const unsigned char JXThemeVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 暴走的鑫鑫
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "JXTheme",
8 | platforms: [
9 | .iOS(.v9)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "JXTheme",
15 | targets: ["JXTheme"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "JXTheme",
26 | path: "Sources"),
27 |
28 | ],
29 | swiftLanguageVersions: [.version("5")]
30 | )
31 |
--------------------------------------------------------------------------------
/README-CN.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | JXTheme是一个提供主题属性配置的轻量级基础库。
5 |
6 |
7 | # 特性
8 |
9 | - [x] 支持iOS 9+,让你的APP更早的实现`DarkMode`;
10 | - [x] 使用`theme`命名空间属性:`view.theme.xx = xx`。告别`theme_xx`属性扩展用法;
11 | - [x] 使用`ThemeProvider`传入闭包配置。根据不同的`ThemeStyle`完成主题属性配置,实现最大化的自定义;
12 | - [x] `ThemeStyle`可通过`extension`自定义style,不再局限于`light`和`dark`;
13 | - [x] 提供`customization`属性,作为主题切换的回调入口,可以灵活配置任何属性。不再局限于提供的`backgroundColor`、`textColor`等属性;
14 | - [x] 支持控件设置`overrideThemeStyle`,会影响到其子视图;
15 | - [x] 提供根据`ThemeStyle`配置属性的常规封装、Plist文件静态加载、服务器动态加载示例;
16 |
17 | # 预览
18 | 
19 |
20 | # 要求
21 |
22 | - iOS 9.0+
23 | - XCode 10.2.1+
24 | - Swift 5.0+
25 |
26 | # 安装
27 |
28 | ## 手动
29 |
30 | Clone代码,把Sources文件夹拖入项目,就可以使用了;
31 |
32 | ## CocoaPods
33 |
34 | ```ruby
35 | target '' do
36 | pod 'JXTheme'
37 | end
38 | ```
39 | 先执行`pod repo update`,再执行`pod install`
40 |
41 | ## Carthage
42 | 在cartfile文件添加:
43 | ```
44 | github "pujiaxin33/JXTheme"
45 | ```
46 | 然后执行`carthage update --platform iOS` ,其他配置请参考Carthage文档
47 |
48 | # 使用示例
49 |
50 | ## 扩展`ThemeStyle`添加自定义style
51 | `ThemeStyle`内部仅提供了一个默认的`unspecified`style,其他的业务style需要自己添加,比如只支持`light`和`dark`,代码如下:
52 | ```Swift
53 | extension ThemeStyle {
54 | static let light = ThemeStyle(rawValue: "light")
55 | static let dark = ThemeStyle(rawValue: "dark")
56 | }
57 | ```
58 |
59 | ## 基础使用
60 | ```Swift
61 | view.theme.backgroundColor = ThemeProvider({ (style) in
62 | if style == .dark {
63 | return .white
64 | }else {
65 | return .black
66 | }
67 | })
68 | imageView.theme.image = ThemeProvider({ (style) in
69 | if style == .dark {
70 | return UIImage(named: "catWhite")!
71 | }else {
72 | return UIImage(named: "catBlack")!
73 | }
74 | })
75 | ```
76 |
77 | ## 自定义属性配置
78 | 如果库没有原生支持某个属性,可以在customization里面统一处理。
79 | ```Swift
80 | view.theme.customization = ThemeProvider({[weak self] style in
81 | //可以选择任一其他属性
82 | if style == .dark {
83 | self?.view.bounds = CGRect(x: 0, y: 0, width: 30, height: 30)
84 | }else {
85 | self?.view.bounds = CGRect(x: 0, y: 0, width: 80, height: 80)
86 | }
87 | })
88 | ```
89 |
90 | ## extension ThemeWrapper添加属性
91 |
92 | 如果某一个属性在项目中经常使用,使用上面的**自定义属性配置**觉得麻烦,就可以自己extension ThemeWrapper添加想要的属性。(ps:你也可以提交一个Pull Request申请添加哟)
93 |
94 | 下面是UILabel添加shadowColor的示例:
95 | ```Swift
96 | //自定义添加ThemeProperty,目前仅支持UIView、CALayer、UIBarItem及其它们的子类
97 | extension ThemeWrapper where Base: UILabel {
98 | var shadowColor: ThemeProvider? {
99 | set(new) {
100 | let baseItem = self.base
101 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.shadowColor", provider: new) {[weak baseItem] (style) in
102 | baseItem?.shadowColor = new?.provider(style)
103 | }
104 | }
105 | get { return ThemeTool.getThemeProvider(target: self.base, with: "UILabel.shadowColor") as? ThemeProvider }
106 | }
107 | }
108 | ```
109 | 调用还是一样的:
110 | ```Swift
111 | //自定义属性shadowColor
112 | shadowColorLabel.shadowOffset = CGSize(width: 0, height: 2)
113 | shadowColorLabel.theme.shadowColor = ThemeProvider({ style in
114 | if style == .dark {
115 | return .red
116 | }else {
117 | return .green
118 | }
119 | })
120 | ```
121 |
122 | ## 配置封装示例
123 | `JXTheme`是一个提供主题属性配置的轻量级基础库,不限制使用哪种方式加载资源。下面提供的三个示例仅供参考。
124 |
125 | ### ThemeProvider自定义初始化器
126 | 比如在项目中添加如下代码:
127 | ```Swift
128 | extension ThemeProvider {
129 | //根据项目支持的ThemeStyle调整
130 | init(light: T, dark: T) {
131 | self.init { style in
132 | switch style {
133 | case .light: return light
134 | case .dark: return dark
135 | default: return light
136 | }
137 | }
138 | }
139 | }
140 | ```
141 | 在业务代码中调用:
142 | ```Swift
143 | tableView.theme.backgroundColor = ThemeProvider(light: UIColor.white, dark: UIColor.white)
144 | ```
145 | 这样就可以避免ThemeProvider闭包的形式,更加简洁。
146 |
147 | ### 根据枚举定义封装示例
148 |
149 | 一般的换肤需求,都会有一个UI标准。比如`UILabel.textColor`定义三个等级,代码如下:
150 | ```Swift
151 | enum TextColorLevel: String {
152 | case normal
153 | case mainTitle
154 | case subTitle
155 | }
156 | ```
157 | 然后可以封装一个全局函数传入`TextColorLevel`返回对应的配置闭包,就可以极大的减少配置时的代码量,全局函数如下:
158 | ```Swift
159 | func dynamicTextColor(_ level: TextColorLevel) -> ThemeProvider {
160 | switch level {
161 | case .normal:
162 | return ThemeProvider({ (style) in
163 | if style == .dark {
164 | return UIColor.white
165 | }else {
166 | return UIColor.gray
167 | }
168 | })
169 | case .mainTitle:
170 | ...
171 | case .subTitle:
172 | ...
173 | }
174 | }
175 | ```
176 | 主题属性配置时的代码如下:
177 | ```Swift
178 | themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
179 | ```
180 |
181 | ### 本地Plist文件配置示例
182 | 与**常规配置封装**一样,只是该方法是从本地Plist文件加载配置的具体值,具体代码参加`Example`的`StaticSourceManager`类
183 |
184 | ### 根据服务器动态添加主题
185 | 与**常规配置封装**一样,只是该方法是从服务器加载配置的具体值,具体代码参加`Example`的`DynamicSourceManager`类
186 |
187 | ## 有状态的控件
188 | 某些业务需求会存在一个控件有多种状态,比如选中与未选中。不同的状态对于不同的主题又会有不同的配置。配置代码参考如下:
189 | ```Swift
190 | statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
191 | if self?.statusLabelStatus == .isSelected {
192 | //选中状态一种配置
193 | if style == .dark {
194 | return .red
195 | }else {
196 | return .green
197 | }
198 | }else {
199 | //未选中状态另一种配置
200 | if style == .dark {
201 | return .white
202 | }else {
203 | return .black
204 | }
205 | }
206 | })
207 | ```
208 |
209 | 当控件的状态更新时,需要刷新当前的主题属性配置,代码如下:
210 | ```Swift
211 | func statusDidChange() {
212 | statusLabel.theme.textColor?.refresh()
213 | }
214 | ```
215 |
216 | 如果你的控件支持多个状态属性,比如有`textColor`、`backgroundColor`、`font`等等,你可以不用一个一个的主题属性调用`refresh`方法,可以使用下面的代码完成所有配置的主题属性刷新:
217 | ```Swift
218 | func statusDidChange() {
219 | statusLabel.theme.refresh()
220 | }
221 | ```
222 |
223 | ## overrideThemeStyle
224 | 不管主题如何切换,`overrideThemeStyleParentView`及其子视图的`themeStyle`都是`dark`
225 | ```Swift
226 | overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
227 | ```
228 |
229 | # 实现原理
230 |
231 | - [实现原理](https://github.com/pujiaxin33/JXTheme/blob/master/Document/%E5%8E%9F%E7%90%86.md)
232 |
233 |
234 | # 其他说明
235 |
236 | ## 为什么使用`theme`命名空间属性,而不是使用`theme_xx`扩展属性呢?
237 | - 如果你给系统的类扩展了N个函数,当你在使用该类时,进行函数索引时,就会有N个扩展的方法干扰你的选择。尤其是你在进行其他业务开发,而不是想配置主题属性时。
238 | - 像`Kingfisher`、`SnapKit`等知名三方库,都使用了命名空间属性实现对系统类的扩展,这是一个更`Swift`的写法,值得学习。
239 |
240 | ## 主题切换通知
241 | ```Swift
242 | extension Notification.Name {
243 | public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
244 | }
245 | ```
246 |
247 | ## `ThemeManager`根据用户ID存储主题配置
248 |
249 | ```
250 | /// 配置存储的标志key。可以设置为用户的ID,这样在同一个手机,可以分别记录不同用户的配置。需要优先设置该属性再设置其他值。
251 | public var storeConfigsIdentifierKey: String = "default"
252 | ```
253 |
254 | ## 迁移到系统API指南
255 | 当你的应用最低支持iOS13时,如果需要的话可以按照如下指南,迁移到系统方案。
256 | [迁移到系统API指南,点击阅读](https://github.com/pujiaxin33/JXTheme/blob/master/Document/%E8%BF%81%E7%A7%BB%E5%88%B0%E7%B3%BB%E7%BB%9FAPI%E6%8C%87%E5%8D%97.md)
257 |
258 | # 目前支持的类及其属性
259 |
260 | 这里的属性是有继承关系的,比如`UIView`支持`backgroundColor`属性,那么它的子类`UILabel`等也就支持`backgroundColor`。如果没有你想要支持的类或属性,欢迎提PullRequest进行扩展。
261 |
262 | ## UIView
263 |
264 | - `backgroundColor`
265 | - `tintColor`
266 | - `alpha`
267 | - `customization`
268 |
269 | ## UILabel
270 |
271 | - `font`
272 | - `textColor`
273 | - `shadowColor`
274 | - `highlightedTextColor`
275 | - `attributedText`
276 |
277 | ## UIButton
278 |
279 | - `func setTitleColor(_ colorProvider: ThemeColorDynamicProvider?, for state: UIControl.State)`
280 | - `func setTitleShadowColor(_ colorProvider: ThemeColorDynamicProvider?, for state: UIControl.State)`
281 | - `func setAttributedTitle(_ textProvider: ThemeAttributedTextDynamicProvider?, for state: UIControl.State)`
282 | - `func setImage(_ imageProvider: ThemeImageDynamicProvider?, for state: UIControl.State)`
283 | - `func setBackgroundImage(_ imageProvider: ThemeImageDynamicProvider?, for state: UIControl.State)`
284 |
285 | ## UITextField
286 |
287 | - `font`
288 | - `textColor`
289 | - `attributedText`
290 | - `attributedPlaceholder`
291 | - `keyboardAppearance`
292 |
293 | ## UITextView
294 |
295 | - `font`
296 | - `textColor`
297 | - `attributedText`
298 | - `keyboardAppearance`
299 |
300 | ## UIImageView
301 |
302 | - `image`
303 |
304 | ## CALayer
305 |
306 | - `backgroundColor`
307 | - `borderColor`
308 | - `borderWidth`
309 | - `shadowColor`
310 | - `customization`
311 |
312 | ## CAShapeLayer
313 |
314 | - `fillColor`
315 | - `strokeColor`
316 |
317 | ## UINavigationBar
318 |
319 | - `barStyle`
320 | - `barTintColor`
321 | - `titleTextAttributes`
322 | - `largeTitleTextAttributes`
323 |
324 | ## UITabBar
325 |
326 | - `barStyle`
327 | - `barTintColor`
328 | - `shadowImage`
329 |
330 | ## UISearchBar
331 |
332 | - `barStyle`
333 | - `barTintColor`
334 | - `keyboardAppearance`
335 |
336 | ## UIToolbar
337 |
338 | - `barStyle`
339 | - `barTintColor`
340 |
341 | ## UISwitch
342 |
343 | - `onTintColor`
344 | - `thumbTintColor`
345 |
346 | ## UISlider
347 |
348 | - `thumbTintColor`
349 | - `minimumTrackTintColor`
350 | - `maximumTrackTintColor`
351 | - `minimumValueImage`
352 | - `maximumValueImage`
353 |
354 | ## UIRefreshControl
355 |
356 | - `attributedTitle`
357 |
358 | ## UIProgressView
359 |
360 | - `progressTintColor`
361 | - `trackTintColor`
362 | - `progressImage`
363 | - `trackImage`
364 |
365 | ## UIPageControl
366 |
367 | - `pageIndicatorTintColor`
368 | - `currentPageIndicatorTintColor`
369 |
370 | ## UIBarItem
371 |
372 | - `func setTitleTextAttributes(_ attributesProvider: ThemeAttributesDynamicProvider?, for state: UIControl.State)`
373 | - `image`
374 |
375 | ## UIBarButtonItem
376 |
377 | - `tintColor`
378 |
379 | ## UIActivityIndicatorView
380 |
381 | - `style`
382 | - `color`
383 |
384 | ## UIScrollView
385 |
386 | - `indicatorStyle`
387 |
388 | ## UITableView
389 |
390 | - `separatorColor`
391 | - `sectionIndexColor`
392 | - `sectionIndexBackgroundColor`
393 |
394 | # Contribution
395 |
396 | 有任何疑问或建议,欢迎提Issue和Pull Request进行交流🤝
397 |
398 |
399 |
400 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | [中文文档](https://github.com/pujiaxin33/JXTheme/blob/master/README-CN.md)
5 |
6 | JXTheme is a lightweight library for theme properties configuration.
7 |
8 | # Feature
9 |
10 | - [x] Support for iOS 9+, let your app implement `DarkMode` earlier;
11 | - [x] Use the `theme` namespace attribute: `view.theme.xx = xx`. Say goodbye to the `theme_xx` attribute extension usage;
12 | - [x] `ThemeStyle` can be customized by `extension`, no longer limited to `light` and `dark`;
13 | - [x] provides the `customization` attribute as a callback entry for theme switching, with the flexibility to configure any property. It is no longer limited to the provided attributes such as `backgroundColor` and `textColor`;
14 | - [x] supports the control setting `overrideThemeStyle`, which affects its child views;
15 |
16 | # Preview
17 | 
18 |
19 | # Requirements
20 |
21 | - iOS 9.0+
22 | - XCode 10.2.1+
23 | - Swift 5.0+
24 |
25 | # Install
26 |
27 | ## Manual
28 |
29 | Clone code, drag the Sources folder into the project, you can use it;
30 |
31 | ## CocoaPods
32 |
33 | ```ruby
34 | Target '' do
35 | Pod 'JXTheme'
36 | End
37 | ```
38 | Execute `pod repo update` first, then execute `pod install`
39 |
40 | ## Carthage
41 | Add in the cartfile:
42 | ```
43 | Github "pujiaxin33/JXTheme"
44 | ```
45 | Then execute `carthage update --platform iOS`. For other configurations, please refer to the Carthage documentation.
46 |
47 | #Usage
48 |
49 | ## Add a custom style by extension`ThemeStyle`
50 | `ThemeStyle` only provides a default `unspecified` style. Other business styles need to be added by themselves. For example, only `light` and `dark` are supported. The code is as follows:
51 | ```Swift
52 | Extension ThemeStyle {
53 | Static let light = ThemeStyle(rawValue: "light")
54 | Static let dark = ThemeStyle(rawValue: "dark")
55 | }
56 | ```
57 |
58 | ## Basic use
59 | ```Swift
60 | view.theme.backgroundColor = ThemeProvider({ (style) in
61 | If style == .dark {
62 | Return .white
63 | }else {
64 | Return .black
65 | }
66 | })
67 | imageView.theme.image = ThemeProvider({ (style) in
68 | If style == .dark {
69 | Return UIImage(named: "catWhite")!
70 | }else {
71 | Return UIImage(named: "catBlack")!
72 | }
73 | })
74 | ```
75 |
76 | ## Custom Properties Configuration
77 | If the library does not natively support a certain attribute, it can be handled uniformly in the customization.
78 | ```Swift
79 | View.theme.customization = ThemeProvider({[weak self] style in
80 | / / You can choose any other property
81 | If style == .dark {
82 | Self?.view.bounds = CGRect(x: 0, y: 0, width: 30, height: 30)
83 | }else {
84 | Self?.view.bounds = CGRect(x: 0, y: 0, width: 80, height: 80)
85 | }
86 | })
87 | ```
88 |
89 | ## extension ThemeWrapper add property
90 |
91 | If a certain attribute is frequently used in the project, and it is troublesome to use the above **Custom Properties Configuration**, you can add the desired property by yourself with the extension ThemeWrapper. (Ps: You can also submit a Pull Request to add)
92 |
93 | The following is an example of UILabel adding shadowColor:
94 | ```Swift
95 | //Custom add ThemeProperty, currently only supports UIView, CALayer, UIBarItem and their subclasses
96 | extension ThemeWrapper where Base: UILabel {
97 | var shadowColor: ThemeProvider? {
98 | set(new) {
99 | let baseItem = self.base
100 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.shadowColor", provider: new) {[weak baseItem] (style) in
101 | baseItem?.shadowColor = new?.provider(style)
102 | }
103 | }
104 | get {return ThemeTool.getThemeProvider(target: self.base, with: "UILabel.shadowColor") as? ThemeProvider}
105 | }
106 | }
107 | ```
108 | The call is still the same:
109 | ```Swift
110 | //Custom attribute shadowColor
111 | shadowColorLabel.shadowOffset = CGSize(width: 0, height: 2)
112 | shadowColorLabel.theme.shadowColor = ThemeProvider({ style in
113 | if style == .dark {
114 | return .red
115 | }else {
116 | return .green
117 | }
118 | })
119 | ```
120 |
121 | ## Configuring the package example
122 | `JXTheme` is a lightweight base library that provides configuration of theme properties, and does not restrict which way to load resources. The three examples provided below are for reference only.
123 |
124 | ### ThemeProvider custom initializer
125 | For example, add the following code to the project:
126 | ```Swift
127 | extension ThemeProvider {
128 | //Adjust according to the ThemeStyle supported by the project
129 | init(light: T, dark: T) {
130 | self.init {style in
131 | switch style {
132 | case .light: return light
133 | case .dark: return dark
134 | default: return light
135 | }
136 | }
137 | }
138 | }
139 | ```
140 | Call in business code:
141 | ```Swift
142 | tableView.theme.backgroundColor = ThemeProvider(light: UIColor.white, dark: UIColor.white)
143 | ```
144 | In this way, the form of ThemeProvider closure can be avoided and it is more concise.
145 |
146 | ### General Configuration Package Example
147 |
148 | There is a UI standard for general skinning needs. For example, `UILabel.textColor` defines three levels, the code is as follows:
149 | ```Swift
150 | Enum TextColorLevel: String {
151 | Case normal
152 | Case mainTitle
153 | Case subTitle
154 | }
155 | ```
156 | Then you can encapsulate a global function and pass `TextColorLevel` to return the corresponding configuration closure, which can greatly reduce the amount of code during configuration. The global functions are as follows:
157 | ```Swift
158 | Func dynamicTextColor(_ level: TextColorLevel) -> ThemeProvider {
159 | Switch level {
160 | Case .normal:
161 | Return ThemeProvider({ (style) in
162 | If style == .dark {
163 | Return UIColor.white
164 | }else {
165 | Return UIColor.gray
166 | }
167 | })
168 | Case .mainTitle:
169 | ...
170 | Case .subTitle:
171 | ...
172 | }
173 | }
174 | ```
175 | The code for configuring the theme properties is as follows:
176 | ```Swift
177 | themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
178 | ```
179 |
180 | ### Local Plist file configuration example
181 | Same as **General Configuration Package**, except that the method loads the configuration value from the local Plist file. The specific code participates in the `Example``StaticSourceManager` class.
182 |
183 | ### Add topics based on server dynamics
184 | Same as **General Configuration Package**, except that the method loads the specific values of the configuration from the server. The specific code participates in the `DynamicSourceManager` class of `Example`.
185 |
186 | ## Stateful controls
187 | Some business requirements exist for a control with multiple states, such as checked and unchecked. Different states have different configurations for different theme. The configuration code is as follows:
188 | ```Swift
189 | statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
190 | If self?.statusLabelStatus == .isSelected {
191 | / / selected state a configuration
192 | If style == .dark {
193 | Return .red
194 | }else {
195 | Return .green
196 | }
197 | }else {
198 | //Unselected another configuration
199 | If style == .dark {
200 | Return .white
201 | }else {
202 | Return .black
203 | }
204 | }
205 | })
206 | ```
207 |
208 | When the state of the control is updated, you need to refresh the current theme property configuration, the code is as follows:
209 | ```Swift
210 | Func statusDidChange() {
211 | statusLabel.theme.textColor?.refresh()
212 | }
213 | ```
214 |
215 | If your control supports multiple state properties, such as `textColor`, `backgroundColor`, `font`, etc., you can call the `refresh` method without using one of the theme properties. You can use the following code to complete all the configured themes. Property refresh:
216 | ```Swift
217 | Func statusDidChange() {
218 | statusLabel.theme.refresh()
219 | }
220 | ```
221 |
222 | ## overrideThemeStyle
223 | Regardless of how the theme switches, `overrideThemeStyleParentView` and its subview's `themeStyle` are `dark`
224 | ```Swift
225 | overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
226 | ```
227 |
228 | # Principle
229 |
230 | - [Principle](https://github.com/pujiaxin33/JXTheme/blob/master/Document/Principle.md)
231 |
232 | # Other tips
233 |
234 | ## Why use the `theme` namespace attribute instead of the `theme_xx` extension attribute?
235 | - If you extend N functions to the system class, when you use the class, there are N extended methods that interfere with your choice. Especially if you are doing other business development, not when you want to configure theme properties.
236 | - Well-known three-party libraries like `Kingfisher`, `SnapKit`, etc., all use namespace attributes to implement extensions to system classes. This is a more `Swift` way of writing and worth learning.
237 |
238 | ## Theme Switch Notification
239 | ```Swift
240 | Extension Notification.Name {
241 | Public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
242 | }
243 | ```
244 |
245 | ## `ThemeManager` stores the theme configuration according to the user ID
246 |
247 | ```
248 | /// Configure the stored flag key. Can be set to the user's ID, so that in the same phone, you can record the configuration of different users. You need to set this property first and then set other values.
249 | Public var storeConfigsIdentifierKey: String = "default"
250 | ```
251 |
252 | ## Migrating to System API Guide
253 | When your app supports iOS13 at the minimum, you can migrate to the system plan if you need to follow the guidelines below.
254 | [Migrate to System API Guide, click to read] (https://github.com/pujiaxin33/JXTheme/blob/master/Document/%E8%BF%81%E7%A7%BB%E5%88%B0%E7% B3%BB%E7%BB%9FAPI%E6%8C%87%E5%8D%97.md)
255 |
256 | # Currently supported classes and their properties
257 |
258 | The properties here are inherited. For example, `UIView` supports the `backgroundColor` property, then its subclass `UILabel` also supports `backgroundColor`. If you don't have the class or property you want to support, you are welcome to extend the PullRequest.
259 |
260 | ## UIView
261 |
262 | - `backgroundColor`
263 | - `tintColor`
264 | - `alpha`
265 | - `customization`
266 |
267 | ## UILabel
268 |
269 | - `font`
270 | - `textColor`
271 | - `shadowColor`
272 | - `highlightedTextColor`
273 | - `attributedText`
274 |
275 | ## UIButton
276 |
277 | - `func setTitleColor(_ colorProvider: ThemeColorDynamicProvider?, for state: UIControl.State)`
278 | - `func setTitleShadowColor(_ colorProvider: ThemeColorDynamicProvider?, for state: UIControl.State)`
279 | - `func setAttributedTitle(_ textProvider: ThemeAttributedTextDynamicProvider?, for state: UIControl.State)`
280 | - `func setImage(_ imageProvider: ThemeImageDynamicProvider?, for state: UIControl.State)`
281 | - `func setBackgroundImage(_ imageProvider: ThemeImageDynamicProvider?, for state: UIControl.State)`
282 |
283 | ## UITextField
284 |
285 | - `font`
286 | - `textColor`
287 | - `attributedText`
288 | - `attributedPlaceholder`
289 | - `keyboardAppearance`
290 |
291 | ## UITextView
292 |
293 | - `font`
294 | - `textColor`
295 | - `attributedText`
296 | - `keyboardAppearance`
297 |
298 | ## UIImageView
299 |
300 | - `image`
301 |
302 | ## CALayer
303 |
304 | - `backgroundColor`
305 | - `borderColor`
306 | - `borderWidth`
307 | - `shadowColor`
308 | - `customization`
309 |
310 | ## CAShapeLayer
311 |
312 | - `fillColor`
313 | - `strokeColor`
314 |
315 | ## UINavigationBar
316 |
317 | - `barStyle`
318 | - `barTintColor`
319 | - `titleTextAttributes`
320 | - `largeTitleTextAttributes`
321 |
322 | ## UITabBar
323 |
324 | - `barStyle`
325 | - `barTintColor`
326 | - `shadowImage`
327 |
328 | ## UISearchBar
329 |
330 | - `barStyle`
331 | - `barTintColor`
332 | - `keyboardAppearance`
333 |
334 | ## UIToolbar
335 |
336 | - `barStyle`
337 | - `barTintColor`
338 |
339 | ## UISwitch
340 |
341 | - `onTintColor`
342 | - `thumbTintColor`
343 |
344 | ## UISlider
345 |
346 | - `thumbTintColor`
347 | - `minimumTrackTintColor`
348 | - `maximumTrackTintColor`
349 | - `minimumValueImage`
350 | - `maximumValueImage`
351 |
352 | ## UIRefreshControl
353 |
354 | - `attributedTitle`
355 |
356 | ## UIProgressView
357 |
358 | - `progressTintColor`
359 | - `trackTintColor`
360 | - `progressImage`
361 | - `trackImage`
362 |
363 | ## UIPageControl
364 |
365 | - `pageIndicatorTintColor`
366 | - `currentPageIndicatorTintColor`
367 |
368 | ## UIBarItem
369 |
370 | - `func setTitleTextAttributes(_ attributesProvider: ThemeAttributesDynamicProvider?, for state: UIControl.State)`
371 | - `image`
372 |
373 | ## UIBarButtonItem
374 |
375 | - `tintColor`
376 |
377 | ## UIActivityIndicatorView
378 |
379 | - `style`
380 | - `color`
381 |
382 | ## UIScrollView
383 |
384 | - `indicatorStyle`
385 |
386 | ## UITableView
387 |
388 | - `separatorColor`
389 | - `sectionIndexColor`
390 | - `sectionIndexBackgroundColor`
391 |
392 | # Contribution
393 |
394 | If you have any questions or suggestions, please feel free to contact us by Issue and Pull Request🤝
395 |
396 |
397 |
398 |
--------------------------------------------------------------------------------
/Sources/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // JXTheme
4 | //
5 | // Created by jiaxin on 2019/7/10.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | public class ThemeTool {
13 | public static func setupViewThemeProperty(view: UIView, key: String, provider: ThemeProvider?, customization: @escaping (ThemeStyle) -> ()) {
14 | if provider != nil {
15 | let config: ThemeCustomizationClosure = {(style) in
16 | customization(style)
17 | }
18 | var newProvider = provider
19 | newProvider?.config = config
20 | view.providers[key] = newProvider
21 | ThemeManager.shared.addTrackedObject(view, addedConfig: config)
22 | }else {
23 | view.providers.removeValue(forKey: key)
24 | }
25 | }
26 | public static func setupLayerThemeProperty(layer: CALayer, key: String, provider: ThemeProvider?, customization: @escaping (ThemeStyle) -> ()) {
27 | if provider != nil {
28 | let config: ThemeCustomizationClosure = {(style) in
29 | customization(style)
30 | }
31 | var newProvider = provider
32 | newProvider?.config = config
33 | layer.providers[key] = newProvider
34 | ThemeManager.shared.addTrackedObject(layer, addedConfig: config)
35 | }else {
36 | layer.providers.removeValue(forKey: key)
37 | }
38 | }
39 | public static func setupBarItemThemeProperty(barItem: UIBarItem, key: String, provider: ThemeProvider?, customization: @escaping (ThemeStyle) -> ()) {
40 | if provider != nil {
41 | let config: ThemeCustomizationClosure = {(style) in
42 | customization(style)
43 | }
44 | var newProvider = provider
45 | newProvider?.config = config
46 | barItem.providers[key] = newProvider
47 | ThemeManager.shared.addTrackedObject(barItem, addedConfig: config)
48 | }else {
49 | barItem.providers.removeValue(forKey: key)
50 | }
51 | }
52 |
53 | public static func getThemeProvider(target: UIView, with key: String) -> Any? {
54 | return target.providers[key]
55 | }
56 |
57 | public static func getThemeProvider(target: CALayer, with key: String) -> Any? {
58 | return target.providers[key]
59 | }
60 |
61 | public static func getThemeProvider(target: UIBarItem, with key: String) -> Any? {
62 | return target.providers[key]
63 | }
64 | }
65 |
66 | //MARK: - ThemeWrapper
67 | public extension ThemeWrapper where Base: UIView {
68 | /// 刷新当前控件的所有的主题配置属性
69 | func refresh() {
70 | ThemeManager.shared.refreshTargetObject(self.base)
71 | }
72 | var backgroundColor: ThemeProvider? {
73 | set(new) {
74 | let baseItem = self.base
75 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIView.backgroundColor", provider: new) {[weak baseItem] (style) in
76 | baseItem?.backgroundColor = new?.provider(style)
77 | }
78 | }
79 | get { return self.base.providers["UIView.backgroundColor"] as? ThemeProvider }
80 | }
81 | var tintColor: ThemeProvider? {
82 | set(new) {
83 | let baseItem = self.base
84 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIView.tintColor", provider: new) {[weak baseItem] (style) in
85 | baseItem?.tintColor = new?.provider(style)
86 | }
87 | }
88 | get { return self.base.providers["UIView.tintColor"] as? ThemeProvider }
89 | }
90 | var alpha: ThemeProvider? {
91 | set(new) {
92 | let baseItem = self.base
93 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIView.alpha", provider: new) {[weak baseItem] (style) in
94 | baseItem?.alpha = new?.provider(style) ?? 1
95 | }
96 | }
97 | get { return self.base.providers["UIView.alpha"] as? ThemeProvider }
98 | }
99 | var customization: ThemeProvider? {
100 | set(new) {
101 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIView.customization", provider: new) { (style) in
102 | new?.provider(style)
103 | }
104 | }
105 | get { return self.base.providers["UIView.customization"] as? ThemeProvider }
106 | }
107 | var overrideThemeStyle: ThemeStyle? {
108 | set(new) {
109 | self.base.overrideThemeStyle = new
110 | }
111 | get { return self.base.overrideThemeStyle }
112 | }
113 | }
114 | public extension ThemeWrapper where Base: UILabel {
115 | var font: ThemeProvider? {
116 | set(new) {
117 | let baseItem = self.base
118 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.font", provider: new) {[weak baseItem] (style) in
119 | baseItem?.font = new?.provider(style)
120 | }
121 | }
122 | get { return self.base.providers["UILabel.font"] as? ThemeProvider }
123 | }
124 | var textColor: ThemeProvider? {
125 | set(new) {
126 | let baseItem = self.base
127 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.textColor", provider: new) {[weak baseItem] (style) in
128 | baseItem?.textColor = new?.provider(style)
129 | }
130 | }
131 | get { return self.base.providers["UILabel.textColor"] as? ThemeProvider }
132 | }
133 | var shadowColor: ThemeProvider? {
134 | set(new) {
135 | let baseItem = self.base
136 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.shadowColor", provider: new) {[weak baseItem] (style) in
137 | baseItem?.shadowColor = new?.provider(style)
138 | }
139 | }
140 | get { return self.base.providers["UILabel.shadowColor"] as? ThemeProvider }
141 | }
142 | var highlightedTextColor: ThemeProvider? {
143 | set(new) {
144 | let baseItem = self.base
145 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.highlightedTextColor", provider: new) {[weak baseItem] (style) in
146 | baseItem?.highlightedTextColor = new?.provider(style)
147 | }
148 | }
149 | get { return self.base.providers["UILabel.highlightedTextColor"] as? ThemeProvider }
150 | }
151 | var attributedText: ThemeProvider? {
152 | set(new) {
153 | let baseItem = self.base
154 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UILabel.attributedText", provider: new) {[weak baseItem] (style) in
155 | baseItem?.attributedText = new?.provider(style)
156 | }
157 | }
158 | get { return self.base.providers["UILabel.attributedText"] as? ThemeProvider }
159 | }
160 | }
161 |
162 | public extension ThemeWrapper where Base: UIButton {
163 | func setTitleColor(_ colorProvider: ThemeProvider?, for state: UIControl.State) {
164 | let baseItem = self.base
165 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIButton.titleColor.\(state.rawValue)", provider: colorProvider) {[weak baseItem] (style) in
166 | baseItem?.setTitleColor(colorProvider?.provider(style), for: state)
167 | }
168 | }
169 | func setTitleShadowColor(_ colorProvider: ThemeProvider?, for state: UIControl.State) {
170 | let baseItem = self.base
171 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIButton.titleShadowColor.\(state.rawValue)", provider: colorProvider) {[weak baseItem] (style) in
172 | baseItem?.setTitleShadowColor(colorProvider?.provider(style), for: state)
173 | }
174 | }
175 | func setAttributedTitle(_ textProvider: ThemeProvider?, for state: UIControl.State) {
176 | let baseItem = self.base
177 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIButton.attributedTitle.\(state.rawValue)", provider: textProvider) {[weak baseItem] (style) in
178 | UIView.setAnimationsEnabled(false)
179 | baseItem?.setAttributedTitle(textProvider?.provider(style), for: state)
180 | baseItem?.layoutIfNeeded()
181 | UIView.setAnimationsEnabled(true)
182 | }
183 | }
184 | func setImage(_ imageProvider: ThemeProvider?, for state: UIControl.State) {
185 | let baseItem = self.base
186 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIButton.image.\(state.rawValue)", provider: imageProvider) {[weak baseItem] (style) in
187 | baseItem?.setImage(imageProvider?.provider(style), for: state)
188 | }
189 | }
190 | func setBackgroundImage(_ imageProvider: ThemeProvider?, for state: UIControl.State) {
191 | let baseItem = self.base
192 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIButton.backgroundImage.\(state.rawValue)", provider: imageProvider) {[weak baseItem] (style) in
193 | baseItem?.setBackgroundImage(imageProvider?.provider(style), for: state)
194 | }
195 | }
196 | }
197 | public extension ThemeWrapper where Base: UITextField {
198 | var font: ThemeProvider? {
199 | set(new) {
200 | let baseItem = self.base
201 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextField.font", provider: new) {[weak baseItem] (style) in
202 | baseItem?.font = new?.provider(style)
203 | }
204 | }
205 | get { return self.base.providers["UITextField.font"] as? ThemeProvider }
206 | }
207 | var textColor: ThemeProvider? {
208 | set(new) {
209 | let baseItem = self.base
210 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextField.textColor", provider: new) {[weak baseItem] (style) in
211 | baseItem?.textColor = new?.provider(style)
212 | }
213 | }
214 | get { return self.base.providers["UITextField.textColor"] as? ThemeProvider }
215 | }
216 | var attributedText: ThemeProvider? {
217 | set(new) {
218 | let baseItem = self.base
219 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextField.attributedText", provider: new) {[weak baseItem] (style) in
220 | baseItem?.attributedText = new?.provider(style)
221 | }
222 | }
223 | get { return self.base.providers["UITextField.attributedText"] as? ThemeProvider }
224 | }
225 | var attributedPlaceholder: ThemeProvider? {
226 | set(new) {
227 | let baseItem = self.base
228 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextField.attributedPlaceholder", provider: new) {[weak baseItem] (style) in
229 | baseItem?.attributedPlaceholder = new?.provider(style)
230 | }
231 | }
232 | get { return self.base.providers["UITextField.attributedPlaceholder"] as? ThemeProvider }
233 | }
234 | var keyboardAppearance: ThemeProvider? {
235 | set(new) {
236 | let baseItem = self.base
237 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextField.keyboardAppearance", provider: new) {[weak baseItem] (style) in
238 | baseItem?.keyboardAppearance = new?.provider(style) ?? .default
239 | }
240 | }
241 | get { return self.base.providers["UITextField.keyboardAppearance"] as? ThemeProvider }
242 | }
243 | }
244 | public extension ThemeWrapper where Base: UITextView {
245 | var font: ThemeProvider? {
246 | set(new) {
247 | let baseItem = self.base
248 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextView.font", provider: new) {[weak baseItem] (style) in
249 | baseItem?.font = new?.provider(style)
250 | }
251 | }
252 | get { return self.base.providers["UITextView.font"] as? ThemeProvider }
253 | }
254 | var textColor: ThemeProvider? {
255 | set(new) {
256 | let baseItem = self.base
257 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextView.textColor", provider: new) {[weak baseItem] (style) in
258 | baseItem?.textColor = new?.provider(style)
259 | }
260 | }
261 | get { return self.base.providers["UITextView.textColor"] as? ThemeProvider }
262 | }
263 | var attributedText: ThemeProvider? {
264 | set(new) {
265 | let baseItem = self.base
266 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextView.attributedText", provider: new) {[weak baseItem] (style) in
267 | baseItem?.attributedText = new?.provider(style)
268 | }
269 | }
270 | get { return self.base.providers["UITextView.attributedText"] as? ThemeProvider }
271 | }
272 | var keyboardAppearance: ThemeProvider? {
273 | set(new) {
274 | let baseItem = self.base
275 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITextView.keyboardAppearance", provider: new) {[weak baseItem] (style) in
276 | baseItem?.keyboardAppearance = new?.provider(style) ?? .default
277 | }
278 | }
279 | get { return self.base.providers["UITextView.keyboardAppearance"] as? ThemeProvider }
280 | }
281 | }
282 | public extension ThemeWrapper where Base: UIImageView {
283 | var image: ThemeProvider? {
284 | set(new) {
285 | let baseItem = self.base
286 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIImageView.image", provider: new) {[weak baseItem] (style) in
287 | baseItem?.image = new?.provider(style)
288 | }
289 | }
290 | get { return self.base.providers["UIImageView.image"] as? ThemeProvider }
291 | }
292 | }
293 | public extension ThemeWrapper where Base: CALayer {
294 | /// 刷新当前控件的所有的主题配置属性
295 | func refresh() {
296 | ThemeManager.shared.refreshTargetObject(self.base)
297 | }
298 | var overrideThemeStyle: ThemeStyle? {
299 | set(new) {
300 | self.base.overrideThemeStyle = new
301 | }
302 | get { return self.base.overrideThemeStyle }
303 | }
304 | var backgroundColor: ThemeProvider? {
305 | set(new) {
306 | let baseItem = self.base
307 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CALayer.backgroundColor", provider: new) {[weak baseItem] (style) in
308 | baseItem?.backgroundColor = new?.provider(style).cgColor
309 | }
310 | }
311 | get { return self.base.providers["CALayer.backgroundColor"] as? ThemeProvider }
312 | }
313 | var borderColor: ThemeProvider? {
314 | set(new) {
315 | let baseItem = self.base
316 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CALayer.borderColor", provider: new) {[weak baseItem] (style) in
317 | baseItem?.borderColor = new?.provider(style).cgColor
318 | }
319 | }
320 | get { return self.base.providers["CALayer.borderColor"] as? ThemeProvider }
321 | }
322 | var borderWidth: ThemeProvider? {
323 | set(new) {
324 | let baseItem = self.base
325 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CALayer.borderWidth", provider: new) {[weak baseItem] (style) in
326 | baseItem?.borderWidth = new?.provider(style) ?? 0
327 | }
328 | }
329 | get { return self.base.providers["CALayer.borderWidth"] as? ThemeProvider }
330 | }
331 | var shadowColor: ThemeProvider? {
332 | set(new) {
333 | let baseItem = self.base
334 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CALayer.shadowColor", provider: new) {[weak baseItem] (style) in
335 | baseItem?.shadowColor = new?.provider(style).cgColor
336 | }
337 | }
338 | get { return self.base.providers["CALayer.shadowColor"] as? ThemeProvider }
339 | }
340 | var customization: ThemeProvider? {
341 | set(new) {
342 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CALayer.customization", provider: new) { (style) in
343 | new?.provider(style)
344 | }
345 | }
346 | get { return self.base.providers["CALayer.customization"] as? ThemeProvider }
347 | }
348 | }
349 | public extension ThemeWrapper where Base: CAShapeLayer {
350 | var fillColor: ThemeProvider? {
351 | set(new) {
352 | let baseItem = self.base
353 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CAShapeLayer.fillColor", provider: new) {[weak baseItem] (style) in
354 | baseItem?.fillColor = new?.provider(style).cgColor
355 | }
356 | }
357 | get { return self.base.providers["CAShapeLayer.fillColor"] as? ThemeProvider }
358 | }
359 | var strokeColor: ThemeProvider? {
360 | set(new) {
361 | let baseItem = self.base
362 | ThemeTool.setupLayerThemeProperty(layer: self.base, key: "CAShapeLayer.strokeColor", provider: new) {[weak baseItem] (style) in
363 | baseItem?.strokeColor = new?.provider(style).cgColor
364 | }
365 | }
366 | get { return self.base.providers["CAShapeLayer.strokeColor"] as? ThemeProvider }
367 | }
368 | }
369 | public extension ThemeWrapper where Base: UINavigationBar {
370 | var barStyle: ThemeProvider? {
371 | set(new) {
372 | let baseItem = self.base
373 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UINavigationBar.barStyle", provider: new) {[weak baseItem] (style) in
374 | baseItem?.barStyle = new?.provider(style) ?? .default
375 | }
376 | }
377 | get { return self.base.providers["UINavigationBar.barStyle"] as? ThemeProvider }
378 | }
379 | var barTintColor: ThemeProvider? {
380 | set(new) {
381 | let baseItem = self.base
382 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UINavigationBar.barTintColor", provider: new) {[weak baseItem] (style) in
383 | baseItem?.barTintColor = new?.provider(style)
384 | }
385 | }
386 | get { return self.base.providers["UINavigationBar.barTintColor"] as? ThemeProvider }
387 | }
388 | var titleTextAttributes: ThemeProvider<[NSAttributedString.Key : Any]>? {
389 | set(new) {
390 | let baseItem = self.base
391 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UINavigationBar.titleTextAttributes", provider: new) {[weak baseItem] (style) in
392 | baseItem?.titleTextAttributes = new?.provider(style)
393 | }
394 | }
395 | get { return self.base.providers["UINavigationBar.titleTextAttributes"] as? ThemeProvider<[NSAttributedString.Key : Any]> }
396 | }
397 | @available(iOS 11.0, *)
398 | var largeTitleTextAttributes: ThemeProvider<[NSAttributedString.Key : Any]>? {
399 | set(new) {
400 | let baseItem = self.base
401 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UINavigationBar.largeTitleTextAttributes", provider: new) {[weak baseItem] (style) in
402 | baseItem?.largeTitleTextAttributes = new?.provider(style)
403 | }
404 | }
405 | get { return self.base.providers["UINavigationBar.largeTitleTextAttributes"] as? ThemeProvider<[NSAttributedString.Key : Any]> }
406 | }
407 | }
408 | public extension ThemeWrapper where Base: UITabBar {
409 | var barStyle: ThemeProvider? {
410 | set(new) {
411 | let baseItem = self.base
412 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITabBar.barStyle", provider: new) {[weak baseItem] (style) in
413 | baseItem?.barStyle = new?.provider(style) ?? .default
414 | }
415 | }
416 | get { return self.base.providers["UITabBar.barStyle"] as? ThemeProvider }
417 | }
418 | var barTintColor: ThemeProvider? {
419 | set(new) {
420 | let baseItem = self.base
421 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITabBar.barTintColor", provider: new) {[weak baseItem] (style) in
422 | baseItem?.barTintColor = new?.provider(style)
423 | }
424 | }
425 | get { return self.base.providers["UITabBar.barTintColor"] as? ThemeProvider }
426 | }
427 | var shadowImage: ThemeProvider? {
428 | set(new) {
429 | let baseItem = self.base
430 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITabBar.shadowImage", provider: new) {[weak baseItem] (style) in
431 | baseItem?.shadowImage = new?.provider(style)
432 | }
433 | }
434 | get { return self.base.providers["UITabBar.shadowImage"] as? ThemeProvider }
435 | }
436 | }
437 | public extension ThemeWrapper where Base: UITabBarItem {
438 | var selectedImage: ThemeProvider? {
439 | set(new) {
440 | let baseItem = self.base
441 | ThemeTool.setupBarItemThemeProperty(barItem: self.base, key: "UITabBarItem.selectedImage", provider: new) {[weak baseItem] (style) in
442 | baseItem?.selectedImage = new?.provider(style)
443 | }
444 | }
445 | get { return self.base.providers["UITabBarItem.selectedImage"] as? ThemeProvider }
446 | }
447 | }
448 | public extension ThemeWrapper where Base: UISearchBar {
449 | var barStyle: ThemeProvider? {
450 | set(new) {
451 | let baseItem = self.base
452 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISearchBar.barStyle", provider: new) {[weak baseItem] (style) in
453 | baseItem?.barStyle = new?.provider(style) ?? .default
454 | }
455 | }
456 | get { return self.base.providers["UISearchBar.barStyle"] as? ThemeProvider }
457 | }
458 | var barTintColor: ThemeProvider? {
459 | set(new) {
460 | let baseItem = self.base
461 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISearchBar.barTintColor", provider: new) {[weak baseItem] (style) in
462 | baseItem?.barTintColor = new?.provider(style)
463 | }
464 | }
465 | get { return self.base.providers["UISearchBar.barTintColor"] as? ThemeProvider }
466 | }
467 | var keyboardAppearance: ThemeProvider? {
468 | set(new) {
469 | let baseItem = self.base
470 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISearchBar.keyboardAppearance", provider: new) {[weak baseItem] (style) in
471 | baseItem?.keyboardAppearance = new?.provider(style) ?? .default
472 | }
473 | }
474 | get { return self.base.providers["UISearchBar.keyboardAppearance"] as? ThemeProvider }
475 | }
476 | }
477 | public extension ThemeWrapper where Base: UIToolbar {
478 | var barStyle: ThemeProvider? {
479 | set(new) {
480 | let baseItem = self.base
481 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIToolbar.barStyle", provider: new) {[weak baseItem] (style) in
482 | baseItem?.barStyle = new?.provider(style) ?? .default
483 | }
484 | }
485 | get { return self.base.providers["UIToolbar.barStyle"] as? ThemeProvider }
486 | }
487 | var barTintColor: ThemeProvider? {
488 | set(new) {
489 | let baseItem = self.base
490 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIToolbar.barTintColor", provider: new) {[weak baseItem] (style) in
491 | baseItem?.barTintColor = new?.provider(style)
492 | }
493 | }
494 | get { return self.base.providers["UIToolbar.barTintColor"] as? ThemeProvider }
495 | }
496 | }
497 | public extension ThemeWrapper where Base: UISwitch {
498 | var onTintColor: ThemeProvider? {
499 | set(new) {
500 | let baseItem = self.base
501 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISwitch.onTintColor", provider: new) {[weak baseItem] (style) in
502 | baseItem?.onTintColor = new?.provider(style)
503 | }
504 | }
505 | get { return self.base.providers["UISwitch.onTintColor"] as? ThemeProvider }
506 | }
507 | var thumbTintColor: ThemeProvider? {
508 | set(new) {
509 | let baseItem = self.base
510 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISwitch.thumbTintColor", provider: new) {[weak baseItem] (style) in
511 | baseItem?.thumbTintColor = new?.provider(style)
512 | }
513 | }
514 | get { return self.base.providers["UISwitch.thumbTintColor"] as? ThemeProvider }
515 | }
516 | }
517 | public extension ThemeWrapper where Base: UISlider {
518 | var thumbTintColor: ThemeProvider? {
519 | set(new) {
520 | let baseItem = self.base
521 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISlider.thumbTintColor", provider: new) {[weak baseItem] (style) in
522 | baseItem?.thumbTintColor = new?.provider(style)
523 | }
524 | }
525 | get { return self.base.providers["UISlider.thumbTintColor"] as? ThemeProvider }
526 | }
527 | var minimumTrackTintColor: ThemeProvider? {
528 | set(new) {
529 | let baseItem = self.base
530 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISlider.minimumTrackTintColor", provider: new) {[weak baseItem] (style) in
531 | baseItem?.minimumTrackTintColor = new?.provider(style)
532 | }
533 | }
534 | get { return self.base.providers["UISlider.minimumTrackTintColor"] as? ThemeProvider }
535 | }
536 | var maximumTrackTintColor: ThemeProvider? {
537 | set(new) {
538 | let baseItem = self.base
539 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISlider.maximumTrackTintColor", provider: new) {[weak baseItem] (style) in
540 | baseItem?.maximumTrackTintColor = new?.provider(style)
541 | }
542 | }
543 | get { return self.base.providers["UISlider.maximumTrackTintColor"] as? ThemeProvider }
544 | }
545 | var minimumValueImage: ThemeProvider? {
546 | set(new) {
547 | let baseItem = self.base
548 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISlider.minimumValueImage", provider: new) {[weak baseItem] (style) in
549 | baseItem?.minimumValueImage = new?.provider(style)
550 | }
551 | }
552 | get { return self.base.providers["UISlider.minimumValueImage"] as? ThemeProvider }
553 | }
554 | var maximumValueImage: ThemeProvider? {
555 | set(new) {
556 | let baseItem = self.base
557 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UISlider.maximumValueImage", provider: new) {[weak baseItem] (style) in
558 | baseItem?.maximumValueImage = new?.provider(style)
559 | }
560 | }
561 | get { return self.base.providers["UISlider.maximumValueImage"] as? ThemeProvider }
562 | }
563 | }
564 | public extension ThemeWrapper where Base: UIRefreshControl {
565 | var attributedTitle: ThemeProvider? {
566 | set(new) {
567 | let baseItem = self.base
568 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIRefreshControl.attributedTitle", provider: new) {[weak baseItem] (style) in
569 | baseItem?.attributedTitle = new?.provider(style)
570 | }
571 | }
572 | get { return self.base.providers["UIRefreshControl.attributedTitle"] as? ThemeProvider }
573 | }
574 | }
575 | public extension ThemeWrapper where Base: UIProgressView {
576 | var progressTintColor: ThemeProvider? {
577 | set(new) {
578 | let baseItem = self.base
579 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIProgressView.progressTintColor", provider: new) {[weak baseItem] (style) in
580 | baseItem?.progressTintColor = new?.provider(style)
581 | }
582 | }
583 | get { return self.base.providers["UIProgressView.progressTintColor"] as? ThemeProvider }
584 | }
585 | var trackTintColor: ThemeProvider? {
586 | set(new) {
587 | let baseItem = self.base
588 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIProgressView.trackTintColor", provider: new) {[weak baseItem] (style) in
589 | baseItem?.trackTintColor = new?.provider(style)
590 | }
591 | }
592 | get { return self.base.providers["UIProgressView.trackTintColor"] as? ThemeProvider }
593 | }
594 | var progressImage: ThemeProvider? {
595 | set(new) {
596 | let baseItem = self.base
597 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIProgressView.progressImage", provider: new) {[weak baseItem] (style) in
598 | baseItem?.progressImage = new?.provider(style)
599 | }
600 | }
601 | get { return self.base.providers["UIProgressView.progressImage"] as? ThemeProvider }
602 | }
603 | var trackImage: ThemeProvider? {
604 | set(new) {
605 | let baseItem = self.base
606 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIProgressView.trackImage", provider: new) {[weak baseItem] (style) in
607 | baseItem?.trackImage = new?.provider(style)
608 | }
609 | }
610 | get { return self.base.providers["UIProgressView.trackImage"] as? ThemeProvider }
611 | }
612 | }
613 | public extension ThemeWrapper where Base: UIPageControl {
614 | var pageIndicatorTintColor: ThemeProvider? {
615 | set(new) {
616 | let baseItem = self.base
617 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIPageControl.pageIndicatorTintColor", provider: new) {[weak baseItem] (style) in
618 | baseItem?.pageIndicatorTintColor = new?.provider(style)
619 | }
620 | }
621 | get { return self.base.providers["UIPageControl.pageIndicatorTintColor"] as? ThemeProvider }
622 | }
623 | var currentPageIndicatorTintColor: ThemeProvider? {
624 | set(new) {
625 | let baseItem = self.base
626 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIProgressView.currentPageIndicatorTintColor", provider: new) {[weak baseItem] (style) in
627 | baseItem?.currentPageIndicatorTintColor = new?.provider(style)
628 | }
629 | }
630 | get { return self.base.providers["UIProgressView.currentPageIndicatorTintColor"] as? ThemeProvider }
631 | }
632 | }
633 | public extension ThemeWrapper where Base: UIBarItem {
634 | /// 刷新当前控件的所有的主题配置属性
635 | func refresh() {
636 | ThemeManager.shared.refreshTargetObject(self.base)
637 | }
638 | var overrideThemeStyle: ThemeStyle? {
639 | set(new) {
640 | self.base.overrideThemeStyle = new
641 | }
642 | get { return self.base.overrideThemeStyle }
643 | }
644 | func setTitleTextAttributes(_ attributesProvider: ThemeProvider<[NSAttributedString.Key : Any]>?, for state: UIControl.State) {
645 | let baseItem = self.base
646 | ThemeTool.setupBarItemThemeProperty(barItem: self.base, key: "UIBarItem.setTitleTextAttributes", provider: attributesProvider) {[weak baseItem] (style) in
647 | baseItem?.setTitleTextAttributes(attributesProvider?.provider(style), for: state)
648 | }
649 | }
650 | var image: ThemeProvider? {
651 | set(new) {
652 | let baseItem = self.base
653 | ThemeTool.setupBarItemThemeProperty(barItem: self.base, key: "UIBarItem.image", provider: new) {[weak baseItem] (style) in
654 | baseItem?.image = new?.provider(style)
655 | }
656 | }
657 | get { return self.base.providers["UIBarItem.image"] as? ThemeProvider }
658 | }
659 | }
660 | public extension ThemeWrapper where Base: UIBarButtonItem {
661 | var tintColor: ThemeProvider? {
662 | set(new) {
663 | let baseItem = self.base
664 | ThemeTool.setupBarItemThemeProperty(barItem: self.base, key: "UIBarButtonItem.tintColor", provider: new) {[weak baseItem] (style) in
665 | baseItem?.tintColor = new?.provider(style)
666 | }
667 | }
668 | get { return self.base.providers["UIBarButtonItem.tintColor"] as? ThemeProvider }
669 | }
670 | }
671 | public extension ThemeWrapper where Base: UIActivityIndicatorView {
672 | var style: ThemeProvider? {
673 | set(new) {
674 | let baseItem = self.base
675 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIActivityIndicatorView.style", provider: new) {[weak baseItem] (style) in
676 | baseItem?.style = new?.provider(style) ?? UIActivityIndicatorView.Style.gray
677 | }
678 | }
679 | get { return self.base.providers["UIActivityIndicatorView.style"] as? ThemeProvider }
680 | }
681 | var color: ThemeProvider? {
682 | set(new) {
683 | let baseItem = self.base
684 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIActivityIndicatorView.color", provider: new) {[weak baseItem] (style) in
685 | baseItem?.color = new?.provider(style)
686 | }
687 | }
688 | get { return self.base.providers["UIActivityIndicatorView.color"] as? ThemeProvider }
689 | }
690 | }
691 | public extension ThemeWrapper where Base: UIScrollView {
692 | var indicatorStyle: ThemeProvider? {
693 | set(new) {
694 | let baseItem = self.base
695 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UIScrollView.indicatorStyle", provider: new) {[weak baseItem] (style) in
696 | baseItem?.indicatorStyle = new?.provider(style) ?? UIScrollView.IndicatorStyle.default
697 | }
698 | }
699 | get { return self.base.providers["UIScrollView.indicatorStyle"] as? ThemeProvider }
700 | }
701 | }
702 | public extension ThemeWrapper where Base: UITableView {
703 | var separatorColor: ThemeProvider? {
704 | set(new) {
705 | let baseItem = self.base
706 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITableView.separatorColor", provider: new) {[weak baseItem] (style) in
707 | baseItem?.separatorColor = new?.provider(style)
708 | }
709 | }
710 | get { return self.base.providers["UITableView.separatorColor"] as? ThemeProvider }
711 | }
712 | var sectionIndexColor: ThemeProvider? {
713 | set(new) {
714 | let baseItem = self.base
715 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITableView.sectionIndexColor", provider: new) {[weak baseItem] (style) in
716 | baseItem?.sectionIndexColor = new?.provider(style)
717 | }
718 | }
719 | get { return self.base.providers["UITableView.sectionIndexColor"] as? ThemeProvider }
720 | }
721 | var sectionIndexBackgroundColor: ThemeProvider? {
722 | set(new) {
723 | let baseItem = self.base
724 | ThemeTool.setupViewThemeProperty(view: self.base, key: "UITableView.sectionIndexBackgroundColor", provider: new) {[weak baseItem] (style) in
725 | baseItem?.sectionIndexBackgroundColor = new?.provider(style)
726 | }
727 | }
728 | get { return self.base.providers["UITableView.sectionIndexBackgroundColor"] as? ThemeProvider }
729 | }
730 | }
731 |
732 | //MARK: - Extentsion Property
733 | internal extension UIView {
734 | struct AssociatedKey {
735 | static var providers: Void?
736 | static var overrideThemeStyle: Void?
737 | }
738 | var providers: [String: Any] {
739 | set(new) {
740 | objc_setAssociatedObject(self, &AssociatedKey.providers, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
741 | }
742 | get {
743 | if objc_getAssociatedObject(self, &AssociatedKey.providers) == nil {
744 | self.providers = [String: Any]()
745 | }
746 | return objc_getAssociatedObject(self, &AssociatedKey.providers) as! [String: Any]
747 | }
748 | }
749 | var overrideThemeStyle: ThemeStyle? {
750 | set(new) {
751 | objc_setAssociatedObject(self, &AssociatedKey.overrideThemeStyle, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
752 | subviews.forEach { $0.overrideThemeStyle = self.overrideThemeStyle }
753 | }
754 | get {
755 | return objc_getAssociatedObject(self, &AssociatedKey.overrideThemeStyle) as? ThemeStyle
756 | }
757 | }
758 | }
759 | internal extension CALayer {
760 | struct AssociatedKey {
761 | static var providers: Void?
762 | static var overrideThemeStyle: Void?
763 | }
764 | var providers: [String: Any] {
765 | set(new) {
766 | objc_setAssociatedObject(self, &AssociatedKey.providers, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
767 | }
768 | get {
769 | if objc_getAssociatedObject(self, &AssociatedKey.providers) == nil {
770 | self.providers = [String: Any]()
771 | }
772 | return objc_getAssociatedObject(self, &AssociatedKey.providers) as! [String: Any]
773 | }
774 | }
775 | var overrideThemeStyle: ThemeStyle? {
776 | set(new) {
777 | objc_setAssociatedObject(self, &AssociatedKey.overrideThemeStyle, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
778 | sublayers?.forEach { $0.overrideThemeStyle = self.overrideThemeStyle }
779 | }
780 | get {
781 | return objc_getAssociatedObject(self, &AssociatedKey.overrideThemeStyle) as? ThemeStyle
782 | }
783 | }
784 | }
785 | internal extension UIBarItem {
786 | struct AssociatedKey {
787 | static var providers: Void?
788 | static var overrideThemeStyle: Void?
789 | }
790 | var providers: [String: Any] {
791 | set(new) {
792 | objc_setAssociatedObject(self, &AssociatedKey.providers, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
793 | }
794 | get {
795 | if objc_getAssociatedObject(self, &AssociatedKey.providers) == nil {
796 | self.providers = [String: Any]()
797 | }
798 | return objc_getAssociatedObject(self, &AssociatedKey.providers) as! [String: Any]
799 | }
800 | }
801 | var overrideThemeStyle: ThemeStyle? {
802 | set(new) {
803 | objc_setAssociatedObject(self, &AssociatedKey.overrideThemeStyle, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
804 | }
805 | get {
806 | return objc_getAssociatedObject(self, &AssociatedKey.overrideThemeStyle) as? ThemeStyle
807 | }
808 | }
809 | }
810 |
811 | //MARK: - Swizzle
812 | func swizzleMethod(_ aClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
813 | if let originalMethod = class_getInstanceMethod(aClass, originalSelector),
814 | let swizzledMehod = class_getInstanceMethod(aClass, swizzledSelector) {
815 | let didAddMethod: Bool = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMehod), method_getTypeEncoding(swizzledMehod))
816 | if didAddMethod {
817 | class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
818 | }else {
819 | method_exchangeImplementations(originalMethod, swizzledMehod)
820 | }
821 | }
822 | }
823 |
824 | extension UIView {
825 | static let swizzleAddSubview: Void = {
826 | swizzleMethod(UIView.self, originalSelector: #selector(addSubview(_:)), swizzledSelector: #selector(swizzledAddSubview(_:)))
827 | }()
828 |
829 | @objc func swizzledAddSubview(_ subview: UIView) {
830 | swizzledAddSubview(subview)
831 | if overrideThemeStyle != nil {
832 | subview.overrideThemeStyle = overrideThemeStyle
833 | }
834 | }
835 | }
836 |
837 | extension CALayer {
838 | static let swizzleAddSublayer: Void = {
839 | swizzleMethod(CALayer.self, originalSelector: #selector(addSublayer(_:)), swizzledSelector: #selector(swizzledAddSublayer(_:)))
840 | }()
841 |
842 | @objc func swizzledAddSublayer(_ sublayer: CALayer) {
843 | swizzledAddSublayer(sublayer)
844 | if overrideThemeStyle != nil {
845 | sublayer.overrideThemeStyle = overrideThemeStyle
846 | }
847 | }
848 | }
849 |
--------------------------------------------------------------------------------
/Sources/ThemeDefines.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeDefines.swift
3 | // JXTheme
4 | //
5 | // Created by jiaxin on 2019/7/10.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | //MARK: - ThemeCompatible
13 | public protocol ThemeCompatible: AnyObject { }
14 | public extension ThemeCompatible {
15 | var theme: ThemeWrapper {
16 | get { return ThemeWrapper(self) }
17 | set { }
18 | }
19 | }
20 | extension UIView: ThemeCompatible { }
21 | extension CALayer: ThemeCompatible { }
22 | extension UIBarItem: ThemeCompatible { }
23 |
24 | //MARK: - ThemeWrapper
25 | public struct ThemeWrapper {
26 | public let base: Base
27 | init(_ base: Base) {
28 | self.base = base
29 | }
30 | }
31 |
32 | //MARK: - Defines
33 | public struct ThemeStyle: RawRepresentable, Equatable, Hashable, Comparable {
34 | public typealias RawValue = String
35 | public var rawValue: String
36 | public var hashValue: Int { return rawValue.hashValue }
37 | public static let unspecified = ThemeStyle(rawValue: "unspecified")
38 |
39 | public init(rawValue: String) {
40 | self.rawValue = rawValue
41 | }
42 |
43 | public static func <(lhs: ThemeStyle, rhs: ThemeStyle) -> Bool {
44 | return lhs.rawValue < rhs.rawValue
45 | }
46 | public static func == (lhs: ThemeStyle, rhs: ThemeStyle) -> Bool {
47 | return lhs.rawValue == rhs.rawValue
48 | }
49 | }
50 |
51 | public struct ThemeProvider {
52 | public var provider: ThemePropertyProvider
53 | var config: ThemeCustomizationClosure?
54 | public init(_ provider: @escaping ThemePropertyProvider) {
55 | self.provider = provider
56 | }
57 | /// Refresh theme value with style.The default value is `.unspecified` that's say use `ThemeManager.shared.currentThemeStyle`.
58 | ///
59 | /// - Parameter style: ThemeStyle
60 | public func refresh(style: ThemeStyle = .unspecified) {
61 | if style == .unspecified {
62 | config?(ThemeManager.shared.currentThemeStyle)
63 | }else {
64 | config?(style)
65 | }
66 | }
67 | }
68 |
69 | public typealias ThemePropertyProvider = (ThemeStyle) -> T
70 | internal typealias ThemeCustomizationClosure = (ThemeStyle) -> ()
71 |
72 |
--------------------------------------------------------------------------------
/Sources/ThemeManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeManager.swift
3 | // JXTheme
4 | //
5 | // Created by jiaxin on 2019/7/10.
6 | // Copyright © 2019 jiaxin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Notification.Name {
12 | public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
13 | }
14 |
15 | public class ThemeManager {
16 | public static let shared = ThemeManager()
17 | public private(set) var currentThemeStyle: ThemeStyle = .unspecified {
18 | didSet {
19 | storeCurrentThemeStyle()
20 | }
21 | }
22 | /// The unique identifier key for store configs. For example,set userID. You should set this before config other property.
23 | public var storeConfigsIdentifierKey: String = "default" {
24 | didSet {
25 | refreshStoreConfigs()
26 | }
27 | }
28 | internal lazy var trackedHashTable: NSHashTable = {
29 | return NSHashTable.init(options: .weakMemory)
30 | }()
31 |
32 | init() {
33 | refreshStoreConfigs()
34 | UIView.swizzleAddSubview
35 | CALayer.swizzleAddSublayer
36 | }
37 |
38 | public func changeTheme(to style: ThemeStyle) {
39 | currentThemeStyle = style
40 | NotificationCenter.default.post(name: Notification.Name.JXThemeDidChange, object: nil, userInfo: ["style" : style])
41 | DispatchQueue.main.async {
42 | self.trackedHashTable.allObjects.forEach { (object) in
43 | self.refreshTargetObject(object)
44 | }
45 | }
46 | }
47 |
48 | func addTrackedObject(_ object: AnyObject, addedConfig: ThemeCustomizationClosure) {
49 | trackedHashTable.add(object)
50 | addedConfig(currentThemeStyle)
51 | }
52 |
53 | func refreshTargetObject(_ object: AnyObject) {
54 | if let view = object as? UIView {
55 | let style = view.overrideThemeStyle ?? self.currentThemeStyle
56 | view.providers.values.forEach { self.resolveProvider($0, style: style) }
57 | }else if let layer = object as? CALayer {
58 | let style = layer.overrideThemeStyle ?? self.currentThemeStyle
59 | layer.providers.values.forEach { self.resolveProvider($0, style: style) }
60 | }else if let barItem = object as? UIBarItem {
61 | barItem.providers.values.forEach { self.resolveProvider($0, style: self.currentThemeStyle) }
62 | }
63 | }
64 |
65 | func resolveProvider(_ object: Any, style: ThemeStyle) {
66 | if let provider = object as? ThemeProvider {
67 | provider.config?(style)
68 | }else if let provider = object as? ThemeProvider {
69 | provider.config?(style)
70 | }else if let provider = object as? ThemeProvider {
71 | provider.config?(style)
72 | }else if let provider = object as? ThemeProvider {
73 | provider.config?(style)
74 | }else if let provider = object as? ThemeProvider {
75 | provider.config?(style)
76 | }else if let provider = object as? ThemeProvider<[NSAttributedString.Key : Any]> {
77 | provider.config?(style)
78 | }else if let provider = object as? ThemeProvider {
79 | provider.config?(style)
80 | }else if let provider = object as? ThemeProvider {
81 | provider.config?(style)
82 | }else if let provider = object as? ThemeProvider {
83 | provider.config?(style)
84 | }else if let provider = object as? ThemeProvider {
85 | provider.config?(style)
86 | }else if let provider = object as? ThemeProvider {
87 | provider.config?(style)
88 | }
89 | }
90 | }
91 |
92 | //MARK: - Store
93 | extension ThemeManager {
94 | private var currentThemeStyleUserDefaultsKey: String {
95 | return "com.jiaxin.theme.currentThemeStyleUserDefaultsKey:" + storeConfigsIdentifierKey
96 | }
97 |
98 | fileprivate func storeCurrentThemeStyle() {
99 | UserDefaults.standard.setValue(currentThemeStyle.rawValue, forKey: currentThemeStyleUserDefaultsKey)
100 | }
101 |
102 | fileprivate func refreshStoreConfigs() {
103 | let currentThemeStyleValue = UserDefaults.standard.string(forKey: currentThemeStyleUserDefaultsKey)
104 | if currentThemeStyleValue == nil {
105 | currentThemeStyle = .unspecified
106 | }else {
107 | currentThemeStyle = ThemeStyle(rawValue: currentThemeStyleValue!)
108 | }
109 | }
110 | }
111 |
112 |
--------------------------------------------------------------------------------