├── .github ├── ChromaColorPicker.gif └── Logo.png ├── .gitignore ├── .travis.yml ├── ChromaColorPicker.podspec ├── ChromaColorPicker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ChromaColorPicker.xcscheme │ └── ChromaColorPickerTests.xcscheme ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── home.imageset │ │ ├── Contents.json │ │ └── home.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Source ├── ChromaBrightnessSlider.swift ├── ChromaColorPicker.h ├── ChromaColorPicker.swift ├── ChromaControlStylable.swift ├── ColorWheelView.swift ├── Extensions │ ├── UIColor+Brightness.swift │ ├── UIColor+Utils.swift │ ├── UIView+DropShadow.swift │ └── UIView+Utils.swift ├── Info.plist └── Supporting Views │ ├── ChromaColorHandle.swift │ ├── SliderHandleView.swift │ └── SliderTrackView.swift ├── Tests ├── ChromaBrightnessSliderTests.swift ├── ChromaColorPickerTests.swift ├── ChromaControlStylableTests.swift ├── ColorWheelViewTests.swift ├── Extensions │ ├── UIColor+BrightnessTests.swift │ ├── UIColor+UtilsTests.swift │ └── UIView+DropShadowTests.swift ├── Info.plist ├── Supporting Views │ ├── ChromaColorHandleTests.swift │ └── SliderHandleViewTests.swift └── TestHelpers │ ├── FakeTouch.swift │ └── UIColor+TestHelpers.swift └── fastlane ├── Fastfile └── README.md /.github/ChromaColorPicker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncardasis/ChromaColorPicker/4fa7a0b3cb779d97b771a5bf0de8927d6a37153f/.github/ChromaColorPicker.gif -------------------------------------------------------------------------------- /.github/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncardasis/ChromaColorPicker/4fa7a0b3cb779d97b771a5bf0de8927d6a37153f/.github/Logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | .DS_Store 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | # CocoaPods 33 | # 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 37 | # 38 | Pods/ 39 | 40 | # Carthage 41 | # 42 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 43 | # Carthage/Checkouts 44 | 45 | Carthage/Build 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 53 | 54 | fastlane/report.xml 55 | fastlane/Preview.html 56 | fastlane/screenshots 57 | fastlane/test_output 58 | 59 | # Code Injection 60 | # 61 | # After new code Injection tools there's a generated folder /iOSInjectionProject 62 | # https://github.com/johnno1962/injectionforxcode 63 | 64 | iOSInjectionProject/ 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.3 3 | before_install: 4 | - gem install bundler --version '2.0.1' 5 | install: 6 | - bundle install 7 | jobs: 8 | include: 9 | - stage: test 10 | script: 11 | - fastlane test 12 | - stage: pod-lint 13 | script: 14 | - pod lib lint 15 | stages: 16 | - test 17 | - pod-lint 18 | - name: test-lint 19 | if: type = pull_request -------------------------------------------------------------------------------- /ChromaColorPicker.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ChromaColorPicker" 3 | s.version = "2.0.2" 4 | s.summary = "An intuitive iOS color picker built in Swift." 5 | s.swift_version = '5.0' 6 | 7 | s.description = <<-DESC 8 | A Swift color picker UIControl which allows users to select color(s) on a color wheel. Supports multiple handles, an optional brightness slider, and can be customized. 9 | DESC 10 | 11 | s.homepage = "https://github.com/joncardasis/ChromaColorPicker" 12 | s.license = { :type => "MIT", :file => "LICENSE" } 13 | s.author = "Jonathan Cardasis" 14 | s.platform = :ios, "10.0" 15 | s.source = { :git => "https://github.com/joncardasis/ChromaColorPicker.git", :tag => "#{s.version}" } 16 | s.source_files = "Source/**/*.swift" 17 | 18 | end 19 | -------------------------------------------------------------------------------- /ChromaColorPicker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3503B8331F2689BC00750356 /* ChromaColorPicker.h in Headers */ = {isa = PBXBuildFile; fileRef = 3503B8311F2689BC00750356 /* ChromaColorPicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 3503B8361F2689BC00750356 /* ChromaColorPicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */; }; 12 | 3503B8371F2689BC00750356 /* ChromaColorPicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 13 | 35C376D11D5CF5300069D7A1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376D01D5CF5300069D7A1 /* AppDelegate.swift */; }; 14 | 35C376D31D5CF5300069D7A1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376D21D5CF5300069D7A1 /* ViewController.swift */; }; 15 | 35C376D61D5CF5300069D7A1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35C376D41D5CF5300069D7A1 /* Main.storyboard */; }; 16 | 35C376D81D5CF5300069D7A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 35C376D71D5CF5300069D7A1 /* Assets.xcassets */; }; 17 | 35C376DB1D5CF5300069D7A1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35C376D91D5CF5300069D7A1 /* LaunchScreen.storyboard */; }; 18 | C83A935D23760C1600564E01 /* UIColor+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C83A935C23760C1600564E01 /* UIColor+Utils.swift */; }; 19 | C83A936023760C5900564E01 /* UIColor+UtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C83A935E23760C3800564E01 /* UIColor+UtilsTests.swift */; }; 20 | FC1BD8C02207D7B700817AF3 /* ChromaColorPickerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BD8BF2207D7B700817AF3 /* ChromaColorPickerTests.swift */; }; 21 | FC1BD8C22207D7B700817AF3 /* ChromaColorPicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */; }; 22 | FC4387C422603AE900F739F1 /* ColorWheelViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42AB226038A400BE2FF9 /* ColorWheelViewTests.swift */; }; 23 | FC4387C62262556600F739F1 /* ChromaBrightnessSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387C52262556600F739F1 /* ChromaBrightnessSlider.swift */; }; 24 | FC4387C922625C7000F739F1 /* SliderTrackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387C822625C7000F739F1 /* SliderTrackView.swift */; }; 25 | FC4387CB22625DA800F739F1 /* SliderHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387CA22625DA800F739F1 /* SliderHandleView.swift */; }; 26 | FC4387CD2262B82600F739F1 /* ChromaControlStylable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387CC2262B82600F739F1 /* ChromaControlStylable.swift */; }; 27 | FC4387DE226974FD00F739F1 /* UIColor+Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4387DC226972EB00F739F1 /* UIColor+Brightness.swift */; }; 28 | FC89B7E02325B07F00D007AB /* ChromaBrightnessSliderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC89B7DF2325B07F00D007AB /* ChromaBrightnessSliderTests.swift */; }; 29 | FC89B7E22325B8B900D007AB /* FakeTouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC89B7E12325B8B900D007AB /* FakeTouch.swift */; }; 30 | FC89B7E42325C24F00D007AB /* UIView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC89B7E32325C24F00D007AB /* UIView+Utils.swift */; }; 31 | FCCA42A5226022A800BE2FF9 /* ColorWheelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */; }; 32 | FCCA42A7226023F000BE2FF9 /* ChromaColorHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */; }; 33 | FCCA42AA2260329900BE2FF9 /* UIView+DropShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCCA42A92260329900BE2FF9 /* UIView+DropShadow.swift */; }; 34 | FCE07BFE227B4A8100920217 /* UIColor+BrightnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE07BFD227B4A8100920217 /* UIColor+BrightnessTests.swift */; }; 35 | FCE07C01227B4C8B00920217 /* UIColor+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE07C00227B4C8B00920217 /* UIColor+TestHelpers.swift */; }; 36 | FCE07C04227B4DB900920217 /* UIView+DropShadowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE07C03227B4DB900920217 /* UIView+DropShadowTests.swift */; }; 37 | FCE07C06227B525000920217 /* ChromaControlStylableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE07C05227B525000920217 /* ChromaControlStylableTests.swift */; }; 38 | FCE07C0A228A0F6800920217 /* SliderHandleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE07C09228A0F6800920217 /* SliderHandleViewTests.swift */; }; 39 | FCE07C0C228A0F7500920217 /* ChromaColorHandleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE07C0B228A0F7500920217 /* ChromaColorHandleTests.swift */; }; 40 | FCEA4E272235AAA200C0A1B6 /* ChromaColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | 3503B8341F2689BC00750356 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 35C376C51D5CF5300069D7A1 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = 3503B82E1F2689BC00750356; 49 | remoteInfo = ChromaColorPicker; 50 | }; 51 | FC1BD8C32207D7B700817AF3 /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = 35C376C51D5CF5300069D7A1 /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = 3503B82E1F2689BC00750356; 56 | remoteInfo = ChromaColorPicker; 57 | }; 58 | FC89B7DD2325706E00D007AB /* PBXContainerItemProxy */ = { 59 | isa = PBXContainerItemProxy; 60 | containerPortal = 35C376C51D5CF5300069D7A1 /* Project object */; 61 | proxyType = 1; 62 | remoteGlobalIDString = 35C376CC1D5CF5300069D7A1; 63 | remoteInfo = Example; 64 | }; 65 | /* End PBXContainerItemProxy section */ 66 | 67 | /* Begin PBXCopyFilesBuildPhase section */ 68 | 3503B83B1F2689BC00750356 /* Embed Frameworks */ = { 69 | isa = PBXCopyFilesBuildPhase; 70 | buildActionMask = 2147483647; 71 | dstPath = ""; 72 | dstSubfolderSpec = 10; 73 | files = ( 74 | 3503B8371F2689BC00750356 /* ChromaColorPicker.framework in Embed Frameworks */, 75 | ); 76 | name = "Embed Frameworks"; 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXCopyFilesBuildPhase section */ 80 | 81 | /* Begin PBXFileReference section */ 82 | 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ChromaColorPicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 3503B8311F2689BC00750356 /* ChromaColorPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChromaColorPicker.h; sourceTree = ""; }; 84 | 3503B8321F2689BC00750356 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85 | 35C376CD1D5CF5300069D7A1 /* ChromaColorPickerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChromaColorPickerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | 35C376D01D5CF5300069D7A1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 87 | 35C376D21D5CF5300069D7A1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 88 | 35C376D51D5CF5300069D7A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 89 | 35C376D71D5CF5300069D7A1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 90 | 35C376DA1D5CF5300069D7A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 91 | 35C376DC1D5CF5300069D7A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 92 | C83A935C23760C1600564E01 /* UIColor+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Utils.swift"; sourceTree = ""; }; 93 | C83A935E23760C3800564E01 /* UIColor+UtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+UtilsTests.swift"; sourceTree = ""; }; 94 | FC1BD8BD2207D7B700817AF3 /* ChromaColorPickerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChromaColorPickerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 95 | FC1BD8BF2207D7B700817AF3 /* ChromaColorPickerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorPickerTests.swift; sourceTree = ""; }; 96 | FC1BD8C12207D7B700817AF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 97 | FC4387C52262556600F739F1 /* ChromaBrightnessSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaBrightnessSlider.swift; sourceTree = ""; }; 98 | FC4387C822625C7000F739F1 /* SliderTrackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderTrackView.swift; sourceTree = ""; }; 99 | FC4387CA22625DA800F739F1 /* SliderHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderHandleView.swift; sourceTree = ""; }; 100 | FC4387CC2262B82600F739F1 /* ChromaControlStylable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaControlStylable.swift; sourceTree = ""; }; 101 | FC4387DC226972EB00F739F1 /* UIColor+Brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Brightness.swift"; sourceTree = ""; }; 102 | FC89B7DF2325B07F00D007AB /* ChromaBrightnessSliderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaBrightnessSliderTests.swift; sourceTree = ""; }; 103 | FC89B7E12325B8B900D007AB /* FakeTouch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTouch.swift; sourceTree = ""; }; 104 | FC89B7E32325C24F00D007AB /* UIView+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utils.swift"; sourceTree = ""; }; 105 | FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWheelView.swift; sourceTree = ""; }; 106 | FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorHandle.swift; sourceTree = ""; }; 107 | FCCA42A92260329900BE2FF9 /* UIView+DropShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+DropShadow.swift"; sourceTree = ""; }; 108 | FCCA42AB226038A400BE2FF9 /* ColorWheelViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWheelViewTests.swift; sourceTree = ""; }; 109 | FCE07BFD227B4A8100920217 /* UIColor+BrightnessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+BrightnessTests.swift"; sourceTree = ""; }; 110 | FCE07C00227B4C8B00920217 /* UIColor+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+TestHelpers.swift"; sourceTree = ""; }; 111 | FCE07C03227B4DB900920217 /* UIView+DropShadowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+DropShadowTests.swift"; sourceTree = ""; }; 112 | FCE07C05227B525000920217 /* ChromaControlStylableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaControlStylableTests.swift; sourceTree = ""; }; 113 | FCE07C09228A0F6800920217 /* SliderHandleViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderHandleViewTests.swift; sourceTree = ""; }; 114 | FCE07C0B228A0F7500920217 /* ChromaColorHandleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorHandleTests.swift; sourceTree = ""; }; 115 | FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromaColorPicker.swift; sourceTree = ""; }; 116 | /* End PBXFileReference section */ 117 | 118 | /* Begin PBXFrameworksBuildPhase section */ 119 | 3503B82B1F2689BC00750356 /* Frameworks */ = { 120 | isa = PBXFrameworksBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | 35C376CA1D5CF5300069D7A1 /* Frameworks */ = { 127 | isa = PBXFrameworksBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | 3503B8361F2689BC00750356 /* ChromaColorPicker.framework in Frameworks */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | FC1BD8BA2207D7B700817AF3 /* Frameworks */ = { 135 | isa = PBXFrameworksBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | FC1BD8C22207D7B700817AF3 /* ChromaColorPicker.framework in Frameworks */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXFrameworksBuildPhase section */ 143 | 144 | /* Begin PBXGroup section */ 145 | 3503B8301F2689BC00750356 /* Source */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | FC4387C722625C5F00F739F1 /* Supporting Views */, 149 | FCCA42A82260325F00BE2FF9 /* Extensions */, 150 | 3503B8311F2689BC00750356 /* ChromaColorPicker.h */, 151 | 3503B8321F2689BC00750356 /* Info.plist */, 152 | FCEA4E262235AAA200C0A1B6 /* ChromaColorPicker.swift */, 153 | FC4387C52262556600F739F1 /* ChromaBrightnessSlider.swift */, 154 | FCCA42A4226022A800BE2FF9 /* ColorWheelView.swift */, 155 | FC4387CC2262B82600F739F1 /* ChromaControlStylable.swift */, 156 | ); 157 | path = Source; 158 | sourceTree = ""; 159 | }; 160 | 35C376C41D5CF5300069D7A1 = { 161 | isa = PBXGroup; 162 | children = ( 163 | 35C376CF1D5CF5300069D7A1 /* Example */, 164 | 3503B8301F2689BC00750356 /* Source */, 165 | FC1BD8BE2207D7B700817AF3 /* Tests */, 166 | 35C376CE1D5CF5300069D7A1 /* Products */, 167 | ); 168 | sourceTree = ""; 169 | }; 170 | 35C376CE1D5CF5300069D7A1 /* Products */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 35C376CD1D5CF5300069D7A1 /* ChromaColorPickerExample.app */, 174 | 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */, 175 | FC1BD8BD2207D7B700817AF3 /* ChromaColorPickerTests.xctest */, 176 | ); 177 | name = Products; 178 | sourceTree = ""; 179 | }; 180 | 35C376CF1D5CF5300069D7A1 /* Example */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 35C376D01D5CF5300069D7A1 /* AppDelegate.swift */, 184 | 35C376D21D5CF5300069D7A1 /* ViewController.swift */, 185 | 35C376D41D5CF5300069D7A1 /* Main.storyboard */, 186 | 35C376D71D5CF5300069D7A1 /* Assets.xcassets */, 187 | 35C376D91D5CF5300069D7A1 /* LaunchScreen.storyboard */, 188 | 35C376DC1D5CF5300069D7A1 /* Info.plist */, 189 | ); 190 | path = Example; 191 | sourceTree = ""; 192 | }; 193 | FC1BD8BE2207D7B700817AF3 /* Tests */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | FCE07BFF227B4C7A00920217 /* TestHelpers */, 197 | FCE07C02227B4D9500920217 /* Supporting Views */, 198 | FCCA42AD226038BD00BE2FF9 /* Extensions */, 199 | FC1BD8BF2207D7B700817AF3 /* ChromaColorPickerTests.swift */, 200 | FC89B7DF2325B07F00D007AB /* ChromaBrightnessSliderTests.swift */, 201 | FCCA42AB226038A400BE2FF9 /* ColorWheelViewTests.swift */, 202 | FCE07C05227B525000920217 /* ChromaControlStylableTests.swift */, 203 | FC1BD8C12207D7B700817AF3 /* Info.plist */, 204 | ); 205 | path = Tests; 206 | sourceTree = ""; 207 | }; 208 | FC4387C722625C5F00F739F1 /* Supporting Views */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | FCCA42A6226023F000BE2FF9 /* ChromaColorHandle.swift */, 212 | FC4387CA22625DA800F739F1 /* SliderHandleView.swift */, 213 | FC4387C822625C7000F739F1 /* SliderTrackView.swift */, 214 | ); 215 | path = "Supporting Views"; 216 | sourceTree = ""; 217 | }; 218 | FCCA42A82260325F00BE2FF9 /* Extensions */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | FC4387DC226972EB00F739F1 /* UIColor+Brightness.swift */, 222 | C83A935C23760C1600564E01 /* UIColor+Utils.swift */, 223 | FCCA42A92260329900BE2FF9 /* UIView+DropShadow.swift */, 224 | FC89B7E32325C24F00D007AB /* UIView+Utils.swift */, 225 | ); 226 | path = Extensions; 227 | sourceTree = ""; 228 | }; 229 | FCCA42AD226038BD00BE2FF9 /* Extensions */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | FCE07BFD227B4A8100920217 /* UIColor+BrightnessTests.swift */, 233 | C83A935E23760C3800564E01 /* UIColor+UtilsTests.swift */, 234 | FCE07C03227B4DB900920217 /* UIView+DropShadowTests.swift */, 235 | ); 236 | path = Extensions; 237 | sourceTree = ""; 238 | }; 239 | FCE07BFF227B4C7A00920217 /* TestHelpers */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | FCE07C00227B4C8B00920217 /* UIColor+TestHelpers.swift */, 243 | FC89B7E12325B8B900D007AB /* FakeTouch.swift */, 244 | ); 245 | path = TestHelpers; 246 | sourceTree = ""; 247 | }; 248 | FCE07C02227B4D9500920217 /* Supporting Views */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | FCE07C0B228A0F7500920217 /* ChromaColorHandleTests.swift */, 252 | FCE07C09228A0F6800920217 /* SliderHandleViewTests.swift */, 253 | ); 254 | path = "Supporting Views"; 255 | sourceTree = ""; 256 | }; 257 | /* End PBXGroup section */ 258 | 259 | /* Begin PBXHeadersBuildPhase section */ 260 | 3503B82C1F2689BC00750356 /* Headers */ = { 261 | isa = PBXHeadersBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | 3503B8331F2689BC00750356 /* ChromaColorPicker.h in Headers */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXHeadersBuildPhase section */ 269 | 270 | /* Begin PBXNativeTarget section */ 271 | 3503B82E1F2689BC00750356 /* ChromaColorPicker */ = { 272 | isa = PBXNativeTarget; 273 | buildConfigurationList = 3503B83A1F2689BC00750356 /* Build configuration list for PBXNativeTarget "ChromaColorPicker" */; 274 | buildPhases = ( 275 | 3503B82A1F2689BC00750356 /* Sources */, 276 | 3503B82B1F2689BC00750356 /* Frameworks */, 277 | 3503B82C1F2689BC00750356 /* Headers */, 278 | 3503B82D1F2689BC00750356 /* Resources */, 279 | ); 280 | buildRules = ( 281 | ); 282 | dependencies = ( 283 | ); 284 | name = ChromaColorPicker; 285 | productName = ChromaColorPicker; 286 | productReference = 3503B82F1F2689BC00750356 /* ChromaColorPicker.framework */; 287 | productType = "com.apple.product-type.framework"; 288 | }; 289 | 35C376CC1D5CF5300069D7A1 /* Example */ = { 290 | isa = PBXNativeTarget; 291 | buildConfigurationList = 35C376DF1D5CF5300069D7A1 /* Build configuration list for PBXNativeTarget "Example" */; 292 | buildPhases = ( 293 | 35C376C91D5CF5300069D7A1 /* Sources */, 294 | 35C376CA1D5CF5300069D7A1 /* Frameworks */, 295 | 35C376CB1D5CF5300069D7A1 /* Resources */, 296 | 3503B83B1F2689BC00750356 /* Embed Frameworks */, 297 | ); 298 | buildRules = ( 299 | ); 300 | dependencies = ( 301 | 3503B8351F2689BC00750356 /* PBXTargetDependency */, 302 | ); 303 | name = Example; 304 | productName = "JCColorPicker-Demo"; 305 | productReference = 35C376CD1D5CF5300069D7A1 /* ChromaColorPickerExample.app */; 306 | productType = "com.apple.product-type.application"; 307 | }; 308 | FC1BD8BC2207D7B700817AF3 /* ChromaColorPickerTests */ = { 309 | isa = PBXNativeTarget; 310 | buildConfigurationList = FC1BD8C52207D7B700817AF3 /* Build configuration list for PBXNativeTarget "ChromaColorPickerTests" */; 311 | buildPhases = ( 312 | FC1BD8B92207D7B700817AF3 /* Sources */, 313 | FC1BD8BA2207D7B700817AF3 /* Frameworks */, 314 | FC1BD8BB2207D7B700817AF3 /* Resources */, 315 | ); 316 | buildRules = ( 317 | ); 318 | dependencies = ( 319 | FC1BD8C42207D7B700817AF3 /* PBXTargetDependency */, 320 | FC89B7DE2325706E00D007AB /* PBXTargetDependency */, 321 | ); 322 | name = ChromaColorPickerTests; 323 | productName = ChromaColorPickerTests; 324 | productReference = FC1BD8BD2207D7B700817AF3 /* ChromaColorPickerTests.xctest */; 325 | productType = "com.apple.product-type.bundle.unit-test"; 326 | }; 327 | /* End PBXNativeTarget section */ 328 | 329 | /* Begin PBXProject section */ 330 | 35C376C51D5CF5300069D7A1 /* Project object */ = { 331 | isa = PBXProject; 332 | attributes = { 333 | LastSwiftUpdateCheck = 1010; 334 | LastUpgradeCheck = 1000; 335 | ORGANIZATIONNAME = "Jonathan Cardasis"; 336 | TargetAttributes = { 337 | 3503B82E1F2689BC00750356 = { 338 | CreatedOnToolsVersion = 8.2.1; 339 | LastSwiftMigration = 1010; 340 | ProvisioningStyle = Automatic; 341 | }; 342 | 35C376CC1D5CF5300069D7A1 = { 343 | CreatedOnToolsVersion = 7.3; 344 | DevelopmentTeam = 9H97MWKJ22; 345 | LastSwiftMigration = 1000; 346 | }; 347 | FC1BD8BC2207D7B700817AF3 = { 348 | CreatedOnToolsVersion = 10.1; 349 | DevelopmentTeam = 9H97MWKJ22; 350 | ProvisioningStyle = Automatic; 351 | TestTargetID = 35C376CC1D5CF5300069D7A1; 352 | }; 353 | }; 354 | }; 355 | buildConfigurationList = 35C376C81D5CF5300069D7A1 /* Build configuration list for PBXProject "ChromaColorPicker" */; 356 | compatibilityVersion = "Xcode 3.2"; 357 | developmentRegion = English; 358 | hasScannedForEncodings = 0; 359 | knownRegions = ( 360 | English, 361 | en, 362 | Base, 363 | ); 364 | mainGroup = 35C376C41D5CF5300069D7A1; 365 | productRefGroup = 35C376CE1D5CF5300069D7A1 /* Products */; 366 | projectDirPath = ""; 367 | projectRoot = ""; 368 | targets = ( 369 | 35C376CC1D5CF5300069D7A1 /* Example */, 370 | 3503B82E1F2689BC00750356 /* ChromaColorPicker */, 371 | FC1BD8BC2207D7B700817AF3 /* ChromaColorPickerTests */, 372 | ); 373 | }; 374 | /* End PBXProject section */ 375 | 376 | /* Begin PBXResourcesBuildPhase section */ 377 | 3503B82D1F2689BC00750356 /* Resources */ = { 378 | isa = PBXResourcesBuildPhase; 379 | buildActionMask = 2147483647; 380 | files = ( 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | }; 384 | 35C376CB1D5CF5300069D7A1 /* Resources */ = { 385 | isa = PBXResourcesBuildPhase; 386 | buildActionMask = 2147483647; 387 | files = ( 388 | 35C376DB1D5CF5300069D7A1 /* LaunchScreen.storyboard in Resources */, 389 | 35C376D81D5CF5300069D7A1 /* Assets.xcassets in Resources */, 390 | 35C376D61D5CF5300069D7A1 /* Main.storyboard in Resources */, 391 | ); 392 | runOnlyForDeploymentPostprocessing = 0; 393 | }; 394 | FC1BD8BB2207D7B700817AF3 /* Resources */ = { 395 | isa = PBXResourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | ); 399 | runOnlyForDeploymentPostprocessing = 0; 400 | }; 401 | /* End PBXResourcesBuildPhase section */ 402 | 403 | /* Begin PBXSourcesBuildPhase section */ 404 | 3503B82A1F2689BC00750356 /* Sources */ = { 405 | isa = PBXSourcesBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | FCEA4E272235AAA200C0A1B6 /* ChromaColorPicker.swift in Sources */, 409 | FC4387CD2262B82600F739F1 /* ChromaControlStylable.swift in Sources */, 410 | FC89B7E42325C24F00D007AB /* UIView+Utils.swift in Sources */, 411 | FCCA42AA2260329900BE2FF9 /* UIView+DropShadow.swift in Sources */, 412 | FC4387DE226974FD00F739F1 /* UIColor+Brightness.swift in Sources */, 413 | FC4387C62262556600F739F1 /* ChromaBrightnessSlider.swift in Sources */, 414 | C83A935D23760C1600564E01 /* UIColor+Utils.swift in Sources */, 415 | FCCA42A7226023F000BE2FF9 /* ChromaColorHandle.swift in Sources */, 416 | FCCA42A5226022A800BE2FF9 /* ColorWheelView.swift in Sources */, 417 | FC4387C922625C7000F739F1 /* SliderTrackView.swift in Sources */, 418 | FC4387CB22625DA800F739F1 /* SliderHandleView.swift in Sources */, 419 | ); 420 | runOnlyForDeploymentPostprocessing = 0; 421 | }; 422 | 35C376C91D5CF5300069D7A1 /* Sources */ = { 423 | isa = PBXSourcesBuildPhase; 424 | buildActionMask = 2147483647; 425 | files = ( 426 | 35C376D31D5CF5300069D7A1 /* ViewController.swift in Sources */, 427 | 35C376D11D5CF5300069D7A1 /* AppDelegate.swift in Sources */, 428 | ); 429 | runOnlyForDeploymentPostprocessing = 0; 430 | }; 431 | FC1BD8B92207D7B700817AF3 /* Sources */ = { 432 | isa = PBXSourcesBuildPhase; 433 | buildActionMask = 2147483647; 434 | files = ( 435 | FCE07C06227B525000920217 /* ChromaControlStylableTests.swift in Sources */, 436 | C83A936023760C5900564E01 /* UIColor+UtilsTests.swift in Sources */, 437 | FC89B7E02325B07F00D007AB /* ChromaBrightnessSliderTests.swift in Sources */, 438 | FC89B7E22325B8B900D007AB /* FakeTouch.swift in Sources */, 439 | FCE07C0C228A0F7500920217 /* ChromaColorHandleTests.swift in Sources */, 440 | FC1BD8C02207D7B700817AF3 /* ChromaColorPickerTests.swift in Sources */, 441 | FCE07BFE227B4A8100920217 /* UIColor+BrightnessTests.swift in Sources */, 442 | FCE07C0A228A0F6800920217 /* SliderHandleViewTests.swift in Sources */, 443 | FC4387C422603AE900F739F1 /* ColorWheelViewTests.swift in Sources */, 444 | FCE07C01227B4C8B00920217 /* UIColor+TestHelpers.swift in Sources */, 445 | FCE07C04227B4DB900920217 /* UIView+DropShadowTests.swift in Sources */, 446 | ); 447 | runOnlyForDeploymentPostprocessing = 0; 448 | }; 449 | /* End PBXSourcesBuildPhase section */ 450 | 451 | /* Begin PBXTargetDependency section */ 452 | 3503B8351F2689BC00750356 /* PBXTargetDependency */ = { 453 | isa = PBXTargetDependency; 454 | target = 3503B82E1F2689BC00750356 /* ChromaColorPicker */; 455 | targetProxy = 3503B8341F2689BC00750356 /* PBXContainerItemProxy */; 456 | }; 457 | FC1BD8C42207D7B700817AF3 /* PBXTargetDependency */ = { 458 | isa = PBXTargetDependency; 459 | target = 3503B82E1F2689BC00750356 /* ChromaColorPicker */; 460 | targetProxy = FC1BD8C32207D7B700817AF3 /* PBXContainerItemProxy */; 461 | }; 462 | FC89B7DE2325706E00D007AB /* PBXTargetDependency */ = { 463 | isa = PBXTargetDependency; 464 | target = 35C376CC1D5CF5300069D7A1 /* Example */; 465 | targetProxy = FC89B7DD2325706E00D007AB /* PBXContainerItemProxy */; 466 | }; 467 | /* End PBXTargetDependency section */ 468 | 469 | /* Begin PBXVariantGroup section */ 470 | 35C376D41D5CF5300069D7A1 /* Main.storyboard */ = { 471 | isa = PBXVariantGroup; 472 | children = ( 473 | 35C376D51D5CF5300069D7A1 /* Base */, 474 | ); 475 | name = Main.storyboard; 476 | sourceTree = ""; 477 | }; 478 | 35C376D91D5CF5300069D7A1 /* LaunchScreen.storyboard */ = { 479 | isa = PBXVariantGroup; 480 | children = ( 481 | 35C376DA1D5CF5300069D7A1 /* Base */, 482 | ); 483 | name = LaunchScreen.storyboard; 484 | sourceTree = ""; 485 | }; 486 | /* End PBXVariantGroup section */ 487 | 488 | /* Begin XCBuildConfiguration section */ 489 | 3503B8381F2689BC00750356 /* Debug */ = { 490 | isa = XCBuildConfiguration; 491 | buildSettings = { 492 | CLANG_ENABLE_MODULES = YES; 493 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 494 | CODE_SIGN_IDENTITY = "iPhone Developer"; 495 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 496 | CURRENT_PROJECT_VERSION = 1; 497 | DEFINES_MODULE = YES; 498 | DYLIB_COMPATIBILITY_VERSION = 1; 499 | DYLIB_CURRENT_VERSION = 1; 500 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 501 | INFOPLIST_FILE = Source/Info.plist; 502 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 503 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 504 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 505 | PRODUCT_BUNDLE_IDENTIFIER = com.jonathancardasis.ChromaColorPicker; 506 | PRODUCT_NAME = "$(TARGET_NAME)"; 507 | SKIP_INSTALL = YES; 508 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 509 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 510 | SWIFT_VERSION = 5.0; 511 | TARGETED_DEVICE_FAMILY = "1,2"; 512 | VERSIONING_SYSTEM = "apple-generic"; 513 | VERSION_INFO_PREFIX = ""; 514 | }; 515 | name = Debug; 516 | }; 517 | 3503B8391F2689BC00750356 /* Release */ = { 518 | isa = XCBuildConfiguration; 519 | buildSettings = { 520 | CLANG_ENABLE_MODULES = YES; 521 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 522 | CODE_SIGN_IDENTITY = "iPhone Developer"; 523 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 524 | CURRENT_PROJECT_VERSION = 1; 525 | DEFINES_MODULE = YES; 526 | DYLIB_COMPATIBILITY_VERSION = 1; 527 | DYLIB_CURRENT_VERSION = 1; 528 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 529 | INFOPLIST_FILE = Source/Info.plist; 530 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 531 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 532 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 533 | PRODUCT_BUNDLE_IDENTIFIER = com.jonathancardasis.ChromaColorPicker; 534 | PRODUCT_NAME = "$(TARGET_NAME)"; 535 | SKIP_INSTALL = YES; 536 | SWIFT_VERSION = 5.0; 537 | TARGETED_DEVICE_FAMILY = "1,2"; 538 | VERSIONING_SYSTEM = "apple-generic"; 539 | VERSION_INFO_PREFIX = ""; 540 | }; 541 | name = Release; 542 | }; 543 | 35C376DD1D5CF5300069D7A1 /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ALWAYS_SEARCH_USER_PATHS = NO; 547 | CLANG_ANALYZER_NONNULL = YES; 548 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 549 | CLANG_CXX_LIBRARY = "libc++"; 550 | CLANG_ENABLE_MODULES = YES; 551 | CLANG_ENABLE_OBJC_ARC = YES; 552 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 553 | CLANG_WARN_BOOL_CONVERSION = YES; 554 | CLANG_WARN_COMMA = YES; 555 | CLANG_WARN_CONSTANT_CONVERSION = YES; 556 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 557 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 558 | CLANG_WARN_EMPTY_BODY = YES; 559 | CLANG_WARN_ENUM_CONVERSION = YES; 560 | CLANG_WARN_INFINITE_RECURSION = YES; 561 | CLANG_WARN_INT_CONVERSION = YES; 562 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 563 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 564 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 565 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 566 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 567 | CLANG_WARN_STRICT_PROTOTYPES = YES; 568 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 569 | CLANG_WARN_UNREACHABLE_CODE = YES; 570 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 571 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 572 | COPY_PHASE_STRIP = NO; 573 | DEBUG_INFORMATION_FORMAT = dwarf; 574 | ENABLE_STRICT_OBJC_MSGSEND = YES; 575 | ENABLE_TESTABILITY = YES; 576 | GCC_C_LANGUAGE_STANDARD = gnu99; 577 | GCC_DYNAMIC_NO_PIC = NO; 578 | GCC_NO_COMMON_BLOCKS = YES; 579 | GCC_OPTIMIZATION_LEVEL = 0; 580 | GCC_PREPROCESSOR_DEFINITIONS = ( 581 | "DEBUG=1", 582 | "$(inherited)", 583 | ); 584 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 585 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 586 | GCC_WARN_UNDECLARED_SELECTOR = YES; 587 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 588 | GCC_WARN_UNUSED_FUNCTION = YES; 589 | GCC_WARN_UNUSED_VARIABLE = YES; 590 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 591 | MTL_ENABLE_DEBUG_INFO = YES; 592 | ONLY_ACTIVE_ARCH = YES; 593 | SDKROOT = iphoneos; 594 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 595 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 596 | }; 597 | name = Debug; 598 | }; 599 | 35C376DE1D5CF5300069D7A1 /* Release */ = { 600 | isa = XCBuildConfiguration; 601 | buildSettings = { 602 | ALWAYS_SEARCH_USER_PATHS = NO; 603 | CLANG_ANALYZER_NONNULL = YES; 604 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 605 | CLANG_CXX_LIBRARY = "libc++"; 606 | CLANG_ENABLE_MODULES = YES; 607 | CLANG_ENABLE_OBJC_ARC = YES; 608 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 609 | CLANG_WARN_BOOL_CONVERSION = YES; 610 | CLANG_WARN_COMMA = YES; 611 | CLANG_WARN_CONSTANT_CONVERSION = YES; 612 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 613 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 614 | CLANG_WARN_EMPTY_BODY = YES; 615 | CLANG_WARN_ENUM_CONVERSION = YES; 616 | CLANG_WARN_INFINITE_RECURSION = YES; 617 | CLANG_WARN_INT_CONVERSION = YES; 618 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 619 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 620 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 621 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 622 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 623 | CLANG_WARN_STRICT_PROTOTYPES = YES; 624 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 625 | CLANG_WARN_UNREACHABLE_CODE = YES; 626 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 627 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 628 | COPY_PHASE_STRIP = NO; 629 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 630 | ENABLE_NS_ASSERTIONS = NO; 631 | ENABLE_STRICT_OBJC_MSGSEND = YES; 632 | GCC_C_LANGUAGE_STANDARD = gnu99; 633 | GCC_NO_COMMON_BLOCKS = YES; 634 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 635 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 636 | GCC_WARN_UNDECLARED_SELECTOR = YES; 637 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 638 | GCC_WARN_UNUSED_FUNCTION = YES; 639 | GCC_WARN_UNUSED_VARIABLE = YES; 640 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 641 | MTL_ENABLE_DEBUG_INFO = NO; 642 | SDKROOT = iphoneos; 643 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 644 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 645 | VALIDATE_PRODUCT = YES; 646 | }; 647 | name = Release; 648 | }; 649 | 35C376E01D5CF5300069D7A1 /* Debug */ = { 650 | isa = XCBuildConfiguration; 651 | buildSettings = { 652 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 653 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 654 | CODE_SIGN_IDENTITY = "iPhone Developer"; 655 | DEFINES_MODULE = NO; 656 | DEVELOPMENT_TEAM = 9H97MWKJ22; 657 | INFOPLIST_FILE = Example/Info.plist; 658 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 659 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 660 | PRODUCT_BUNDLE_IDENTIFIER = com.jonathancardasis.ChromaColorPickerExample; 661 | PRODUCT_NAME = ChromaColorPickerExample; 662 | SWIFT_VERSION = 5.0; 663 | }; 664 | name = Debug; 665 | }; 666 | 35C376E11D5CF5300069D7A1 /* Release */ = { 667 | isa = XCBuildConfiguration; 668 | buildSettings = { 669 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 670 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 671 | CODE_SIGN_IDENTITY = "iPhone Developer"; 672 | DEFINES_MODULE = NO; 673 | DEVELOPMENT_TEAM = 9H97MWKJ22; 674 | INFOPLIST_FILE = Example/Info.plist; 675 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 676 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 677 | PRODUCT_BUNDLE_IDENTIFIER = com.jonathancardasis.ChromaColorPickerExample; 678 | PRODUCT_NAME = ChromaColorPickerExample; 679 | SWIFT_VERSION = 5.0; 680 | }; 681 | name = Release; 682 | }; 683 | FC1BD8C62207D7B700817AF3 /* Debug */ = { 684 | isa = XCBuildConfiguration; 685 | buildSettings = { 686 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 687 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 688 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 689 | CLANG_ENABLE_OBJC_WEAK = YES; 690 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 691 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 692 | CODE_SIGN_IDENTITY = "iPhone Developer"; 693 | CODE_SIGN_STYLE = Automatic; 694 | DEVELOPMENT_TEAM = 9H97MWKJ22; 695 | GCC_C_LANGUAGE_STANDARD = gnu11; 696 | INFOPLIST_FILE = Tests/Info.plist; 697 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 698 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 699 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 700 | MTL_FAST_MATH = YES; 701 | PRODUCT_BUNDLE_IDENTIFIER = com.jonathancardasis.ChromaColorPickerTests; 702 | PRODUCT_NAME = "$(TARGET_NAME)"; 703 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 704 | SWIFT_VERSION = 5.0; 705 | TARGETED_DEVICE_FAMILY = "1,2"; 706 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChromaColorPickerExample.app/ChromaColorPickerExample"; 707 | }; 708 | name = Debug; 709 | }; 710 | FC1BD8C72207D7B700817AF3 /* Release */ = { 711 | isa = XCBuildConfiguration; 712 | buildSettings = { 713 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 714 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 715 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 716 | CLANG_ENABLE_OBJC_WEAK = YES; 717 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 718 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 719 | CODE_SIGN_IDENTITY = "iPhone Developer"; 720 | CODE_SIGN_STYLE = Automatic; 721 | DEVELOPMENT_TEAM = 9H97MWKJ22; 722 | GCC_C_LANGUAGE_STANDARD = gnu11; 723 | INFOPLIST_FILE = Tests/Info.plist; 724 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 725 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 726 | MTL_FAST_MATH = YES; 727 | PRODUCT_BUNDLE_IDENTIFIER = com.jonathancardasis.ChromaColorPickerTests; 728 | PRODUCT_NAME = "$(TARGET_NAME)"; 729 | SWIFT_VERSION = 5.0; 730 | TARGETED_DEVICE_FAMILY = "1,2"; 731 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChromaColorPickerExample.app/ChromaColorPickerExample"; 732 | }; 733 | name = Release; 734 | }; 735 | /* End XCBuildConfiguration section */ 736 | 737 | /* Begin XCConfigurationList section */ 738 | 3503B83A1F2689BC00750356 /* Build configuration list for PBXNativeTarget "ChromaColorPicker" */ = { 739 | isa = XCConfigurationList; 740 | buildConfigurations = ( 741 | 3503B8381F2689BC00750356 /* Debug */, 742 | 3503B8391F2689BC00750356 /* Release */, 743 | ); 744 | defaultConfigurationIsVisible = 0; 745 | defaultConfigurationName = Release; 746 | }; 747 | 35C376C81D5CF5300069D7A1 /* Build configuration list for PBXProject "ChromaColorPicker" */ = { 748 | isa = XCConfigurationList; 749 | buildConfigurations = ( 750 | 35C376DD1D5CF5300069D7A1 /* Debug */, 751 | 35C376DE1D5CF5300069D7A1 /* Release */, 752 | ); 753 | defaultConfigurationIsVisible = 0; 754 | defaultConfigurationName = Release; 755 | }; 756 | 35C376DF1D5CF5300069D7A1 /* Build configuration list for PBXNativeTarget "Example" */ = { 757 | isa = XCConfigurationList; 758 | buildConfigurations = ( 759 | 35C376E01D5CF5300069D7A1 /* Debug */, 760 | 35C376E11D5CF5300069D7A1 /* Release */, 761 | ); 762 | defaultConfigurationIsVisible = 0; 763 | defaultConfigurationName = Release; 764 | }; 765 | FC1BD8C52207D7B700817AF3 /* Build configuration list for PBXNativeTarget "ChromaColorPickerTests" */ = { 766 | isa = XCConfigurationList; 767 | buildConfigurations = ( 768 | FC1BD8C62207D7B700817AF3 /* Debug */, 769 | FC1BD8C72207D7B700817AF3 /* Release */, 770 | ); 771 | defaultConfigurationIsVisible = 0; 772 | defaultConfigurationName = Release; 773 | }; 774 | /* End XCConfigurationList section */ 775 | }; 776 | rootObject = 35C376C51D5CF5300069D7A1 /* Project object */; 777 | } 778 | -------------------------------------------------------------------------------- /ChromaColorPicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ChromaColorPicker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ChromaColorPicker.xcodeproj/xcshareddata/xcschemes/ChromaColorPicker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /ChromaColorPicker.xcodeproj/xcshareddata/xcschemes/ChromaColorPickerTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ChromaColorPicker-Demo 4 | // 5 | // Created by Cardasis, Jonathan (J.) on 8/11/16. 6 | // Copyright © 2016 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "home.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/home.imageset/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncardasis/ChromaColorPicker/4fa7a0b3cb779d97b771a5bf0de8927d6a37153f/Example/Assets.xcassets/home.imageset/home.png -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ChromaColorPicker-Demo 4 | // 5 | // Created by Cardasis, Jonathan (J.) on 8/11/16. 6 | // Copyright © 2016 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ChromaColorPicker 11 | 12 | class ViewController: UIViewController { 13 | @IBOutlet weak var colorDisplayView: UIView! 14 | 15 | let colorPicker = ChromaColorPicker() 16 | let brightnessSlider = ChromaBrightnessSlider() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | setupColorPicker() 21 | setupBrightnessSlider() 22 | setupColorPickerHandles() 23 | } 24 | 25 | override var preferredStatusBarStyle: UIStatusBarStyle { 26 | return .lightContent 27 | } 28 | 29 | // MARK: - Private 30 | private var homeHandle: ChromaColorHandle! // reference to home handle 31 | 32 | private func setupColorPicker() { 33 | colorPicker.delegate = self 34 | colorPicker.translatesAutoresizingMaskIntoConstraints = false 35 | view.addSubview(colorPicker) 36 | 37 | let verticalOffset = -defaultColorPickerSize.height / 6 38 | 39 | NSLayoutConstraint.activate([ 40 | colorPicker.centerXAnchor.constraint(equalTo: view.centerXAnchor), 41 | colorPicker.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: verticalOffset), 42 | colorPicker.widthAnchor.constraint(equalToConstant: defaultColorPickerSize.width), 43 | colorPicker.heightAnchor.constraint(equalToConstant: defaultColorPickerSize.height) 44 | ]) 45 | } 46 | 47 | private func setupBrightnessSlider() { 48 | brightnessSlider.connect(to: colorPicker) 49 | 50 | // Style 51 | brightnessSlider.trackColor = UIColor.blue 52 | brightnessSlider.handle.borderWidth = 3.0 // Example of customizing the handle's properties. 53 | 54 | // Layout 55 | brightnessSlider.translatesAutoresizingMaskIntoConstraints = false 56 | view.addSubview(brightnessSlider) 57 | 58 | NSLayoutConstraint.activate([ 59 | brightnessSlider.centerXAnchor.constraint(equalTo: colorPicker.centerXAnchor), 60 | brightnessSlider.topAnchor.constraint(equalTo: colorPicker.bottomAnchor, constant: 28), 61 | brightnessSlider.widthAnchor.constraint(equalTo: colorPicker.widthAnchor, multiplier: 0.9), 62 | brightnessSlider.heightAnchor.constraint(equalTo: brightnessSlider.widthAnchor, multiplier: brightnessSliderWidthHeightRatio) 63 | ]) 64 | } 65 | 66 | private func setupColorPickerHandles() { 67 | // (Optional) Assign a custom handle size - all handles appear as the same size 68 | // colorPicker.handleSize = CGSize(width: 48, height: 60) 69 | 70 | // 1. Add handle and then customize 71 | addHomeHandle() 72 | 73 | // 2. Add a handle via a color 74 | let peachColor = UIColor(red: 1, green: 203 / 255, blue: 164 / 255, alpha: 1) 75 | colorPicker.addHandle(at: peachColor) 76 | 77 | // 3. Create a custom handle and add to picker 78 | let customHandle = ChromaColorHandle() 79 | customHandle.color = UIColor.purple 80 | colorPicker.addHandle(customHandle) 81 | } 82 | 83 | private func addHomeHandle() { 84 | homeHandle = colorPicker.addHandle(at: .blue) 85 | 86 | // Setup custom handle view with insets 87 | let customImageView = UIImageView(image: #imageLiteral(resourceName: "home").withRenderingMode(.alwaysTemplate)) 88 | customImageView.contentMode = .scaleAspectFit 89 | customImageView.tintColor = .white 90 | homeHandle.accessoryView = customImageView 91 | homeHandle.accessoryViewEdgeInsets = UIEdgeInsets(top: 2, left: 4, bottom: 4, right: 4) 92 | } 93 | } 94 | 95 | extension ViewController: ChromaColorPickerDelegate { 96 | func colorPickerHandleDidChange(_ colorPicker: ChromaColorPicker, handle: ChromaColorHandle, to color: UIColor) { 97 | colorDisplayView.backgroundColor = color 98 | 99 | // Here I can detect when the color is too bright to show a white icon 100 | // on the handle and change its tintColor. 101 | if handle === homeHandle, let imageView = homeHandle.accessoryView as? UIImageView { 102 | let colorIsBright = color.isLight 103 | 104 | UIView.animate(withDuration: 0.2, animations: { 105 | imageView.tintColor = colorIsBright ? .black : .white 106 | }, completion: nil) 107 | } 108 | } 109 | } 110 | 111 | 112 | private let defaultColorPickerSize = CGSize(width: 320, height: 320) 113 | private let brightnessSliderWidthHeightRatio: CGFloat = 0.1 114 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "cocoapods" 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.1) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | aws-eventstream (1.0.3) 17 | aws-sdk (2.11.463) 18 | aws-sdk-resources (= 2.11.463) 19 | aws-sdk-core (2.11.463) 20 | aws-sigv4 (~> 1.0) 21 | jmespath (~> 1.0) 22 | aws-sdk-resources (2.11.463) 23 | aws-sdk-core (= 2.11.463) 24 | aws-sigv4 (1.1.1) 25 | aws-eventstream (~> 1.0, >= 1.0.2) 26 | babosa (1.0.3) 27 | claide (1.0.3) 28 | cocoapods (1.9.1) 29 | activesupport (>= 4.0.2, < 5) 30 | claide (>= 1.0.2, < 2.0) 31 | cocoapods-core (= 1.9.1) 32 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 33 | cocoapods-downloader (>= 1.2.2, < 2.0) 34 | cocoapods-plugins (>= 1.0.0, < 2.0) 35 | cocoapods-search (>= 1.0.0, < 2.0) 36 | cocoapods-stats (>= 1.0.0, < 2.0) 37 | cocoapods-trunk (>= 1.4.0, < 2.0) 38 | cocoapods-try (>= 1.1.0, < 2.0) 39 | colored2 (~> 3.1) 40 | escape (~> 0.0.4) 41 | fourflusher (>= 2.3.0, < 3.0) 42 | gh_inspector (~> 1.0) 43 | molinillo (~> 0.6.6) 44 | nap (~> 1.0) 45 | ruby-macho (~> 1.4) 46 | xcodeproj (>= 1.14.0, < 2.0) 47 | cocoapods-core (1.9.1) 48 | activesupport (>= 4.0.2, < 6) 49 | algoliasearch (~> 1.0) 50 | concurrent-ruby (~> 1.1) 51 | fuzzy_match (~> 2.0.4) 52 | nap (~> 1.0) 53 | netrc (~> 0.11) 54 | typhoeus (~> 1.0) 55 | cocoapods-deintegrate (1.0.4) 56 | cocoapods-downloader (1.3.0) 57 | cocoapods-plugins (1.0.0) 58 | nap 59 | cocoapods-search (1.0.0) 60 | cocoapods-stats (1.1.0) 61 | cocoapods-trunk (1.4.1) 62 | nap (>= 0.8, < 2.0) 63 | netrc (~> 0.11) 64 | cocoapods-try (1.1.0) 65 | colored (1.2) 66 | colored2 (3.1.2) 67 | commander-fastlane (4.4.6) 68 | highline (~> 1.7.2) 69 | concurrent-ruby (1.1.6) 70 | declarative (0.0.10) 71 | declarative-option (0.1.0) 72 | digest-crc (0.5.1) 73 | domain_name (0.5.20190701) 74 | unf (>= 0.0.5, < 1.0.0) 75 | dotenv (2.7.5) 76 | emoji_regex (1.0.1) 77 | escape (0.0.4) 78 | ethon (0.12.0) 79 | ffi (>= 1.3.0) 80 | excon (0.72.0) 81 | faraday (0.17.3) 82 | multipart-post (>= 1.2, < 3) 83 | faraday-cookie_jar (0.0.6) 84 | faraday (>= 0.7.4) 85 | http-cookie (~> 1.0.0) 86 | faraday_middleware (0.13.1) 87 | faraday (>= 0.7.4, < 1.0) 88 | fastimage (2.1.7) 89 | fastlane (2.143.0) 90 | CFPropertyList (>= 2.3, < 4.0.0) 91 | addressable (>= 2.3, < 3.0.0) 92 | aws-sdk (~> 2.3) 93 | babosa (>= 1.0.2, < 2.0.0) 94 | bundler (>= 1.12.0, < 3.0.0) 95 | colored 96 | commander-fastlane (>= 4.4.6, < 5.0.0) 97 | dotenv (>= 2.1.1, < 3.0.0) 98 | emoji_regex (>= 0.1, < 2.0) 99 | excon (>= 0.71.0, < 1.0.0) 100 | faraday (~> 0.17) 101 | faraday-cookie_jar (~> 0.0.6) 102 | faraday_middleware (~> 0.13.1) 103 | fastimage (>= 2.1.0, < 3.0.0) 104 | gh_inspector (>= 1.1.2, < 2.0.0) 105 | google-api-client (>= 0.29.2, < 0.37.0) 106 | google-cloud-storage (>= 1.15.0, < 2.0.0) 107 | highline (>= 1.7.2, < 2.0.0) 108 | json (< 3.0.0) 109 | jwt (~> 2.1.0) 110 | mini_magick (>= 4.9.4, < 5.0.0) 111 | multi_xml (~> 0.5) 112 | multipart-post (~> 2.0.0) 113 | plist (>= 3.1.0, < 4.0.0) 114 | public_suffix (~> 2.0.0) 115 | rubyzip (>= 1.3.0, < 2.0.0) 116 | security (= 0.1.3) 117 | simctl (~> 1.6.3) 118 | slack-notifier (>= 2.0.0, < 3.0.0) 119 | terminal-notifier (>= 2.0.0, < 3.0.0) 120 | terminal-table (>= 1.4.5, < 2.0.0) 121 | tty-screen (>= 0.6.3, < 1.0.0) 122 | tty-spinner (>= 0.8.0, < 1.0.0) 123 | word_wrap (~> 1.0.0) 124 | xcodeproj (>= 1.13.0, < 2.0.0) 125 | xcpretty (~> 0.3.0) 126 | xcpretty-travis-formatter (>= 0.0.3) 127 | ffi (1.12.2) 128 | fourflusher (2.3.1) 129 | fuzzy_match (2.0.4) 130 | gh_inspector (1.1.3) 131 | google-api-client (0.36.4) 132 | addressable (~> 2.5, >= 2.5.1) 133 | googleauth (~> 0.9) 134 | httpclient (>= 2.8.1, < 3.0) 135 | mini_mime (~> 1.0) 136 | representable (~> 3.0) 137 | retriable (>= 2.0, < 4.0) 138 | signet (~> 0.12) 139 | google-cloud-core (1.5.0) 140 | google-cloud-env (~> 1.0) 141 | google-cloud-errors (~> 1.0) 142 | google-cloud-env (1.3.1) 143 | faraday (>= 0.17.3, < 2.0) 144 | google-cloud-errors (1.0.0) 145 | google-cloud-storage (1.25.1) 146 | addressable (~> 2.5) 147 | digest-crc (~> 0.4) 148 | google-api-client (~> 0.33) 149 | google-cloud-core (~> 1.2) 150 | googleauth (~> 0.9) 151 | mini_mime (~> 1.0) 152 | googleauth (0.11.0) 153 | faraday (>= 0.17.3, < 2.0) 154 | jwt (>= 1.4, < 3.0) 155 | memoist (~> 0.16) 156 | multi_json (~> 1.11) 157 | os (>= 0.9, < 2.0) 158 | signet (~> 0.12) 159 | highline (1.7.10) 160 | http-cookie (1.0.3) 161 | domain_name (~> 0.5) 162 | httpclient (2.8.3) 163 | i18n (0.9.5) 164 | concurrent-ruby (~> 1.0) 165 | jmespath (1.4.0) 166 | json (2.3.0) 167 | jwt (2.1.0) 168 | memoist (0.16.2) 169 | mini_magick (4.10.1) 170 | mini_mime (1.0.2) 171 | minitest (5.14.0) 172 | molinillo (0.6.6) 173 | multi_json (1.14.1) 174 | multi_xml (0.6.0) 175 | multipart-post (2.0.0) 176 | nanaimo (0.2.6) 177 | nap (1.1.0) 178 | naturally (2.2.0) 179 | netrc (0.11.0) 180 | os (1.0.1) 181 | plist (3.5.0) 182 | public_suffix (2.0.5) 183 | representable (3.0.4) 184 | declarative (< 0.1.0) 185 | declarative-option (< 0.2.0) 186 | uber (< 0.2.0) 187 | retriable (3.1.2) 188 | rouge (2.0.7) 189 | ruby-macho (1.4.0) 190 | rubyzip (1.3.0) 191 | security (0.1.3) 192 | signet (0.13.0) 193 | addressable (~> 2.3) 194 | faraday (>= 0.17.3, < 2.0) 195 | jwt (>= 1.5, < 3.0) 196 | multi_json (~> 1.10) 197 | simctl (1.6.8) 198 | CFPropertyList 199 | naturally 200 | slack-notifier (2.3.2) 201 | terminal-notifier (2.0.0) 202 | terminal-table (1.8.0) 203 | unicode-display_width (~> 1.1, >= 1.1.1) 204 | thread_safe (0.3.6) 205 | tty-cursor (0.7.1) 206 | tty-screen (0.7.1) 207 | tty-spinner (0.9.3) 208 | tty-cursor (~> 0.7) 209 | typhoeus (1.3.1) 210 | ethon (>= 0.9.0) 211 | tzinfo (1.2.6) 212 | thread_safe (~> 0.1) 213 | uber (0.1.0) 214 | unf (0.1.4) 215 | unf_ext 216 | unf_ext (0.0.7.6) 217 | unicode-display_width (1.7.0) 218 | word_wrap (1.0.0) 219 | xcodeproj (1.15.0) 220 | CFPropertyList (>= 2.3.3, < 4.0) 221 | atomos (~> 0.1.3) 222 | claide (>= 1.0.2, < 2.0) 223 | colored2 (~> 3.1) 224 | nanaimo (~> 0.2.6) 225 | xcpretty (0.3.0) 226 | rouge (~> 2.0.7) 227 | xcpretty-travis-formatter (1.0.0) 228 | xcpretty (~> 0.2, >= 0.0.7) 229 | 230 | PLATFORMS 231 | ruby 232 | 233 | DEPENDENCIES 234 | cocoapods 235 | fastlane 236 | 237 | BUNDLED WITH 238 | 2.0.2 239 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jonathan Cardasis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ChromaColorPicker", 7 | platforms: [ 8 | .iOS(.v10) 9 | ], 10 | products: [ 11 | .library( 12 | name: "ChromaColorPicker", 13 | targets: ["ChromaColorPicker"] 14 | ) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "ChromaColorPicker", 19 | path: "Source" 20 | ), 21 | .testTarget( 22 | name: "ChromaColorPickerTests", 23 | dependencies: ["ChromaColorPicker"], 24 | path: "Tests" 25 | ) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ChromaColorPicker 2.0 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | An intuitive HSB color picker built in Swift. Supports multiple selection handles and is customizable to your needs. 15 | 16 |

