├── .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 | ![preview](https://github.com/pujiaxin33/JXTheme/blob/master/GIF/preview.gif) 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 | ![preview](https://github.com/pujiaxin33/JXTheme/blob/master/GIF/preview.gif) 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 | --------------------------------------------------------------------------------