├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcbaselines │ └── ChameleonTests.xcbaseline │ ├── 8341A37E-AEA5-4935-9652-39C679C43F11.plist │ └── Info.plist ├── Chameleon.podspec ├── Demos ├── Demos.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Public │ ├── TestColors.swift │ └── ThemeInfo.swift ├── ios │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift └── macos │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── Main.storyboard │ ├── NSBox+.swift │ ├── TestAppearanceManager.swift │ ├── ViewController.swift │ └── macos.entitlements ├── LICENSE ├── Package.swift ├── README.md ├── Resources ├── Docs.md ├── Images │ ├── callable.png │ ├── classes.png │ └── leak.png └── graph.graffle ├── Sources └── Chameleon │ ├── Chameleon.swift │ ├── Core │ ├── APPError.swift │ ├── AppearanceManager.swift │ ├── Appearanced │ │ ├── AppearancedProtocol.swift │ │ ├── Callable+Appearanced.swift │ │ ├── Callable+Attributed.swift │ │ ├── Callable+Collection.swift │ │ ├── Callable+Customized.swift │ │ └── Callable+Original.swift │ ├── Callable.swift │ ├── NSObject+Appearance.swift │ ├── NSObject+Cache.swift │ └── Typealias.swift │ ├── Helper │ ├── AttributedString+.swift │ ├── Chameleon+Notifier.swift │ ├── Dictionary+Value.swift │ ├── NSObject+Swizzing.swift │ ├── NSUIColor.swift │ ├── NSUIImage.swift │ ├── Numeric.swift │ ├── SaftyCheck.swift │ └── String+.swift │ └── NSUIKits │ ├── APPKit │ ├── NSBox+.swift │ ├── NSButton+.swift │ ├── NSButtonTouchBarItem+.swift │ ├── NSClipView+.swift │ ├── NSCollectionView+.swift │ ├── NSDatePicker+.swift │ ├── NSImageView+.swift │ ├── NSLevelIndicator+.swift │ ├── NSPathControl+.swift │ ├── NSPathControlItem+.swift │ ├── NSScrollView+.swift │ ├── NSSegmentedControl+.swift │ ├── NSSlider+.swift │ ├── NSTableRowView+.swift │ ├── NSTableView+.swift │ ├── NSText+.swift │ ├── NSTextField+.swift │ ├── NSTextView+.swift │ ├── NSView+.swift │ └── NSWindow+.swift │ ├── Implementation.swift │ └── UIKit │ ├── UIActivityIndicatorView+.swift │ ├── UIBarItem+.swift │ ├── UIButton+.swift │ ├── UIImageView+.swift │ ├── UILabel+.swift │ ├── UINavigationBar+.swift │ ├── UIPageControl+.swift │ ├── UIProgressView+.swift │ ├── UIRefreshControl+.swift │ ├── UISearchBar+.swift │ ├── UISearchTextField+.swift │ ├── UISegmentedControl+.swift │ ├── UISlider+.swift │ ├── UIStepper+.swift │ ├── UISwitch+.swift │ ├── UITabBar+.swift │ ├── UITableView+.swift │ ├── UITextField+.swift │ ├── UITextView+.swift │ ├── UIToolbar+.swift │ └── UIView+.swift ├── Tests └── ChameleonTests │ ├── AppearancedTests.swift │ ├── AttributedStringTests.swift │ ├── AttributesTests.swift │ ├── ChameleonTests.swift │ ├── CheckTests.swift │ ├── ColorImages.swift │ ├── ComplexTests.swift │ ├── DictionaryTests.swift │ ├── Recycle.swift │ ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── theme0 │ │ │ ├── Contents.json │ │ │ ├── bullseye_icon_252137.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── bullseye_icon_252137.png │ │ │ ├── dinosaur_icon_252012.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dinosaur_icon_252012.png │ │ │ └── heart_love_icon_251954.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── heart_love_icon_251954.png │ │ ├── theme1 │ │ │ ├── Contents.json │ │ │ ├── home_house_icon_251952.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── home_house_icon_251952.png │ │ │ ├── human_handsup_icon_251948.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── human_handsup_icon_251948.png │ │ │ └── mood_happy_icon_251870.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── mood_happy_icon_251870.png │ │ └── theme2 │ │ │ ├── Contents.json │ │ │ ├── trash_delete_remove_icon_251766.imageset │ │ │ ├── Contents.json │ │ │ └── trash_delete_remove_icon_251766.png │ │ │ ├── visible_eye_icon_251745.imageset │ │ │ ├── Contents.json │ │ │ └── visible_eye_icon_251745.png │ │ │ └── zap_icon_251733.imageset │ │ │ ├── Contents.json │ │ │ └── zap_icon_251733.png │ └── bullseye_icon_252137.png │ ├── SwizzingTests.swift │ └── ThemeInfo.swift └── Tools ├── Breeder ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Breeder.entitlements ├── DataCenter.swift ├── DetailViewController.swift ├── FilesViewController.swift ├── InfosViewController.swift └── MainWindowController.swift └── Tools.xcodeproj ├── project.pbxproj └── project.xcworkspace ├── contents.xcworkspacedata └── xcshareddata ├── IDEWorkspaceChecks.plist └── swiftpm └── Package.resolved /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/ChameleonTests.xcbaseline/8341A37E-AEA5-4935-9652-39C679C43F11.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | AppearancedTests 8 | 9 | testLargeKits() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 3.667114 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcbaselines/ChameleonTests.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 8341A37E-AEA5-4935-9652-39C679C43F11 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 0 13 | cpuCount 14 | 1 15 | cpuKind 16 | Apple M1 17 | cpuSpeedInMHz 18 | 0 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro17,1 23 | physicalCPUCoresPerPackage 24 | 8 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | arm64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone15,2 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Chameleon.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Chameleon" 3 | s.version = "0.1.0" 4 | s.summary = "This is an easy framework for change your APP theme." 5 | s.description = <<-DESC 6 | Chameleon is a skin-changing (theme) framework for iOS/macOS that supports dynamic skin changing and custom attributes, and also supports non-invasive skin changing (swizzling). 7 | DESC 8 | s.homepage = "https://github.com/JyHu/Chameleon.git" 9 | s.license = "MIT" 10 | s.authors = { 11 | "JyHu" => "auu.aug@gmail.com", 12 | } 13 | 14 | s.ios.deployment_target = '11.0' 15 | s.osx.deployment_target = '10.13' 16 | 17 | s.source = { :git => "https://github.com/JyHu/Chameleon.git", :tag => s.version } 18 | s.requires_arc = true 19 | 20 | s.source_files = 'Sources/Chameleon/**/*.swift' 21 | end 22 | -------------------------------------------------------------------------------- /Demos/Demos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demos/Demos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demos/Public/TestColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestColors.swift 3 | // macos 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | #if os(macOS) 9 | import Cocoa 10 | #else 11 | import UIKit 12 | #endif 13 | 14 | import Chameleon 15 | 16 | extension NSUIAppearanceColor { 17 | static let C001 = NSUIAppearanceColor.colorWith(hexAString: "F7ACBC") 18 | .withAppearanceIdentifier("color/text/C001") 19 | static let C002 = NSUIAppearanceColor.colorWith(hexAString: "DEAB8A") 20 | .withAppearanceIdentifier("color/text/C002") 21 | static let C003 = NSUIAppearanceColor.colorWith(hexAString: "B17936") 22 | .withAppearanceIdentifier("color/text/C003") 23 | static let C004 = NSUIAppearanceColor.colorWith(hexAString: "444693") 24 | .withAppearanceIdentifier("color/text/C004") 25 | static let C005 = NSUIAppearanceColor.colorWith(hexAString: "EF5B9C") 26 | .withAppearanceIdentifier("color/text/C005") 27 | static let C006 = NSUIAppearanceColor.colorWith(hexAString: "FEDCBD") 28 | .withAppearanceIdentifier("color/text/C006") 29 | static let C007 = NSUIAppearanceColor.colorWith(hexAString: "7F7522") 30 | .withAppearanceIdentifier("color/text/C007") 31 | static let C008 = NSUIAppearanceColor.colorWith(hexAString: "2B4490") 32 | .withAppearanceIdentifier("color/text/C008") 33 | 34 | 35 | static let C101 = NSUIAppearanceColor.colorWith(hexAString: "FEEEED") 36 | .withAppearanceIdentifier("color/background/C101") 37 | static let C102 = NSUIAppearanceColor.colorWith(hexAString: "747920") 38 | .withAppearanceIdentifier("color/background/C102") 39 | static let C103 = NSUIAppearanceColor.colorWith(hexAString: "80752C") 40 | .withAppearanceIdentifier("color/background/C103") 41 | static let C104 = NSUIAppearanceColor.colorWith(hexAString: "2A5CAA") 42 | .withAppearanceIdentifier("color/background/C104") 43 | static let C105 = NSUIAppearanceColor.colorWith(hexAString: "F05B72") 44 | .withAppearanceIdentifier("color/background/C105") 45 | static let C106 = NSUIAppearanceColor.colorWith(hexAString: "905A3D") 46 | .withAppearanceIdentifier("color/background/C106") 47 | static let C107 = NSUIAppearanceColor.colorWith(hexAString: "87843B") 48 | .withAppearanceIdentifier("color/background/C107") 49 | static let C108 = NSUIAppearanceColor.colorWith(hexAString: "224B8F") 50 | .withAppearanceIdentifier("color/background/C108") 51 | 52 | 53 | 54 | 55 | 56 | static func colorWith(hexAString: String) -> NSUIAppearanceColor { 57 | let components = hexAString.components(separatedBy: " ") 58 | if components.count <= 1 { 59 | return colorWith(hexString: hexAString) 60 | } 61 | 62 | guard let hexString = components.first, 63 | let alphaString = components.last, 64 | let alpha = Double(alphaString) else { 65 | return .clear 66 | } 67 | 68 | let color = colorWith(hexString: hexString) 69 | 70 | if alpha == 1 { 71 | return color 72 | } 73 | 74 | if alpha == 0 { 75 | return .clear 76 | } 77 | 78 | return color.withAlphaComponent(alpha) 79 | } 80 | 81 | private static func colorWith(hexString: String) -> NSUIAppearanceColor { 82 | var hexValue: UInt = 0 83 | let scanner = Scanner(string: hexString) 84 | scanner.scanInt(&hexValue) 85 | 86 | return NSUIAppearanceColor(red: CGFloat((hexValue & 0xFF0000) >> 16) / 255, green: CGFloat((hexValue & 0xFF00) >> 8) / 255, blue: CGFloat(hexValue & 0xFF) / 255, alpha: 1) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Demos/Public/ThemeInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeInfo.swift 3 | // Demos 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ThemeInfo { 11 | static let theme0: [String: Any] = [ 12 | "color": [ 13 | "text": [ 14 | "C001": "F7ACBC", 15 | "C002": "DEAB8A", 16 | "C003": "B17936", 17 | "C004": "444693", 18 | "C005": "EF5B9C", 19 | "C006": "FEDCBD", 20 | "C007": "7F7522", 21 | "C008": "2B4490" 22 | ], 23 | "background": [ 24 | "C101": "FEEEED", 25 | "C102": "747920", 26 | "C103": "80752C", 27 | "C104": "2A5CAA", 28 | "C105": "F05B72", 29 | "C106": "905A3D", 30 | "C107": "87843B", 31 | "C108": "224B8F", 32 | ] 33 | ], 34 | "others": [ 35 | "border": [ 36 | "width": 5 37 | ] 38 | ] 39 | ] 40 | 41 | static let theme1: [String: Any] = [ 42 | "color": [ 43 | "text": [ 44 | "C001": "F69C9F 0.9", 45 | "C002": "5F3C23 0.9", 46 | "C003": "2E3A1F 0.9", 47 | "C004": "426AB3 0.9", 48 | "C005": "F58F98 0.9", 49 | "C006": "6B473C 0.9", 50 | "C007": "4D4F36 0.9", 51 | "C008": "46485F 0.9" 52 | ], 53 | "background": [ 54 | "C101": "F15B6C 0.9", 55 | "C102": "8F4B2E 0.9", 56 | "C103": "726930 0.9", 57 | "C104": "003A6C 0.9", 58 | "C105": "F8ABA6 0.9", 59 | "C106": "87481F 0.9", 60 | "C107": "454926 0.9", 61 | "C108": "102B6A 0.9", 62 | ] 63 | ], 64 | "others": [ 65 | "border": [ 66 | "width": 2 67 | ] 68 | ] 69 | ] 70 | 71 | static let theme2: [String: Any] = [ 72 | "color": [ 73 | "text": [ 74 | "C001": "CA8687 0.8", 75 | "C002": "FAA755 0.8", 76 | "C003": "B7BA6B 0.8", 77 | "C004": "4E72B8 0.8", 78 | "C005": "F391A6 0.8", 79 | "C006": "FAB27B 0.8", 80 | "C007": "B2D235 0.8", 81 | "C008": "181D4B 0.8" 82 | ], 83 | "background": [ 84 | "C101": "BD675B 0.8", 85 | "C102": "F5B220 0.8", 86 | "C103": "5C7A29 0.8", 87 | "C104": "1A2933 0.8", 88 | "C105": "D71345 0.8", 89 | "C106": "843900 0.8", 90 | "C107": "BED742 0.8", 91 | "C108": "6A6DA9 0.8", 92 | ] 93 | ], 94 | "others": [ 95 | "border": [ 96 | "width": 10 97 | ] 98 | ] 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /Demos/ios/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ios 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Demos/ios/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demos/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demos/ios/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demos/ios/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 | -------------------------------------------------------------------------------- /Demos/ios/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 | -------------------------------------------------------------------------------- /Demos/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demos/ios/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ios 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Demos/ios/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ios 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view. 15 | } 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Demos/macos/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // macos 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | // Insert code here to initialize your application 15 | 16 | } 17 | 18 | func applicationWillTerminate(_ aNotification: Notification) { 19 | // Insert code here to tear down your application 20 | } 21 | 22 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 23 | return true 24 | } 25 | 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Demos/macos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demos/macos/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demos/macos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demos/macos/NSBox+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSBox+.swift 3 | // macos 4 | // 5 | // Created by Jo on 2023/4/5. 6 | // 7 | 8 | import Cocoa 9 | import Chameleon 10 | 11 | /// 12 | /// 13 | /// 14 | /// 测试自定义换肤属性 15 | /// 16 | /// 17 | /// 18 | extension NSBox { 19 | var app_borderWidth: CGFloat { 20 | set { 21 | cache(appearanceCallable: Callable.One( 22 | firstParam: Callable.Appearanced(newValue, identifier: "others/border/width"), 23 | identifier: "NSBox.__setBorderWidth(_:)", 24 | action: { [weak self] va in self?.__setBorderWidth(va) }) 25 | ) 26 | } 27 | get { 28 | borderWidth 29 | } 30 | } 31 | 32 | // var optionalAttributedString: NSAttributedString? { 33 | // set { 34 | // cache( 35 | // firstParam: Callable.Attributed(newValue), 36 | // identifier: "NSBox.__setOptionalAttributedString(_:)") { val in 37 | // print("--> \(val)") 38 | // } 39 | // } 40 | // get { 41 | // return nil 42 | // } 43 | // } 44 | 45 | func customized(color1: NSColor, color2: NSColor?) { 46 | cache(appearanceCallable: 47 | Callable.Two( 48 | firstParam: Callable.Customized(color1, identifier: color1.appearanceIdentifier, converter: __colorConverter(_:)), 49 | secondParam: Callable.Customized(color2, identifier: color2?.appearanceIdentifier, converter: __colorConverter(_:)), 50 | identifier: "NSBox.__customized(color1:color2:)", 51 | action: { [weak self] va, vb in { self?.__customized(color1: va, color2: vb) }} 52 | ) 53 | ) 54 | } 55 | } 56 | 57 | private extension NSBox { 58 | func __setBorderWidth(_ borderWidth: CGFloat) { 59 | self.borderWidth = borderWidth 60 | } 61 | 62 | func __customized(color1: NSColor, color2: NSColor?) { 63 | print("--> \(color1), \(String(describing: color2))") 64 | } 65 | 66 | func __colorConverter(_ appearanceInfo: Any?) -> NSColor? { 67 | let callable = findCallable(with: "NSBox.__customized(color1:color2:)") as? Callable.Two, Callable.Customized> 68 | let p1 = callable?.firstParam.original 69 | let p2 = callable?.secondParam.original 70 | print("--> \(String(describing: appearanceInfo)) \(String(describing: p1)) \(String(describing: p2))") 71 | return nil 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Demos/macos/TestAppearanceManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAppearanceManager.swift 3 | // macos 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | import Foundation 9 | import Chameleon 10 | 11 | class MyTheme { 12 | var identifier: String = UUID().uuidString 13 | var name: String = "My Theme" 14 | var folder: URL? = nil 15 | 16 | fileprivate var appearanceInfo: [String: Any]? 17 | } 18 | 19 | class TestAppearanceManager: NSObject { 20 | static var shared = TestAppearanceManager() 21 | 22 | private(set) var themes: [MyTheme] = [] 23 | private var themesMap: [String: MyTheme] = [:] 24 | 25 | override init() { 26 | super.init() 27 | /// 这两个协议可用可不用,主要是为了开放更多的能力给使用者,这里仅作示例 28 | AppearanceManager.shared.delegate = self 29 | AppearanceManager.shared.customizedDelegate = self 30 | 31 | loadLocalThemes() 32 | downloadThemes() 33 | } 34 | 35 | func switchTheme(_ identifier: String) { 36 | guard let theme = themesMap[identifier] else { return } 37 | if theme.appearanceInfo == nil { 38 | // let themeInfo = .... 39 | // theme.appearanceInfo = .... 40 | } 41 | 42 | guard let themeInfo = theme.appearanceInfo else { return } 43 | AppearanceManager.shared.changeThemeWith(themeInfo: themeInfo) 44 | } 45 | } 46 | 47 | private extension TestAppearanceManager { 48 | func loadLocalThemes() { 49 | /// 加载本地的皮肤资源文件 50 | /// 然后缓存到themes中 51 | } 52 | 53 | func downloadThemes() { 54 | /// 下载云端的换肤资源 55 | } 56 | 57 | func transferToMap() { 58 | /// themes -> themesMap 59 | /// 方便查找 60 | for theme in themes { 61 | themesMap[theme.identifier] = theme 62 | } 63 | } 64 | } 65 | 66 | /// 自己管理换肤资源,需要向框架内提供 67 | extension TestAppearanceManager: AppearanceManagerProtocol { 68 | func appearanceInfo(with identifier: Chameleon.AppearanceCallableIdentifier) throws -> Any? { 69 | return nil 70 | } 71 | 72 | var currentThemePath: String? { 73 | return nil 74 | } 75 | } 76 | 77 | /// 开放给使用者自己来处理颜色、图片的转换和查找 78 | extension TestAppearanceManager: CustomizedAppearanceProtocol { 79 | func customizedAppearancedColor(of appearanceInfo: Any, identifier: Chameleon.AppearanceCallableIdentifier) -> Chameleon.CustomizedAppearanceResult { 80 | return .result(.red, true) 81 | } 82 | 83 | func customizedAppearancedImage(of appearanceInfo: Any, identifier: Chameleon.AppearanceCallableIdentifier) -> Chameleon.CustomizedAppearanceResult { 84 | return .ignored 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Demos/macos/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // macos 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import Cocoa 9 | import Chameleon 10 | 11 | class ViewController: NSViewController { 12 | 13 | @IBOutlet weak var testView: NSView! 14 | 15 | @IBOutlet weak var testBox: NSBox! 16 | 17 | @IBOutlet var testTextView: NSTextView! 18 | 19 | @IBOutlet weak var testTextField: NSTextField! 20 | 21 | @IBOutlet weak var multilineLabel: NSTextField! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | AppearanceManager.exchangeImplementations() 27 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme0) 28 | 29 | testBox.customized(color1: .C101, color2: .C103) 30 | 31 | testBox.borderWidth = 5 32 | 33 | testView.app_layerBackgroundColor = .C101 34 | testView.app_layerBorderColor = .C108 35 | testView.layer?.borderWidth = 5 36 | 37 | testBox.app_borderColor = .C001 38 | testBox.app_fillColor = .C002 39 | testBox.app_borderWidth = 5 40 | testTextView.app_backgroundColor = .C003 41 | testTextView.app_textColor = .C004 42 | testTextField.app_backgroundColor = .C107 43 | testTextField.app_textColor = .C005 44 | 45 | let attributedString = NSMutableAttributedString(string: "Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.") 46 | attributedString.addAttributes([.foregroundColor: NSColor.C002], range: NSMakeRange(0, 4)) 47 | attributedString.addAttributes([.foregroundColor: NSColor.C006], range: NSMakeRange(8, 5)) 48 | attributedString.addAttributes([.font: NSFont.systemFont(ofSize: 18)], range: NSMakeRange(2, 10)) 49 | multilineLabel.app_attributedStringValue = attributedString 50 | 51 | // testBox.borderColor = .c002 52 | // testBox.fillColor = .priceColor 53 | // testTextView.backgroundColor = .link 54 | // testTextView.textColor = .priceColor 55 | } 56 | 57 | @IBAction func switchThemeAction(_ sender: NSSegmentedControl) { 58 | if sender.indexOfSelectedItem == 0 { 59 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme0) 60 | } else if sender.indexOfSelectedItem == 1 { 61 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme1) 62 | } else if sender.indexOfSelectedItem == 2 { 63 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme2) 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Demos/macos/macos.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 胡金友 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.7 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: "Chameleon", 8 | platforms: [ .macOS(.v10_13), .iOS(.v11) ], 9 | products: [ 10 | .library( 11 | name: "Chameleon", 12 | targets: ["Chameleon"] 13 | ), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "Chameleon", 18 | dependencies: [] 19 | ), 20 | .testTarget( 21 | name: "ChameleonTests", 22 | dependencies: ["Chameleon"], 23 | resources: [.process("Resources")] 24 | ) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chameleon 2 | 3 | Chameleon is a theme framework designed for iOS/macOS, which allows for dynamic theme changes and customization of attributes. It also supports non-intrusive theme changes (swizzling) to alter the interface style. In other words, Chameleon provides a convenient way for developers to implement theme changes and custom attribute functionality in their applications without requiring extensive modifications to the existing codebase. 4 | 5 | ## Requirements 6 | 7 | - iOS 11.0+/ macOS 10.13+ 8 | - Xcode 10.0+ 9 | - Swift 5.0+ 10 | 11 | ## Installation 12 | 13 | ### Swift Package Manager 14 | 15 | [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 16 | 17 | ``` 18 | dependencies: [ 19 | .package(url: "https://github.com/JyHu/Chameleon.git", .upToNextMajor(from: "0.1.0")) 20 | ] 21 | ``` 22 | 23 | ### Cocoapods 24 | 25 | [CocoaPods](http://cocoapods.org/) is a dependency manager for Cocoa projects. You can install it with the following command: 26 | 27 | ``` 28 | $ gem install cocoapods 29 | ``` 30 | 31 | To integrate Chameleon into your Xcode project using CocoaPods, specify it in your `Podfile`: 32 | 33 | ``` 34 | source 'https://github.com/CocoaPods/Specs.git' 35 | platform :ios, '11.0' 36 | # platform: macos, '10.13' 37 | use_frameworks! 38 | 39 | target '' do 40 | pod 'Chameleon', :git => 'https://github.com/JyHu/Chameleon.git' 41 | end 42 | ``` 43 | 44 | Then, run the following command: 45 | 46 | ``` 47 | $ pod install 48 | ``` 49 | 50 | ## Resources 51 | 52 | - [Documentation](Resources/Docs.md) 53 | 54 | ## License 55 | 56 | Chameleon is released under the MIT license. See LICENSE for details. 57 | -------------------------------------------------------------------------------- /Resources/Images/callable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Resources/Images/callable.png -------------------------------------------------------------------------------- /Resources/Images/classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Resources/Images/classes.png -------------------------------------------------------------------------------- /Resources/Images/leak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Resources/Images/leak.png -------------------------------------------------------------------------------- /Resources/graph.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Resources/graph.graffle -------------------------------------------------------------------------------- /Sources/Chameleon/Chameleon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/2. 6 | // 7 | 8 | import Foundation 9 | 10 | let version = "1.0" 11 | 12 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/APPError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/2. 6 | // 7 | 8 | import Foundation 9 | 10 | enum APPError: Error { 11 | /// 解析换肤属性索引错误 12 | /// - 当前换肤数据的key 13 | /// - 当前遍历到的索引 14 | case parseIndexError(String, Int) 15 | 16 | /// 根据换肤标识获取对应的换肤属性的时候失败,找不到有效的字典数据 17 | case unknownAppearanceInfo(Any) 18 | 19 | /// 解析换肤文件中图片资源的时候遇到未知的图片数据 20 | case unknowImageAppearance(Any) 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Appearanced/AppearancedProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 11 | /// 每一个换肤属性持有者都必须要遵守的协议 12 | /// 13 | public protocol AppearancedProtocol { 14 | 15 | /// 缓存当前入参的数据类型 16 | associatedtype InputType 17 | 18 | /// 缓存的原始参数值 19 | var original: InputType { get } 20 | 21 | /// 是否是支持换肤的属性 22 | var isAppearanced: Bool { get } 23 | 24 | /// 根据当前参数数据从换肤资源中获取对应有效的换肤数据 25 | var correct: InputType { get } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Appearanced/Callable+Appearanced.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Callable { 11 | /// 数值类型 12 | enum NumType { 13 | case int 14 | case int8 15 | case int16 16 | case int32 17 | case int64 18 | 19 | case uint 20 | case uint8 21 | case uint16 22 | case uint32 23 | case uing64 24 | 25 | case double 26 | case cgfloat 27 | 28 | case number 29 | } 30 | 31 | /// 当前换肤参数的类型 32 | enum ClsType { 33 | /// 颜色类型参数 34 | case color 35 | /// 图片类型参数 36 | case image 37 | /// 数值类型 38 | case numeric(NumType) 39 | /// 其他类型参数 40 | case other 41 | 42 | init(obj: Any) { 43 | if obj is NSUIAppearanceColor { self = .color } 44 | else if obj is NSUIAppearanceImage { self = .image } 45 | else if obj is Int { self = .numeric(.int) } 46 | else if obj is Int8 { self = .numeric(.int8) } 47 | else if obj is Int16 { self = .numeric(.int16) } 48 | else if obj is Int32 { self = .numeric(.int32) } 49 | else if obj is Int64 { self = .numeric(.int64) } 50 | else if obj is UInt { self = .numeric(.uint) } 51 | else if obj is UInt8 { self = .numeric(.uint8) } 52 | else if obj is UInt16 { self = .numeric(.uint16) } 53 | else if obj is UInt32 { self = .numeric(.uint32) } 54 | else if obj is UInt64 { self = .numeric(.uing64) } 55 | else if obj is Double { self = .numeric(.double) } 56 | else if obj is CGFloat { self = .numeric(.cgfloat) } 57 | else if obj is NSNumber { self = .numeric(.number) } 58 | else { self = .other } 59 | } 60 | } 61 | } 62 | 63 | public extension Callable { 64 | /// 对单个换肤参数的更高一层的包装,用于记录当前入参的一些通用属性 65 | /// 66 | /// 67 | /// 该类支持Color、Image、常用数值类型的换肤属性支持 68 | /// 69 | /// 70 | struct Appearanced: AppearancedProtocol { 71 | public typealias InputType = T 72 | 73 | /// 缓存的原始参数值 74 | public let original: T 75 | /// 换肤参数的标识符,用于从换肤资源中找到对应的换肤属性 76 | public let identifier: AppearanceCallableIdentifier? 77 | /// 参数类型 78 | public let clsType: ClsType 79 | /// 是否是支持换肤的属性 80 | public var isAppearanced: Bool { 81 | identifier != nil 82 | } 83 | 84 | /// 初始化方法 85 | /// - Parameter original: 原始的参数值 86 | public init(_ original: T) { 87 | self.original = original 88 | self.identifier = (original as? AppearancedParamProtocol)?.appearanceIdentifier 89 | self.clsType = ClsType(obj: original) 90 | } 91 | 92 | /// 初始化方法 93 | /// - Parameters: 94 | /// - original: 传入进来的原始值 95 | /// - identifier: 换肤标识 96 | /// - clsType: 原始值的类型,如果没有设定,框架内部会根据原始值自己判断 97 | public init(_ original: T, identifier: AppearanceCallableIdentifier?, clsType: ClsType? = nil) { 98 | self.original = original 99 | self.identifier = identifier 100 | if let clsType = clsType { 101 | self.clsType = clsType 102 | } else { 103 | self.clsType = ClsType(obj: original) 104 | } 105 | } 106 | 107 | /// 根据当前参数数据从换肤资源中获取对应有效的换肤数据 108 | public var correct: T { 109 | /// 如果不支持换肤,返回原始值 110 | guard let identifier = identifier else { return original } 111 | 112 | func pickup(_ object: Any?) -> T { 113 | guard let object = object else { return original } 114 | return (object as? T) ?? original 115 | } 116 | 117 | switch clsType { 118 | case .color: return pickup(AppearanceManager.shared.color(with: identifier)) 119 | case .image: return pickup(AppearanceManager.shared.image(with: identifier)) 120 | case .numeric(let numType): 121 | let numVal = (try? AppearanceManager.shared.appearanceInfo(with: identifier)) 122 | let matchedRes: T? = matchedAppearancedNumValue(from: numVal, of: numType) 123 | return matchedRes ?? original 124 | case .other: return pickup(try? AppearanceManager.shared.appearanceInfo(with: identifier)) 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Appearanced/Callable+Attributed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Callable { 11 | 12 | /// 13 | /// 14 | /// 15 | /// 该类支持富文本换肤属性的换肤支持 16 | /// 17 | /// 对于富文本换肤,暂时只支持颜色属性的换肤 18 | /// 19 | /// 20 | struct Attributed: AppearancedProtocol { 21 | public typealias InputType = T 22 | 23 | public let original: T 24 | 25 | /// 缓存的当前富文本中所有颜色属性的位置信息 26 | private let elements: [NSAttributedString.ColorElement] 27 | 28 | public var isAppearanced: Bool { elements.count > 0 } 29 | 30 | public var correct: T { 31 | ((original as? NSAttributedString)?.updateAppearancedValues(elements) as? T) ?? original 32 | } 33 | } 34 | } 35 | 36 | public extension Callable.Attributed where T == NSAttributedString { 37 | init(_ original: T) { 38 | self.original = original 39 | self.elements = original.colorAppearancedElements() 40 | } 41 | } 42 | 43 | public extension Callable.Attributed where T == NSAttributedString? { 44 | init(_ original: T) { 45 | self.original = original 46 | self.elements = original?.colorAppearancedElements() ?? [] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Appearanced/Callable+Collection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - 实际的数组、字典换肤数据的持有及转换者 11 | 12 | /// 13 | /// 14 | /// 转换协议,因为数组、字典的换肤支持逻辑不一样,需要区别去处理, 15 | /// 但是又需要统一类型,所以用协议来抹平数据类型 16 | /// 17 | /// 18 | private protocol __CollectionAppearanceConvert { 19 | associatedtype OutPut 20 | var correct: OutPut { get } 21 | var isAppearanced: Bool { get } 22 | } 23 | 24 | private extension Callable { 25 | /// 数组类换肤数据的持有、转换者 26 | struct __ArrayHolder: __CollectionAppearanceConvert { 27 | /// 输出类型,即原范型T 28 | typealias OutPut = [V] 29 | 30 | /// 缓存的数组中的各换肤属性,用于后续的还原和查找对应的换肤属性值 31 | private let arrayAppearanced: [Appearanced] 32 | 33 | var correct: [V] { 34 | arrayAppearanced.map { $0.correct } 35 | } 36 | 37 | var isAppearanced: Bool { arrayAppearanced.contains { $0.isAppearanced } } 38 | 39 | init(original: [V]) { 40 | self.arrayAppearanced = original.map { Appearanced($0) } 41 | } 42 | } 43 | 44 | /// 字典类型换肤数据的持有、转换者 45 | struct __DictionaryHolder: __CollectionAppearanceConvert where K: Hashable { 46 | /// 输出类型,即原范型T 47 | typealias OutPut = [K: V] 48 | 49 | /// 缓存的数组中的各换肤属性,用于后续的还原和查找对应的换肤属性值 50 | private let dictionaryAppearanced: [K: Appearanced] 51 | 52 | var correct: [K : V] { 53 | var result: [K: V] = [:] 54 | for (key, value) in dictionaryAppearanced { 55 | result[key] = value.correct 56 | } 57 | 58 | return result 59 | } 60 | 61 | var isAppearanced: Bool { dictionaryAppearanced.contains { $0.value.isAppearanced }} 62 | 63 | init(original: [K: V]) { 64 | var appearances: [K: Callable.Appearanced] = [:] 65 | 66 | for (key, value) in original { 67 | appearances[key] = Callable.Appearanced(value) 68 | } 69 | 70 | self.dictionaryAppearanced = appearances 71 | } 72 | } 73 | } 74 | 75 | // MARK: - 集合属性换肤支持 76 | 77 | public extension Callable { 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 支持数组、字典类型的换肤属性 84 | /// 85 | /// T:原始的入参类型,比如数组、字典类型 86 | /// E:根据入参类型不同会有不同的定义,可以见后面的4个extension 87 | /// 88 | /// 89 | /// 90 | struct Collection: AppearancedProtocol { 91 | public typealias InputType = T 92 | 93 | public let original: T 94 | 95 | public let isAppearanced: Bool 96 | 97 | public var correct: T { 98 | (converter?.correct as? T) ?? original 99 | } 100 | 101 | private var converter: (any __CollectionAppearanceConvert)? 102 | } 103 | } 104 | 105 | // MARK: - 字典属性 106 | 107 | /// 108 | /// 109 | /// 对于字典类型的集合来说,此处的E表示的是字典的Key, 110 | /// 此处还额外的多了一个范型V,表示字典中的Value, 111 | /// 在这里将入参范型T和入参范型V、E做下关联,即 T == [E: V] 112 | /// 113 | /// 114 | 115 | extension Callable.Collection where E: Hashable { 116 | public init(_ original: T) where T == [E: V] { 117 | self.original = original 118 | self.converter = Callable.__DictionaryHolder(original: original) 119 | self.isAppearanced = self.converter?.isAppearanced ?? false 120 | } 121 | } 122 | 123 | extension Callable.Collection where E: Hashable { 124 | public init(_ original: T) where T == [E: V]? { 125 | self.original = original 126 | 127 | guard let original = original else { 128 | self.isAppearanced = false 129 | return 130 | } 131 | 132 | self.converter = Callable.__DictionaryHolder(original: original) 133 | self.isAppearanced = self.converter?.isAppearanced ?? false 134 | } 135 | } 136 | 137 | // MARK: - 数组属性 138 | 139 | /// 140 | /// 141 | /// 142 | /// 对于数组来说,此处的E表示数组中的Element,即每个元素的类型 143 | /// 在这里将入参范性T和E做下关联,即 T == [E] 144 | /// 145 | /// 146 | 147 | extension Callable.Collection where T == [E] { 148 | public init(_ original: T) { 149 | self.original = original 150 | self.converter = Callable.__ArrayHolder(original: original) 151 | self.isAppearanced = self.converter?.isAppearanced ?? false 152 | } 153 | } 154 | 155 | extension Callable.Collection where T == [E]? { 156 | public init(_ original: T) { 157 | self.original = original 158 | 159 | guard let original = original else { 160 | self.isAppearanced = false 161 | return 162 | } 163 | 164 | self.converter = Callable.__ArrayHolder(original: original) 165 | self.isAppearanced = self.converter?.isAppearanced ?? false 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Appearanced/Callable+Customized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Callable { 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 用于处理自定义的数据类型 16 | /// 17 | /// 18 | /// 19 | /// 20 | struct Customized: AppearancedProtocol { 21 | 22 | public typealias InputType = T 23 | 24 | public let original: T 25 | public let identifier: AppearanceCallableIdentifier? 26 | public let isAppearanced: Bool = true 27 | 28 | /// 获取有效的值 29 | public var correct: T { 30 | guard let identifier = identifier else { return original } 31 | return converter(try? AppearanceManager.shared.appearanceInfo(with: identifier)) ?? original 32 | } 33 | 34 | private var converter: (Any?) -> T? 35 | 36 | /// 初始化方法 37 | /// - Parameters: 38 | /// - original: 原始值 39 | /// - identifier: 当前自定义富文本的换肤资源ID 40 | /// - converter: 转换方法,将主题资源中的数据转换为对应的有效皮肤信息 41 | public init(_ original: T, identifier: AppearanceCallableIdentifier?, converter: @escaping (Any?) -> T?) { 42 | self.original = original 43 | self.identifier = identifier 44 | self.converter = converter 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Appearanced/Callable+Original.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Callable { 11 | /// 12 | /// 13 | /// 14 | /// 过渡的中间类,不需要执行换肤操作的 15 | /// 16 | /// 17 | /// 18 | struct Original: AppearancedProtocol { 19 | public typealias InputType = T 20 | 21 | public var original: InputType 22 | 23 | public var isAppearanced: Bool { false } 24 | 25 | public var correct: InputType { original } 26 | 27 | public init(_ original: InputType) { 28 | self.original = original 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/NSObject+Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/3. 6 | // 7 | 8 | #if os(macOS) 9 | import Cocoa 10 | #else 11 | import UIKit 12 | #endif 13 | 14 | /// 换肤资源附加标识符的协议,用于唯一的标识这个属性,并用于从换肤资源中找到对应的源数据 15 | public protocol AppearancedParamProtocol { 16 | var appearanceIdentifier: AppearanceCallableIdentifier? { get } 17 | } 18 | 19 | extension NSUIAppearanceColor: AppearancedParamProtocol { 20 | private struct AssociationKey { 21 | static var appearanceIdentifierKey = "com.auu.chameleon.association.appearanceIdentifier" 22 | } 23 | 24 | /// 当前对象的唯一标识符,用于在主题文件中找到对应的唯一属性值 25 | public private(set) var appearanceIdentifier: AppearanceCallableIdentifier? { 26 | set { 27 | objc_setAssociatedObject(self, &AssociationKey.appearanceIdentifierKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) 28 | } 29 | get { 30 | return objc_getAssociatedObject(self, &AssociationKey.appearanceIdentifierKey) as? AppearanceCallableIdentifier 31 | } 32 | } 33 | 34 | /// 一个中转方法,用于给一个对象添加一个标识符 35 | /// - Parameter identifier: 换肤的标识符 36 | /// - Returns: 原对象 37 | public func withAppearanceIdentifier(_ identifier: AppearanceCallableIdentifier) -> NSUIAppearanceColor { 38 | appearanceIdentifier = identifier 39 | return self 40 | } 41 | } 42 | 43 | extension NSUIAppearanceImage: AppearancedParamProtocol { 44 | private struct AssociationKey { 45 | static var appearanceIdentifierKey = "com.auu.chameleon.association.appearanceIdentifier" 46 | } 47 | 48 | /// 当前对象的唯一标识符,用于在主题文件中找到对应的唯一属性值 49 | public private(set) var appearanceIdentifier: AppearanceCallableIdentifier? { 50 | set { 51 | objc_setAssociatedObject(self, &AssociationKey.appearanceIdentifierKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) 52 | } 53 | get { 54 | return objc_getAssociatedObject(self, &AssociationKey.appearanceIdentifierKey) as? AppearanceCallableIdentifier 55 | } 56 | } 57 | 58 | /// 一个中转方法,用于给一个对象添加一个标识符 59 | /// - Parameter identifier: 换肤的标识符 60 | /// - Returns: 原对象 61 | public func withAppearanceIdentifier(_ identifier: AppearanceCallableIdentifier) -> Self { 62 | if let obj = NSUIAppearanceImage.imageWith(appearanceIdentifier: identifier) { 63 | obj.appearanceIdentifier = identifier 64 | return obj as! Self 65 | } 66 | 67 | appearanceIdentifier = identifier 68 | return self 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Chameleon/Core/Typealias.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/5. 6 | // 7 | 8 | #if canImport(Cocoa) 9 | import Cocoa 10 | public typealias NSUIAppearanceColor = NSColor 11 | public typealias NSUIAppearanceImage = NSImage 12 | #elseif canImport(UIKit) 13 | import UIKit 14 | public typealias NSUIAppearanceColor = UIColor 15 | public typealias NSUIAppearanceImage = UIImage 16 | #endif 17 | 18 | public typealias AppearanceCallableCategory = String 19 | public typealias AppearanceCallableIdentifier = String 20 | 21 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/AttributedString+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension NSAttributedString { 11 | /// 缓存颜色属性的位置信息 12 | struct ColorElement { 13 | /// 颜色值 14 | let color: NSUIAppearanceColor 15 | /// 换肤的颜色属性标识 16 | let identifier: AppearanceCallableIdentifier 17 | /// 颜色属性的位置 18 | let range: NSRange 19 | } 20 | 21 | /// 获取当前富文本的所有颜色属性的位置信息 22 | func colorAppearancedElements() -> [ColorElement] { 23 | var elements: [ColorElement] = [] 24 | 25 | func enumerate(_ key: NSAttributedString.Key) { 26 | enumerateAttribute(key, in: NSMakeRange(0, length)) { color, range, stop in 27 | guard let color = color as? NSUIAppearanceColor else { return } 28 | guard let appearanceIdentifier = color.appearanceIdentifier else { return } 29 | elements.append(ColorElement(color: color, identifier: appearanceIdentifier, range: range)) 30 | } 31 | } 32 | 33 | enumerate(.foregroundColor) 34 | enumerate(.backgroundColor) 35 | enumerate(.strokeColor) 36 | enumerate(.underlineColor) 37 | enumerate(.strikethroughColor) 38 | 39 | return elements 40 | } 41 | 42 | /// 使用缓存的颜色属性位置信息来更新富文本 43 | func updateAppearancedValues(_ elements: [ColorElement]) -> NSAttributedString { 44 | func getMutableAttributedString() -> NSMutableAttributedString { 45 | if let target = self as? NSMutableAttributedString { 46 | return target 47 | } 48 | 49 | return NSMutableAttributedString(attributedString: self) 50 | } 51 | 52 | let mutableAttributedString = getMutableAttributedString() 53 | 54 | /// 遍历所有的缓存信息,用换肤资源中有效的值来替换 55 | for element in elements { 56 | guard element.range.upperBound <= self.length else { continue } 57 | let color = NSUIAppearanceColor.colorWith(appearanceIdentifier: element.identifier) ?? element.color 58 | mutableAttributedString.addAttributes([.foregroundColor: color], range: element.range) 59 | } 60 | 61 | return mutableAttributedString 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/Chameleon+Notifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/2. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension AppearanceManager { 11 | /// 换肤后发送的通知 12 | static let appearanceChanged = Notification.Name("com.auu.chameleon.notification.appearanceChanged") 13 | } 14 | 15 | internal extension AppearanceManager { 16 | /// 发送主题改变的通知,通知所有注册了主题监听的组件刷新 17 | func postThemeChangeNotification() { 18 | notificationCenter.post(name: AppearanceManager.appearanceChanged, object: nil) 19 | } 20 | 21 | /// 注册主题改变的通知 22 | /// - Parameters: 23 | /// - observer: 要接收通知的对象 24 | /// - action: 接收通知后的操作方法 25 | func registerAppearanceObserver(_ observer: NSObject, action: Selector) { 26 | notificationCenter.addObserver(observer, selector: action, name: AppearanceManager.appearanceChanged, object: nil) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/Dictionary+Value.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/2. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Dictionary where Key == String { 11 | 12 | /// 根据给定的属性标识符,获取对应的属性信息 13 | /// 遍历方式是类似于文件路径,一级一级的查找,如: 14 | /// image/background/xxxxxx 15 | /// 会从当前字典中一层一层的查找 16 | /// 17 | /// - Parameter identifier: 属性标识符 18 | /// - Returns: 换肤资源信息 19 | func appearanceInfo(with identifier: AppearanceCallableIdentifier) throws -> Any? { 20 | if !identifier.contains("/") { 21 | return self[identifier] 22 | } 23 | 24 | let pathComponents = identifier.components(separatedBy: "/") 25 | return try appearanceInfo(pathComponents: pathComponents, originalIdentifier: identifier) 26 | } 27 | 28 | private func appearanceInfo(at index: Int = 0, pathComponents: [String], originalIdentifier: AppearanceCallableIdentifier) throws -> Any? { 29 | guard index < pathComponents.count else { 30 | throw APPError.parseIndexError(originalIdentifier, index) 31 | } 32 | 33 | let key = pathComponents[index] 34 | 35 | guard let curObj = self[key] else { 36 | return nil 37 | } 38 | 39 | if index == pathComponents.count - 1 { 40 | return curObj 41 | } 42 | 43 | guard let curObj = curObj as? [String: Any] else { 44 | throw APPError.unknownAppearanceInfo(curObj) 45 | } 46 | 47 | 48 | return try curObj.appearanceInfo(at: index + 1, pathComponents: pathComponents, originalIdentifier: originalIdentifier) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/NSObject+Swizzing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/3. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension NSObject { 11 | /// 12 | /// Runtime method swizzling 13 | /// 14 | static func app_swizzling(originalSelector: Selector, newSelector: Selector) { 15 | if let originalMethod = class_getInstanceMethod(self, originalSelector), 16 | let swizedMethod = class_getInstanceMethod(self, newSelector) { 17 | method_exchangeImplementations(swizedMethod, originalMethod) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/NSUIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/3. 6 | // 7 | 8 | #if canImport(Cocoa) 9 | import Cocoa 10 | #elseif canImport(UIKit) 11 | import UIKit 12 | #endif 13 | 14 | 15 | public extension NSUIAppearanceColor { 16 | /// 使用颜色属性标识符,从换肤资源中找到对应的有效的颜色值 17 | static func colorWith(appearanceIdentifier: AppearanceCallableIdentifier) -> NSUIAppearanceColor? { 18 | return AppearanceManager.shared.color(with: appearanceIdentifier) 19 | } 20 | 21 | /// 将颜色资源信息转换为有效的颜色值 22 | static func colorWith(appearancedValue: String) -> NSUIAppearanceColor? { 23 | if appearancedValue == "random" { 24 | return app_random() 25 | } 26 | 27 | if appearancedValue.hasPrefix("rgba") { 28 | guard let rgbaStr = appearancedValue.app_substringWithPattern("(?<=<).*?(?=>)"), rgbaStr.count > 0 else { return nil } 29 | let components = rgbaStr.replacingOccurrences(of: " ", with: "").components(separatedBy: ",") 30 | guard components.count == 4 else { return nil } 31 | guard let red = Double(components[0]), 32 | let green = Double(components[1]), 33 | let blue = Double(components[2]), 34 | let alpha = Double(components[3]) else { return nil } 35 | 36 | return app_colorWith(red: red, green: green, blue: blue, alpha: alpha) 37 | } 38 | 39 | if appearancedValue.hasPrefix("hex") { 40 | guard let hexString = appearancedValue.app_substringWithPattern("(?<=<).*?(?=>)"), hexString.count > 0 else { return nil } 41 | return app_colorWith(hexAString: hexString) 42 | } 43 | 44 | return app_colorWith(hexAString: appearancedValue) 45 | } 46 | } 47 | 48 | internal extension NSUIAppearanceColor { 49 | /// 生成随机颜色 50 | static func app_random() -> NSUIAppearanceColor { 51 | return app_colorWith( 52 | red: CGFloat(arc4random_uniform(256)), 53 | green: CGFloat(arc4random_uniform(256)), 54 | blue: CGFloat(arc4random_uniform(256)) 55 | ) 56 | } 57 | 58 | /// RGBA 颜色 59 | static func app_colorWith(hexAString: String) -> NSUIAppearanceColor? { 60 | let components = hexAString.components(separatedBy: " ") 61 | if components.count <= 1 { 62 | return app_colorWith(hexString: hexAString) 63 | } 64 | 65 | guard let hexString = components.first, 66 | let alphaString = components.last, 67 | let color = app_colorWith(hexString: hexString), 68 | let alpha = Double(alphaString) else { 69 | return nil 70 | } 71 | 72 | if alpha == 1 { 73 | return color 74 | } 75 | 76 | if alpha == 0 { 77 | return .clear 78 | } 79 | 80 | return color.withAlphaComponent(alpha) 81 | } 82 | 83 | /// 16进制色值字符串转换为颜色对象 84 | static func app_colorWith(hexString: String) -> NSUIAppearanceColor? { 85 | if hexString.isEmpty { return nil } 86 | var hexValue: UInt64 = 0 87 | let scanner = Scanner(string: hexString) 88 | scanner.scanHexInt64(&hexValue) 89 | return app_colorWith(hexValue: hexValue) 90 | } 91 | 92 | /// 16进制颜色值转换为颜色对象 93 | static func app_colorWith(hexValue: UInt64) -> NSUIAppearanceColor { 94 | return app_colorWith( 95 | red: CGFloat((hexValue & 0xFF0000) >> 16), 96 | green: CGFloat((hexValue & 0xFF00) >> 8), 97 | blue: CGFloat(hexValue & 0xFF) 98 | ) 99 | } 100 | 101 | /// 将RGBA转换为颜色对象 102 | static func app_colorWith(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1) -> NSUIAppearanceColor { 103 | return NSUIAppearanceColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/NSUIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/3. 6 | // 7 | 8 | #if canImport(Cocoa) 9 | import Cocoa 10 | #elseif canImport(UIKit) 11 | import UIKit 12 | #endif 13 | 14 | public extension NSUIAppearanceImage { 15 | static func imageWith(appearanceIdentifier: AppearanceCallableIdentifier) -> NSUIAppearanceImage? { 16 | return AppearanceManager.shared.image(with: appearanceIdentifier) 17 | } 18 | 19 | static func imageWith(appearancedValue: Any) throws -> NSUIAppearanceImage? { 20 | if let imageInfo = appearancedValue as? String { 21 | if imageInfo.hasPrefix("file://") { 22 | return app_imageWith(fileName: imageInfo.app_substringFrom(7)) 23 | } 24 | 25 | return NSUIAppearanceImage(named: imageInfo) 26 | } 27 | 28 | guard let imageInfo = appearancedValue as? [String: Any] else { 29 | throw APPError.unknowImageAppearance(appearancedValue) 30 | } 31 | 32 | if let name = imageInfo["name"] as? String { 33 | return NSUIAppearanceImage(named: name) 34 | } 35 | 36 | if let file = imageInfo["file"] as? String { 37 | return app_imageWith(fileName: file) 38 | } 39 | 40 | if let colorInfo = imageInfo["color"] as? String, 41 | let color = NSUIAppearanceColor.colorWith(appearancedValue: colorInfo) { 42 | return NSUIAppearanceImage.app_imageWith(color: color) 43 | } 44 | 45 | return nil 46 | } 47 | } 48 | 49 | private extension NSUIAppearanceImage { 50 | static func app_imageWith(color: NSUIAppearanceColor, size: CGSize = CGSize(width: 1, height: 1)) -> NSUIAppearanceImage? { 51 | #if os(macOS) 52 | return NSImage(size: size, flipped: true) { dstRect in 53 | color.setFill() 54 | dstRect.fill() 55 | return true 56 | } 57 | #else 58 | let rect = CGRect(origin: .zero, size: size) 59 | UIGraphicsBeginImageContext(size) 60 | let context = UIGraphicsGetCurrentContext() 61 | 62 | context?.setFillColor(color.cgColor) 63 | context?.fill([rect]) 64 | 65 | let image = UIGraphicsGetImageFromCurrentImageContext() 66 | UIGraphicsEndImageContext() 67 | 68 | return image 69 | #endif 70 | } 71 | 72 | static func app_imageWith(fileName: String?) -> NSUIAppearanceImage? { 73 | guard let themePath = AppearanceManager.shared.currentThemePath, let fileName = fileName else { return nil } 74 | let imagePath = NSString(string: themePath).appendingPathComponent(fileName) 75 | 76 | guard FileManager.default.fileExists(atPath: imagePath) else { 77 | return nil 78 | } 79 | 80 | return NSUIAppearanceImage(contentsOfFile: imagePath) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/SaftyCheck.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2024/1/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 用于比较的资源信息结构体 11 | public struct CheckableAppearanceInfo { 12 | /// 资源的标识符,用于在检查出错时确定是哪个文件 13 | public let identifier: String 14 | 15 | /// 需要比较的资源内容 16 | public let appearanceInfos: [String: Any] 17 | 18 | /// 初始化方法,用于创建一个资源比较的对象 19 | public init(identifier: String, appearanceInfos: [String : Any]) { 20 | self.identifier = identifier 21 | self.appearanceInfos = appearanceInfos 22 | } 23 | 24 | /// 从文件URL初始化,读取文件内容并转换为字典 25 | /// - Parameters: 26 | /// - identifier: 资源的标识符 27 | /// - fileURL: 文件URL 28 | public init(identifier: String, fileURL: URL) { 29 | self.identifier = identifier 30 | 31 | // 检查文件是否存在 32 | guard FileManager.default.fileExists(atPath: fileURL.path) else { 33 | self.appearanceInfos = [:] 34 | return 35 | } 36 | 37 | // 读取文件数据并尝试解析为字典 38 | guard let data = try? Data(contentsOf: fileURL), 39 | let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 40 | self.appearanceInfos = [:] 41 | return 42 | } 43 | 44 | self.appearanceInfos = json 45 | } 46 | } 47 | 48 | /// 表示可能的比较错误的枚举 49 | public enum CheckedAppearanceError: Error, CustomStringConvertible { 50 | /// 传入的资源为空 51 | case emptyFile(_ identifier: String) 52 | 53 | /// 缺少对应路径下的键 54 | case missingKey(_ identifier: String, _ path: String) 55 | 56 | /// 资源类型不匹配 57 | case notMatched(_ path: String) 58 | 59 | /// 描述输出内容 60 | public var description: String { 61 | switch self { 62 | case .emptyFile(let identifier): 63 | return "Empty file of \(identifier)" 64 | case .missingKey(let identifier, let path): 65 | return "Missing key of \(identifier) at \(path)" 66 | case .notMatched(let path): 67 | return "Appearance info not matched at \(path)" 68 | } 69 | } 70 | } 71 | 72 | /// 对外暴露的比较函数,接受一个资源信息数组,返回比较结果的错误数组 73 | /// - Parameter infos: 资源信息数组 74 | /// - Returns: 比较结果的错误数组 75 | public func checkAppearanceInfos(_ infos: [CheckableAppearanceInfo]) -> [CheckedAppearanceError] { 76 | // 至少需要两个资源进行比较 77 | guard infos.count > 1 else { return [] } 78 | 79 | for info in infos { 80 | // 检查每个资源是否为空文件 81 | if info.appearanceInfos.isEmpty { 82 | return [.emptyFile(info.identifier)] 83 | } 84 | } 85 | 86 | // 调用辅助函数进行比较 87 | return _checkAppearanceInfos(infos, path: "") 88 | } 89 | 90 | /// 辅助函数,用于递归比较资源信息 91 | /// - Parameters: 92 | /// - infos: 资源信息数组 93 | /// - path: 当前比较的路径 94 | /// - Returns: 比较结果的错误数组 95 | private func _checkAppearanceInfos(_ infos: [CheckableAppearanceInfo], path: String) -> [CheckedAppearanceError] { 96 | // 初始化所有键的集合 97 | var allKeys: Set = Set(infos[0].appearanceInfos.keys) 98 | var errors: [CheckedAppearanceError] = [] 99 | 100 | // 计算所有资源的键的并集 101 | for checkedInfo in infos[1...] { 102 | allKeys.formUnion(checkedInfo.appearanceInfos.keys) 103 | } 104 | 105 | // 逐个比较键 106 | for key in allKeys { 107 | let curPath = path.isEmpty ? key : (path + "/" + key) 108 | var isValue: Bool = false 109 | var containsError: Bool = false 110 | var nextLevleInfos: [CheckableAppearanceInfo] = [] 111 | 112 | // 遍历每个资源 113 | for (index, checkedInfo) in infos.enumerated() { 114 | // 检查键是否存在 115 | guard let checkedValue = checkedInfo.appearanceInfos[key] else { 116 | containsError = true 117 | errors.append(.missingKey(checkedInfo.identifier, curPath)) 118 | continue 119 | } 120 | 121 | // 如果已经包含错误,跳过后续逻辑,这样的做法只会记录检查键值存在的问题,而不会再匹配数据的一致性 122 | guard !containsError else { continue } 123 | 124 | // 根据值的类型进行不同处理 125 | if let checkedChildren = checkedValue as? [String: Any] { 126 | nextLevleInfos.append(CheckableAppearanceInfo(identifier: checkedInfo.identifier, appearanceInfos: checkedChildren)) 127 | 128 | // 判断是否是第一个资源 129 | if index == 0 { 130 | isValue = false 131 | } else if isValue { 132 | containsError = true 133 | errors.append(.notMatched(curPath)) 134 | } 135 | } else { 136 | // 判断是否是第一个资源 137 | if index == 0 { 138 | isValue = true 139 | } else if !isValue { 140 | containsError = true 141 | errors.append(.notMatched(curPath)) 142 | } 143 | } 144 | } 145 | 146 | // 如果没有包含错误且不是值类型,递归调用进行下一层比较 147 | if !containsError && !isValue { 148 | errors.append(contentsOf: _checkAppearanceInfos(nextLevleInfos, path: curPath)) 149 | } 150 | } 151 | 152 | return errors 153 | } 154 | -------------------------------------------------------------------------------- /Sources/Chameleon/Helper/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/3. 6 | // 7 | 8 | import Foundation 9 | 10 | internal extension String { 11 | /// 使用正则截取需要的字符串部分 12 | func app_substringWithPattern(_ pattern: String) -> String? { 13 | let nsstring = NSString(string: self) 14 | let range = nsstring.range(of: pattern, options: .regularExpression) 15 | if range.location == NSNotFound { 16 | return nil 17 | } 18 | 19 | return nsstring.substring(with: range) 20 | } 21 | 22 | /// 获取从指定位置起的字符串 23 | func app_substringFrom(_ index: Int) -> String? { 24 | if index >= count || index < 0 { 25 | return nil 26 | } 27 | 28 | return NSString(string: self).substring(from: index) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSBox+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/3. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let fillColor = "NSBox.__setFillColor(_:)" 14 | static let borderColor = "NSBox.__setBorderColor(_:)" 15 | } 16 | 17 | public extension NSBox { 18 | var app_fillColor: NSColor { 19 | get { fillColor } 20 | set { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | fillColor = newValue 23 | } else { 24 | swizzled_setFillColor(newValue) 25 | } 26 | } 27 | } 28 | 29 | var app_borderColor: NSColor { 30 | get { borderColor } 31 | set { 32 | if __USING_APPEARANCED_SWIZZLING__ { 33 | borderColor = newValue 34 | } else { 35 | swizzled_setBorderColor(newValue) 36 | } 37 | } 38 | } 39 | } 40 | 41 | internal extension NSBox { 42 | static func silenceExchangeBoxImplementation() { 43 | app_swizzling( 44 | originalSelector: #selector(setter: fillColor), 45 | newSelector: #selector(swizzled_setFillColor(_:)) 46 | ) 47 | 48 | app_swizzling( 49 | originalSelector: #selector(setter: borderColor), 50 | newSelector: #selector(swizzled_setBorderColor(_:)) 51 | ) 52 | } 53 | } 54 | 55 | private extension NSBox { 56 | func __setFillColor(_ fillColor: NSColor) { 57 | if __USING_APPEARANCED_SWIZZLING__ { 58 | swizzled_setFillColor(fillColor) 59 | } else { 60 | self.fillColor = fillColor 61 | } 62 | } 63 | 64 | func __setBorderColor(_ borderColor: NSColor) { 65 | if __USING_APPEARANCED_SWIZZLING__ { 66 | swizzled_setBorderColor(borderColor) 67 | } else { 68 | self.borderColor = borderColor 69 | } 70 | } 71 | 72 | @objc func swizzled_setFillColor(_ fillColor: NSColor) { 73 | cache( 74 | firstParam: Callable.Appearanced(fillColor), 75 | identifier: .fillColor, 76 | action: { [weak self] va in self?.__setFillColor(va) } 77 | ) 78 | } 79 | 80 | @objc func swizzled_setBorderColor(_ borderColor: NSColor) { 81 | cache( 82 | firstParam: Callable.Appearanced(borderColor), 83 | identifier: .borderColor, 84 | action: { [weak self] va in self?.__setBorderColor(va) } 85 | ) 86 | } 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSButtonTouchBarItem+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | 9 | #if os(macOS) 10 | import Cocoa 11 | 12 | @available(macOS 10.15, *) 13 | private extension AppearanceCallableIdentifier { 14 | static let image = "NSButtonTouchBarItem.__setImage(_:)" 15 | static let bezelColor = "NSButtonTouchBarItem.__setBezelColor(_:)" 16 | } 17 | 18 | @available(macOS 10.15, *) 19 | public extension NSButtonTouchBarItem { 20 | var app_image: NSUIAppearanceImage? { 21 | get { image } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | image = newValue 25 | } else { 26 | swizzled_setImage(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_bezelColor: NSUIAppearanceColor? { 32 | get { bezelColor } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | bezelColor = newValue 36 | } else { 37 | swizzled_setBezelColor(newValue) 38 | } 39 | } 40 | } 41 | } 42 | 43 | @available(macOS 10.15, *) 44 | internal extension NSButtonTouchBarItem { 45 | static func silenceExchangeButtonTouchBarItemImplementation() { 46 | app_swizzling( 47 | originalSelector: #selector(setter: image), 48 | newSelector: #selector(swizzled_setImage(_:)) 49 | ) 50 | 51 | app_swizzling( 52 | originalSelector: #selector(setter: bezelColor), 53 | newSelector: #selector(swizzled_setBezelColor(_:)) 54 | ) 55 | } 56 | } 57 | 58 | @available(macOS 10.15, *) 59 | private extension NSButtonTouchBarItem { 60 | func __setImage(_ image: NSUIAppearanceImage?) { 61 | if __USING_APPEARANCED_SWIZZLING__ { 62 | swizzled_setImage(image) 63 | } else { 64 | self.image = image 65 | } 66 | } 67 | 68 | func __setBezelColor(_ bezelColor: NSUIAppearanceColor?) { 69 | if __USING_APPEARANCED_SWIZZLING__ { 70 | swizzled_setBezelColor(bezelColor) 71 | } else { 72 | self.bezelColor = bezelColor 73 | } 74 | } 75 | 76 | @objc func swizzled_setImage(_ image: NSUIAppearanceImage?) { 77 | cache( 78 | firstParam: Callable.Appearanced(image), 79 | identifier: .image, 80 | action: { [weak self] va in self?.__setImage(va) } 81 | ) 82 | } 83 | 84 | @objc func swizzled_setBezelColor(_ bezelColor: NSUIAppearanceColor?) { 85 | cache( 86 | firstParam: Callable.Appearanced(bezelColor), 87 | identifier: .bezelColor, 88 | action: { [weak self] va in self?.__setBezelColor(va) } 89 | ) 90 | } 91 | } 92 | #endif 93 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSClipView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColor = "NSClipView.__setBackgroundColor(_:)" 14 | } 15 | 16 | public extension NSClipView { 17 | var app_backgroundColor: NSColor { 18 | get { backgroundColor } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | self.backgroundColor = backgroundColor 22 | } else { 23 | swizzled_setBackgroundColor(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSClipView { 30 | static func silenceExchangeClipViewImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: backgroundColor), 33 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension NSClipView { 39 | func __setBackgroundColor(_ backgroundColor: NSColor) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setBackgroundColor(backgroundColor) 42 | } else { 43 | self.backgroundColor = backgroundColor 44 | } 45 | } 46 | 47 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 48 | cache( 49 | firstParam: Callable.Appearanced(backgroundColor), 50 | identifier: .backgroundColor, 51 | action: { [weak self] va in self?.__setBackgroundColor(va) } 52 | ) 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSCollectionView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/9. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColors = "NSCollectionView.__setBackgroundColors(_:)" 14 | } 15 | 16 | public extension NSCollectionView { 17 | var app_backgroundColors: [NSColor] { 18 | get { backgroundColors } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | backgroundColors = newValue 22 | } else { 23 | swizzled_setBackgroundColors(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSCollectionView { 30 | static func silenceExchangeCollectionViewImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: backgroundColors), 33 | newSelector: #selector(swizzled_setBackgroundColors(_:))) 34 | } 35 | } 36 | 37 | private extension NSCollectionView { 38 | func __setBackgroundColors(_ backgroundColors: [NSColor]) { 39 | if __USING_APPEARANCED_SWIZZLING__ { 40 | swizzled_setBackgroundColors(backgroundColors) 41 | } else { 42 | self.backgroundColors = backgroundColors 43 | } 44 | } 45 | 46 | @objc func swizzled_setBackgroundColors(_ backgroundColors: [NSColor]) { 47 | cache( 48 | firstParam: Callable.Collection(backgroundColors), 49 | identifier: .backgroundColors, 50 | action: { [weak self] va in self?.__setBackgroundColors(va) } 51 | ) 52 | } 53 | } 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSDatePicker+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let textColor = "NSDatePicker.__setTextColor(_:)" 14 | static let backgroundColor = "NSDatePicker.__setBackgroundColor(_:)" 15 | } 16 | 17 | public extension NSDatePicker { 18 | var app_textColor: NSColor { 19 | get { textColor } 20 | set { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | textColor = newValue 23 | } else { 24 | swizzled_setTextColor(newValue) 25 | } 26 | } 27 | } 28 | 29 | var app_backgroundColor: NSColor { 30 | get { backgroundColor } 31 | set { 32 | if __USING_APPEARANCED_SWIZZLING__ { 33 | backgroundColor = newValue 34 | } else { 35 | swizzled_setBackgroundColor(newValue) 36 | } 37 | } 38 | } 39 | } 40 | 41 | internal extension NSDatePicker { 42 | static func silenceExchangeDatePickerViewImplementation() { 43 | app_swizzling( 44 | originalSelector: #selector(setter: textColor), 45 | newSelector: #selector(swizzled_setTextColor(_:)) 46 | ) 47 | 48 | app_swizzling( 49 | originalSelector: #selector(setter: backgroundColor), 50 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 51 | ) 52 | } 53 | } 54 | 55 | private extension NSDatePicker { 56 | func __setTextColor(_ textColor: NSColor) { 57 | if __USING_APPEARANCED_SWIZZLING__ { 58 | swizzled_setTextColor(textColor) 59 | } else { 60 | self.textColor = textColor 61 | } 62 | } 63 | 64 | func __setBackgroundColor(_ backgroundColor: NSColor) { 65 | if __USING_APPEARANCED_SWIZZLING__ { 66 | swizzled_setBackgroundColor(backgroundColor) 67 | } else { 68 | self.backgroundColor = backgroundColor 69 | } 70 | } 71 | 72 | @objc func swizzled_setTextColor(_ textColor: NSColor) { 73 | cache( 74 | firstParam: Callable.Appearanced(textColor), 75 | identifier: .textColor, 76 | action: { [weak self] va in self?.__setTextColor(va) } 77 | ) 78 | } 79 | 80 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 81 | cache( 82 | firstParam: Callable.Appearanced(backgroundColor), 83 | identifier: .backgroundColor, 84 | action: { [weak self] va in self?.__setBackgroundColor(va) } 85 | ) 86 | } 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSImageView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let image = "NSImageView.__setImage(_:)" 14 | } 15 | 16 | public extension NSImageView { 17 | var app_image: NSImage? { 18 | get { image } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | image = newValue 22 | } else { 23 | swizzled_setImage(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSImageView { 30 | static func silenceExchangeImageViewImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: image), 33 | newSelector: #selector(swizzled_setImage(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension NSImageView { 39 | func __setImage(_ image: NSImage?) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setImage(image) 42 | } else { 43 | self.image = image 44 | } 45 | } 46 | 47 | 48 | @objc func swizzled_setImage(_ image: NSImage?) { 49 | cache( 50 | firstParam: Callable.Appearanced(image), 51 | identifier: .image, 52 | action: { [weak self] va in self?.__setImage(va) } 53 | ) 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSPathControl+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | 9 | #if os(macOS) 10 | 11 | import Cocoa 12 | 13 | private extension AppearanceCallableIdentifier { 14 | static let placeholderAttributedString = "NSPathControl.__setPlaceholderAttributedString(_:)" 15 | static let backgroundColor = "NSPathControl.__setBackgroundColor(_:)" 16 | } 17 | 18 | public extension NSPathControl { 19 | var app_placeholderAttributedString: NSAttributedString? { 20 | get { placeholderAttributedString } 21 | set { 22 | if __USING_APPEARANCED_SWIZZLING__ { 23 | placeholderAttributedString = newValue 24 | } else { 25 | swizzled_setPlaceholderAttributedString(newValue) 26 | } 27 | } 28 | } 29 | 30 | var app_backgroundColor: NSColor? { 31 | get { backgroundColor } 32 | set { 33 | if __USING_APPEARANCED_SWIZZLING__ { 34 | backgroundColor = newValue 35 | } else { 36 | swizzled_setBackgroundColor(newValue) 37 | } 38 | } 39 | } 40 | } 41 | 42 | internal extension NSPathControl { 43 | static func silenceExchangePathControlImplementation() { 44 | app_swizzling( 45 | originalSelector: #selector(setter: placeholderAttributedString), 46 | newSelector: #selector(swizzled_setPlaceholderAttributedString(_:)) 47 | ) 48 | 49 | app_swizzling( 50 | originalSelector: #selector(setter: backgroundColor), 51 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 52 | ) 53 | } 54 | } 55 | 56 | private extension NSPathControl { 57 | func __setBackgroundColor(_ backgroundColor: NSColor?) { 58 | if __USING_APPEARANCED_SWIZZLING__ { 59 | swizzled_setBackgroundColor(backgroundColor) 60 | } else { 61 | self.backgroundColor = backgroundColor 62 | } 63 | } 64 | 65 | func __setPlaceholderAttributedString(_ placeholderAttributedString: NSAttributedString?) { 66 | if __USING_APPEARANCED_SWIZZLING__ { 67 | swizzled_setPlaceholderAttributedString(placeholderAttributedString) 68 | } else { 69 | self.placeholderAttributedString = placeholderAttributedString 70 | } 71 | } 72 | 73 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor?) { 74 | cache( 75 | firstParam: Callable.Appearanced(backgroundColor), 76 | identifier: .backgroundColor, 77 | action: { [weak self] va in self?.__setBackgroundColor(va) } 78 | ) 79 | } 80 | 81 | @objc func swizzled_setPlaceholderAttributedString(_ placeholderAttributedString: NSAttributedString?) { 82 | cache( 83 | firstParam: Callable.Attributed(placeholderAttributedString), 84 | identifier: .placeholderAttributedString, 85 | action: { [weak self] va in self?.__setPlaceholderAttributedString(va) } 86 | ) 87 | } 88 | } 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSPathControlItem+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | 9 | #if os(macOS) 10 | 11 | import Cocoa 12 | 13 | private extension AppearanceCallableIdentifier { 14 | static let attributedTitle = "NSPathControlItem.__setAttributedTitle(_:)" 15 | static let image = "NSPathControlItem.__setImage(_:)" 16 | } 17 | 18 | public extension NSPathControlItem { 19 | var app_attributedTitle: NSAttributedString { 20 | get { attributedTitle } 21 | set { 22 | if __USING_APPEARANCED_SWIZZLING__ { 23 | attributedTitle = newValue 24 | } else { 25 | swizzled_setAttributedTitle(newValue) 26 | } 27 | } 28 | } 29 | 30 | var app_image: NSImage? { 31 | get { image } 32 | set { 33 | if __USING_APPEARANCED_SWIZZLING__ { 34 | image = newValue 35 | } else { 36 | swizzled_setImage(newValue) 37 | } 38 | } 39 | } 40 | } 41 | 42 | internal extension NSPathControlItem { 43 | static func silenceExchangePathControlItemImplementation() { 44 | app_swizzling( 45 | originalSelector: #selector(setter: attributedTitle), 46 | newSelector: #selector(swizzled_setAttributedTitle(_:)) 47 | ) 48 | 49 | app_swizzling( 50 | originalSelector: #selector(setter: image), 51 | newSelector: #selector(swizzled_setImage(_:)) 52 | ) 53 | } 54 | } 55 | 56 | private extension NSPathControlItem { 57 | func __setAttributedTitle(_ attributedTitle: NSAttributedString) { 58 | if __USING_APPEARANCED_SWIZZLING__ { 59 | swizzled_setAttributedTitle(attributedTitle) 60 | } else { 61 | self.attributedTitle = attributedTitle 62 | } 63 | } 64 | 65 | func __setImage(_ image: NSImage?) { 66 | if __USING_APPEARANCED_SWIZZLING__ { 67 | swizzled_setImage(image) 68 | } else { 69 | self.image = image 70 | } 71 | } 72 | 73 | @objc func swizzled_setAttributedTitle(_ attributedTitle: NSAttributedString) { 74 | cache( 75 | firstParam: Callable.Attributed(attributedTitle), 76 | identifier: .attributedTitle, 77 | action: { [weak self] va in self?.__setAttributedTitle(va) } 78 | ) 79 | } 80 | 81 | @objc func swizzled_setImage(_ image: NSImage?) { 82 | cache( 83 | firstParam: Callable.Appearanced(image), 84 | identifier: .image, 85 | action: { [weak self] va in self?.__setImage(va) } 86 | ) 87 | } 88 | } 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSScrollView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColor = "NSScrollView.__setBackgroundColor(_:)" 14 | } 15 | 16 | public extension NSScrollView { 17 | var app_backgroundColor: NSColor { 18 | get { backgroundColor } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | backgroundColor = newValue 22 | } else { 23 | swizzled_setBackgroundColor(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSScrollView { 30 | static func silenceExchangeScrollViewImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: backgroundColor), 33 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension NSScrollView { 39 | func __setBackgroundColor(_ backgroundColor: NSColor) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setBackgroundColor(backgroundColor) 42 | } else { 43 | self.backgroundColor = backgroundColor 44 | } 45 | } 46 | 47 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 48 | cache( 49 | firstParam: Callable.Appearanced(backgroundColor), 50 | identifier: .backgroundColor, 51 | action: { [weak self] va in self?.__setBackgroundColor(va) } 52 | ) 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSSegmentedControl+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let selectedSegmentBezelColor = "NSSegmentedControl.__setSelectedSegmentBezelColor(_:)" 14 | static let setImageForSegment = "NSSegmentedControl.__setImage(_:forSegment:)" 15 | } 16 | 17 | public extension NSSegmentedControl { 18 | var app_selectedSegmentBezelColor: NSColor? { 19 | get { selectedSegmentBezelColor } 20 | set { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | self.selectedSegmentBezelColor = newValue 23 | } else { 24 | swizzled_setSelectedSegmentBezelColor(newValue) 25 | } 26 | } 27 | } 28 | 29 | func app_setImage(_ image: NSImage?, forSegment segment: Int) { 30 | if __USING_APPEARANCED_SWIZZLING__ { 31 | setImage(image, forSegment: segment) 32 | } else { 33 | swizzled_setImage(image, forSegment: segment) 34 | } 35 | } 36 | } 37 | 38 | internal extension NSSegmentedControl { 39 | static func silenceExchangeSegmentedControlImplementation() { 40 | app_swizzling( 41 | originalSelector: #selector(setter: selectedSegmentBezelColor), 42 | newSelector: #selector(swizzled_setSelectedSegmentBezelColor(_:)) 43 | ) 44 | 45 | app_swizzling( 46 | originalSelector: #selector(setImage(_:forSegment:)), 47 | newSelector: #selector(swizzled_setImage(_:forSegment:)) 48 | ) 49 | } 50 | } 51 | 52 | private extension NSSegmentedControl { 53 | func __setSelectedSegmentBezelColor(_ selectedSegmentBezelColor: NSColor?) { 54 | if __USING_APPEARANCED_SWIZZLING__ { 55 | swizzled_setSelectedSegmentBezelColor(selectedSegmentBezelColor) 56 | } else { 57 | self.selectedSegmentBezelColor = selectedSegmentBezelColor 58 | } 59 | } 60 | 61 | func __setImage(_ image: NSImage?, forSegment segment: Int) { 62 | if __USING_APPEARANCED_SWIZZLING__ { 63 | swizzled_setImage(image, forSegment: segment) 64 | } else { 65 | setImage(image, forSegment: segment) 66 | } 67 | } 68 | 69 | @objc func swizzled_setSelectedSegmentBezelColor(_ selectedSegmentBezelColor: NSColor?) { 70 | cache( 71 | firstParam: Callable.Appearanced(selectedSegmentBezelColor), 72 | identifier: .selectedSegmentBezelColor, 73 | action: { [weak self] va in self?.__setSelectedSegmentBezelColor(va) } 74 | ) 75 | } 76 | 77 | @objc func swizzled_setImage(_ image: NSImage?, forSegment segment: Int) { 78 | cache( 79 | firstParam: Callable.Appearanced(image), 80 | secondParam: Callable.Original(segment), 81 | identifier: .setImageForSegment, 82 | action: { [weak self] va, vb in self?.__setImage(va, forSegment: vb) } 83 | ) 84 | } 85 | } 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSSlider+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let trackFillColor = "NSSlider." 14 | } 15 | 16 | public extension NSSlider { 17 | var app_trackFillColor: NSColor? { 18 | get { trackFillColor } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | trackFillColor = newValue 22 | } else { 23 | swizzled_setTrackFillColor(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSSlider { 30 | static func silenceExchangeSliderImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: trackFillColor), 33 | newSelector: #selector(swizzled_setTrackFillColor(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension NSSlider { 39 | func __setTrackFillColor(_ trackFillColor: NSColor?) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setTrackFillColor(trackFillColor) 42 | } else { 43 | self.trackFillColor = trackFillColor 44 | } 45 | } 46 | 47 | 48 | @objc func swizzled_setTrackFillColor(_ trackFillColor: NSColor?) { 49 | cache( 50 | firstParam: Callable.Appearanced(trackFillColor), 51 | identifier: .trackFillColor, 52 | action: { [weak self] va in self?.__setTrackFillColor(va) } 53 | ) 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSTableRowView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColor = "NSTableRowView.__setBackgroundColor(_:)" 14 | } 15 | 16 | public extension NSTableRowView { 17 | var app_backgroundColor: NSColor { 18 | get { backgroundColor } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | backgroundColor = newValue 22 | } else { 23 | swizzled_setBackgroundColor(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSTableRowView { 30 | static func silenceExchangeTableRowViewImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: backgroundColor), 33 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension NSTableRowView { 39 | func __setBackgroundColor(_ backgroundColor: NSColor) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setBackgroundColor(backgroundColor) 42 | } else { 43 | self.backgroundColor = backgroundColor 44 | } 45 | } 46 | 47 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 48 | cache( 49 | firstParam: Callable.Appearanced(backgroundColor), 50 | identifier: .backgroundColor, 51 | action: { [weak self] va in self?.__setBackgroundColor(va) } 52 | ) 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSTableView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColor = "NSTableView.__setBackgroundColor(_:)" 14 | static let gridColor = "NSTableView.__setGridColor(_:)" 15 | static let setIndicatorImageInTableColumn = "NSTableView.__setIndicatorImage(_:in:)" 16 | } 17 | 18 | public extension NSTableView { 19 | var app_backgroundColor: NSColor { 20 | get { backgroundColor } 21 | set { 22 | if __USING_APPEARANCED_SWIZZLING__ { 23 | backgroundColor = newValue 24 | } else { 25 | swizzled_setBackgroundColor(newValue) 26 | } 27 | } 28 | } 29 | 30 | var app_gridColor: NSColor { 31 | get { gridColor } 32 | set { 33 | if __USING_APPEARANCED_SWIZZLING__ { 34 | gridColor = newValue 35 | } else { 36 | swizzled_setGridColor(newValue) 37 | } 38 | } 39 | } 40 | 41 | func app_setIndicatorImage(_ image: NSImage, in tableColumn: NSTableColumn) { 42 | if __USING_APPEARANCED_SWIZZLING__ { 43 | setIndicatorImage(image, in: tableColumn) 44 | } else { 45 | swizzled_setIndicatorImage(image, in: tableColumn) 46 | } 47 | } 48 | } 49 | 50 | internal extension NSTableView { 51 | static func silenceExchangeTableViewImplementation() { 52 | app_swizzling( 53 | originalSelector: #selector(setter: backgroundColor), 54 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 55 | ) 56 | 57 | app_swizzling( 58 | originalSelector: #selector(setter: gridColor), 59 | newSelector: #selector(swizzled_setGridColor(_:)) 60 | ) 61 | 62 | app_swizzling( 63 | originalSelector: #selector(setIndicatorImage(_:in:)), 64 | newSelector: #selector(swizzled_setIndicatorImage(_:in:)) 65 | ) 66 | } 67 | } 68 | 69 | private extension NSTableView { 70 | func __setBackgroundColor(_ backgroundColor: NSColor) { 71 | if __USING_APPEARANCED_SWIZZLING__ { 72 | swizzled_setBackgroundColor(backgroundColor) 73 | } else { 74 | self.backgroundColor = backgroundColor 75 | } 76 | } 77 | 78 | func __setGridColor(_ gridColor: NSColor) { 79 | if __USING_APPEARANCED_SWIZZLING__ { 80 | swizzled_setGridColor(gridColor) 81 | } else { 82 | self.gridColor = gridColor 83 | } 84 | } 85 | 86 | func __setIndicatorImage(_ image: NSImage, in tableColumn: NSTableColumn) { 87 | if __USING_APPEARANCED_SWIZZLING__ { 88 | swizzled_setIndicatorImage(image, in: tableColumn) 89 | } else { 90 | setIndicatorImage(image, in: tableColumn) 91 | } 92 | } 93 | 94 | 95 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 96 | cache( 97 | firstParam: Callable.Appearanced(backgroundColor), 98 | identifier: .backgroundColor, 99 | action: { [weak self] va in self?.__setBackgroundColor(va) } 100 | ) 101 | } 102 | 103 | @objc func swizzled_setGridColor(_ gridColor: NSColor) { 104 | cache( 105 | firstParam: Callable.Appearanced(gridColor), 106 | identifier: .gridColor, 107 | action: { [weak self] va in self?.__setGridColor(va) } 108 | ) 109 | } 110 | 111 | @objc func swizzled_setIndicatorImage(_ image: NSImage, in tableColumn: NSTableColumn) { 112 | cache( 113 | firstParam: Callable.Appearanced(image), 114 | secondParam: Callable.Original(tableColumn), 115 | identifier: .setIndicatorImageInTableColumn, 116 | action: { [weak self] va, vb in self?.__setIndicatorImage(va, in: vb) } 117 | ) 118 | } 119 | } 120 | 121 | #endif 122 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSText+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/2. 6 | // 7 | 8 | #if os(macOS) 9 | import Cocoa 10 | 11 | private extension AppearanceCallableIdentifier { 12 | static let textColor = "NSText.__text_setTextColor(_:)" 13 | static let backgroundColor = "NSText.__text_setBackgroundColor(_:)" 14 | static let textColorRange = "NSText.__setTextColor(_:range:)" 15 | } 16 | 17 | public extension NSText { 18 | var app_text_textColor: NSColor? { 19 | get { textColor } 20 | set { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | textColor = newValue 23 | } else { 24 | swizzled_text_setTextColor(newValue) 25 | } 26 | } 27 | } 28 | 29 | var app_text_backgroundColor: NSColor? { 30 | get { backgroundColor } 31 | set { 32 | if __USING_APPEARANCED_SWIZZLING__ { 33 | backgroundColor = newValue 34 | } else { 35 | swizzled_text_setBackgroundColor(newValue) 36 | } 37 | } 38 | } 39 | 40 | func app_setTextColor(_ color: NSColor?, range: NSRange) { 41 | if __USING_APPEARANCED_SWIZZLING__ { 42 | setTextColor(color, range: range) 43 | } else { 44 | swizzled_setTextColor(color, range: range) 45 | } 46 | } 47 | 48 | } 49 | 50 | internal extension NSText { 51 | static func silenceExchangeTextImplementation() { 52 | app_swizzling( 53 | originalSelector: #selector(setter: textColor), 54 | newSelector: #selector(swizzled_text_setTextColor(_:)) 55 | ) 56 | 57 | app_swizzling( 58 | originalSelector: #selector(setter: backgroundColor), 59 | newSelector: #selector(swizzled_text_setBackgroundColor(_:)) 60 | ) 61 | 62 | app_swizzling( 63 | originalSelector: #selector(setTextColor(_:range:)), 64 | newSelector: #selector(swizzled_setTextColor(_:range:)) 65 | ) 66 | } 67 | } 68 | 69 | private extension NSText { 70 | 71 | func __text_setTextColor(_ textColor: NSColor?) { 72 | if __USING_APPEARANCED_SWIZZLING__ { 73 | swizzled_text_setTextColor(textColor) 74 | } else { 75 | self.textColor = textColor 76 | } 77 | } 78 | 79 | func __text_setBackgroundColor(_ backgroundColor: NSColor?) { 80 | if __USING_APPEARANCED_SWIZZLING__ { 81 | swizzled_text_setBackgroundColor(backgroundColor) 82 | } else { 83 | self.backgroundColor = backgroundColor 84 | } 85 | } 86 | 87 | func __setTextColor(_ color: NSColor?, range: NSRange) { 88 | if __USING_APPEARANCED_SWIZZLING__ { 89 | swizzled_setTextColor(color, range: range) 90 | } else { 91 | setTextColor(color, range: range) 92 | } 93 | } 94 | 95 | 96 | @objc func swizzled_text_setTextColor(_ textColor: NSColor?) { 97 | cache( 98 | firstParam: Callable.Appearanced(textColor), 99 | identifier: .textColor, 100 | action: { [weak self] va in self?.__text_setTextColor(va) } 101 | ) 102 | } 103 | 104 | @objc func swizzled_text_setBackgroundColor(_ backgroundColor: NSColor?) { 105 | cache( 106 | firstParam: Callable.Appearanced(backgroundColor), 107 | identifier: .backgroundColor, 108 | action: { [weak self] va in self?.__text_setBackgroundColor(va) } 109 | ) 110 | } 111 | 112 | @objc func swizzled_setTextColor(_ color: NSColor?, range: NSRange) { 113 | cache( 114 | firstParam: Callable.Appearanced(color), 115 | secondParam: Callable.Original(range), 116 | identifier: .textColorRange, 117 | action: { [weak self] va, vb in self?.__setTextColor(va, range: vb) } 118 | ) 119 | } 120 | } 121 | 122 | #endif 123 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSTextField+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | 9 | #if os(macOS) 10 | 11 | import Cocoa 12 | 13 | private extension AppearanceCallableIdentifier { 14 | static let backgroundColor = "NSTextField.__setBackGroundColor(_:)" 15 | static let textColor = "NSTextField.__setTextColor(_:)" 16 | static let attributedStringValue = "NSTextField.__setAttributedStringValue(_:)" 17 | static let placeholderAttributedString = "NSTextField.__setPlaceholderAttributedString(_:)" 18 | } 19 | 20 | public extension NSTextField { 21 | var app_backgroundColor: NSColor? { 22 | get { backgroundColor } 23 | set { 24 | if __USING_APPEARANCED_SWIZZLING__ { 25 | backgroundColor = newValue 26 | } else { 27 | swizzled_setBackGroundColor(newValue) 28 | } 29 | } 30 | } 31 | 32 | var app_textColor: NSColor? { 33 | get { textColor } 34 | set { 35 | if __USING_APPEARANCED_SWIZZLING__ { 36 | textColor = newValue 37 | } else { 38 | swizzled_setTextColor(newValue) 39 | } 40 | } 41 | } 42 | 43 | var app_attributedStringValue: NSAttributedString { 44 | get { attributedStringValue } 45 | set { 46 | if __USING_APPEARANCED_SWIZZLING__ { 47 | attributedStringValue = newValue 48 | } else { 49 | swizzled_setAttributedStringValue(newValue) 50 | } 51 | } 52 | } 53 | 54 | var app_placeholderAttributedString: NSAttributedString? { 55 | get { placeholderAttributedString } 56 | set { 57 | if __USING_APPEARANCED_SWIZZLING__ { 58 | placeholderAttributedString = newValue 59 | } else { 60 | swizzled_setPlaceholderAttributedString(newValue) 61 | } 62 | } 63 | } 64 | } 65 | 66 | internal extension NSTextField { 67 | static func silenceExchangeTextFieldImplementation() { 68 | app_swizzling( 69 | originalSelector: #selector(setter: backgroundColor), 70 | newSelector: #selector(swizzled_setBackGroundColor(_:)) 71 | ) 72 | 73 | app_swizzling( 74 | originalSelector: #selector(setter: textColor), 75 | newSelector: #selector(swizzled_setTextColor(_:)) 76 | ) 77 | 78 | app_swizzling( 79 | originalSelector: #selector(setter: attributedStringValue), 80 | newSelector: #selector(swizzled_setAttributedStringValue(_:)) 81 | ) 82 | 83 | app_swizzling( 84 | originalSelector: #selector(setter: placeholderAttributedString), 85 | newSelector: #selector(swizzled_setPlaceholderAttributedString(_:)) 86 | ) 87 | } 88 | } 89 | 90 | private extension NSTextField { 91 | func __setBackGroundColor(_ backgroundColor: NSColor?) { 92 | if __USING_APPEARANCED_SWIZZLING__ { 93 | swizzled_setBackGroundColor(backgroundColor) 94 | } else { 95 | self.backgroundColor = backgroundColor 96 | } 97 | } 98 | 99 | func __setTextColor(_ textColor: NSColor?) { 100 | if __USING_APPEARANCED_SWIZZLING__ { 101 | swizzled_setTextColor(textColor) 102 | } else { 103 | self.textColor = textColor 104 | } 105 | } 106 | 107 | func __setAttributedStringValue(_ attributedStringValue: NSAttributedString) { 108 | if __USING_APPEARANCED_SWIZZLING__ { 109 | swizzled_setAttributedStringValue(attributedStringValue) 110 | } else { 111 | self.attributedStringValue = attributedStringValue 112 | } 113 | } 114 | 115 | func __setPlaceholderAttributedString(_ placeholderAttributedString: NSAttributedString?) { 116 | if __USING_APPEARANCED_SWIZZLING__ { 117 | swizzled_setPlaceholderAttributedString(placeholderAttributedString) 118 | } else { 119 | self.placeholderAttributedString = placeholderAttributedString 120 | } 121 | } 122 | 123 | @objc func swizzled_setBackGroundColor(_ backgroundColor: NSColor?) { 124 | cache( 125 | firstParam: Callable.Appearanced(backgroundColor), 126 | identifier: .backgroundColor, 127 | action: { [weak self] va in self?.__setBackGroundColor(va) } 128 | ) 129 | } 130 | 131 | @objc func swizzled_setTextColor(_ textColor: NSColor?) { 132 | cache( 133 | firstParam: Callable.Appearanced(textColor), 134 | identifier: .textColor, 135 | action: { [weak self] va in self?.__setTextColor(va) } 136 | ) 137 | } 138 | 139 | @objc func swizzled_setAttributedStringValue(_ attributedStringValue: NSAttributedString) { 140 | cache( 141 | firstParam: Callable.Attributed(attributedStringValue), 142 | identifier: .attributedStringValue, 143 | action: { [weak self] va in self?.__setAttributedStringValue(va) } 144 | ) 145 | } 146 | 147 | @objc func swizzled_setPlaceholderAttributedString(_ placeholderAttributedString: NSAttributedString?) { 148 | cache( 149 | firstParam: Callable.Attributed(placeholderAttributedString), 150 | identifier: .placeholderAttributedString, 151 | action: { [weak self] va in self?.__setPlaceholderAttributedString(va) } 152 | ) 153 | } 154 | } 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSTextView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | 9 | #if os(macOS) 10 | 11 | import Cocoa 12 | 13 | private extension AppearanceCallableIdentifier { 14 | static let insertionPointColor = "NSTextView.__setInsertionPointColor(_:)" 15 | static let backgroundColor = "NSTextView.__setBackgroundColor(_:)" 16 | static let textColor = "NSTextView.__setTextColor(_:)" 17 | } 18 | 19 | public extension NSTextView { 20 | var app_insertionPointColor: NSColor { 21 | get { insertionPointColor } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | insertionPointColor = newValue 25 | } else { 26 | swizzled_setInsertionPointColor(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_backgroundColor: NSColor { 32 | get { backgroundColor } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | backgroundColor = newValue 36 | } else { 37 | swizzled_setBackgroundColor(newValue) 38 | } 39 | } 40 | } 41 | 42 | var app_textColor: NSColor? { 43 | get { textColor } 44 | set { 45 | if __USING_APPEARANCED_SWIZZLING__ { 46 | textColor = newValue 47 | } else { 48 | swizzled_setTextColor(newValue) 49 | } 50 | } 51 | } 52 | } 53 | 54 | internal extension NSTextView { 55 | static func silenceExchangeTextViewImplementation() { 56 | 57 | app_swizzling( 58 | originalSelector: #selector(setter: insertionPointColor), 59 | newSelector: #selector(swizzled_setInsertionPointColor(_:)) 60 | ) 61 | 62 | app_swizzling( 63 | originalSelector: #selector(setter: backgroundColor), 64 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 65 | ) 66 | 67 | app_swizzling( 68 | originalSelector: #selector(setter: textColor), 69 | newSelector: #selector(swizzled_setTextColor(_:)) 70 | ) 71 | 72 | } 73 | } 74 | 75 | private extension NSTextView { 76 | func __setInsertionPointColor(_ insertionPointColor: NSColor) { 77 | if __USING_APPEARANCED_SWIZZLING__ { 78 | swizzled_setInsertionPointColor(insertionPointColor) 79 | } else { 80 | self.insertionPointColor = insertionPointColor 81 | } 82 | } 83 | 84 | func __setBackgroundColor(_ backgroundColor: NSColor) { 85 | if __USING_APPEARANCED_SWIZZLING__ { 86 | swizzled_setBackgroundColor(backgroundColor) 87 | } else { 88 | self.backgroundColor = backgroundColor 89 | } 90 | } 91 | 92 | func __setTextColor(_ textColor: NSColor?) { 93 | if __USING_APPEARANCED_SWIZZLING__ { 94 | swizzled_setTextColor(textColor) 95 | } else { 96 | self.textColor = textColor 97 | } 98 | } 99 | 100 | @objc func swizzled_setInsertionPointColor(_ insertionPointColor: NSColor) { 101 | cache( 102 | firstParam: Callable.Appearanced(insertionPointColor), 103 | identifier: .insertionPointColor, 104 | action: { [weak self] va in self?.__setInsertionPointColor(va) } 105 | ) 106 | } 107 | 108 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 109 | cache( 110 | firstParam: Callable.Appearanced(backgroundColor), 111 | identifier: .backgroundColor, 112 | action: { [weak self] va in self?.__setBackgroundColor(va) } 113 | ) 114 | } 115 | 116 | @objc func swizzled_setTextColor(_ textColor: NSColor?) { 117 | cache( 118 | firstParam: Callable.Appearanced(textColor), 119 | identifier: .textColor, 120 | action: { [weak self] va in self?.__setTextColor(va) } 121 | ) 122 | } 123 | } 124 | 125 | #endif 126 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let layerBackgroundColor = "NSView.__setLayerBackgroundColor(_:)" 14 | static let layerBorderColor = "NSView.__setLayerBorderColor(_:)" 15 | } 16 | 17 | public extension NSView { 18 | var app_layerBackgroundColor: NSColor? { 19 | set { 20 | cache( 21 | firstParam: Callable.Appearanced(newValue), 22 | identifier: .layerBackgroundColor, 23 | action: { [weak self] va in self?.__setLayerBackgroundColor(va) } 24 | ) 25 | } 26 | get { 27 | guard let cgColor = layer?.backgroundColor else { return nil } 28 | return NSColor(cgColor: cgColor) 29 | } 30 | } 31 | 32 | var app_layerBorderColor: NSColor? { 33 | set { 34 | cache( 35 | firstParam: Callable.Appearanced(newValue), 36 | identifier: .layerBorderColor, 37 | action: { [weak self] va in self?.__setLayerBorderColor(va) } 38 | ) 39 | } 40 | get { 41 | if let cgColor = layer?.borderColor { 42 | return NSColor(cgColor: cgColor) 43 | } 44 | 45 | return nil 46 | } 47 | } 48 | } 49 | 50 | private extension NSView { 51 | func __setLayerBackgroundColor(_ layerBackgroundColor: NSColor?) { 52 | if !self.wantsLayer { 53 | self.wantsLayer = true 54 | } 55 | 56 | self.layer?.backgroundColor = layerBackgroundColor?.cgColor 57 | } 58 | 59 | func __setLayerBorderColor(_ layerBorderColor: NSColor?) { 60 | if !self.wantsLayer { 61 | self.wantsLayer = true 62 | } 63 | 64 | self.layer?.borderColor = layerBorderColor?.cgColor 65 | } 66 | } 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/APPKit/NSWindow+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import Cocoa 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColor = "NSWindow.__setBackgroundColor(_:)" 14 | } 15 | 16 | public extension NSWindow { 17 | var app_backgroundColor: NSColor { 18 | get { backgroundColor } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | backgroundColor = newValue 22 | } else { 23 | swizzled_setBackgroundColor(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension NSWindow { 30 | static func silenceExchangeWindowImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: backgroundColor), 33 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension NSWindow { 39 | func __setBackgroundColor(_ backgroundColor: NSColor) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setBackgroundColor(backgroundColor) 42 | } else { 43 | self.backgroundColor = backgroundColor 44 | } 45 | } 46 | 47 | @objc func swizzled_setBackgroundColor(_ backgroundColor: NSColor) { 48 | cache( 49 | firstParam: Callable.Appearanced(backgroundColor), 50 | identifier: .backgroundColor, 51 | action: { [weak self] va in self?.__setBackgroundColor(va) } 52 | ) 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/Implementation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | #if os(macOS) 9 | import Cocoa 10 | #elseif canImport(UIKit) 11 | import UIKit 12 | #endif 13 | 14 | /// 内部调用 15 | internal extension AppearanceManager { 16 | static func exchangeImplementations() { 17 | #if os(macOS) 18 | NSBox.silenceExchangeBoxImplementation() 19 | NSButton.silenceExchangeButtonImplementation() 20 | NSDatePicker.silenceExchangeDatePickerViewImplementation() 21 | NSImageView.silenceExchangeImageViewImplementation() 22 | NSScrollView.silenceExchangeScrollViewImplementation() 23 | NSSlider.silenceExchangeSliderImplementation() 24 | NSTableView.silenceExchangeTableViewImplementation() 25 | NSText.silenceExchangeTextImplementation() 26 | NSTextField.silenceExchangeTextFieldImplementation() 27 | NSTextView.silenceExchangeTextViewImplementation() 28 | /// NSView 29 | NSWindow.silenceExchangeWindowImplementation() 30 | NSClipView.silenceExchangeClipViewImplementation() 31 | NSPathControl.silenceExchangePathControlImplementation() 32 | NSPathControlItem.silenceExchangePathControlItemImplementation() 33 | NSTableRowView.silenceExchangeTableRowViewImplementation() 34 | NSSegmentedControl.silenceExchangeSegmentedControlImplementation() 35 | NSLevelIndicator.silenceExchangeLevelIndicatorImplementation() 36 | NSCollectionView.silenceExchangeCollectionViewImplementation() 37 | 38 | if #available(macOS 10.15, *) { 39 | NSButtonTouchBarItem.silenceExchangeButtonTouchBarItemImplementation() 40 | } 41 | #elseif canImport(UIKit) 42 | UIButton.silenceExchangeButtonImplementation() 43 | UILabel.silenceExchangeLabelImplementation() 44 | UIView.silenceExchangeViewImplementation() 45 | UIActivityIndicatorView.silenceExchangeActivityIndicatorViewImplementation() 46 | UIBarItem.silenceExchangeBarItemImplementation() 47 | UIImageView.silenceExchangeImageViewImplementation() 48 | UINavigationBar.silenceExchangeNavigationBarImplementation() 49 | UIPageControl.silenceExchangePageControlImplementation() 50 | UIProgressView.silenceExchangeProgressViewImplementation() 51 | UIRefreshControl.silenceExchangeRefreshControlImplementation() 52 | UISearchBar.silenceExchangeSearchBarImplementation() 53 | UISegmentedControl.silenceExchangeSegmentedControlImplementation() 54 | UISlider.silenceExchangeSliderImplementation() 55 | UIStepper.silenceExchangeStepperImplementation() 56 | UISwitch.silenceExchangeSwitchImplementation() 57 | UITabBar.silenceExchangeTabBarImplementation() 58 | UITableView.silenceExchangeTableViewImplementation() 59 | UITextField.silenceExchangeTextFieldImplementation() 60 | UITextView.silenceExchangeTextViewImplementation() 61 | UIToolbar.silenceExchangeToolbarImplementation() 62 | 63 | if #available(iOS 13.0, *) { 64 | UISearchTextField.silenceExchangeSearchTextFieldImplementation() 65 | } 66 | #endif 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIActivityIndicatorView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let color = "UIActivityIndicatorView.__setColor(_:)" 14 | } 15 | 16 | public extension UIActivityIndicatorView { 17 | var app_color: UIColor { 18 | get { color } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | color = newValue 22 | } else { 23 | swizzled_setColor(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension UIActivityIndicatorView { 30 | static func silenceExchangeActivityIndicatorViewImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: color), 33 | newSelector: #selector(swizzled_setColor(_:)) 34 | ) 35 | } 36 | } 37 | 38 | private extension UIActivityIndicatorView { 39 | func __setColor(_ color: UIColor) { 40 | if __USING_APPEARANCED_SWIZZLING__ { 41 | swizzled_setColor(color) 42 | } else { 43 | self.color = color 44 | } 45 | } 46 | 47 | @objc func swizzled_setColor(_ color: UIColor) { 48 | cache( 49 | firstParam: Callable.Appearanced(color), 50 | identifier: .color, 51 | action: { [weak self] va in self?.__setColor(va) } 52 | ) 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIBarItem+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/10. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let image = "UIBarItem.__setImage(_:)" 14 | static let landscapeImagePhone = "UIBarItem.__setLandscapeImagePhone(_:)" 15 | static let largeContentSizeImage = "UIBarItem.__setLargeContentSizeImage(_:)" 16 | } 17 | 18 | public extension UIBarItem { 19 | var app_image: UIImage? { 20 | get { image } 21 | set { 22 | if __USING_APPEARANCED_SWIZZLING__ { 23 | self.image = newValue 24 | } else { 25 | swizzled_setImage(newValue) 26 | } 27 | } 28 | } 29 | 30 | var app_landscapeImagePhone: UIImage? { 31 | get { landscapeImagePhone } 32 | set { 33 | if __USING_APPEARANCED_SWIZZLING__ { 34 | self.landscapeImagePhone = newValue 35 | } else { 36 | swizzled_setLandscapeImagePhone(newValue) 37 | } 38 | } 39 | } 40 | 41 | var app_largeContentSizeImage: UIImage? { 42 | get { largeContentSizeImage } 43 | set { 44 | if __USING_APPEARANCED_SWIZZLING__ { 45 | self.largeContentSizeImage = newValue 46 | } else { 47 | swizzled_setLargeContentSizeImage(newValue) 48 | } 49 | } 50 | } 51 | } 52 | 53 | internal extension UIBarItem { 54 | static func silenceExchangeBarItemImplementation() { 55 | app_swizzling( 56 | originalSelector: #selector(setter: image), 57 | newSelector: #selector(swizzled_setImage(_:)) 58 | ) 59 | 60 | app_swizzling( 61 | originalSelector: #selector(setter: landscapeImagePhone), 62 | newSelector: #selector(swizzled_setLandscapeImagePhone(_:)) 63 | ) 64 | 65 | app_swizzling( 66 | originalSelector: #selector(setter: largeContentSizeImage), 67 | newSelector: #selector(swizzled_setLargeContentSizeImage(_:)) 68 | ) 69 | } 70 | } 71 | 72 | private extension UIBarItem { 73 | func __setImage(_ image: UIImage?) { 74 | if __USING_APPEARANCED_SWIZZLING__ { 75 | swizzled_setImage(image) 76 | } else { 77 | self.image = image 78 | } 79 | } 80 | 81 | func __setLandscapeImagePhone(_ landscapeImagePhone: UIImage?) { 82 | if __USING_APPEARANCED_SWIZZLING__ { 83 | swizzled_setLandscapeImagePhone(landscapeImagePhone) 84 | } else { 85 | self.landscapeImagePhone = landscapeImagePhone 86 | } 87 | } 88 | 89 | func __setLargeContentSizeImage(_ largeContentSizeImage: UIImage?) { 90 | if __USING_APPEARANCED_SWIZZLING__ { 91 | swizzled_setLargeContentSizeImage(largeContentSizeImage) 92 | } else { 93 | self.largeContentSizeImage = largeContentSizeImage 94 | } 95 | } 96 | 97 | @objc func swizzled_setImage(_ image: UIImage?) { 98 | cache( 99 | firstParam: Callable.Appearanced(image), 100 | identifier: .image, 101 | action: { [weak self] va in self?.__setImage(va) } 102 | ) 103 | } 104 | 105 | @objc func swizzled_setLandscapeImagePhone(_ landscapeImagePhone: UIImage?) { 106 | cache( 107 | firstParam: Callable.Appearanced(landscapeImagePhone), 108 | identifier: .landscapeImagePhone, 109 | action: { [weak self] va in self?.__setLandscapeImagePhone(va) } 110 | ) 111 | } 112 | 113 | @objc func swizzled_setLargeContentSizeImage(_ largeContentSizeImage: UIImage?) { 114 | cache( 115 | firstParam: Callable.Appearanced(largeContentSizeImage), 116 | identifier: .largeContentSizeImage, 117 | action: { [weak self] va in self?.__setLargeContentSizeImage(va) } 118 | ) 119 | } 120 | } 121 | 122 | #endif 123 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIButton+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if canImport(UIKit) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let setTitleColorForState = "UIButton.__setTitleColor(_:for:)" 14 | static let setImageForState = "UIButton.__setImage(_:for:)" 15 | static let setBackgroundImageForState = "UIButton.__setBackgroundImage(_:for:)" 16 | static let setAttributedTitleForState = "UIButton.__setAttributedTitle(_:for:)" 17 | } 18 | 19 | public extension UIButton { 20 | func app_setTitleColor(_ titleColor: UIColor?, for state: State) { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | setTitleColor(titleColor, for: state) 23 | } else { 24 | swizzled_setTitleColor(titleColor, for: state) 25 | } 26 | } 27 | 28 | func app_setImage(_ image: UIImage?, for state: State) { 29 | if __USING_APPEARANCED_SWIZZLING__ { 30 | setImage(image, for: state) 31 | } else { 32 | swizzled_setImage(image, for: state) 33 | } 34 | } 35 | 36 | func app_setBackgroundImage(_ backgroundImage: UIImage?, for state: State) { 37 | if __USING_APPEARANCED_SWIZZLING__ { 38 | setBackgroundImage(backgroundImage, for: state) 39 | } 40 | } 41 | 42 | func app_setAttributedTitle(_ attributedTitle: NSAttributedString?, for state: State) { 43 | if __USING_APPEARANCED_SWIZZLING__ { 44 | setAttributedTitle(attributedTitle, for: state) 45 | } else { 46 | swizzled_setAttributedTitle(attributedTitle, for: state) 47 | } 48 | } 49 | } 50 | 51 | internal extension UIButton { 52 | static func silenceExchangeButtonImplementation() { 53 | app_swizzling( 54 | originalSelector: #selector(setTitleColor(_:for:)), 55 | newSelector: #selector(swizzled_setTitleColor(_:for:)) 56 | ) 57 | 58 | app_swizzling( 59 | originalSelector: #selector(setImage(_:for:)), 60 | newSelector: #selector(swizzled_setImage(_:for:)) 61 | ) 62 | 63 | app_swizzling( 64 | originalSelector: #selector(setBackgroundImage(_:for:)), 65 | newSelector: #selector(swizzled_setBackgroundImage(_:for:)) 66 | ) 67 | 68 | app_swizzling( 69 | originalSelector: #selector(setAttributedTitle(_:for:)), 70 | newSelector: #selector(swizzled_setAttributedTitle(_:for:)) 71 | ) 72 | } 73 | } 74 | 75 | private extension UIButton { 76 | func __setTitleColor(_ titleColor: UIColor?, for state: State) { 77 | if __USING_APPEARANCED_SWIZZLING__ { 78 | swizzled_setTitleColor(titleColor, for: state) 79 | } else { 80 | setTitleColor(titleColor, for: state) 81 | } 82 | } 83 | 84 | func __setImage(_ image: UIImage?, for state: State) { 85 | if __USING_APPEARANCED_SWIZZLING__ { 86 | swizzled_setImage(image, for: state) 87 | } else { 88 | setImage(image, for: state) 89 | } 90 | } 91 | 92 | func __setBackgroundImage(_ backgroundImage: UIImage?, for state: State) { 93 | if __USING_APPEARANCED_SWIZZLING__ { 94 | swizzled_setBackgroundImage(backgroundImage, for: state) 95 | } else { 96 | setBackgroundImage(backgroundImage, for: state) 97 | } 98 | } 99 | 100 | func __setAttributedTitle(_ attributedTitle: NSAttributedString?, for state: State) { 101 | if __USING_APPEARANCED_SWIZZLING__ { 102 | swizzled_setAttributedTitle(attributedTitle, for: state) 103 | } else { 104 | setAttributedTitle(attributedTitle, for: state) 105 | } 106 | } 107 | 108 | @objc func swizzled_setTitleColor(_ titleColor: UIColor?, for state: State) { 109 | cache( 110 | firstParam: Callable.Appearanced(titleColor), 111 | secondParam: Callable.Original(state), 112 | identifier: .setTitleColorForState, 113 | action: { [weak self] va, vb in self?.__setTitleColor(va, for: vb) }, 114 | category: "UIButton.state:\(state.rawValue)" 115 | ) 116 | } 117 | 118 | @objc func swizzled_setImage(_ image: UIImage?, for state: State) { 119 | cache( 120 | firstParam: Callable.Appearanced(image), 121 | secondParam: Callable.Original(state), 122 | identifier: .setImageForState, 123 | action: { [weak self] va, vb in self?.__setImage(va, for: vb) }, 124 | category: "UIButton.state:\(state.rawValue)" 125 | ) 126 | } 127 | 128 | @objc func swizzled_setBackgroundImage(_ backgroundImage: UIImage?, for state: State) { 129 | cache( 130 | firstParam: Callable.Appearanced(backgroundImage), 131 | secondParam: Callable.Original(state), 132 | identifier: .setBackgroundImageForState, 133 | action: { [weak self] va, vb in self?.__setBackgroundImage(va, for: vb) }, 134 | category: "UIButton.state:\(state.rawValue)" 135 | ) 136 | } 137 | 138 | @objc func swizzled_setAttributedTitle(_ attributedTitle: NSAttributedString?, for state: State) { 139 | cache( 140 | firstParam: Callable.Attributed(attributedTitle), 141 | secondParam: Callable.Original(state), 142 | identifier: .setAttributedTitleForState, 143 | action: { [weak self] va, vb in self?.__setAttributedTitle(va, for: vb) }, 144 | category: "UIButton.state:\(state.rawValue)" 145 | ) 146 | } 147 | } 148 | 149 | #endif 150 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UILabel+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if canImport(UIKit) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let textColor = "UILabel.__setTextColor(_:)" 14 | static let shadowColor = "UILabel.__setShadowColor(_:)" 15 | static let attributedText = "UILabel.__setAttributedText(_:)" 16 | static let highlightedTextColor = "UILabel.__setHighlightedTextColor(_:)" 17 | } 18 | 19 | public extension UILabel { 20 | var app_textColor: UIColor { 21 | get { textColor } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | self.textColor = newValue 25 | } else { 26 | swizzled_setTextColor(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_shadowColor: UIColor? { 32 | get { shadowColor } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | self.shadowColor = newValue 36 | } else { 37 | swizzled_setShadowColor(newValue) 38 | } 39 | } 40 | } 41 | 42 | var app_attributedText: NSAttributedString? { 43 | get { attributedText } 44 | set { 45 | if __USING_APPEARANCED_SWIZZLING__ { 46 | self.attributedText = newValue 47 | } else { 48 | swizzled_setAttributedText(newValue) 49 | } 50 | } 51 | } 52 | 53 | var app_highlightedTextColor: UIColor? { 54 | get { highlightedTextColor } 55 | set { 56 | if __USING_APPEARANCED_SWIZZLING__ { 57 | self.highlightedTextColor = newValue 58 | } else { 59 | swizzled_setHighlightedTextColor(newValue) 60 | } 61 | } 62 | } 63 | } 64 | 65 | internal extension UILabel { 66 | static func silenceExchangeLabelImplementation() { 67 | app_swizzling( 68 | originalSelector: #selector(setter: textColor), 69 | newSelector: #selector(swizzled_setTextColor(_:)) 70 | ) 71 | 72 | app_swizzling( 73 | originalSelector: #selector(setter: shadowColor), 74 | newSelector: #selector(swizzled_setShadowColor(_:)) 75 | ) 76 | 77 | app_swizzling( 78 | originalSelector: #selector(setter: attributedText), 79 | newSelector: #selector(swizzled_setAttributedText(_:)) 80 | ) 81 | 82 | app_swizzling( 83 | originalSelector: #selector(setter: highlightedTextColor), 84 | newSelector: #selector(swizzled_setHighlightedTextColor(_:)) 85 | ) 86 | } 87 | } 88 | 89 | private extension UILabel { 90 | func __setTextColor(_ textColor: UIColor) { 91 | if __USING_APPEARANCED_SWIZZLING__ { 92 | swizzled_setTextColor(textColor) 93 | } else { 94 | self.textColor = textColor 95 | } 96 | } 97 | 98 | func __setShadowColor(_ shadowColor: UIColor?) { 99 | if __USING_APPEARANCED_SWIZZLING__ { 100 | swizzled_setShadowColor(shadowColor) 101 | } else { 102 | self.shadowColor = shadowColor 103 | } 104 | } 105 | 106 | func __setAttributedText(_ attributedText: NSAttributedString?) { 107 | if __USING_APPEARANCED_SWIZZLING__ { 108 | swizzled_setAttributedText(attributedText) 109 | } else { 110 | self.attributedText = attributedText 111 | } 112 | } 113 | 114 | func __setHighlightedTextColor(_ highlightedTextColor: UIColor?) { 115 | if __USING_APPEARANCED_SWIZZLING__ { 116 | swizzled_setHighlightedTextColor(highlightedTextColor) 117 | } else { 118 | self.highlightedTextColor = highlightedTextColor 119 | } 120 | } 121 | 122 | @objc func swizzled_setTextColor(_ textColor: UIColor) { 123 | cache( 124 | firstParam: Callable.Appearanced(textColor), 125 | identifier: .textColor, 126 | action: { [weak self] va in self?.__setTextColor(va) } 127 | ) 128 | } 129 | 130 | @objc func swizzled_setShadowColor(_ shadowColor: UIColor?) { 131 | cache( 132 | firstParam: Callable.Appearanced(shadowColor), 133 | identifier: .shadowColor, 134 | action: { [weak self] va in self?.__setShadowColor(va) } 135 | ) 136 | } 137 | 138 | @objc func swizzled_setAttributedText(_ attributedText: NSAttributedString?) { 139 | cache( 140 | firstParam: Callable.Appearanced(attributedText), 141 | identifier: .attributedText, 142 | action: { [weak self] va in self?.__setAttributedText(va) } 143 | ) 144 | } 145 | 146 | @objc func swizzled_setHighlightedTextColor(_ highlightedTextColor: UIColor?) { 147 | cache( 148 | firstParam: Callable.Appearanced(highlightedTextColor), 149 | identifier: .highlightedTextColor, 150 | action: { [weak self] va in self?.__setHighlightedTextColor(va) } 151 | ) 152 | } 153 | } 154 | 155 | #endif 156 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIProgressView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let progressTintColor = "UIProgressView.__setProgressTintColor(_:)" 14 | static let trackTintColor = "UIProgressView.__setTrackTintColor(_:)" 15 | static let progressImage = "UIProgressView.__setProgressImage(_:)" 16 | static let trackImage = "UIProgressView.__setTrackImage(_:)" 17 | } 18 | 19 | public extension UIProgressView { 20 | var app_progressTintColor: UIColor? { 21 | get { progressTintColor } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | self.progressTintColor = newValue 25 | } else { 26 | swizzled_setProgressTintColor(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_trackTintColor: UIColor? { 32 | get { trackTintColor } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | self.trackTintColor = newValue 36 | } else { 37 | swizzled_setTrackTintColor(newValue) 38 | } 39 | } 40 | } 41 | 42 | var app_progressImage: UIImage? { 43 | get { progressImage } 44 | set { 45 | if __USING_APPEARANCED_SWIZZLING__ { 46 | self.progressImage = newValue 47 | } else { 48 | swizzled_setProgressImage(newValue) 49 | } 50 | } 51 | } 52 | 53 | var app_trackImage: UIImage? { 54 | get { trackImage } 55 | set { 56 | if __USING_APPEARANCED_SWIZZLING__ { 57 | self.trackImage = newValue 58 | } else { 59 | swizzled_setTrackImage(newValue) 60 | } 61 | } 62 | } 63 | } 64 | 65 | internal extension UIProgressView { 66 | static func silenceExchangeProgressViewImplementation() { 67 | app_swizzling( 68 | originalSelector: #selector(setter: progressTintColor), 69 | newSelector: #selector(swizzled_setProgressTintColor(_:)) 70 | ) 71 | 72 | app_swizzling( 73 | originalSelector: #selector(setter: trackTintColor), 74 | newSelector: #selector(swizzled_setTrackTintColor(_:)) 75 | ) 76 | 77 | app_swizzling( 78 | originalSelector: #selector(setter: progressImage), 79 | newSelector: #selector(swizzled_setProgressImage(_:)) 80 | ) 81 | 82 | app_swizzling( 83 | originalSelector: #selector(setter: trackImage), 84 | newSelector: #selector(swizzled_setTrackImage(_:)) 85 | ) 86 | } 87 | } 88 | 89 | private extension UIProgressView { 90 | func __setProgressTintColor(_ progressTintColor: UIColor?) { 91 | if __USING_APPEARANCED_SWIZZLING__ { 92 | swizzled_setProgressTintColor(progressTintColor) 93 | } else { 94 | self.progressTintColor = progressTintColor 95 | } 96 | } 97 | 98 | func __setTrackTintColor(_ trackTintColor: UIColor?) { 99 | if __USING_APPEARANCED_SWIZZLING__ { 100 | swizzled_setTrackTintColor(trackTintColor) 101 | } else { 102 | self.trackTintColor = trackTintColor 103 | } 104 | } 105 | 106 | func __setProgressImage(_ progressImage: UIImage?) { 107 | if __USING_APPEARANCED_SWIZZLING__ { 108 | swizzled_setProgressImage(progressImage) 109 | } else { 110 | self.progressImage = progressImage 111 | } 112 | } 113 | 114 | func __setTrackImage(_ trackImage: UIImage?) { 115 | if __USING_APPEARANCED_SWIZZLING__ { 116 | swizzled_setTrackImage(trackImage) 117 | } else { 118 | self.trackImage = trackImage 119 | } 120 | } 121 | 122 | 123 | @objc func swizzled_setProgressTintColor(_ progressTintColor: UIColor?) { 124 | cache( 125 | firstParam: Callable.Appearanced(progressTintColor), 126 | identifier: .progressTintColor, 127 | action: { [weak self] va in self?.__setProgressTintColor(va) } 128 | ) 129 | } 130 | 131 | @objc func swizzled_setTrackTintColor(_ trackTintColor: UIColor?) { 132 | cache( 133 | firstParam: Callable.Appearanced(trackTintColor), 134 | identifier: .trackTintColor, 135 | action: { [weak self] va in self?.__setTrackTintColor(va) } 136 | ) 137 | } 138 | 139 | @objc func swizzled_setProgressImage(_ progressImage: UIImage?) { 140 | cache( 141 | firstParam: Callable.Appearanced(progressImage), 142 | identifier: .progressImage, 143 | action: { [weak self] va in self?.__setProgressImage(va) } 144 | ) 145 | } 146 | 147 | @objc func swizzled_setTrackImage(_ trackImage: UIImage?) { 148 | cache( 149 | firstParam: Callable.Appearanced(trackImage), 150 | identifier: .trackImage, 151 | action: { [weak self] va in self?.__setTrackImage(va) } 152 | ) 153 | } 154 | } 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIRefreshControl+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let attributedTitle = "UIRefreshControl.__setAttributedTitle(_:)" 14 | } 15 | 16 | public extension UIRefreshControl { 17 | var app_attributedTitle: NSAttributedString? { 18 | get { attributedTitle } 19 | set { 20 | if __USING_APPEARANCED_SWIZZLING__ { 21 | self.attributedTitle = newValue 22 | } else { 23 | swizzled_setAttributedTitle(newValue) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension UIRefreshControl { 30 | static func silenceExchangeRefreshControlImplementation() { 31 | app_swizzling( 32 | originalSelector: #selector(setter: attributedTitle), 33 | newSelector: #selector(swizzled_setAttributedTitle(_:)) 34 | ) 35 | } 36 | } 37 | 38 | 39 | private extension UIRefreshControl { 40 | func __setAttributedTitle(_ attributedTitle: NSAttributedString?) { 41 | if __USING_APPEARANCED_SWIZZLING__ { 42 | swizzled_setAttributedTitle(attributedTitle) 43 | } else { 44 | self.attributedTitle = attributedTitle 45 | } 46 | } 47 | 48 | @objc func swizzled_setAttributedTitle(_ attributedTitle: NSAttributedString?) { 49 | cache( 50 | firstParam: Callable.Attributed(attributedTitle), 51 | identifier: .attributedTitle, 52 | action: { [weak self] va in self?.__setAttributedTitle(va) } 53 | ) 54 | } 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UISearchTextField+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let tokenBackgroundColor = "UISearchTextField.__setTokenBackgroundColor(_:)" 14 | } 15 | 16 | @available(iOS 13.0, *) 17 | public extension UISearchTextField { 18 | var app_tokenBackgroundColor: UIColor { 19 | get { tokenBackgroundColor } 20 | set { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | self.tokenBackgroundColor = newValue 23 | } else { 24 | swizzled_setTokenBackgroundColor(newValue) 25 | } 26 | } 27 | } 28 | } 29 | 30 | @available(iOS 13.0, *) 31 | internal extension UISearchTextField { 32 | static func silenceExchangeSearchTextFieldImplementation() { 33 | app_swizzling( 34 | originalSelector: #selector(setter: tokenBackgroundColor), 35 | newSelector: #selector(swizzled_setTokenBackgroundColor(_:)) 36 | ) 37 | } 38 | } 39 | 40 | @available(iOS 13.0, *) 41 | private extension UISearchTextField { 42 | func __setTokenBackgroundColor(_ tokenBackgroundColor: UIColor) { 43 | if __USING_APPEARANCED_SWIZZLING__ { 44 | swizzled_setTokenBackgroundColor(tokenBackgroundColor) 45 | } else { 46 | self.tokenBackgroundColor = tokenBackgroundColor 47 | } 48 | } 49 | 50 | @objc func swizzled_setTokenBackgroundColor(_ tokenBackgroundColor: UIColor) { 51 | cache( 52 | firstParam: Callable.Appearanced(tokenBackgroundColor), 53 | identifier: .tokenBackgroundColor, 54 | action: { [weak self] va in self?.__setTokenBackgroundColor(va) } 55 | ) 56 | } 57 | } 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UISwitch+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let onTintColor = "UISwitch.__setOnTintColor(_:)" 14 | static let thumbTintColor = "UISwitch.__setThumbTintColor(_:)" 15 | static let onImage = "UISwitch.__setOnImage(_:)" 16 | static let offImage = "UISwitch.__setOffImage(_:)" 17 | } 18 | 19 | public extension UISwitch { 20 | var app_onTintColor: UIColor? { 21 | get { onTintColor } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | self.onTintColor = newValue 25 | } else { 26 | swizzled_setOnTintColor(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_thumbTintColor: UIColor? { 32 | get { thumbTintColor } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | self.thumbTintColor = newValue 36 | } else { 37 | swizzled_setThumbTintColor(newValue) 38 | } 39 | } 40 | } 41 | 42 | var app_onImage: UIImage? { 43 | get { onImage } 44 | set { 45 | if __USING_APPEARANCED_SWIZZLING__ { 46 | self.onImage = newValue 47 | } else { 48 | swizzled_setOnImage(newValue) 49 | } 50 | } 51 | } 52 | 53 | var app_offImage: UIImage? { 54 | get { offImage } 55 | set { 56 | if __USING_APPEARANCED_SWIZZLING__ { 57 | self.offImage = newValue 58 | } else { 59 | swizzled_setOffImage(newValue) 60 | } 61 | } 62 | } 63 | } 64 | 65 | internal extension UISwitch { 66 | static func silenceExchangeSwitchImplementation() { 67 | app_swizzling( 68 | originalSelector: #selector(setter: onTintColor), 69 | newSelector: #selector(swizzled_setOnTintColor(_:)) 70 | ) 71 | app_swizzling( 72 | originalSelector: #selector(setter: thumbTintColor), 73 | newSelector: #selector(swizzled_setThumbTintColor(_:)) 74 | ) 75 | app_swizzling( 76 | originalSelector: #selector(setter: onImage), 77 | newSelector: #selector(swizzled_setOnImage(_:)) 78 | ) 79 | app_swizzling( 80 | originalSelector: #selector(setter: offImage), 81 | newSelector: #selector(swizzled_setOffImage(_:)) 82 | ) 83 | } 84 | } 85 | 86 | private extension UISwitch { 87 | func __setOnTintColor(_ onTintColor: UIColor?) { 88 | if __USING_APPEARANCED_SWIZZLING__ { 89 | swizzled_setOnTintColor(onTintColor) 90 | } else { 91 | self.onTintColor = onTintColor 92 | } 93 | } 94 | 95 | func __setThumbTintColor(_ thumbTintColor: UIColor?) { 96 | if __USING_APPEARANCED_SWIZZLING__ { 97 | swizzled_setThumbTintColor(thumbTintColor) 98 | } else { 99 | self.thumbTintColor = thumbTintColor 100 | } 101 | } 102 | 103 | func __setOnImage(_ onImage: UIImage?) { 104 | if __USING_APPEARANCED_SWIZZLING__ { 105 | swizzled_setOnImage(onImage) 106 | } else { 107 | self.onImage = onImage 108 | } 109 | } 110 | 111 | func __setOffImage(_ offImage: UIImage?) { 112 | if __USING_APPEARANCED_SWIZZLING__ { 113 | swizzled_setOffImage(offImage) 114 | } else { 115 | self.offImage = offImage 116 | } 117 | } 118 | 119 | 120 | @objc func swizzled_setOnTintColor(_ onTintColor: UIColor?) { 121 | cache( 122 | firstParam: Callable.Appearanced(onTintColor), 123 | identifier: .onTintColor, 124 | action: { [weak self] va in self?.__setOnTintColor(va) } 125 | ) 126 | } 127 | 128 | @objc func swizzled_setThumbTintColor(_ thumbTintColor: UIColor?) { 129 | cache( 130 | firstParam: Callable.Appearanced(thumbTintColor), 131 | identifier: .thumbTintColor, 132 | action: { [weak self] va in self?.__setThumbTintColor(va) } 133 | ) 134 | } 135 | 136 | @objc func swizzled_setOnImage(_ onImage: UIImage?) { 137 | cache( 138 | firstParam: Callable.Appearanced(onImage), 139 | identifier: .onImage, 140 | action: { [weak self] va in self?.__setOnImage(va) } 141 | ) 142 | } 143 | 144 | @objc func swizzled_setOffImage(_ offImage: UIImage?) { 145 | cache( 146 | firstParam: Callable.Appearanced(offImage), 147 | identifier: .offImage, 148 | action: { [weak self] va in self?.__setOffImage(va) } 149 | ) 150 | } 151 | } 152 | 153 | #endif 154 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UITableView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let sectionIndexColor = "UITableView.__setSectionIndexColor(_:)" 14 | static let sectionIndexBackgroundColor = "UITableView.__setSectionIndexBackgroundColor(_:)" 15 | static let sectionIndexTrackingBackgroundColor = "UITableView.__setSectionIndexTrackingBackgroundColor(_:)" 16 | static let separatorColor = "UITableView.__setSeparatorColor(_:)" 17 | } 18 | 19 | public extension UITableView { 20 | var app_sectionIndexColor: UIColor? { 21 | get { sectionIndexColor } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | self.sectionIndexColor = newValue 25 | } else { 26 | swizzled_setSectionIndexColor(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_sectionIndexBackgroundColor: UIColor? { 32 | get { sectionIndexBackgroundColor } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | self.sectionIndexBackgroundColor = newValue 36 | } else { 37 | swizzled_setSectionIndexBackgroundColor(newValue) 38 | } 39 | } 40 | } 41 | 42 | var app_sectionIndexTrackingBackgroundColor: UIColor? { 43 | get { sectionIndexTrackingBackgroundColor } 44 | set { 45 | if __USING_APPEARANCED_SWIZZLING__ { 46 | self.sectionIndexTrackingBackgroundColor = newValue 47 | } else { 48 | swizzled_setSectionIndexTrackingBackgroundColor(newValue) 49 | } 50 | } 51 | } 52 | 53 | var app_separatorColor: UIColor? { 54 | get { separatorColor } 55 | set { 56 | if __USING_APPEARANCED_SWIZZLING__ { 57 | self.separatorColor = newValue 58 | } else { 59 | swizzled_setSeparatorColor(newValue) 60 | } 61 | } 62 | } 63 | } 64 | 65 | internal extension UITableView { 66 | static func silenceExchangeTableViewImplementation() { 67 | app_swizzling( 68 | originalSelector: #selector(setter: sectionIndexColor), 69 | newSelector: #selector(swizzled_setSectionIndexColor) 70 | ) 71 | 72 | app_swizzling( 73 | originalSelector: #selector(setter: sectionIndexBackgroundColor), 74 | newSelector: #selector(swizzled_setSectionIndexBackgroundColor) 75 | ) 76 | 77 | app_swizzling( 78 | originalSelector: #selector(setter: sectionIndexTrackingBackgroundColor), 79 | newSelector: #selector(swizzled_setSectionIndexTrackingBackgroundColor) 80 | ) 81 | 82 | app_swizzling( 83 | originalSelector: #selector(setter: separatorColor), 84 | newSelector: #selector(swizzled_setSeparatorColor) 85 | ) 86 | } 87 | } 88 | 89 | private extension UITableView { 90 | func __setSectionIndexColor(_ sectionIndexColor: UIColor?) { 91 | if __USING_APPEARANCED_SWIZZLING__ { 92 | swizzled_setSectionIndexColor(sectionIndexColor) 93 | } else { 94 | self.sectionIndexColor = sectionIndexColor 95 | } 96 | } 97 | 98 | func __setSectionIndexBackgroundColor(_ sectionIndexBackgroundColor: UIColor?) { 99 | if __USING_APPEARANCED_SWIZZLING__ { 100 | swizzled_setSectionIndexBackgroundColor(sectionIndexBackgroundColor) 101 | } else { 102 | self.sectionIndexBackgroundColor = sectionIndexBackgroundColor 103 | } 104 | } 105 | 106 | func __setSectionIndexTrackingBackgroundColor(_ sectionIndexTrackingBackgroundColor: UIColor?) { 107 | if __USING_APPEARANCED_SWIZZLING__ { 108 | swizzled_setSectionIndexTrackingBackgroundColor(sectionIndexTrackingBackgroundColor) 109 | } else { 110 | self.sectionIndexTrackingBackgroundColor = sectionIndexTrackingBackgroundColor 111 | } 112 | } 113 | 114 | func __setSeparatorColor(_ separatorColor: UIColor?) { 115 | if __USING_APPEARANCED_SWIZZLING__ { 116 | swizzled_setSeparatorColor(separatorColor) 117 | } else { 118 | self.separatorColor = separatorColor 119 | } 120 | } 121 | 122 | 123 | @objc func swizzled_setSectionIndexColor(_ sectionIndexColor: UIColor?) { 124 | cache( 125 | firstParam: Callable.Appearanced(sectionIndexColor), 126 | identifier: .sectionIndexColor, 127 | action: { [weak self] va in self?.__setSectionIndexColor(va) } 128 | ) 129 | } 130 | 131 | @objc func swizzled_setSectionIndexBackgroundColor(_ sectionIndexBackgroundColor: UIColor?) { 132 | cache( 133 | firstParam: Callable.Appearanced(sectionIndexBackgroundColor), 134 | identifier: .sectionIndexBackgroundColor, 135 | action: { [weak self] va in self?.__setSectionIndexBackgroundColor(va) } 136 | ) 137 | } 138 | 139 | @objc func swizzled_setSectionIndexTrackingBackgroundColor(_ sectionIndexTrackingBackgroundColor: UIColor?) { 140 | cache( 141 | firstParam: Callable.Appearanced(sectionIndexTrackingBackgroundColor), 142 | identifier: .sectionIndexTrackingBackgroundColor, 143 | action: { [weak self] va in self?.__setSectionIndexTrackingBackgroundColor(va) } 144 | ) 145 | } 146 | 147 | @objc func swizzled_setSeparatorColor(_ separatorColor: UIColor?) { 148 | cache( 149 | firstParam: Callable.Appearanced(separatorColor), 150 | identifier: .separatorColor, 151 | action: { [weak self] va in self?.__setSeparatorColor(va) } 152 | ) 153 | } 154 | 155 | 156 | } 157 | 158 | #endif 159 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UITextView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let textColor = "UITextView.__setTextColor(:)" 14 | static let attributedText = "UITextView.__setAttributedText(:)" 15 | static let typingAttributes = "UITextView.__setTypingAttributes(:)" 16 | static let linkTextAttributes = "UITextView.__setLinkTextAttributes(:)" 17 | } 18 | 19 | public extension UITextView { 20 | var app_textColor: UIColor? { 21 | get { textColor } 22 | set { 23 | if __USING_APPEARANCED_SWIZZLING__ { 24 | self.textColor = newValue 25 | } else { 26 | swizzled_setTextColor(newValue) 27 | } 28 | } 29 | } 30 | 31 | var app_attributedText: NSAttributedString { 32 | get { attributedText } 33 | set { 34 | if __USING_APPEARANCED_SWIZZLING__ { 35 | self.attributedText = newValue 36 | } else { 37 | swizzled_setAttributedText(newValue) 38 | } 39 | } 40 | } 41 | 42 | var app_typingAttributes: [NSAttributedString.Key: Any] { 43 | get { typingAttributes } 44 | set { 45 | if __USING_APPEARANCED_SWIZZLING__ { 46 | self.typingAttributes = newValue 47 | } else { 48 | swizzled_setTypingAttributes(newValue) 49 | } 50 | } 51 | } 52 | 53 | var app_linkTextAttributes: [NSAttributedString.Key: Any] { 54 | get { linkTextAttributes } 55 | set { 56 | if __USING_APPEARANCED_SWIZZLING__ { 57 | self.linkTextAttributes = newValue 58 | } else { 59 | swizzled_setLinkTextAttributes(newValue) 60 | } 61 | } 62 | } 63 | } 64 | 65 | internal extension UITextView { 66 | static func silenceExchangeTextViewImplementation() { 67 | app_swizzling( 68 | originalSelector: #selector(setter: textColor), 69 | newSelector: #selector(swizzled_setTextColor(_:)) 70 | ) 71 | 72 | app_swizzling( 73 | originalSelector: #selector(setter: attributedText), 74 | newSelector: #selector(swizzled_setAttributedText(_:)) 75 | ) 76 | 77 | app_swizzling( 78 | originalSelector: #selector(setter: typingAttributes), 79 | newSelector: #selector(swizzled_setTypingAttributes(_:)) 80 | ) 81 | 82 | app_swizzling( 83 | originalSelector: #selector(setter: linkTextAttributes), 84 | newSelector: #selector(swizzled_setLinkTextAttributes(_:)) 85 | ) 86 | } 87 | } 88 | 89 | private extension UITextView { 90 | func __setTextColor(_ textColor: UIColor?) { 91 | if __USING_APPEARANCED_SWIZZLING__ { 92 | swizzled_setTextColor(textColor) 93 | } else { 94 | self.textColor = textColor 95 | } 96 | } 97 | 98 | func __setAttributedText(_ attributedText: NSAttributedString) { 99 | if __USING_APPEARANCED_SWIZZLING__ { 100 | swizzled_setAttributedText(attributedText) 101 | } else { 102 | self.attributedText = attributedText 103 | } 104 | } 105 | 106 | func __setTypingAttributes(_ typingAttributes: [NSAttributedString.Key: Any]) { 107 | if __USING_APPEARANCED_SWIZZLING__ { 108 | swizzled_setTypingAttributes(typingAttributes) 109 | } else { 110 | self.typingAttributes = typingAttributes 111 | } 112 | } 113 | 114 | func __setLinkTextAttributes(_ linkTextAttributes: [NSAttributedString.Key: Any]) { 115 | if __USING_APPEARANCED_SWIZZLING__ { 116 | swizzled_setLinkTextAttributes(linkTextAttributes) 117 | } else { 118 | self.linkTextAttributes = linkTextAttributes 119 | } 120 | } 121 | 122 | 123 | @objc func swizzled_setTextColor(_ textColor: UIColor?) { 124 | cache( 125 | firstParam: Callable.Appearanced(textColor), 126 | identifier: .textColor, 127 | action: { [weak self] va in self?.__setTextColor(va) } 128 | ) 129 | } 130 | 131 | @objc func swizzled_setAttributedText(_ attributedText: NSAttributedString) { 132 | cache( 133 | firstParam: Callable.Attributed(attributedText), 134 | identifier: .attributedText, 135 | action: { [weak self] va in self?.__setAttributedText(va) } 136 | ) 137 | } 138 | 139 | @objc func swizzled_setTypingAttributes(_ typingAttributes: [NSAttributedString.Key: Any]) { 140 | cache( 141 | firstParam: Callable.Collection(typingAttributes), 142 | identifier: .typingAttributes, 143 | action: { [weak self] va in self?.__setTypingAttributes(va) } 144 | ) 145 | } 146 | 147 | @objc func swizzled_setLinkTextAttributes(_ linkTextAttributes: [NSAttributedString.Key: Any]) { 148 | cache( 149 | firstParam: Callable.Collection(linkTextAttributes), 150 | identifier: .linkTextAttributes, 151 | action: { [weak self] va in self?.__setLinkTextAttributes(va) } 152 | ) 153 | } 154 | } 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIToolbar+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/11. 6 | // 7 | 8 | #if os(iOS) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let barTintColor = "__setBarTintColor(_:)" 14 | static let setBackgroundImageForPositionMetrics = "__setBackgroundImage(_:forToolbarPosition:barMetrics:)" 15 | static let setShadowImageFor = "__setShadowImage(_:forToolbarPosition:)" 16 | } 17 | 18 | public extension UIToolbar { 19 | var app_barTintColor: UIColor? { 20 | get { barTintColor } 21 | set { 22 | if __USING_APPEARANCED_SWIZZLING__ { 23 | self.barTintColor = newValue 24 | } else { 25 | swizzled_setBarTintColor(newValue) 26 | } 27 | } 28 | } 29 | 30 | func app_setBackgroundImage(_ backgroundImage: UIImage?, forToolbarPosition position: UIBarPosition, barMetrics: UIBarMetrics) { 31 | if __USING_APPEARANCED_SWIZZLING__ { 32 | setBackgroundImage(backgroundImage, forToolbarPosition: position, barMetrics: barMetrics) 33 | } else { 34 | swizzled_setBackgroundImage(backgroundImage, forToolbarPosition: position, barMetrics: barMetrics) 35 | } 36 | } 37 | 38 | func app_setShadowImage(_ shadowImage: UIImage?, forToolbarPosition position: UIBarPosition) { 39 | if __USING_APPEARANCED_SWIZZLING__ { 40 | setShadowImage(shadowImage, forToolbarPosition: position) 41 | } else { 42 | swizzled_setShadowImage(shadowImage, forToolbarPosition: position) 43 | } 44 | } 45 | } 46 | 47 | internal extension UIToolbar { 48 | static func silenceExchangeToolbarImplementation() { 49 | app_swizzling( 50 | originalSelector: #selector(setter: barTintColor), 51 | newSelector: #selector(swizzled_setBarTintColor(_:)) 52 | ) 53 | 54 | app_swizzling( 55 | originalSelector: #selector(setBackgroundImage(_:forToolbarPosition:barMetrics:)), 56 | newSelector: #selector(swizzled_setBackgroundImage(_:forToolbarPosition:barMetrics:)) 57 | ) 58 | 59 | app_swizzling( 60 | originalSelector: #selector(setShadowImage(_:forToolbarPosition:)), 61 | newSelector: #selector(swizzled_setShadowImage(_:forToolbarPosition:)) 62 | ) 63 | } 64 | } 65 | 66 | private extension UIToolbar { 67 | func __setBarTintColor(_ barTintColor: UIColor?) { 68 | if __USING_APPEARANCED_SWIZZLING__ { 69 | swizzled_setBarTintColor(barTintColor) 70 | } else { 71 | self.barTintColor = barTintColor 72 | } 73 | } 74 | 75 | func __setBackgroundImage(_ backgroundImage: UIImage?, forToolbarPosition position: UIBarPosition, barMetrics: UIBarMetrics) { 76 | if __USING_APPEARANCED_SWIZZLING__ { 77 | swizzled_setBackgroundImage(backgroundImage, forToolbarPosition: position, barMetrics: barMetrics) 78 | } else { 79 | setBackgroundImage(backgroundImage, forToolbarPosition: position, barMetrics: barMetrics) 80 | } 81 | } 82 | 83 | func __setShadowImage(_ shadowImage: UIImage?, forToolbarPosition position: UIBarPosition) { 84 | if __USING_APPEARANCED_SWIZZLING__ { 85 | swizzled_setShadowImage(shadowImage, forToolbarPosition: position) 86 | } else { 87 | setShadowImage(shadowImage, forToolbarPosition: position) 88 | } 89 | } 90 | 91 | @objc func swizzled_setBarTintColor(_ barTintColor: UIColor?) { 92 | cache( 93 | firstParam: Callable.Appearanced(barTintColor), 94 | identifier: .barTintColor, 95 | action: { [weak self] va in self?.__setBarTintColor(va) } 96 | ) 97 | } 98 | 99 | @objc func swizzled_setBackgroundImage(_ backgroundImage: UIImage?, forToolbarPosition position: UIBarPosition, barMetrics: UIBarMetrics) { 100 | cache( 101 | firstParam: Callable.Appearanced(backgroundImage), 102 | secondParam: Callable.Original(position), 103 | thirdParam: Callable.Original(barMetrics), 104 | identifier: .setBackgroundImageForPositionMetrics, 105 | action: { [weak self] va, vb, vc in self?.__setBackgroundImage(va, forToolbarPosition: vb, barMetrics: vc) }, 106 | category: "UIToolBar.position:\(position.rawValue).metrics:\(barMetrics.rawValue)" 107 | ) 108 | } 109 | 110 | @objc func swizzled_setShadowImage(_ shadowImage: UIImage?, forToolbarPosition position: UIBarPosition) { 111 | cache( 112 | firstParam: Callable.Appearanced(shadowImage), 113 | secondParam: Callable.Original(position), 114 | identifier: .setShadowImageFor, 115 | action: { [weak self] va, vb in self?.__setShadowImage(va, forToolbarPosition: vb) }, 116 | category: "UIToolBar.position:\(position.rawValue)" 117 | ) 118 | } 119 | } 120 | 121 | #endif 122 | -------------------------------------------------------------------------------- /Sources/Chameleon/NSUIKits/UIKit/UIView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | #if canImport(UIKit) 9 | 10 | import UIKit 11 | 12 | private extension AppearanceCallableIdentifier { 13 | static let backgroundColor = "UIView.__setBackgroundColor(_:)" 14 | static let tintColor = "UIView.__setTintColor(_:)" 15 | } 16 | 17 | public extension UIView { 18 | var app_backgroundColor: UIColor? { 19 | get { backgroundColor } 20 | set { 21 | if __USING_APPEARANCED_SWIZZLING__ { 22 | backgroundColor = newValue 23 | } else { 24 | swizzled_setBackgroundColor(newValue) 25 | } 26 | } 27 | } 28 | 29 | var app_tintColor: UIColor { 30 | get { tintColor } 31 | set { 32 | if __USING_APPEARANCED_SWIZZLING__ { 33 | self.tintColor = newValue 34 | } else { 35 | swizzled_setTintColor(newValue) 36 | } 37 | } 38 | } 39 | } 40 | 41 | internal extension UIView { 42 | static func silenceExchangeViewImplementation() { 43 | app_swizzling( 44 | originalSelector: #selector(setter: backgroundColor), 45 | newSelector: #selector(swizzled_setBackgroundColor(_:)) 46 | ) 47 | 48 | app_swizzling( 49 | originalSelector: #selector(setter: tintColor), 50 | newSelector: #selector(swizzled_setTintColor(_:)) 51 | ) 52 | } 53 | } 54 | 55 | private extension UIView { 56 | func __setBackgroundColor(_ backgroundColor: UIColor?) { 57 | if __USING_APPEARANCED_SWIZZLING__ { 58 | swizzled_setBackgroundColor(backgroundColor) 59 | } else { 60 | self.backgroundColor = backgroundColor 61 | } 62 | } 63 | 64 | func __setTintColor(_ tintColor: UIColor) { 65 | if __USING_APPEARANCED_SWIZZLING__ { 66 | swizzled_setTintColor(tintColor) 67 | } else { 68 | self.tintColor = tintColor 69 | } 70 | } 71 | 72 | @objc func swizzled_setBackgroundColor(_ backgroundColor: UIColor?) { 73 | cache( 74 | firstParam: Callable.Appearanced(backgroundColor), 75 | identifier: .backgroundColor, 76 | action: { [weak self] va in self?.__setBackgroundColor(va) } 77 | ) 78 | } 79 | 80 | @objc func swizzled_setTintColor(_ tintColor: UIColor) { 81 | cache( 82 | firstParam: Callable.Appearanced(tintColor), 83 | identifier: .tintColor, 84 | action: { [weak self] va in self?.__setTintColor(va) } 85 | ) 86 | } 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/AttributedStringTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryTests.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | import XCTest 9 | import Chameleon 10 | 11 | final class AttributedStringTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | 28 | #if os(macOS) 29 | let attributedString = NSMutableAttributedString(string: "Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.") 30 | attributedString.addAttributes([.foregroundColor: NSColor.red], range: NSMakeRange(0, 4)) 31 | attributedString.addAttributes([.foregroundColor: NSColor.blue], range: NSMakeRange(8, 5)) 32 | attributedString.addAttributes([.font: NSFont.systemFont(ofSize: 18)], range: NSMakeRange(2, 8)) 33 | 34 | func enumerated() { 35 | attributedString.enumerateAttributes(in: NSMakeRange(0, attributedString.length)) { attributes, range, stop in 36 | print("---> \(range.location)->\(range.location + range.length): \(attributes.keys)") 37 | } 38 | attributedString.enumerateAttribute(.foregroundColor, in: NSMakeRange(0, attributedString.length)) { value, range, stop in 39 | print("===> \(range.location)->\(range.location + range.length): \(String(describing: value))") 40 | } 41 | } 42 | 43 | enumerated() 44 | 45 | print("\n\n\n") 46 | attributedString.addAttributes([.foregroundColor: NSColor.cyan], range: NSMakeRange(8, 5)) 47 | 48 | enumerated() 49 | #endif 50 | } 51 | 52 | func testPerformanceExample() throws { 53 | // This is an example of a performance test case. 54 | self.measure { 55 | // Put the code you want to measure the time of here. 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/AttributesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/16. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | #if os(macOS) 11 | import Cocoa 12 | #else 13 | import UIKit 14 | #endif 15 | import Chameleon 16 | 17 | extension Dictionary where Key == NSAttributedString.Key { 18 | func attributes(equalTo dict2Block: @autoclosure () -> [Key: Any]) -> Bool { 19 | let dict2 = dict2Block() 20 | 21 | guard self.count == dict2.count else { return false } 22 | 23 | for (key, value) in self { 24 | guard let value2 = dict2[key] else { return false } 25 | 26 | if key == .foregroundColor || key == .backgroundColor { 27 | guard let color1 = value as? NSUIAppearanceColor, 28 | let color2 = value2 as? NSUIAppearanceColor else { return false } 29 | guard color1 == color2 else { return false } 30 | } 31 | } 32 | 33 | return true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/ChameleonTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if os(macOS) 4 | import Cocoa 5 | #else 6 | import UIKit 7 | #endif 8 | 9 | @testable import Chameleon 10 | 11 | final class ChameleonTests: XCTestCase { 12 | func testExample() throws { 13 | // This is an example of a functional test case. 14 | // Use XCTAssert and related functions to verify your tests produce the correct 15 | // results. 16 | 17 | #if os(macOS) 18 | // let text = NSText() 19 | // text.app_setTextColor(.red, range: NSMakeRange(0, 10)) 20 | // text.app_setTextColor(.red, range: NSMakeRange(0, 20)) 21 | 22 | // RunTimeKit.changeExchangeImplementations() 23 | 24 | AppearanceManager.activeSwizzling() 25 | 26 | let color = NSColor.red 27 | print(color) 28 | let box = NSBox() 29 | box.fillColor = color 30 | box.fillColor = .blue 31 | box.fillColor = .yellow 32 | let _ = box.fillColor 33 | print(box.fillColor) 34 | 35 | #endif 36 | } 37 | 38 | func testRepeatSet() { 39 | #if os(macOS) 40 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme0) 41 | 42 | let label = NSTextField(labelWithString: "hello world") 43 | 44 | label.app_textColor = ThemeInfos.Theme0.C001 45 | 46 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme1) 47 | 48 | XCTAssertEqual(label.textColor, ThemeInfos.Theme1.C001) 49 | 50 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme2) 51 | 52 | XCTAssertEqual(label.textColor, ThemeInfos.Theme2.C001) 53 | 54 | label.app_textColor = ThemeInfos.Theme0.C002 55 | label.app_textColor = ThemeInfos.Theme0.C003 56 | 57 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme0) 58 | 59 | XCTAssertEqual(label.textColor, ThemeInfos.Theme0.C003) 60 | XCTAssertNotEqual(label.textColor, ThemeInfos.Theme0.C001) 61 | 62 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme1) 63 | 64 | XCTAssertEqual(label.textColor, ThemeInfos.Theme1.C003) 65 | XCTAssertNotEqual(label.textColor, ThemeInfos.Theme1.C001) 66 | 67 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme2) 68 | 69 | XCTAssertEqual(label.textColor, ThemeInfos.Theme2.C003) 70 | XCTAssertNotEqual(label.textColor, ThemeInfos.Theme2.C001) 71 | 72 | #endif 73 | } 74 | 75 | func testExecution() { 76 | let color1 = NSUIAppearanceColor.red 77 | let color2 = NSUIAppearanceColor.red 78 | 79 | testFunc1(color1) 80 | testFunc2(color2) 81 | } 82 | 83 | func testFunc1(_ color: NSUIAppearanceColor) { 84 | Cacher(param: color, callback: callback1(_:)).execute() 85 | } 86 | 87 | func testFunc2(_ color: NSUIAppearanceColor?) { 88 | Cacher(param: color, callback: callback2(_:)).execute() 89 | } 90 | 91 | func callback1(_ color: NSUIAppearanceColor) { 92 | print(String(describing: color)) 93 | } 94 | 95 | func callback2(_ color: NSUIAppearanceColor?) { 96 | print(String(describing: color)) 97 | } 98 | } 99 | 100 | struct Cacher { 101 | let param: T 102 | let callback: ((T) -> Void) 103 | 104 | func execute() { 105 | callback(param) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/CheckTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckTests.swift 3 | // 4 | // 5 | // Created by Jo on 2024/1/26. 6 | // 7 | 8 | import XCTest 9 | @testable import Chameleon 10 | 11 | final class CheckTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | let d1 = [ 23 | "a": [ 24 | "b": [ 25 | "k": 1, 26 | "d": 2 27 | ], 28 | "e": [ 29 | "f": 3, 30 | "g": 4 31 | ], 32 | "h": 5, 33 | "i": 6 34 | ] 35 | ] 36 | 37 | let d2 = [ 38 | "a": [ 39 | "b": [ 40 | "k": 7, 41 | "d": 8 42 | ], 43 | "e": [ 44 | "f": 9, 45 | "g": 10 46 | ], 47 | "h": 11, 48 | "i": 12 49 | ] 50 | ] 51 | 52 | let d3 = [ 53 | "a": [ 54 | "b": [ 55 | "c": 13, 56 | "d": 14 57 | ], 58 | "e": [ 59 | "f": 15, 60 | "g": 16 61 | ], 62 | "h": 17, 63 | "i": 18 64 | ] 65 | ] 66 | 67 | let d4 = [ 68 | "a": [ 69 | "b": [ 70 | "c": 19, 71 | "d": 20 72 | ], 73 | "e": [ 74 | "l": 21, 75 | "g": 22 76 | ], 77 | "h": 23, 78 | "i": ["m": 44] 79 | ] 80 | ] 81 | 82 | let result = checkAppearanceInfos([ 83 | CheckableAppearanceInfo(identifier: "d1", appearanceInfos: d1), 84 | CheckableAppearanceInfo(identifier: "d2", appearanceInfos: d2), 85 | CheckableAppearanceInfo(identifier: "d3", appearanceInfos: d3), 86 | CheckableAppearanceInfo(identifier: "d4", appearanceInfos: d4) 87 | ]) 88 | 89 | print(result.map { "\($0)" }.joined(separator: "\n")) 90 | 91 | XCTAssertTrue(result.count > 0) 92 | } 93 | 94 | func testPerformanceExample() throws { 95 | // This is an example of a performance test case. 96 | self.measure { 97 | // Put the code you want to measure the time of here. 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/ComplexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplexTests.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/17. 6 | // 7 | 8 | import XCTest 9 | import Chameleon 10 | 11 | final class ComplexTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | #if os(iOS) 23 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme0) 24 | 25 | let theme0 = ThemeInfos.Theme0.self 26 | 27 | let view = UIView() 28 | 29 | view.backgroundColor = theme0.C001 30 | 31 | XCTAssertEqual(view.backgroundColor, ThemeInfos.Theme0.C001) 32 | 33 | view.app_backgroundColor = theme0.C001 34 | 35 | XCTAssertEqual(view.backgroundColor, ThemeInfos.Theme0.C001) 36 | 37 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme1) 38 | 39 | XCTAssertEqual(view.backgroundColor, ThemeInfos.Theme1.C001) 40 | 41 | AppearanceManager.activeSwizzling() 42 | 43 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme2) 44 | 45 | XCTAssertEqual(view.backgroundColor, ThemeInfos.Theme2.C001) 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/DictionaryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryTests.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/6. 6 | // 7 | 8 | import XCTest 9 | import Chameleon 10 | 11 | final class DictionaryTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | 28 | let id0 = "color/text/C001" 29 | let id1 = "color/background/C102" 30 | let id2 = "image/bg/tips" 31 | 32 | XCTAssertEqual((try? ThemeInfo.theme0.appearanceInfo(with: id0)) as! String, "F7ACBC") 33 | XCTAssertEqual((try? ThemeInfo.theme1.appearanceInfo(with: id1)) as! String, "525F42 0.9") 34 | XCTAssertNil((try? ThemeInfo.theme2.appearanceInfo(with: id2))) 35 | XCTAssertNil((try? ThemeInfo.theme0.appearanceInfo(with: "color/textc000"))) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Recycle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recycle.swift 3 | // 4 | // 5 | // Created by Jo on 2023/4/13. 6 | // 7 | 8 | import XCTest 9 | import Chameleon 10 | 11 | #if os(iOS) 12 | import UIKit 13 | 14 | 15 | private class MyLabel: UILabel { 16 | var key: String = "" 17 | deinit { 18 | print("deinit \(key)") 19 | } 20 | 21 | override var description: String { 22 | return "\(key) deinit \(self)" 23 | } 24 | } 25 | 26 | private class MyView: UIView { 27 | var key: String = "" 28 | deinit { 29 | print("deinit \(key)") 30 | } 31 | 32 | override var description: String { 33 | return "\(key) deinit \(self)" 34 | } 35 | } 36 | 37 | final class Recycle: XCTestCase { 38 | 39 | override func setUpWithError() throws { 40 | // Put setup code here. This method is called before the invocation of each test method in the class. 41 | } 42 | 43 | override func tearDownWithError() throws { 44 | // Put teardown code here. This method is called after the invocation of each test method in the class. 45 | } 46 | 47 | func testExample() throws { 48 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme0) 49 | 50 | func test1() { 51 | let label = MyLabel() 52 | label.app_textColor = ThemeInfos.Theme0.C004 53 | label.key = "appearnced Label1" 54 | 55 | let label2 = MyLabel() 56 | label2.key = "not appearanced label2" 57 | } 58 | 59 | func test2() { 60 | let view = MyView() 61 | view.app_backgroundColor = ThemeInfos.Theme0.C001 62 | view.key = "appearanced view1" 63 | 64 | let view2 = MyView() 65 | view2.key = "not appearanced view2" 66 | } 67 | 68 | let label3 = MyLabel() 69 | label3.app_textColor = ThemeInfos.Theme0.C004 70 | label3.key = "appearnced Label3" 71 | 72 | let view3 = MyView() 73 | view3.app_backgroundColor = ThemeInfos.Theme0.C001 74 | view3.key = "appearanced view3" 75 | 76 | test1() 77 | test2() 78 | 79 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme1) 80 | 81 | AppearanceManager.shared.changeThemeWith(themeInfo: ThemeInfo.theme2) 82 | } 83 | } 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/bullseye_icon_252137.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bullseye_icon_252137.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/bullseye_icon_252137.imageset/bullseye_icon_252137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme0/bullseye_icon_252137.imageset/bullseye_icon_252137.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/dinosaur_icon_252012.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dinosaur_icon_252012.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/dinosaur_icon_252012.imageset/dinosaur_icon_252012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme0/dinosaur_icon_252012.imageset/dinosaur_icon_252012.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/heart_love_icon_251954.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heart_love_icon_251954.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme0/heart_love_icon_251954.imageset/heart_love_icon_251954.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme0/heart_love_icon_251954.imageset/heart_love_icon_251954.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/home_house_icon_251952.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "home_house_icon_251952.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/home_house_icon_251952.imageset/home_house_icon_251952.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme1/home_house_icon_251952.imageset/home_house_icon_251952.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/human_handsup_icon_251948.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "human_handsup_icon_251948.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/human_handsup_icon_251948.imageset/human_handsup_icon_251948.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme1/human_handsup_icon_251948.imageset/human_handsup_icon_251948.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/mood_happy_icon_251870.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mood_happy_icon_251870.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme1/mood_happy_icon_251870.imageset/mood_happy_icon_251870.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme1/mood_happy_icon_251870.imageset/mood_happy_icon_251870.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/trash_delete_remove_icon_251766.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "trash_delete_remove_icon_251766.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/trash_delete_remove_icon_251766.imageset/trash_delete_remove_icon_251766.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme2/trash_delete_remove_icon_251766.imageset/trash_delete_remove_icon_251766.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/visible_eye_icon_251745.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "visible_eye_icon_251745.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/visible_eye_icon_251745.imageset/visible_eye_icon_251745.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme2/visible_eye_icon_251745.imageset/visible_eye_icon_251745.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/zap_icon_251733.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "zap_icon_251733.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/Assets.xcassets/theme2/zap_icon_251733.imageset/zap_icon_251733.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/Assets.xcassets/theme2/zap_icon_251733.imageset/zap_icon_251733.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/Resources/bullseye_icon_252137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JyHu/Chameleon/9118e990720748fa2f14c4b75aeb21df0eb4f32c/Tests/ChameleonTests/Resources/bullseye_icon_252137.png -------------------------------------------------------------------------------- /Tests/ChameleonTests/ThemeInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeInfo.swift 3 | // Demos 4 | // 5 | // Created by Jo on 2023/4/4. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ThemeInfo { 11 | static let theme0: [String: Any] = [ 12 | "color": [ 13 | "text": [ 14 | "C001": "F7ACBC", 15 | "C002": "EF5B9C", 16 | "C003": "FEEEED", 17 | "C004": "F05B72", 18 | "C005": "FAA755", 19 | "C006": "D71345", 20 | "C007": "905D1D", 21 | "C008": "1D953F" 22 | ], 23 | "background": [ 24 | "C101": "C99979", 25 | "C102": "DE773F", 26 | "C103": "B7704F", 27 | "C104": "B4532A", 28 | "C105": "F36C21", 29 | "C106": "8E3E1F", 30 | "C107": "6A3427", 31 | "C108": "C85D44", 32 | ] 33 | ], 34 | "image": [ 35 | "no01": "bullseye_icon_252137", 36 | "no02": "dinosaur_icon_252012", 37 | "no03": "heart_love_icon_251954" 38 | ], 39 | "others": [ 40 | "border": [ 41 | "width": 5 42 | ], 43 | "imageView": [ 44 | "animationDuration": 1, 45 | "animationRepeatCount": 3 46 | ] 47 | ] 48 | ] 49 | 50 | static let theme1: [String: Any] = [ 51 | "color": [ 52 | "text": [ 53 | "C001": "B17936 0.9", 54 | "C002": "rgba<254,220,189,0.9>", 55 | "C003": "rgba<244,121,32,0.9>", 56 | "C004": "hex<905A3D 0.9>", 57 | "C005": "hex", 58 | "C006": "5C7A29 0.9", 59 | "C007": "A3CF62 0.9", 60 | "C008": "694D9F" 61 | ], 62 | "background": [ 63 | "C101": "5F5D46 0.9", 64 | "C102": "525F42 0.9", 65 | "C103": "596032 0.9", 66 | "C104": "6E6B41 0.9", 67 | "C105": "CBC547 0.9", 68 | "C106": "DECB00 0.9", 69 | "C107": "FCF16E 0.9", 70 | "C108": "FFE600 0.9", 71 | ] 72 | ], 73 | "image": [ 74 | "no01": "home_house_icon_251952", 75 | "no02": "human_handsup_icon_251948", 76 | "no03": "mood_happy_icon_251870" 77 | ], 78 | "others": [ 79 | "border": [ 80 | "width": 2 81 | ], 82 | "imageView": [ 83 | "animationDuration": 3, 84 | "animationRepeatCount": 5 85 | ] 86 | ] 87 | ] 88 | 89 | static let theme2: [String: Any] = [ 90 | "color": [ 91 | "text": [ 92 | "C001": "444693 0.8", 93 | "C002": "2B4490 0.8", 94 | "C003": "2A5CAA 0.8", 95 | "C004": "102B6A 0.8", 96 | "C005": "1A2933 0.8", 97 | "C006": "6A6DA9 0.8", 98 | "C007": "494E8F 0.8", 99 | "C008": "FCAF17" 100 | ], 101 | "background": [ 102 | "C101": "1B315E 0.8", 103 | "C102": "2585A6 0.8", 104 | "C103": "2570A1 0.8", 105 | "C104": "2468A2 0.8", 106 | "C105": "228F8D 0.8", 107 | "C106": "7BBFEA 0.8", 108 | "C107": "11264F 0.8", 109 | "C108": "6C4C49 0.8", 110 | ] 111 | ], 112 | "image": [ 113 | "no01": "trash_delete_remove_icon_251766", 114 | "no02": "visible_eye_icon_251745", 115 | "no03": "zap_icon_251733" 116 | ], 117 | "others": [ 118 | "border": [ 119 | "width": 10 120 | ], 121 | "imageView": [ 122 | "animationDuration": 5, 123 | "animationRepeatCount": 7 124 | ] 125 | ] 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /Tools/Breeder/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Breeder 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | private var mainWindowController = MainWindowController() 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | mainWindowController.window?.makeKeyAndOrderFront(nil) 17 | } 18 | 19 | func applicationWillTerminate(_ aNotification: Notification) { 20 | // Insert code here to tear down your application 21 | } 22 | 23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 24 | return false 25 | } 26 | 27 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 28 | if !flag { 29 | mainWindowController.window?.makeKeyAndOrderFront(nil) 30 | } 31 | return true 32 | } 33 | 34 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 35 | return true 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Tools/Breeder/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tools/Breeder/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tools/Breeder/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tools/Breeder/Breeder.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tools/Breeder/DataCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // Breeder 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class DataCenter { 12 | static var shared = DataCenter() 13 | 14 | @Published var files: [FileInfo] = [] 15 | } 16 | 17 | class FileInfo { 18 | let identifier = UUID().uuidString 19 | var name: String? 20 | var url: URL? 21 | } 22 | -------------------------------------------------------------------------------- /Tools/Breeder/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // Breeder 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Cocoa 9 | 10 | class DetailViewController: NSViewController { 11 | 12 | override func loadView() { 13 | view = NSView(frame: NSMakeRect(0, 0, 100, 100)) 14 | } 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | // Do view setup here. 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Tools/Breeder/FilesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilesViewController.swift 3 | // Breeder 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Cocoa 9 | import SwiftExtensions 10 | import Combine 11 | 12 | private extension NSUserInterfaceItemIdentifier { 13 | static let index = NSUserInterfaceItemIdentifier("com.auu.chameleon.files.index") 14 | static let name = NSUserInterfaceItemIdentifier("com.auu.chameleon.files.name") 15 | static let path = NSUserInterfaceItemIdentifier("com.auu.chameleon.files.path") 16 | static let delete = NSUserInterfaceItemIdentifier("com.auu.chameleon.files.delete") 17 | } 18 | 19 | class FilesViewController: NSViewController { 20 | 21 | private var tableView = NSTableView(frame: NSMakeRect(0, 0, 100, 100)) 22 | private var subscription: AnyCancellable? 23 | 24 | override func loadView() { 25 | view = NSScrollView(documentView: tableView, isHorizontal: false) 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | tableView.delegate = self 32 | tableView.dataSource = self 33 | tableView.focusRingType = .none 34 | tableView.allowsColumnResizing = true 35 | tableView.usesAlternatingRowBackgroundColors = true 36 | tableView.addTableColumn(identifier: .index, title: "Index", width: 36) 37 | tableView.addTableColumn(identifier: .name, title: "Name", minWidth: 48, maxWidth: 120) 38 | tableView.addTableColumn(identifier: .path, title: "Path", minWidth: 150) 39 | tableView.addTableColumn(identifier: .delete, title: "", width: 64) 40 | 41 | subscription = DataCenter.shared.$files.sink { [weak self] _ in 42 | self?.tableView.reloadData() 43 | } 44 | } 45 | } 46 | 47 | extension FilesViewController: NSTableViewDelegate, NSTableViewDataSource { 48 | func numberOfRows(in tableView: NSTableView) -> Int { 49 | return DataCenter.shared.files.count 50 | } 51 | 52 | func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { 53 | guard let identifier = tableColumn?.identifier else { return nil } 54 | if identifier == .index { 55 | return "\(row)" 56 | } 57 | 58 | let fileInfo = DataCenter.shared.files[row] 59 | 60 | if identifier == .name { 61 | return fileInfo.name 62 | } 63 | 64 | if identifier == .path { 65 | return fileInfo.url?.getPath() 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func tableView(_ tableView: NSTableView, dataCellFor tableColumn: NSTableColumn?, row: Int) -> NSCell? { 72 | guard let identifier = tableColumn?.identifier else { return nil } 73 | if identifier == .index { 74 | return NSTextFieldCell() 75 | } else if identifier == .path { 76 | return NSTextFieldCell() 77 | } else if identifier == .delete { 78 | let cell = NSButtonCell(textCell: "Delete") 79 | cell.bezelStyle = .rounded 80 | cell.target = self 81 | cell.action = #selector(deleteFilesAction) 82 | return cell 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func controlTextDidChange(_ obj: Notification) { 89 | 90 | } 91 | } 92 | 93 | private extension FilesViewController { 94 | @objc func deleteFilesAction() { 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tools/Breeder/InfosViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfosViewController.swift 3 | // Breeder 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Cocoa 9 | import SwiftExtensions 10 | 11 | class InfosViewController: NSViewController { 12 | 13 | private var outlineView = NSOutlineView(frame: NSMakeRect(0, 0, 100, 100)) 14 | 15 | override func loadView() { 16 | view = NSScrollView(documentView: outlineView) 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | // Do view setup here. 22 | 23 | outlineView.focusRingType = .none 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tools/Breeder/MainWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWindowController.swift 3 | // Breeder 4 | // 5 | // Created by Jo on 2023/4/7. 6 | // 7 | 8 | import Cocoa 9 | import SFSymbols 10 | import SwiftExtensions 11 | 12 | private extension NSToolbar.Identifier { 13 | static let mainToolbar = NSToolbar.Identifier("com.auu.chameleon.toolbar") 14 | } 15 | 16 | private extension NSToolbarItem.Identifier { 17 | static let newItem = NSToolbarItem.Identifier("com.auu.chameleon.new") 18 | static let expandGroup = NSToolbarItem.Identifier("com.auu.chameleon.expand") 19 | static let filesGroup = NSToolbarItem.Identifier("com.auu.chameleon.files") 20 | } 21 | 22 | class MainWindowController: NSWindowController { 23 | init() { 24 | super.init(window: MainWindow()) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | } 31 | 32 | class MainWindow: NSWindow, NSToolbarDelegate { 33 | 34 | private var mainSplit = NSSplitViewController() 35 | private var fileSplit = NSSplitViewController() 36 | 37 | private var filesPage = FilesViewController() 38 | private var infosPage = InfosViewController() 39 | private var detailPage = DetailViewController() 40 | 41 | init() { 42 | let styleMask: NSWindow.StyleMask = [ .closable, .titled, .resizable, .miniaturizable ] 43 | super.init(contentRect: NSMakeRect(0, 0, 720, 540), styleMask: styleMask, backing: .buffered, defer: true) 44 | 45 | fileSplit.isVertical = false 46 | fileSplit.add(filesPage, minThickness: 200) 47 | fileSplit.add(infosPage, minThickness: 200) 48 | mainSplit.add(fileSplit, minThickness: 300) 49 | mainSplit.add(detailPage, minThickness: 200, maxThickness: 500) 50 | 51 | contentViewController = mainSplit 52 | 53 | let bar = NSToolbar(identifier: .mainToolbar) 54 | bar.delegate = self 55 | bar.displayMode = .iconOnly 56 | bar.sizeMode = .default 57 | bar.showsBaselineSeparator = true 58 | toolbar = bar 59 | 60 | title = "Breeder" 61 | contentViewController = mainSplit 62 | minSize = NSMakeSize(720, 540) 63 | setFrame(NSMakeRect(0, 0, 720, 540), display: true) 64 | titlebarAppearsTransparent = false 65 | titlebarSeparatorStyle = .line 66 | toolbarStyle = .unifiedCompact 67 | center() 68 | } 69 | 70 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 71 | return [ .filesGroup, .expandGroup ] 72 | } 73 | 74 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 75 | return [ .filesGroup, .expandGroup ] 76 | } 77 | 78 | func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 79 | return [] 80 | } 81 | 82 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 83 | 84 | if itemIdentifier == .filesGroup { 85 | return NSToolbarItemGroup(itemIdentifier: .filesGroup, sfnames: [ .plus, .squareAndArrowDown, .squareAndArrowUp ], selectionModel: .momentary, labels: nil, target: self, action: #selector(filesGroupAction)) 86 | } 87 | 88 | if itemIdentifier == .expandGroup { 89 | return NSToolbarItemGroup(itemIdentifier: .expandGroup, sfnames: [ .rectangleTophalfFilled, .rectangleRighthalfFilled ], selectionModel: .momentary, labels: nil, target: self, action: #selector(expandsAction)) 90 | } 91 | 92 | return NSToolbarItem(itemIdentifier: itemIdentifier) 93 | } 94 | } 95 | 96 | private extension MainWindow { 97 | @objc func filesGroupAction(_ itemGroup: NSToolbarItemGroup) { 98 | if itemGroup.selectedIndex == 0 { 99 | DataCenter.shared.files.append(FileInfo()) 100 | } 101 | } 102 | 103 | @objc func expandsAction(_ itemGroup: NSToolbarItemGroup) { 104 | let firstItem = (itemGroup.selectedIndex == 0 ? fileSplit : mainSplit).splitViewItems[itemGroup.selectedIndex] 105 | firstItem.isCollapsed = !firstItem.isCollapsed 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tools/Tools.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tools/Tools.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tools/Tools.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "sfsymbols", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/JyHu/SFSymbols.git", 7 | "state" : { 8 | "revision" : "f8e464d8e49d4a3e726d47f82e50bf48ceaefdb9" 9 | } 10 | }, 11 | { 12 | "identity" : "swiftextensions", 13 | "kind" : "remoteSourceControl", 14 | "location" : "https://github.com/JyHu/SwiftExtensions", 15 | "state" : { 16 | "branch" : "main", 17 | "revision" : "1483220781095b6bcfca41bd65f417be62a92f31" 18 | } 19 | } 20 | ], 21 | "version" : 2 22 | } 23 | --------------------------------------------------------------------------------