17 | ChromaColorPicker GIF 18 |

19 | 20 |
21 | Looking for version 1.x? 22 | Version 1.x.x can be found on the legacy branch. While the pod is still available, it is deprecated and projects should migrate to 2.0.
23 | 24 |
25 | 26 | ## Examples 27 | ```Swift 28 | let colorPicker = ChromaColorPicker(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 29 | addSubview(colorPicker) 30 | 31 | // Optional: Attach a ChromaBrightnessSlider to a ChromaColorPicker 32 | let brightnessSlider = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 280, height: 32)) 33 | addSubview(brightnessSlider) 34 | 35 | colorPicker.connect(brightnessSlider) // or `brightnessSlider.connect(to: colorPicker)` 36 | ``` 37 | 38 | - View the _Example_ app for more. 39 | 40 | ## Usage 41 | ### Multiple Handles 42 | ```Swift 43 | // Add handle at color 44 | let peachColor = UIColor(red: 1, green: 203 / 255, blue: 164 / 255, alpha: 1) 45 | colorPicker.addHandle(at: peachColor) 46 | 47 | // Add handle with reference 48 | let customHandle = ChromaColorHandle() 49 | customHandle.color = UIColor.purple 50 | colorPicker.addHandle(customHandle) 51 | 52 | // Add handle and keep reference 53 | let handle = colorPicker.addHandle(at: .blue) 54 | ``` 55 | 56 | ### Custom Handle Icon 57 | ```Swift 58 | let homeHandle = ChomaColorHandle(color: .blue) 59 | let imageView = UIImageView(image: #imageLiteral(resourceName: "home-icon").withRenderingMode(.alwaysTemplate)) 60 | imageView.contentMode = .scaleAspectFit 61 | imageView.tintColor = .white 62 | homeHandle.accessoryView = imageView 63 | homeHandle.accessoryViewEdgeInsets = UIEdgeInsets(top: 2, left: 4, bottom: 4, right: 4) 64 | 65 | colorPicker.addHandle(homeHandle) 66 | ``` 67 | 68 | ## Installation 69 | ### Carthage 70 | ```bash 71 | github "joncardasis/ChromaColorPicker" 72 | ``` 73 | 74 | ### Cocoapods 75 | ```bash 76 | pod 'ChromaColorPicker' 77 | ``` 78 | ### Manually 79 | Add all files from the `Source` folder to your project. 80 | 81 | ## Components 82 | | Component | Description | 83 | | :-------: | :---------: | 84 | | ChromaColorPicker | An HSB color picker with support for adding multiple color selection handles. | 85 | | ChromaBrightnessSlider | A slider UIControl which can be attached to any `ChromaColorPicker` via the `connect(to:)` method. ChromaBrightnessSlider can also function as a stand-alone UIControl. | 86 | 87 | ## Supported UIControlEvents 88 | Both `ChromaBrightnessSlider` and `ChromaColorPicker` conform to UIControl. Each send UIControlEvents which can be observed via via `UIControl`'s `addTarget` method. 89 | 90 | _ChromaColorPicker_ 91 | | Event | Description | 92 | | :-----------------:|:-------------| 93 | | `.valueChanged` | Called whenever the color has changed. | 94 | | `.touchUpInside` | Called when a handle is released. | 95 | 96 | _ChromaBrightnessSlider_ 97 | | Event | Description | 98 | | :-----------------:|:-------------| 99 | | `.touchDown` | Called when a the slider is grabbed. | 100 | | `.valueChanged` | Called whenever the slider is moved and the value has changed. | 101 | | `.touchUpInside` | Called when the slider handle is released. | 102 | 103 | ```Swift 104 | // Example 105 | brightnessSlider.addTarget(self, action: #selector(sliderDidValueChange(_:)), for: .valueChanged) 106 | 107 | @objc func sliderDidValueChange(_ slider: ChromaBrightnessSlider) { 108 | print("new color: \(slider.currentColor)") 109 | } 110 | ``` 111 | 112 | ## License 113 | ChromaColorPicker is available under the MIT license. See the LICENSE file for more info. 114 | -------------------------------------------------------------------------------- /Source/ChromaBrightnessSlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaBrightnessSlider.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/13/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class ChromaBrightnessSlider: UIControl, ChromaControlStylable { 12 | 13 | /// The value of the slider between [0.0, 1.0]. 14 | public var currentValue: CGFloat = 0.0 { 15 | didSet { updateControl(to: currentValue) } 16 | } 17 | 18 | /// The base color the slider on the track. 19 | public var trackColor: UIColor = .white { 20 | didSet { updateTrackColor(to: trackColor) } 21 | } 22 | 23 | /// The value of the color the handle is currently displaying. 24 | public var currentColor: UIColor { 25 | return handle.handleColor 26 | } 27 | 28 | /// The handle control of the slider. 29 | public let handle = SliderHandleView() 30 | 31 | public var borderWidth: CGFloat = 4.0 { 32 | didSet { layoutNow() } 33 | } 34 | 35 | public var borderColor: UIColor = .white { 36 | didSet { layoutNow() } 37 | } 38 | 39 | public var showsShadow: Bool = true { 40 | didSet { layoutNow() } 41 | } 42 | 43 | //MARK: - Initialization 44 | 45 | public override init(frame: CGRect) { 46 | super.init(frame: frame) 47 | commonInit() 48 | } 49 | 50 | public required init?(coder aDecoder: NSCoder) { 51 | super.init(coder: aDecoder) 52 | commonInit() 53 | } 54 | 55 | public override func layoutSubviews() { 56 | super.layoutSubviews() 57 | sliderTrackView.layer.cornerRadius = sliderTrackView.bounds.height / 2.0 58 | sliderTrackView.layer.borderColor = borderColor.cgColor 59 | sliderTrackView.layer.borderWidth = borderWidth 60 | 61 | moveHandle(to: currentValue) 62 | updateShadowIfNeeded() 63 | } 64 | 65 | // MARK: - Public 66 | 67 | /// Attaches control to the provided color picker. 68 | public func connect(to colorPicker: ChromaColorPicker) { 69 | colorPicker.connect(self) 70 | } 71 | 72 | /// Returns the relative value on the slider [0.0, 1.0] for the given color brightness ([0.0, 1.0]). 73 | public func value(brightness: CGFloat) -> CGFloat { 74 | let clamedBrightness = max(0, min(brightness, 1.0)) 75 | return 1.0 - clamedBrightness 76 | } 77 | 78 | // MARK: - Control 79 | 80 | public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { 81 | let location = touch.location(in: self) 82 | let shouldBeginTracking = interactableBounds.contains(location) 83 | if shouldBeginTracking { 84 | sendActions(for: .touchDown) 85 | } 86 | return shouldBeginTracking 87 | } 88 | 89 | public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { 90 | let location = touch.location(in: self) 91 | let clampedPositionX: CGFloat = max(0, min(location.x, confiningTrackFrame.width)) 92 | let value = clampedPositionX / confiningTrackFrame.width 93 | 94 | currentValue = value 95 | sendActions(for: .valueChanged) 96 | return true 97 | } 98 | 99 | public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { 100 | sendActions(for: .touchUpInside) 101 | } 102 | 103 | public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 104 | if interactableBounds.contains(point) { 105 | return true 106 | } 107 | return super.point(inside: point, with: event) 108 | } 109 | 110 | internal func updateShadowIfNeeded() { 111 | let views = [handle, sliderTrackView] 112 | 113 | if showsShadow { 114 | let shadowProps = shadowProperties(forHeight: bounds.height) 115 | views.forEach { $0.applyDropShadow(shadowProps) } 116 | } else { 117 | views.forEach { $0.removeDropShadow() } 118 | } 119 | } 120 | 121 | // MARK: - Private 122 | private let sliderTrackView = SliderTrackView() 123 | 124 | /// The amount of padding caused by visual stylings 125 | private var horizontalPadding: CGFloat { 126 | return sliderTrackView.layer.cornerRadius / 2.0 127 | } 128 | 129 | private var confiningTrackFrame: CGRect { 130 | return sliderTrackView.frame.insetBy(dx: horizontalPadding, dy: 0) 131 | } 132 | 133 | private var interactableBounds: CGRect { 134 | let horizontalOffset = -(handle.bounds.width / 2) + horizontalPadding 135 | return bounds.insetBy(dx: horizontalOffset, dy: 0) 136 | } 137 | 138 | private func commonInit() { 139 | backgroundColor = .clear 140 | setupSliderTrackView() 141 | setupSliderHandleView() 142 | updateTrackColor(to: trackColor) 143 | } 144 | 145 | private func setupSliderTrackView() { 146 | sliderTrackView.isUserInteractionEnabled = false 147 | sliderTrackView.translatesAutoresizingMaskIntoConstraints = false 148 | addSubview(sliderTrackView) 149 | NSLayoutConstraint.activate([ 150 | sliderTrackView.leadingAnchor.constraint(equalTo: leadingAnchor), 151 | sliderTrackView.trailingAnchor.constraint(equalTo: trailingAnchor), 152 | sliderTrackView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.75), 153 | sliderTrackView.centerYAnchor.constraint(equalTo: centerYAnchor), 154 | ]) 155 | } 156 | 157 | private func setupSliderHandleView() { 158 | handle.isUserInteractionEnabled = false 159 | addSubview(handle) 160 | } 161 | 162 | private func updateControl(to value: CGFloat) { 163 | let brightness = 1 - max(0, min(1, value)) 164 | var hue: CGFloat = 0 165 | var saturation: CGFloat = 0 166 | trackColor.getHue(&hue, saturation: &saturation, brightness: nil, alpha: nil) 167 | 168 | let newColor = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) 169 | 170 | CATransaction.begin() 171 | CATransaction.setDisableActions(true) 172 | handle.handleColor = newColor 173 | CATransaction.commit() 174 | 175 | moveHandle(to: value) 176 | } 177 | 178 | private func updateTrackColor(to color: UIColor) { 179 | var hue: CGFloat = 0 180 | var saturation: CGFloat = 0 181 | var brightness: CGFloat = 0 182 | color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) 183 | 184 | let colorWithMaxBrightness = UIColor(hue: hue, saturation: saturation, brightness: 1, alpha: 1) 185 | 186 | updateTrackViewGradient(for: colorWithMaxBrightness) 187 | currentValue = 1 - brightness 188 | } 189 | 190 | private func updateTrackViewGradient(for color: UIColor) { 191 | CATransaction.begin() 192 | CATransaction.setDisableActions(true) 193 | sliderTrackView.gradientValues = (color, .black) 194 | CATransaction.commit() 195 | } 196 | 197 | private func moveHandle(to value: CGFloat) { 198 | let clampedValue = max(0, min(1, value)) 199 | let xPos = (clampedValue * confiningTrackFrame.width) + horizontalPadding 200 | let size = CGSize(width: bounds.height * 1.15, height: bounds.height) 201 | 202 | handle.frame = CGRect(origin: CGPoint(x: xPos - (size.width / 2), y: 0), size: size) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Source/ChromaColorPicker.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaColorPicker.h 3 | // ChromaColorPicker 4 | // 5 | // Created by Cardasis, Jonathan (J.) on 7/24/17. 6 | // Copyright © 2017 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | FOUNDATION_EXPORT double ChromaColorPickerVersionNumber; 12 | FOUNDATION_EXPORT const unsigned char ChromaColorPickerVersionString[]; 13 | -------------------------------------------------------------------------------- /Source/ChromaColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaColorPicker.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 3/10/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol ChromaColorPickerDelegate: class { 12 | /// When a handle's value has changed. 13 | func colorPickerHandleDidChange(_ colorPicker: ChromaColorPicker, handle: ChromaColorHandle, to color: UIColor) 14 | } 15 | 16 | 17 | @IBDesignable 18 | public class ChromaColorPicker: UIControl, ChromaControlStylable { 19 | 20 | public weak var delegate: ChromaColorPickerDelegate? 21 | 22 | @IBInspectable public var borderWidth: CGFloat = 6.0 { 23 | didSet { layoutNow() } 24 | } 25 | 26 | @IBInspectable public var borderColor: UIColor = .white { 27 | didSet { layoutNow() } 28 | } 29 | 30 | @IBInspectable public var showsShadow: Bool = true { 31 | didSet { layoutNow() } 32 | } 33 | 34 | /// A brightness slider attached via the `connect(_:)` method. 35 | private(set) public weak var brightnessSlider: ChromaBrightnessSlider? { 36 | didSet { 37 | oldValue?.removeTarget(self, action: nil, for: .valueChanged) 38 | } 39 | } 40 | 41 | /// The size handles should be displayed at. 42 | public var handleSize: CGSize = defaultHandleSize { 43 | didSet { setNeedsLayout() } 44 | } 45 | 46 | /// An extension to handles' hitboxes in the +Y direction. 47 | /// Allows for handles to be grabbed more easily. 48 | public var handleHitboxExtensionY: CGFloat = 10.0 49 | 50 | /// Handles added to the color picker. 51 | private(set) public var handles: [ChromaColorHandle] = [] 52 | 53 | /// The last active handle. 54 | private(set) public var currentHandle: ChromaColorHandle? 55 | 56 | //MARK: - Initialization 57 | 58 | override public init(frame: CGRect) { 59 | super.init(frame: frame) 60 | self.commonInit() 61 | } 62 | 63 | required public init?(coder aDecoder: NSCoder) { 64 | super.init(coder: aDecoder) 65 | self.commonInit() 66 | } 67 | 68 | public override func layoutSubviews() { 69 | super.layoutSubviews() 70 | updateShadowIfNeeded() 71 | updateBorderIfNeeded() 72 | 73 | handles.forEach { handle in 74 | let location = colorWheelView.location(of: handle.color) 75 | handle.frame.size = handleSize 76 | positionHandle(handle, forColorLocation: location) 77 | } 78 | } 79 | 80 | // MARK: - Public 81 | 82 | @discardableResult 83 | public func addHandle(at color: UIColor? = nil) -> ChromaColorHandle { 84 | let handle = ChromaColorHandle() 85 | handle.color = color ?? defaultHandleColorPosition 86 | addHandle(handle) 87 | return handle 88 | } 89 | 90 | public func addHandle(_ handle: ChromaColorHandle) { 91 | handles.append(handle) 92 | colorWheelView.addSubview(handle) 93 | brightnessSlider?.trackColor = handle.color 94 | 95 | if currentHandle == nil { 96 | currentHandle = handle 97 | } 98 | } 99 | 100 | public func connect(_ slider: ChromaBrightnessSlider) { 101 | slider.addTarget(self, action: #selector(brightnessSliderDidValueChange(_:)), for: .valueChanged) 102 | brightnessSlider = slider 103 | } 104 | 105 | // MARK: - Control 106 | 107 | public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { 108 | let location = touch.location(in: colorWheelView) 109 | 110 | for handle in handles { 111 | if extendedHitFrame(for: handle).contains(location) { 112 | colorWheelView.bringSubviewToFront(handle) 113 | animateHandleScale(handle, shouldGrow: true) 114 | 115 | if let slider = brightnessSlider { 116 | slider.trackColor = handle.color.withBrightness(1) 117 | slider.currentValue = slider.value(brightness: handle.color.brightness) 118 | } 119 | 120 | currentHandle = handle 121 | return true 122 | } 123 | } 124 | return false 125 | } 126 | 127 | public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { 128 | var location = touch.location(in: colorWheelView) 129 | guard let handle = currentHandle else { return false } 130 | 131 | if !colorWheelView.pointIsInColorWheel(location) { 132 | // Touch is outside color wheel and should map to outermost edge. 133 | let center = colorWheelView.middlePoint 134 | let radius = colorWheelView.radius 135 | let angleToCenter = atan2(location.x - center.x, location.y - center.y) 136 | let positionOnColorWheelEdge = CGPoint(x: center.x + radius * sin(angleToCenter), 137 | y: center.y + radius * cos(angleToCenter)) 138 | location = positionOnColorWheelEdge 139 | } 140 | 141 | if let pixelColor = colorWheelView.pixelColor(at: location) { 142 | let previousBrightness = handle.color.brightness 143 | handle.color = pixelColor.withBrightness(previousBrightness) 144 | positionHandle(handle, forColorLocation: location) 145 | 146 | if let slider = brightnessSlider { 147 | slider.trackColor = pixelColor 148 | slider.currentValue = slider.value(brightness: previousBrightness) 149 | } 150 | 151 | informDelegateOfColorChange(on: handle) 152 | sendActions(for: .valueChanged) 153 | } 154 | 155 | return true 156 | } 157 | 158 | public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { 159 | if let handle = currentHandle { 160 | animateHandleScale(handle, shouldGrow: false) 161 | } 162 | sendActions(for: .touchUpInside) 163 | } 164 | 165 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 166 | // Self should handle all touch events, forwarding if needed. 167 | let touchableBounds = bounds.insetBy(dx: -handleSize.width, dy: -handleSize.height) 168 | return touchableBounds.contains(point) ? self : super.hitTest(point, with: event) 169 | } 170 | 171 | // MARK: - Private 172 | 173 | internal let colorWheelView = ColorWheelView() 174 | internal var colorWheelViewWidthConstraint: NSLayoutConstraint! 175 | 176 | internal func commonInit() { 177 | self.backgroundColor = UIColor.clear 178 | setupColorWheelView() 179 | } 180 | 181 | internal func setupColorWheelView() { 182 | colorWheelView.translatesAutoresizingMaskIntoConstraints = false 183 | addSubview(colorWheelView) 184 | colorWheelViewWidthConstraint = colorWheelView.widthAnchor.constraint(equalTo: self.widthAnchor) 185 | 186 | NSLayoutConstraint.activate([ 187 | colorWheelView.centerXAnchor.constraint(equalTo: self.centerXAnchor), 188 | colorWheelView.centerYAnchor.constraint(equalTo: self.centerYAnchor), 189 | colorWheelViewWidthConstraint, 190 | colorWheelView.heightAnchor.constraint(equalTo: colorWheelView.widthAnchor), 191 | ]) 192 | } 193 | 194 | func updateShadowIfNeeded() { 195 | if showsShadow { 196 | applyDropShadow(shadowProperties(forHeight: bounds.height)) 197 | } else { 198 | removeDropShadow() 199 | } 200 | } 201 | 202 | internal func updateBorderIfNeeded() { 203 | // Use view's background as a border so colorWheel subviews (handles) 204 | // may appear above the border. 205 | backgroundColor = borderWidth > 0 ? borderColor : .clear 206 | layer.cornerRadius = bounds.height / 2.0 207 | layer.masksToBounds = false 208 | colorWheelViewWidthConstraint.constant = -borderWidth * 2.0 209 | } 210 | 211 | // MARK: Actions 212 | 213 | @objc 214 | internal func brightnessSliderDidValueChange(_ slider: ChromaBrightnessSlider) { 215 | guard let currentHandle = currentHandle else { return } 216 | 217 | currentHandle.color = slider.currentColor 218 | informDelegateOfColorChange(on: currentHandle) 219 | } 220 | 221 | internal func informDelegateOfColorChange(on handle: ChromaColorHandle) { // TEMP: 222 | delegate?.colorPickerHandleDidChange(self, handle: handle, to: handle.color) 223 | } 224 | 225 | // MARK: - Helpers 226 | 227 | internal func extendedHitFrame(for handle: ChromaColorHandle) -> CGRect { 228 | var frame = handle.frame 229 | frame.size.height += handleHitboxExtensionY 230 | return frame 231 | } 232 | 233 | internal func positionHandle(_ handle: ChromaColorHandle, forColorLocation location: CGPoint) { 234 | handle.center = location.applying(CGAffineTransform.identity.translatedBy(x: 0, y: -handle.bounds.height / 2)) 235 | } 236 | 237 | internal func animateHandleScale(_ handle: ChromaColorHandle, shouldGrow: Bool) { 238 | if shouldGrow && handle.transform.d > 1 { return } // Already grown 239 | let scalar: CGFloat = 1.25 240 | 241 | var transform: CGAffineTransform = .identity 242 | if shouldGrow { 243 | let translateY = -handle.bounds.height * (scalar - 1) / 2 244 | transform = CGAffineTransform(scaleX: scalar, y: scalar).translatedBy(x: 0, y: translateY) 245 | } 246 | 247 | UIView.animate(withDuration: 0.15, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.6, options: .curveEaseInOut, animations: { 248 | handle.transform = transform 249 | }, completion: nil) 250 | } 251 | } 252 | 253 | internal let defaultHandleColorPosition: UIColor = .white 254 | internal let defaultHandleSize: CGSize = CGSize(width: 42, height: 52) 255 | -------------------------------------------------------------------------------- /Source/ChromaControlStylable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaControlStylable.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/13/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal protocol ChromaControlStylable { 12 | var borderWidth: CGFloat { get set } 13 | var borderColor: UIColor { get set } 14 | var showsShadow: Bool { get set } 15 | 16 | func updateShadowIfNeeded() 17 | } 18 | 19 | internal extension ChromaControlStylable where Self: UIView { 20 | 21 | func shadowProperties(forHeight height: CGFloat) -> ShadowProperties { 22 | let dropShadowHeight = height * 0.01 23 | return ShadowProperties(color: UIColor.black.cgColor, opacity: 0.35, offset: CGSize(width: 0, height: dropShadowHeight), radius: 4) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Source/ColorWheelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWheelView.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/11/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This value is used to expand the imageView's bounds and then mask back to its normal size 12 | /// such that any displayed image may have perfectly rounded corners. 13 | private let defaultImageViewCurveInset: CGFloat = 1.0 14 | 15 | public class ColorWheelView: UIView { 16 | 17 | public override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | commonInit() 20 | } 21 | 22 | public required init?(coder aDecoder: NSCoder) { 23 | super.init(coder: aDecoder) 24 | commonInit() 25 | } 26 | 27 | public override func layoutSubviews() { 28 | super.layoutSubviews() 29 | layer.masksToBounds = false 30 | layer.cornerRadius = radius 31 | 32 | let screenScale: CGFloat = UIScreen.main.scale 33 | if let colorWheelImage: CIImage = makeColorWheelImage(radius: radius * screenScale) { 34 | imageView.image = UIImage(ciImage: colorWheelImage, scale: screenScale, orientation: .up) 35 | } 36 | 37 | // Mask imageview so the generated colorwheel has smooth edges. 38 | // We mask the imageview instead of image so we get the benefits of using the CIImage 39 | // rendering directly on the GPU. 40 | imageViewMask.frame = imageView.bounds.insetBy(dx: defaultImageViewCurveInset, dy: defaultImageViewCurveInset) 41 | imageViewMask.layer.cornerRadius = imageViewMask.bounds.width / 2.0 42 | imageView.mask = imageViewMask 43 | } 44 | 45 | public var radius: CGFloat { 46 | return max(bounds.width, bounds.height) / 2.0 47 | } 48 | 49 | public var middlePoint: CGPoint { 50 | return CGPoint(x: bounds.midX, y: bounds.midY) 51 | } 52 | 53 | /** 54 | Returns the (x,y) location of the color provided within the ColorWheelView. 55 | Disregards color's brightness component. 56 | */ 57 | public func location(of color: UIColor) -> CGPoint { 58 | var hue: CGFloat = 0 59 | var saturation: CGFloat = 0 60 | color.getHue(&hue, saturation: &saturation, brightness: nil, alpha: nil) 61 | 62 | let radianAngle = hue * (2 * .pi) 63 | let distance = saturation * radius 64 | let colorTranslation = CGPoint(x: distance * cos(radianAngle), y: -distance * sin(radianAngle)) 65 | let colorPoint = CGPoint(x: bounds.midX + colorTranslation.x, y: bounds.midY + colorTranslation.y) 66 | 67 | return colorPoint 68 | } 69 | 70 | /** 71 | Returns the color on the wheel on a given point relative to the view. nil is returned if 72 | the point does not exist within the bounds of the color wheel. 73 | */ 74 | // TODO: replace this function with a mathmatically based one in ChromaColorPicker 75 | public func pixelColor(at point: CGPoint) -> UIColor? { 76 | guard pointIsInColorWheel(point) else { return nil } 77 | 78 | // Values on the edge of the circle should be calculated instead of obtained 79 | // from the rendered view layer. This ensures we obtain correct values where 80 | // image smoothing may have taken place. 81 | guard !pointIsOnColorWheelEdge(point) else { 82 | let angleToCenter = atan2(point.x - middlePoint.x, point.y - middlePoint.y) 83 | return edgeColor(for: angleToCenter) 84 | } 85 | 86 | let pixel = UnsafeMutablePointer.allocate(capacity: 4) 87 | let colorSpace = CGColorSpaceCreateDeviceRGB() 88 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) 89 | guard let context = CGContext( 90 | data: pixel, 91 | width: 1, 92 | height: 1, 93 | bitsPerComponent: 8, 94 | bytesPerRow: 4, 95 | space: colorSpace, 96 | bitmapInfo: bitmapInfo.rawValue 97 | ) else { 98 | return nil 99 | } 100 | 101 | context.translateBy(x: -point.x, y: -point.y) 102 | imageView.layer.render(in: context) 103 | let color = UIColor( 104 | red: CGFloat(pixel[0]) / 255.0, 105 | green: CGFloat(pixel[1]) / 255.0, 106 | blue: CGFloat(pixel[2]) / 255.0, 107 | alpha: 1.0 108 | ) 109 | 110 | pixel.deallocate() 111 | return color 112 | } 113 | 114 | /** 115 | Returns whether or not the point is in the circular area of the color wheel. 116 | */ 117 | public func pointIsInColorWheel(_ point: CGPoint) -> Bool { 118 | guard bounds.insetBy(dx: -1, dy: -1).contains(point) else { return false } 119 | 120 | let distanceFromCenter: CGFloat = hypot(middlePoint.x - point.x, middlePoint.y - point.y) 121 | let pointExistsInRadius: Bool = distanceFromCenter <= (radius - layer.borderWidth) 122 | return pointExistsInRadius 123 | } 124 | 125 | public func pointIsOnColorWheelEdge(_ point: CGPoint) -> Bool { 126 | let distanceToCenter = hypot(middlePoint.x - point.x, middlePoint.y - point.y) 127 | let isPointOnEdge = distanceToCenter >= radius - 1.0 128 | return isPointOnEdge 129 | } 130 | 131 | // MARK: - Private 132 | internal let imageView = UIImageView() 133 | internal let imageViewMask = UIView() 134 | 135 | internal func commonInit() { 136 | backgroundColor = .clear 137 | setupImageView() 138 | } 139 | 140 | internal func setupImageView() { 141 | imageView.contentMode = .scaleAspectFit 142 | imageViewMask.backgroundColor = .black 143 | 144 | imageView.translatesAutoresizingMaskIntoConstraints = false 145 | addSubview(imageView) 146 | 147 | NSLayoutConstraint.activate([ 148 | imageView.widthAnchor.constraint(equalTo: widthAnchor, constant: defaultImageViewCurveInset * 2), 149 | imageView.heightAnchor.constraint(equalTo: heightAnchor, constant: defaultImageViewCurveInset * 2), 150 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor), 151 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor), 152 | ]) 153 | } 154 | 155 | /** 156 | Generates a color wheel image from a given radius. 157 | - Parameters: 158 | - radius: The radius of the wheel in points. A radius of 100 would generate an 159 | image of 200x200 points (400x400 pixels on a device with 2x scaling.) 160 | */ 161 | internal func makeColorWheelImage(radius: CGFloat) -> CIImage? { 162 | let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [ 163 | "inputColorSpace": CGColorSpaceCreateDeviceRGB(), 164 | "inputDither": 0, 165 | "inputRadius": radius, 166 | "inputSoftness": 0, 167 | "inputValue": 1 168 | ]) 169 | return filter?.outputImage 170 | } 171 | 172 | /** 173 | Returns a color for a provided radian angle on the color wheel. 174 | - Note: Adjusts angle for the local color space and returns a color of 175 | max saturation and brightness with variable hue. 176 | */ 177 | internal func edgeColor(for angle: CGFloat) -> UIColor { 178 | var normalizedAngle = angle + .pi // normalize to [0, 2pi] 179 | normalizedAngle += (.pi / 2) // rotate pi/2 for color wheel 180 | var hue = normalizedAngle / (2 * .pi) 181 | if hue > 1 { hue -= 1 } 182 | return UIColor(hue: hue, saturation: 1, brightness: 1.0, alpha: 1.0) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Source/Extensions/UIColor+Brightness.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Brightness.swift 3 | // Example 4 | // 5 | // Created by Jon Cardasis on 4/18/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIColor { 12 | 13 | /// Returns a color with the specified brightness component. 14 | func withBrightness(_ value: CGFloat) -> UIColor { 15 | var hue: CGFloat = 0 16 | var saturation: CGFloat = 0 17 | var alpha: CGFloat = 0 18 | let brightness = max(0, min(value, 1)) 19 | getHue(&hue, saturation: &saturation, brightness: nil, alpha: &alpha) 20 | 21 | return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) 22 | } 23 | 24 | /// The value of the brightness component. 25 | var brightness: CGFloat { 26 | var brightness: CGFloat = 0 27 | getHue(nil, saturation: nil, brightness: &brightness, alpha: nil) 28 | return brightness 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/Extensions/UIColor+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Utils.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jonathan Cardasis on 11/8/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIColor { 12 | 13 | /// The value of lightness a color has. Value between [0.0, 1.0] 14 | /// Based on YIQ color space for constrast (https://www.w3.org/WAI/ER/WD-AERT/#color-contrast) 15 | var lightness: CGFloat { 16 | var red: CGFloat = 0 17 | var green: CGFloat = 0 18 | var blue: CGFloat = 0 19 | getRed(&red, green: &green, blue: &blue, alpha: nil) 20 | 21 | return ((red * 299) + (green * 587) + (blue * 114)) / 1000 22 | } 23 | 24 | /// Whether or not the color is considered 'light' in terms of contrast. 25 | var isLight: Bool { 26 | return lightness >= 0.5 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Extensions/UIView+DropShadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+DropShadow.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/11/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal struct ShadowProperties { 12 | internal let color: CGColor 13 | internal let opacity: Float 14 | internal let offset: CGSize 15 | internal let radius: CGFloat 16 | } 17 | 18 | internal extension UIView { 19 | 20 | var dropShadowProperties: ShadowProperties? { 21 | guard let shadowColor = layer.shadowColor else { return nil } 22 | return ShadowProperties(color: shadowColor, opacity: layer.shadowOpacity, offset: layer.shadowOffset, radius: layer.shadowRadius) 23 | } 24 | 25 | func applyDropShadow(color: UIColor, opacity: Float, offset: CGSize, radius: CGFloat) { 26 | clipsToBounds = false 27 | layer.masksToBounds = false 28 | layer.shadowColor = color.cgColor 29 | layer.shadowOpacity = opacity 30 | layer.shadowOffset = offset 31 | layer.shadowRadius = radius 32 | 33 | layer.shouldRasterize = true 34 | layer.rasterizationScale = UIScreen.main.scale 35 | } 36 | 37 | func applyDropShadow(_ properties: ShadowProperties) { 38 | applyDropShadow(color: UIColor(cgColor: properties.color), opacity: properties.opacity, offset: properties.offset, radius: properties.radius) 39 | } 40 | 41 | func removeDropShadow() { 42 | layer.shadowColor = nil 43 | layer.shadowOpacity = 0 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Extensions/UIView+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Utils.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 9/8/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIView { 12 | 13 | /// Forces the view to layout synchronously and immediately 14 | func layoutNow() { 15 | setNeedsLayout() 16 | layoutIfNeeded() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/Supporting Views/ChromaColorHandle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaColorHandle.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/11/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class ChromaColorHandle: UIView, ChromaControlStylable { 12 | 13 | /// Current selected color of the handle. 14 | public var color: UIColor = .black { 15 | didSet { layoutNow() } 16 | } 17 | 18 | /// An image to display in the handle. Updates `accessoryView` to be a UIImageView. 19 | public var accessoryImage: UIImage? { 20 | didSet { 21 | let imageView = UIImageView(image: accessoryImage) 22 | imageView.contentMode = .scaleAspectFit 23 | accessoryView = imageView 24 | } 25 | } 26 | 27 | /// A view to display in the handle. Overrides any previously set `accessoryImage`. 28 | public var accessoryView: UIView? { 29 | didSet { 30 | oldValue?.removeFromSuperview() 31 | if let accessoryView = accessoryView { 32 | addAccessoryView(accessoryView) 33 | } 34 | } 35 | } 36 | 37 | /// The amount an accessory view's frame should be inset by. 38 | public var accessoryViewEdgeInsets: UIEdgeInsets = .zero { 39 | didSet { layoutNow() } 40 | } 41 | 42 | public var borderWidth: CGFloat = 3.0 { 43 | didSet { layoutNow() } 44 | } 45 | 46 | public var borderColor: UIColor = .white { 47 | didSet { layoutNow() } 48 | } 49 | 50 | public var showsShadow: Bool = true { 51 | didSet { layoutNow() } 52 | } 53 | 54 | // MARK: - Initialization 55 | 56 | public convenience init(color: UIColor) { 57 | self.init(frame: .zero) 58 | self.color = color 59 | commonInit() 60 | } 61 | 62 | public override init(frame: CGRect) { 63 | super.init(frame: frame) 64 | commonInit() 65 | } 66 | 67 | public required init?(coder aDecoder: NSCoder) { 68 | super.init(coder: aDecoder) 69 | commonInit() 70 | } 71 | 72 | public override func layoutSubviews() { 73 | super.layoutSubviews() 74 | layoutHandleShape() 75 | layoutAccessoryViewIfNeeded() 76 | updateShadowIfNeeded() 77 | 78 | layer.masksToBounds = false 79 | } 80 | 81 | // MARK: - Private 82 | internal let handleShape = CAShapeLayer() 83 | 84 | internal func commonInit() { 85 | layer.addSublayer(handleShape) 86 | } 87 | 88 | internal func updateShadowIfNeeded() { 89 | if showsShadow { 90 | let shadowProps = ShadowProperties(color: UIColor.black.cgColor, 91 | opacity: 0.3, 92 | offset: CGSize(width: 0, height: bounds.height / 8.0), 93 | radius: 4.0) 94 | applyDropShadow(shadowProps) 95 | } else { 96 | removeDropShadow() 97 | } 98 | } 99 | 100 | internal func makeHandlePath(frame: CGRect) -> CGPath { 101 | let path = UIBezierPath() 102 | path.move(to: CGPoint(x: frame.minX + 0.5 * frame.width, y: frame.minY + 1 * frame.height)) 103 | path.addCurve(to: CGPoint(x: frame.minX + 1 * frame.width, y: frame.minY + 0.40310 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.83333 * frame.width, y: frame.minY + 0.80216 * frame.height), controlPoint2: CGPoint(x: frame.minX + 1 * frame.width, y: frame.minY + 0.60320 * frame.height)) 104 | path.addCurve(to: CGPoint(x: frame.minX + 0.5 * frame.width, y: frame.minY * frame.height), controlPoint1: CGPoint(x: frame.minX + 1 * frame.width, y: frame.minY + 0.18047 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.77614 * frame.width, y: frame.minY * frame.height)) 105 | path.addCurve(to: CGPoint(x: frame.minX * frame.width, y: frame.minY + 0.40310 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.22386 * frame.width, y: frame.minY * frame.height), controlPoint2: CGPoint(x: frame.minX * frame.width, y: frame.minY + 0.18047 * frame.height)) 106 | path.addCurve(to: CGPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 1 * frame.height), controlPoint1: CGPoint(x: frame.minX * frame.width, y: frame.minY + 0.60837 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.16667 * frame.width, y: frame.minY + 0.80733 * frame.height)) 107 | path.close() 108 | return path.cgPath 109 | } 110 | 111 | internal func layoutHandleShape() { 112 | let size = CGSize(width: bounds.width - borderWidth, height: bounds.height - borderWidth) 113 | handleShape.path = makeHandlePath(frame: CGRect(origin: .zero, size: size)) 114 | handleShape.frame = CGRect(origin: CGPoint(x: bounds.midX - (size.width / 2), y: bounds.midY - (size.height / 2)), size: size) 115 | 116 | handleShape.fillColor = color.cgColor 117 | handleShape.strokeColor = borderColor.cgColor 118 | handleShape.lineWidth = borderWidth 119 | } 120 | 121 | internal func layoutAccessoryViewIfNeeded() { 122 | if let accessoryLayer = accessoryView?.layer { 123 | let width = bounds.width - borderWidth * 2 124 | let size = CGSize(width: width - (accessoryViewEdgeInsets.left + accessoryViewEdgeInsets.right), 125 | height: width - (accessoryViewEdgeInsets.top + accessoryViewEdgeInsets.bottom)) 126 | accessoryLayer.frame = CGRect(origin: CGPoint(x: (borderWidth / 2) + accessoryViewEdgeInsets.left, y: (borderWidth / 2) + accessoryViewEdgeInsets.top), size: size) 127 | 128 | accessoryLayer.cornerRadius = size.height / 2 129 | accessoryLayer.masksToBounds = true 130 | } 131 | } 132 | 133 | internal func addAccessoryView(_ view: UIView) { 134 | let accessoryLayer = view.layer 135 | handleShape.addSublayer(accessoryLayer) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Source/Supporting Views/SliderHandleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SliderHandleView.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/13/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class SliderHandleView: UIView { 12 | 13 | public var handleColor: UIColor = .black { 14 | didSet { updateHandleColor(to: handleColor) } 15 | } 16 | 17 | public var borderWidth: CGFloat = 3.0 { 18 | didSet { layoutNow() } 19 | } 20 | 21 | public var borderColor: UIColor = .white { 22 | didSet { layoutNow() } 23 | } 24 | 25 | override public init(frame: CGRect) { 26 | super.init(frame: frame) 27 | commonInit() 28 | } 29 | 30 | required public init?(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder) 32 | commonInit() 33 | } 34 | 35 | override public func layoutSubviews() { 36 | let radius: CGFloat = bounds.height / 10 37 | handleLayer.path = makeRoundedTrianglePath(width: bounds.width, height: bounds.height, radius: radius) 38 | handleLayer.strokeColor = borderColor.cgColor 39 | handleLayer.lineWidth = borderWidth 40 | handleLayer.position = CGPoint(x: bounds.width / 2, y: (bounds.height / 2.0) - (radius / 4.0)) 41 | } 42 | 43 | // MARK: - Private 44 | private let handleLayer = CAShapeLayer() 45 | 46 | private func commonInit() { 47 | layer.addSublayer(handleLayer) 48 | updateHandleColor(to: handleColor) 49 | } 50 | 51 | private func updateHandleColor(to color: UIColor) { 52 | handleLayer.fillColor = color.cgColor 53 | } 54 | 55 | private func makeRoundedTrianglePath(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath { 56 | let point1 = CGPoint(x: -width / 2, y: height / 2) 57 | let point2 = CGPoint(x: 0, y: -height / 2) 58 | let point3 = CGPoint(x: width / 2, y: height / 2) 59 | 60 | let path = CGMutablePath() 61 | path.move(to: CGPoint(x: 0, y: height / 2)) 62 | path.addArc(tangent1End: point1, tangent2End: point2, radius: radius) 63 | path.addArc(tangent1End: point2, tangent2End: point3, radius: radius) 64 | path.addArc(tangent1End: point3, tangent2End: point1, radius: radius) 65 | path.closeSubpath() 66 | 67 | return path 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Source/Supporting Views/SliderTrackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SliderTrackView.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jon Cardasis on 4/13/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal class SliderTrackView: UIView { 12 | typealias GradientValues = (start: UIColor, end: UIColor) 13 | 14 | var gradientValues: GradientValues = (.white, .black) { 15 | didSet { updateGradient(for: gradientValues) } 16 | } 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | commonInit() 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | commonInit() 26 | } 27 | 28 | override func layoutSubviews() { 29 | super.layoutSubviews() 30 | gradient.frame = layer.bounds 31 | gradient.cornerRadius = layer.cornerRadius 32 | } 33 | 34 | func updateGradient(for values: GradientValues) { 35 | gradient.colors = [values.start.cgColor, values.end.cgColor] 36 | } 37 | 38 | // MARK: - Private 39 | private let gradient = CAGradientLayer() 40 | 41 | private func commonInit() { 42 | gradient.masksToBounds = true 43 | gradient.actions = ["position" : NSNull(), "bounds" : NSNull(), "path" : NSNull()] 44 | gradient.startPoint = CGPoint(x: 0, y: 0.5) 45 | gradient.endPoint = CGPoint(x: 1, y: 0.5) 46 | updateGradient(for: gradientValues) 47 | layer.addSublayer(gradient) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/ChromaBrightnessSliderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaBrightnessSliderTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 9/8/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class ChromaBrightnessSliderTests: XCTestCase { 13 | 14 | // MARK: - Properties 15 | 16 | func testCurrentColorShouldReturnBlueWhenTrackColorIsBlueAndCurrentValueIsZero() { 17 | // Given 18 | let subject = ChromaBrightnessSlider() 19 | let expectedColor: UIColor = .blue 20 | 21 | // When 22 | subject.trackColor = .blue 23 | subject.currentValue = 0.0 24 | 25 | // Then 26 | XCTAssertEqual(subject.currentColor, expectedColor) 27 | } 28 | 29 | func testCurrentColorShouldReturnBlackWhenTrackColorIsAnyAndCurrentValueIsOne() { 30 | // Given 31 | let subject = ChromaBrightnessSlider() 32 | let expectedColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1) 33 | 34 | // When 35 | subject.trackColor = .blue 36 | subject.currentValue = 1.0 37 | 38 | // Then 39 | XCTAssertEqual(subject.currentColor, expectedColor) 40 | } 41 | 42 | func testBorderWidthShouldUpdateSliderTrack() { 43 | // Given 44 | let subject = ChromaBrightnessSlider() 45 | let sliderTrack = subject.test_sliderTrackView 46 | let expectedBorderWidth: CGFloat = 11 47 | 48 | // When 49 | subject.borderWidth = expectedBorderWidth 50 | 51 | // Then 52 | XCTAssertEqual(sliderTrack.layer.borderWidth, expectedBorderWidth) 53 | } 54 | 55 | func testBorderColorShouldUpdateSliderTrack() { 56 | // Given 57 | let subject = ChromaBrightnessSlider() 58 | let sliderTrack = subject.test_sliderTrackView 59 | let expectedBorderColor: UIColor = .green 60 | 61 | // When 62 | subject.borderColor = expectedBorderColor 63 | 64 | // Then 65 | XCTAssertEqual(sliderTrack.layer.borderColor!, expectedBorderColor.cgColor) 66 | } 67 | 68 | // MARK: - Layout 69 | 70 | func testShadowsAreRenderedOnBothHandleAndTrackView() { 71 | // Given 72 | let subject = ChromaBrightnessSlider() 73 | let sliderTrack = subject.test_sliderTrackView 74 | 75 | // When 76 | subject.showsShadow = true 77 | 78 | // Then 79 | XCTAssertGreaterThan(sliderTrack.layer.shadowOpacity, 0) 80 | XCTAssertGreaterThan(subject.handle.layer.shadowOpacity, 0) 81 | } 82 | 83 | func testShadowsAreRemovedFromHandleAndTrackView() { 84 | // Given 85 | let subject = ChromaBrightnessSlider() 86 | let sliderTrack = subject.test_sliderTrackView 87 | 88 | // When 89 | subject.showsShadow = false 90 | 91 | // Then 92 | XCTAssertEqual(sliderTrack.layer.shadowOpacity, 0) 93 | XCTAssertEqual(subject.handle.layer.shadowOpacity, 0) 94 | } 95 | 96 | func testHandleIsRepositionedAccordingToCurrentValueOnLayout() { 97 | // Given 98 | let subject = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 300, height: 30)) 99 | //subject.layoutIfNeeded() 100 | let expectedPosition = subject.center 101 | 102 | // When 103 | subject.currentValue = 0.5 104 | subject.layoutIfNeeded() 105 | 106 | // Then 107 | XCTAssertEqual(subject.handle.center, expectedPosition) 108 | } 109 | 110 | // MARK: - Convenience Functions 111 | 112 | func testConnectCallsConnectFunctionOfColorPicker() { 113 | // Given 114 | let subject = ChromaBrightnessSlider() 115 | let colorPicker = FakeColorPicker() 116 | 117 | // When 118 | subject.connect(to: colorPicker) 119 | 120 | // Then 121 | XCTAssertTrue(colorPicker.didCallConnect) 122 | } 123 | 124 | func testValueShouldReturn1WhenBrightnessIs0() { 125 | // Given 126 | let subject = ChromaBrightnessSlider() 127 | 128 | // When 129 | let value = subject.value(brightness: 0) 130 | 131 | // Then 132 | XCTAssertEqual(value, 1, accuracy: 0.0001) 133 | } 134 | 135 | func testValueShouldReturn0WhenBrightnessIs1() { 136 | // Given 137 | let subject = ChromaBrightnessSlider() 138 | 139 | // When 140 | let value = subject.value(brightness: 1) 141 | 142 | // Then 143 | XCTAssertEqual(value, 0, accuracy: 0.0001) 144 | } 145 | 146 | func testValueShouldReturn0WhenBrightnessIsLargerThan1() { 147 | // Given 148 | let subject = ChromaBrightnessSlider() 149 | 150 | // When 151 | let value = subject.value(brightness: 100) 152 | 153 | // Then 154 | XCTAssertEqual(value, 0, accuracy: 0.0001) 155 | } 156 | 157 | func testValueShouldReturn1WhenBrightnessIsLessThan0() { 158 | // Given 159 | let subject = ChromaBrightnessSlider() 160 | 161 | // When 162 | let value = subject.value(brightness: -100) 163 | 164 | // Then 165 | XCTAssertEqual(value, 1, accuracy: 0.0001) 166 | } 167 | 168 | func testValueShouldReturn0_25WhenBrightnessIs0_75() { 169 | // Given 170 | let subject = ChromaBrightnessSlider() 171 | 172 | // When 173 | let value = subject.value(brightness: 0.75) 174 | 175 | // Then 176 | XCTAssertEqual(value, 0.25, accuracy: 0.0001) 177 | } 178 | 179 | // MARK: - Control 180 | 181 | func testBeginTrackingSendsTouchDownActionWhenTouchIsInBounds() { 182 | // Given 183 | let subject = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 300, height: 30)) 184 | subject.layoutIfNeeded() 185 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: subject.bounds.midX, 186 | y: subject.bounds.midY)) 187 | let eventReceiver = FakeEventReceiver(listensFor: .touchDown) 188 | subject.addTarget(eventReceiver, action: #selector(FakeEventReceiver.catchEvent(_:)), for: .touchDown) 189 | 190 | var eventDidTrigger = false 191 | eventReceiver.eventCaught = { 192 | eventDidTrigger = true 193 | } 194 | 195 | // When 196 | let shouldTrack = subject.beginTracking(fakeTouch, with: nil) 197 | 198 | // Then 199 | XCTAssertTrue(eventDidTrigger) 200 | XCTAssertTrue(shouldTrack) 201 | } 202 | 203 | func testBeginTrackingDoesNotSendTouchDownActionWhenTouchIsOutOfBounds() { 204 | // Given 205 | let subject = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 300, height: 30)) 206 | subject.layoutIfNeeded() 207 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: -100, y: subject.bounds.midY)) 208 | let eventReceiver = FakeEventReceiver(listensFor: .touchDown) 209 | subject.addTarget(eventReceiver, action: #selector(FakeEventReceiver.catchEvent(_:)), for: .touchDown) 210 | 211 | var eventDidTrigger = false 212 | eventReceiver.eventCaught = { 213 | eventDidTrigger = true 214 | } 215 | 216 | // When 217 | let shouldTrack = subject.beginTracking(fakeTouch, with: nil) 218 | 219 | // Then 220 | XCTAssertFalse(eventDidTrigger) 221 | XCTAssertFalse(shouldTrack) 222 | } 223 | 224 | func testContinueTrackingSendsValueChangedActionAndHasClampedValue() { 225 | // Given 226 | let subject = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 300, height: 30)) 227 | subject.layoutIfNeeded() 228 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: subject.bounds.width + 100, y: subject.bounds.midY)) 229 | let eventReceiver = FakeEventReceiver(listensFor: .valueChanged) 230 | subject.addTarget(eventReceiver, action: #selector(FakeEventReceiver.catchEvent(_:)), for: .valueChanged) 231 | 232 | var eventDidTrigger = false 233 | var value: CGFloat? 234 | eventReceiver.eventCaught = { 235 | eventDidTrigger = true 236 | value = subject.currentValue 237 | } 238 | 239 | // When 240 | let _ = subject.continueTracking(fakeTouch, with: nil) 241 | 242 | // Then 243 | XCTAssertTrue(eventDidTrigger) 244 | XCTAssertEqual(value, 1.0, "ChromaBrightnessSlider did not clamp value when touch is out of bounds") 245 | } 246 | 247 | func testEndTrackingSendsTouchUpInsideAction() { 248 | // Given 249 | let subject = ChromaBrightnessSlider(frame: CGRect(x: 0, y: 0, width: 300, height: 30)) 250 | subject.layoutIfNeeded() 251 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: 0, y: 0)) 252 | let eventReceiver = FakeEventReceiver(listensFor: .touchUpInside) 253 | subject.addTarget(eventReceiver, action: #selector(FakeEventReceiver.catchEvent(_:)), for: .touchUpInside) 254 | 255 | var eventDidTrigger = false 256 | eventReceiver.eventCaught = { 257 | eventDidTrigger = true 258 | } 259 | 260 | // When 261 | let _ = subject.endTracking(fakeTouch, with: nil) 262 | 263 | // Then 264 | XCTAssertTrue(eventDidTrigger) 265 | } 266 | } 267 | 268 | fileprivate extension ChromaBrightnessSlider { 269 | var test_sliderTrackView: SliderTrackView { 270 | return subviews.first(where: { $0 is SliderTrackView }) as! SliderTrackView 271 | } 272 | } 273 | 274 | 275 | fileprivate class FakeColorPicker: ChromaColorPicker { 276 | var didCallConnect: Bool = false 277 | 278 | override func connect(_ slider: ChromaBrightnessSlider) { 279 | didCallConnect = true 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /Tests/ChromaColorPickerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaColorPickerTest.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 2/3/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class ChromaColorPickerTests: XCTestCase { 13 | var subject: ChromaColorPicker! 14 | 15 | override func setUp() { 16 | subject = ChromaColorPicker(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 17 | } 18 | 19 | // MARK: Handles 20 | 21 | func testAddHandleAddsHandleToArray() { 22 | // Given, When 23 | subject.addHandle() 24 | 25 | // Then 26 | XCTAssertEqual(subject.handles.count, 1) 27 | } 28 | 29 | func testAddHandleAddsCustomHandleToArray() { 30 | // Given 31 | let handle = ChromaColorHandle(color: .black) 32 | 33 | // When 34 | subject.addHandle(handle) 35 | 36 | // Then 37 | XCTAssertEqual(subject.handles.first!, handle) 38 | } 39 | 40 | func testAddHandlePlacesHandleAtWhiteIfColorIsNil() { 41 | // Given, When 42 | subject.addHandle(at: nil) 43 | 44 | // Then 45 | XCTAssertEqual(subject.handles.first!.color, .white) 46 | } 47 | 48 | func testAddHandleUpdatesBrightnessSliderIfAttached() { 49 | // Given 50 | let slider = ChromaBrightnessSlider(frame: .zero) 51 | subject.connect(slider) 52 | let color: UIColor = .purple 53 | 54 | // When 55 | subject.addHandle(at: color) 56 | 57 | // Then 58 | XCTAssertEqual(slider.trackColor, color) 59 | } 60 | 61 | // MARK: Brightness Slider 62 | 63 | func testConnectingSliderAddsEventTarget() { 64 | // Given 65 | let slider = ChromaBrightnessSlider(frame: .zero) 66 | 67 | // When 68 | subject.connect(slider) 69 | 70 | // Then 71 | XCTAssertEqual(subject.brightnessSlider, slider) 72 | XCTAssertEqual(slider.allTargets.first, subject) 73 | XCTAssertTrue(slider.allControlEvents.contains(.valueChanged)) 74 | } 75 | 76 | func testOldBrightnessSliderRemovesTargetWhenInstanceChanges() { 77 | // Given 78 | let slider1 = ChromaBrightnessSlider(frame: .zero) 79 | let slider2 = ChromaBrightnessSlider(frame: .zero) 80 | 81 | // When 82 | subject.connect(slider1) 83 | subject.connect(slider2) 84 | 85 | // Then 86 | XCTAssertEqual(slider1.allTargets.count, 0) 87 | } 88 | 89 | // MARK: UI Control 90 | 91 | func testShouldBeginTrackingAndSetCurrentHandleIfTouchedHandle() { 92 | // Given 93 | let handleFrame = CGRect(x: subject.bounds.width / 2.0, y: subject.bounds.height / 2.0, width: 10, height: 10) 94 | let handle = ChromaColorHandle(frame: handleFrame) 95 | subject.addHandle(handle) 96 | let fakeTouch = FakeUITouch(locationInParent: handleFrame.origin) 97 | 98 | // When 99 | let result = subject.beginTracking(fakeTouch, with: nil) 100 | 101 | // Then 102 | XCTAssertTrue(result) 103 | XCTAssertEqual(subject.currentHandle, handle) 104 | } 105 | 106 | func testShouldBeginTrackingIfTouchedOnHandleWithExtendedHitBox() { 107 | // Given 108 | let yExtension: CGFloat = 8.0 109 | let handleFrame = CGRect(x: subject.bounds.width / 2.0, y: subject.bounds.height / 2.0, width: 10, height: 10) 110 | subject.addHandle(ChromaColorHandle(frame: handleFrame)) 111 | subject.handleHitboxExtensionY = yExtension 112 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: handleFrame.origin.x, 113 | y: handleFrame.origin.y + handleFrame.height + yExtension - 1)) 114 | 115 | // When 116 | let result = subject.beginTracking(fakeTouch, with: nil) 117 | 118 | // Then 119 | XCTAssertTrue(result) 120 | } 121 | 122 | func testBeginTrackingUpdatesBrightnessSliderIfAttached() { 123 | // Given 124 | let slider = ChromaBrightnessSlider(frame: .zero) 125 | subject.connect(slider) 126 | 127 | let handleFrame = CGRect(x: subject.bounds.width / 2.0, y: subject.bounds.height / 2.0, width: 10, height: 10) 128 | let handle = ChromaColorHandle(frame: handleFrame) 129 | subject.addHandle(handle) 130 | let fakeTouch = FakeUITouch(locationInParent: handleFrame.origin) 131 | 132 | // When 133 | let _ = subject.beginTracking(fakeTouch, with: nil) 134 | 135 | // Then 136 | XCTAssertEqual(slider.trackColor, handle.color.withBrightness(1)) 137 | XCTAssertEqual(slider.currentValue, slider.value(brightness: handle.color.brightness)) 138 | } 139 | 140 | func testStopContinueTrackingIfCurrentHandleIsNil() { 141 | // Given, When 142 | let result = subject.continueTracking(FakeUITouch(locationInParent: .zero), with: nil) 143 | 144 | // Then 145 | XCTAssertNil(subject.currentHandle) 146 | XCTAssertFalse(result) 147 | } 148 | 149 | func testContinueTrackingSendsValueChangedActionForValidLocation() { 150 | subject.colorWheelView.layoutIfNeeded() 151 | 152 | // Given 153 | setCurrentHandle(to: makeFakeHandle()) 154 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: subject.colorWheelView.bounds.midX, 155 | y: subject.colorWheelView.bounds.midY)) 156 | let eventReceiver = FakeEventReceiver(listensFor: .valueChanged) 157 | subject.addTarget(eventReceiver, action: #selector(FakeEventReceiver.catchEvent), for: .valueChanged) 158 | 159 | var eventDidTrigger = false 160 | eventReceiver.eventCaught = { 161 | eventDidTrigger = true 162 | } 163 | 164 | // When 165 | let _ = subject.continueTracking(fakeTouch, with: nil) 166 | 167 | // Then 168 | XCTAssertTrue(eventDidTrigger) 169 | } 170 | 171 | 172 | func testEndTrackingSendsTouchUpInsideAction() { 173 | subject.colorWheelView.layoutIfNeeded() 174 | 175 | // Given 176 | setCurrentHandle(to: makeFakeHandle()) 177 | let fakeTouch = FakeUITouch(locationInParent: CGPoint(x: subject.colorWheelView.bounds.midX, 178 | y: subject.colorWheelView.bounds.midY)) 179 | let eventReceiver = FakeEventReceiver(listensFor: .touchUpInside) 180 | subject.addTarget(eventReceiver, action: #selector(FakeEventReceiver.catchEvent), for: .touchUpInside) 181 | 182 | var eventDidTrigger = false 183 | eventReceiver.eventCaught = { 184 | eventDidTrigger = true 185 | } 186 | 187 | // When 188 | let _ = subject.endTracking(fakeTouch, with: nil) 189 | 190 | // Then 191 | XCTAssertTrue(eventDidTrigger) 192 | } 193 | 194 | func testHitTestShouldReturnSelfWhenTouchIsWithinSelf() { 195 | subject.colorWheelView.layoutIfNeeded() 196 | 197 | // Given 198 | let touchLocation = subject.center 199 | 200 | // When 201 | let hitView = subject.hitTest(touchLocation, with: UIEvent()) 202 | 203 | // Then 204 | XCTAssertEqual(hitView, subject) 205 | } 206 | 207 | func testHitTestShouldReturnSelfWhenTouchIsWithinSelfPlusHandleRadius() { 208 | subject.colorWheelView.layoutIfNeeded() 209 | 210 | // Given 211 | let touchLocation = CGPoint(x: -subject.handleSize.width / 2.0, y: subject.center.y) 212 | 213 | // When 214 | let hitView = subject.hitTest(touchLocation, with: UIEvent()) 215 | 216 | // Then 217 | XCTAssertEqual(hitView, subject) 218 | } 219 | 220 | func testHitTestShouldReturnNilWhenTouchIsOutsideHitbox() { 221 | subject.colorWheelView.layoutIfNeeded() 222 | 223 | // Given 224 | let extendedSize: CGFloat = 12 225 | subject.handleSize = CGSize(width: extendedSize, height: extendedSize) 226 | let fakeHandle = makeFakeHandle() 227 | let touchLocation = CGPoint(x: -extendedSize - 1, y: subject.center.y) 228 | setCurrentHandle(to: fakeHandle) 229 | 230 | // When 231 | let hitView = subject.hitTest(touchLocation, with: UIEvent()) 232 | 233 | // Then 234 | XCTAssertEqual(hitView, nil) 235 | } 236 | } 237 | 238 | 239 | // Test Helpers 240 | extension ChromaColorPickerTests { 241 | 242 | func makeFakeHandle() -> ChromaColorHandle { 243 | return ChromaColorHandle(frame: CGRect(x: subject.bounds.midX, y: subject.bounds.midY, width: 10, height: 10)) 244 | } 245 | 246 | func setCurrentHandle(to handle: ChromaColorHandle) { 247 | subject.addHandle(handle) 248 | let fakeTouch = FakeUITouch(locationInParent: handle.frame.origin) 249 | let _ = subject.beginTracking(fakeTouch, with: nil) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Tests/ChromaControlStylableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaControlStylableTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 5/2/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class ChromaControlStyableTests: XCTestCase { 13 | 14 | func testUIViewShadowPropertiesShouldReturnBlackColor() { 15 | // Given 16 | let view = TestView() 17 | 18 | // When 19 | let shadowProps = view.shadowProperties(forHeight: 120) 20 | 21 | // Then 22 | XCTAssertEqual(shadowProps.color, UIColor.black.cgColor) 23 | } 24 | 25 | func testUIViewShadowPropertiesOpacity() { 26 | // Given 27 | let view = TestView() 28 | 29 | // When 30 | let shadowProps = view.shadowProperties(forHeight: 120) 31 | 32 | // Then 33 | XCTAssertEqual(shadowProps.opacity, 0.35) 34 | } 35 | 36 | func testUIViewShadowPropertiesRadius() { 37 | // Given 38 | let view = TestView() 39 | 40 | // When 41 | let shadowProps = view.shadowProperties(forHeight: 120) 42 | 43 | // Then 44 | XCTAssertEqual(shadowProps.radius, 4) 45 | } 46 | 47 | func testUIViewShadowPropertiesOffsetHeightShouldBeOnePercentOfProvidedHeight() { 48 | // Given 49 | let view = TestView() 50 | 51 | // When 52 | let shadowProps = view.shadowProperties(forHeight: 1200) 53 | 54 | // Then 55 | XCTAssertEqual(shadowProps.offset.width, 0) 56 | XCTAssertEqual(shadowProps.offset.height, 12) 57 | } 58 | 59 | } 60 | 61 | private class TestView: UIView, ChromaControlStylable { 62 | var borderWidth: CGFloat = 2.0 63 | var borderColor: UIColor = UIColor.green 64 | var showsShadow: Bool = true 65 | func updateShadowIfNeeded() {} 66 | } 67 | -------------------------------------------------------------------------------- /Tests/ColorWheelViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWheelTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 4/11/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class ColorWheelViewTests: XCTestCase { 13 | 14 | var subject: ColorWheelView! 15 | 16 | override func setUp() { 17 | subject = ColorWheelView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 18 | } 19 | 20 | func testBackgroundIsClear() { 21 | XCTAssertEqual(subject.backgroundColor, .clear) 22 | } 23 | 24 | func testImageViewUsesAspectFit() { 25 | XCTAssertEqual(subject.imageView.contentMode, .scaleAspectFit) 26 | } 27 | 28 | func testLayerCornerRadiusUpdatesDuringLayout() { 29 | // Given 30 | subject.frame = CGRect(x: 0, y: 0, width: 200, height: 0) 31 | 32 | // When 33 | subject.layoutSubviews() 34 | 35 | // Then 36 | XCTAssertEqual(subject.layer.cornerRadius, subject.radius) 37 | } 38 | 39 | func testRadiusIfHalfOfWidthWhenWidthIsLargestDimension() { 40 | // Given, When 41 | subject.frame = CGRect(x: 0, y: 0, width: 200, height: 0) 42 | 43 | // Then 44 | XCTAssertEqual(subject.radius, 100) 45 | } 46 | 47 | func testRadiusIfHalfOfHeightWhenHeightIsLargestDimension() { 48 | // Given, When 49 | subject.frame = CGRect(x: 0, y: 0, width: 0, height: 200) 50 | 51 | // Then 52 | XCTAssertEqual(subject.radius, 100) 53 | } 54 | 55 | func testColorWheelImageIsGeneratedEveryLayoutCycle() { 56 | // Given 57 | let width: CGFloat = 200.0 58 | let expectedFinalSize = CGSize(width: width, height: width) 59 | 60 | // When 61 | subject.layoutSubviews() 62 | let firstImage = subject.imageView.image! 63 | subject.frame = CGRect(x: 0, y: 0, width: width, height: width) 64 | subject.layoutSubviews() 65 | let secondImage = subject.imageView.image! 66 | 67 | // Then 68 | XCTAssertNotEqual(firstImage, secondImage) 69 | XCTAssertEqual(secondImage.size, expectedFinalSize) 70 | } 71 | 72 | func testPixelColorShouldReturnNilForPointOutsideBounds() { 73 | // Given, When 74 | let testPoint = CGPoint(x: -100, y: -100) 75 | // Then 76 | XCTAssertNil(subject.pixelColor(at: testPoint)) 77 | } 78 | 79 | func testPixelColorShouldReturnNilForPointInBoundsButOutsideRadius() { 80 | // Given, When 81 | let testPoint = CGPoint(x: 0, y: 0) 82 | // Then 83 | XCTAssertNil(subject.pixelColor(at: testPoint)) 84 | } 85 | 86 | func testPixelColorShouldReturnNilForPointOnBorder() { 87 | // Given 88 | subject.layer.borderWidth = 20 89 | subject.layer.borderColor = UIColor.white.cgColor 90 | 91 | // When 92 | let testPoint = CGPoint(x: subject.bounds.width - 10, y: subject.frame.midY) 93 | 94 | // Then 95 | XCTAssertNil(subject.pixelColor(at: testPoint)) 96 | } 97 | 98 | func testPixelColorShouldBeRedAtMaxXMidY() { 99 | // Given 100 | let size = subject.frame.size 101 | let testPoint = CGPoint(x: size.width, y: size.height / 2.0) 102 | let (expectedRedValue, _, _) = UIColor.red.rgbValues 103 | 104 | let vc = UIViewController() 105 | vc.view.addSubview(subject) 106 | vc.beginAppearanceTransition(true, animated: false) 107 | vc.endAppearanceTransition() 108 | subject.layoutSubviews() 109 | 110 | // When 111 | let (actualRedValue, _, _) = subject.pixelColor(at: testPoint)!.rgbValues 112 | 113 | // Then 114 | XCTAssertEqual(actualRedValue, expectedRedValue, accuracy: 0.001) 115 | } 116 | 117 | func testPixelColorShouldBeCyanAtMinXMidY() { 118 | // Given 119 | let size = subject.frame.size 120 | let testPoint = CGPoint(x: 0, y: size.height / 2.0) 121 | let expectedColorValues = UIColor.cyan.rgbValues 122 | 123 | let vc = UIViewController() 124 | vc.view.addSubview(subject) 125 | vc.beginAppearanceTransition(true, animated: false) 126 | vc.endAppearanceTransition() 127 | subject.layoutSubviews() 128 | 129 | // When 130 | let actualColorValues = subject.pixelColor(at: testPoint)!.rgbValues 131 | 132 | // Then 133 | XCTAssertEqual(actualColorValues.red, expectedColorValues.red, accuracy: 0.005) 134 | XCTAssertEqual(actualColorValues.green, expectedColorValues.green, accuracy: 0.005) 135 | XCTAssertEqual(actualColorValues.blue, expectedColorValues.blue, accuracy: 0.005) 136 | } 137 | 138 | func testPixelColorShouldBeWhiteAtTheCenter() { 139 | // Given 140 | let size = subject.frame.size 141 | let testPoint = CGPoint(x: size.width / 2.0, y: size.height / 2.0) 142 | let expectedColorValues = UIColor.white.rgbValues 143 | 144 | let vc = UIViewController() 145 | vc.view.addSubview(subject) 146 | vc.beginAppearanceTransition(true, animated: false) 147 | vc.endAppearanceTransition() 148 | subject.layoutSubviews() 149 | 150 | // When 151 | let actualColorValues = subject.pixelColor(at: testPoint)!.rgbValues 152 | 153 | // Then 154 | XCTAssertEqual(actualColorValues.red, expectedColorValues.red, accuracy: 0.005) 155 | XCTAssertEqual(actualColorValues.green, expectedColorValues.green, accuracy: 0.005) 156 | XCTAssertEqual(actualColorValues.blue, expectedColorValues.blue, accuracy: 0.005) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Tests/Extensions/UIColor+BrightnessTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+BrightnessTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 5/2/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class UIColor_BrightnessTests: XCTestCase { 13 | 14 | func testWithBrightnessReturnsNewInstanceWithNewBrightness() { 15 | // Given 16 | let color: UIColor = UIColor(hue: 25, saturation: 0.65, brightness: 0, alpha: 0.73) 17 | let newBrightness: CGFloat = 1.0 18 | 19 | // When 20 | let newColor = color.withBrightness(newBrightness) 21 | 22 | // Then 23 | let expectedValues = color.hsbaValues 24 | let actualValues = newColor.hsbaValues 25 | 26 | XCTAssertNotEqual(color, newColor) 27 | XCTAssertEqual(expectedValues.hue, actualValues.hue) 28 | XCTAssertEqual(expectedValues.saturation, actualValues.saturation) 29 | XCTAssertEqual(actualValues.brightness, newBrightness) 30 | XCTAssertEqual(expectedValues.alpha, actualValues.alpha) 31 | } 32 | 33 | func testBrightnessReturnsZeroForBlack() { 34 | // Given 35 | let color: UIColor = .black 36 | 37 | // When, Then 38 | XCTAssertEqual(color.brightness, 0) 39 | } 40 | 41 | func testBrightnessReturnsOneForWhite() { 42 | // Given 43 | let color: UIColor = .white 44 | 45 | // When, Then 46 | XCTAssertEqual(color.brightness, 1) 47 | } 48 | 49 | func testBrightnessReturnsCustomColorBrightness() { 50 | // Given 51 | let color: UIColor = UIColor(hue: 25, saturation: 1, brightness: 0.36, alpha: 1) 52 | 53 | // When, Then 54 | XCTAssertEqual(color.brightness, 0.36) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/Extensions/UIColor+UtilsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+UtilsTests.swift 3 | // ChromaColorPicker 4 | // 5 | // Created by Jonathan Cardasis on 11/8/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class UIColor_UtilsTests: XCTestCase { 13 | 14 | func testLightnessReturns1ForWhite() { 15 | XCTAssertEqual(UIColor.white.lightness, 1.0, accuracy: 0.001) 16 | } 17 | 18 | func testLightnessReturns0ForBlack() { 19 | XCTAssertEqual(UIColor.black.lightness, 0.0, accuracy: 0.001) 20 | } 21 | 22 | func testLightnessReturnsCorrectLightnessForYellow() { 23 | XCTAssertEqual(UIColor.yellow.lightness, 0.886, accuracy: 0.001) 24 | } 25 | 26 | func testLightnessReturnsCorrectLightnessForBlue() { 27 | XCTAssertEqual(UIColor.blue.lightness, 0.114, accuracy: 0.001) 28 | } 29 | 30 | func testBlueIsNotLight() { 31 | XCTAssertFalse(UIColor.blue.isLight) 32 | } 33 | 34 | func testRedIsNotLight() { 35 | XCTAssertFalse(UIColor.red.isLight) 36 | } 37 | 38 | func testPurpleIsNotLight() { 39 | XCTAssertFalse(UIColor.purple.isLight) 40 | } 41 | 42 | func tesBlackIsNotLight() { 43 | XCTAssertFalse(UIColor.black.isLight) 44 | } 45 | 46 | func testWhiteIsLight() { 47 | XCTAssertTrue(UIColor.white.isLight) 48 | } 49 | 50 | func testGreenIsLight() { 51 | XCTAssertTrue(UIColor.green.isLight) 52 | } 53 | 54 | func testOrangeIsLight() { 55 | XCTAssertTrue(UIColor.orange.isLight) 56 | } 57 | 58 | func testYellowIsLight() { 59 | XCTAssertTrue(UIColor.yellow.isLight) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Extensions/UIView+DropShadowTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+DropShadowTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 5/2/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class UIView_DropShadowTests: XCTestCase { 13 | 14 | var subject: UIView! 15 | 16 | override func setUp() { 17 | subject = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 18 | } 19 | 20 | func testApplyingDropShadowUpdatesLayerProperties() { 21 | // Given 22 | let color: UIColor = .blue 23 | let opacity: Float = 0.35 24 | let offset: CGSize = CGSize(width: 12, height: 10) 25 | let radius: CGFloat = 4.0 26 | 27 | // When 28 | subject.applyDropShadow(color: color, opacity: opacity, offset: offset, radius: radius) 29 | 30 | // Then 31 | XCTAssertFalse(subject.layer.masksToBounds) 32 | XCTAssertEqual(subject.layer.shadowColor, color.cgColor) 33 | XCTAssertEqual(subject.layer.shadowOpacity, opacity) 34 | XCTAssertEqual(subject.layer.shadowOffset, offset) 35 | XCTAssertEqual(subject.layer.shadowRadius, radius) 36 | } 37 | 38 | func testConvenienceMethodApplyDropShadowUpdatesTheSameLayerProperties() { 39 | // Given 40 | let color: UIColor = .blue 41 | let opacity: Float = 0.35 42 | let offset: CGSize = CGSize(width: 12, height: 10) 43 | let radius: CGFloat = 4.0 44 | 45 | let shadowProps = ShadowProperties(color: color.cgColor, opacity: opacity, offset: offset, radius: radius) 46 | 47 | // When 48 | subject.applyDropShadow(shadowProps) 49 | 50 | // Then 51 | XCTAssertFalse(subject.layer.masksToBounds) 52 | XCTAssertEqual(subject.layer.shadowColor, color.cgColor) 53 | XCTAssertEqual(subject.layer.shadowOpacity, opacity) 54 | XCTAssertEqual(subject.layer.shadowOffset, offset) 55 | XCTAssertEqual(subject.layer.shadowRadius, radius) 56 | } 57 | 58 | func testViewShouldNotClipToBoundsWhenDropShadowApplied() { 59 | // Given, When 60 | subject.applyDropShadow(defaultShadowProperties) 61 | 62 | // Then 63 | XCTAssertFalse(subject.clipsToBounds) 64 | } 65 | 66 | func testApplyingDropShadowShouldRasterizeLayer() { 67 | // Given, When 68 | subject.applyDropShadow(defaultShadowProperties) 69 | 70 | // Then 71 | XCTAssertTrue(subject.layer.shouldRasterize) 72 | XCTAssertEqual(subject.layer.rasterizationScale, UIScreen.main.scale) 73 | } 74 | 75 | func testDropShadowPropertiesReturnsCurrentShadowProps() { 76 | // Given, When 77 | subject.applyDropShadow(defaultShadowProperties) 78 | 79 | // Then 80 | XCTAssertEqual(subject.dropShadowProperties!.color, defaultShadowProperties.color) 81 | XCTAssertEqual(subject.dropShadowProperties!.opacity, defaultShadowProperties.opacity) 82 | XCTAssertEqual(subject.dropShadowProperties!.offset, defaultShadowProperties.offset) 83 | XCTAssertEqual(subject.dropShadowProperties!.radius, defaultShadowProperties.radius) 84 | } 85 | 86 | func testDropShadowPropertiesReturnsNilWhenNoShadowColor() { 87 | // Given, When 88 | subject.applyDropShadow(defaultShadowProperties) 89 | subject.layer.shadowColor = nil 90 | 91 | // Then 92 | XCTAssertNil(subject.dropShadowProperties) 93 | } 94 | 95 | func testRemoveDropShadowRemovesVisualShadowProperties() { 96 | // Given 97 | subject.applyDropShadow(defaultShadowProperties) 98 | 99 | // When 100 | subject.removeDropShadow() 101 | 102 | // Then 103 | XCTAssertNil(subject.layer.shadowColor) 104 | XCTAssertEqual(subject.layer.shadowOpacity, 0) 105 | } 106 | 107 | } 108 | 109 | private var defaultShadowProperties: ShadowProperties { 110 | return ShadowProperties(color: UIColor.black.cgColor, opacity: 0.5, offset: CGSize(width: 2, height: 4), radius: 4) 111 | } 112 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/Supporting Views/ChromaColorHandleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChromaColorHandleTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 5/13/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class ChromaColorHandleTests: XCTestCase { 13 | 14 | func testSettingAcessoryImageMakesAccessoryViewUIImageView() { 15 | // Given 16 | let subject = ChromaColorHandle() 17 | 18 | // When 19 | subject.accessoryImage = UIImage() 20 | 21 | // Then 22 | XCTAssertNotNil(subject.accessoryView) 23 | XCTAssertTrue(subject.accessoryView is UIImageView) 24 | } 25 | 26 | func testSettingAccessoryImageMakesImageViewAspectFit() { 27 | // Given 28 | let subject = ChromaColorHandle() 29 | 30 | // When 31 | subject.accessoryImage = UIImage() 32 | 33 | // Then 34 | XCTAssertEqual(subject.accessoryView!.contentMode, .scaleAspectFit) 35 | } 36 | 37 | func testSettingAccessoryViewOverridesImage() { 38 | // Given 39 | let subject = ChromaColorHandle() 40 | subject.accessoryImage = UIImage() 41 | let view = UIView() 42 | 43 | // When 44 | subject.accessoryView = view 45 | 46 | // Then 47 | XCTAssertEqual(subject.accessoryView, view) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Tests/Supporting Views/SliderHandleViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SliderHandleViewTests.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 5/13/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ChromaColorPicker 11 | 12 | class SliderHandleViewTests: XCTestCase { 13 | 14 | func testSettingHandleColorUpdatesHandleLayer() { 15 | // Given 16 | let subject = SliderHandleView() 17 | let layer = handleLayer(for: subject) 18 | let expectedColor: UIColor = .purple 19 | 20 | // When 21 | subject.handleColor = expectedColor 22 | 23 | // Then 24 | XCTAssertEqual(layer.fillColor!, expectedColor.cgColor) 25 | } 26 | 27 | func testSettingBorderColorUpdatesHandleLayer() { 28 | // Given 29 | let subject = SliderHandleView() 30 | let layer = handleLayer(for: subject) 31 | let expectedColor: UIColor = .purple 32 | 33 | // When 34 | subject.borderColor = expectedColor 35 | 36 | // Then 37 | XCTAssertEqual(layer.strokeColor!, expectedColor.cgColor); 38 | } 39 | 40 | func testSettingBorderWidthUpdatesHandleLayer() { 41 | // Given 42 | let subject = SliderHandleView() 43 | let layer = handleLayer(for: subject) 44 | let expectedWidth: CGFloat = 12.0 45 | 46 | // When 47 | subject.borderWidth = expectedWidth 48 | 49 | // Then 50 | XCTAssertEqual(layer.lineWidth, expectedWidth); 51 | } 52 | } 53 | 54 | /// Return the internal cashape sublayer for the provided view. 55 | fileprivate func handleLayer(for handleView: SliderHandleView) -> CAShapeLayer { 56 | return handleView.layer.sublayers!.first(where: { $0 is CAShapeLayer }) as! CAShapeLayer 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Tests/TestHelpers/FakeTouch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeTouch.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 9/8/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FakeUITouch: UITouch { 12 | 13 | let locationInParent: CGPoint 14 | 15 | init(locationInParent: CGPoint) { 16 | self.locationInParent = locationInParent 17 | } 18 | 19 | override func location(in view: UIView?) -> CGPoint { 20 | return locationInParent 21 | } 22 | } 23 | 24 | class FakeEventReceiver: NSObject { 25 | var eventCaught: (() -> ())? 26 | let event: UIControl.Event 27 | 28 | init(listensFor event: UIControl.Event) { 29 | self.event = event 30 | } 31 | 32 | @objc func catchEvent(_ sender: UIControl) { 33 | if sender.allControlEvents.contains(event) { 34 | eventCaught?() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/TestHelpers/UIColor+TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+TestHelpers.swift 3 | // ChromaColorPickerTests 4 | // 5 | // Created by Jon Cardasis on 5/2/19. 6 | // Copyright © 2019 Jonathan Cardasis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | var rgbValues: (red: CGFloat, green: CGFloat, blue: CGFloat) { 14 | var red: CGFloat = 0 15 | var green: CGFloat = 0 16 | var blue: CGFloat = 0 17 | getRed(&red, green: &green, blue: &blue, alpha: nil) 18 | return (red, green, blue) 19 | } 20 | 21 | var hsbaValues: (hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat) { 22 | var h: CGFloat = 0 23 | var s: CGFloat = 0 24 | var b: CGFloat = 0 25 | var a: CGFloat = 0 26 | getHue(&h, saturation: &s, brightness: &b, alpha: &a) 27 | return (h,s,b,a) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | ENV['PODSPEC'] = "ChromaColorPicker.podspec" 2 | 3 | default_platform(:ios) 4 | 5 | platform :ios do 6 | 7 | desc "Runs unit tests, publishing a new pod version if tests succeed." 8 | lane :test_and_publish do 9 | test 10 | publish_pod 11 | end 12 | 13 | desc "Publish podspec, incrementing the patch version." 14 | lane :publish_pod do 15 | publish_podspec 16 | end 17 | 18 | desc "Runs unit tests." 19 | lane :test do 20 | run_tests(scheme: "ChromaColorPickerTests") 21 | end 22 | end 23 | 24 | 25 | def publish_podspec 26 | podspec = ENV['PODSPEC'] 27 | version = version_bump_podspec(path: podspec, bump_type: "patch") 28 | commit(podspec, 'Updated Podspec version.') 29 | push_to_git_remote(local_branch: git_branch) 30 | tag_build(version) 31 | pod_push(path: podspec) 32 | end 33 | 34 | def tag_build(tag) 35 | tag_exists_in_remote = `git ls-remote origin refs/tags/"#{tag}"`.length > 0 36 | sh "git fetch --tags --prune origin" 37 | 38 | if tag_exists_in_remote 39 | sh "git push --delete origin #{tag}" 40 | end 41 | 42 | add_git_tag( 43 | tag: tag, 44 | force: true, 45 | prefix: 'dev' # TEMP: This is temporary only intended for use in the `develop` branch. 46 | ) 47 | push_git_tags 48 | end 49 | 50 | def commit(path, message) 51 | # Ensure we are on the correct branch before committing. 52 | sh "git checkout #{git_branch}" 53 | ensure_git_branch(branch: git_branch) 54 | 55 | message = "Fastfile: #{message}" 56 | git_commit( 57 | path: path, 58 | message: message 59 | ) 60 | end -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios test_and_publish 20 | ``` 21 | fastlane ios test_and_publish 22 | ``` 23 | Runs unit tests, publishing a new pod version if tests succeed. 24 | ### ios publish_pod 25 | ``` 26 | fastlane ios publish_pod 27 | ``` 28 | Publish podspec, incrementing the patch version. 29 | ### ios test 30 | ``` 31 | fastlane ios test 32 | ``` 33 | Runs unit tests. 34 | 35 | ---- 36 | 37 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 38 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 39 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 40 | --------------------------------------------------------------------------------