From 0830687e0fc7050d60b1f9661c057cdfafbc87a3 Mon Sep 17 00:00:00 2001 From: Cardasis Date: Thu, 11 Aug 2016 14:40:04 -0400 Subject: [PATCH] Initial commit --- JCColorPicker-Demo.xcodeproj/project.pbxproj | 32 ++ .../xcschemes/JCColorPicker-Demo.xcscheme | 45 +++ JCColorPicker-Demo/Base.lproj/Main.storyboard | 51 ++- JCColorPicker/JCColorAddButton.swift | 77 ++++ JCColorPicker/JCColorHandle.swift | 57 +++ JCColorPicker/JCColorPicker.swift | 359 ++++++++++++++++++ JCColorPicker/JCShadeSlider.swift | 184 +++++++++ JCColorPicker/UIColor+Utilities.swift | 59 +++ 8 files changed, 859 insertions(+), 5 deletions(-) create mode 100644 JCColorPicker/JCColorAddButton.swift create mode 100644 JCColorPicker/JCColorHandle.swift create mode 100644 JCColorPicker/JCColorPicker.swift create mode 100644 JCColorPicker/JCShadeSlider.swift create mode 100644 JCColorPicker/UIColor+Utilities.swift diff --git a/JCColorPicker-Demo.xcodeproj/project.pbxproj b/JCColorPicker-Demo.xcodeproj/project.pbxproj index 46ed77f..46b511f 100644 --- a/JCColorPicker-Demo.xcodeproj/project.pbxproj +++ b/JCColorPicker-Demo.xcodeproj/project.pbxproj @@ -12,6 +12,11 @@ 35C376D61D5CF5300069D7A1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35C376D41D5CF5300069D7A1 /* Main.storyboard */; }; 35C376D81D5CF5300069D7A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 35C376D71D5CF5300069D7A1 /* Assets.xcassets */; }; 35C376DB1D5CF5300069D7A1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35C376D91D5CF5300069D7A1 /* LaunchScreen.storyboard */; }; + 35C376E71D5CF9400069D7A1 /* JCColorAddButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376E21D5CF9400069D7A1 /* JCColorAddButton.swift */; }; + 35C376E81D5CF9400069D7A1 /* JCColorHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376E31D5CF9400069D7A1 /* JCColorHandle.swift */; }; + 35C376E91D5CF9400069D7A1 /* JCColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376E41D5CF9400069D7A1 /* JCColorPicker.swift */; }; + 35C376EA1D5CF9400069D7A1 /* JCShadeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376E51D5CF9400069D7A1 /* JCShadeSlider.swift */; }; + 35C376EB1D5CF9400069D7A1 /* UIColor+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C376E61D5CF9400069D7A1 /* UIColor+Utilities.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -22,6 +27,11 @@ 35C376D71D5CF5300069D7A1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35C376DA1D5CF5300069D7A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 35C376DC1D5CF5300069D7A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 35C376E21D5CF9400069D7A1 /* JCColorAddButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JCColorAddButton.swift; path = JCColorPicker/JCColorAddButton.swift; sourceTree = SOURCE_ROOT; }; + 35C376E31D5CF9400069D7A1 /* JCColorHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JCColorHandle.swift; path = JCColorPicker/JCColorHandle.swift; sourceTree = SOURCE_ROOT; }; + 35C376E41D5CF9400069D7A1 /* JCColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JCColorPicker.swift; path = JCColorPicker/JCColorPicker.swift; sourceTree = SOURCE_ROOT; }; + 35C376E51D5CF9400069D7A1 /* JCShadeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JCShadeSlider.swift; path = JCColorPicker/JCShadeSlider.swift; sourceTree = SOURCE_ROOT; }; + 35C376E61D5CF9400069D7A1 /* UIColor+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIColor+Utilities.swift"; path = "JCColorPicker/UIColor+Utilities.swift"; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,6 +64,7 @@ 35C376CF1D5CF5300069D7A1 /* JCColorPicker-Demo */ = { isa = PBXGroup; children = ( + 35C376EC1D5CF9450069D7A1 /* JCColorPicker */, 35C376D01D5CF5300069D7A1 /* AppDelegate.swift */, 35C376D21D5CF5300069D7A1 /* ViewController.swift */, 35C376D41D5CF5300069D7A1 /* Main.storyboard */, @@ -64,6 +75,18 @@ path = "JCColorPicker-Demo"; sourceTree = ""; }; + 35C376EC1D5CF9450069D7A1 /* JCColorPicker */ = { + isa = PBXGroup; + children = ( + 35C376E41D5CF9400069D7A1 /* JCColorPicker.swift */, + 35C376E31D5CF9400069D7A1 /* JCColorHandle.swift */, + 35C376E21D5CF9400069D7A1 /* JCColorAddButton.swift */, + 35C376E51D5CF9400069D7A1 /* JCShadeSlider.swift */, + 35C376E61D5CF9400069D7A1 /* UIColor+Utilities.swift */, + ); + name = JCColorPicker; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -96,6 +119,7 @@ TargetAttributes = { 35C376CC1D5CF5300069D7A1 = { CreatedOnToolsVersion = 7.3; + DevelopmentTeam = 43DKZUY8C6; }; }; }; @@ -135,8 +159,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 35C376E91D5CF9400069D7A1 /* JCColorPicker.swift in Sources */, 35C376D31D5CF5300069D7A1 /* ViewController.swift in Sources */, 35C376D11D5CF5300069D7A1 /* AppDelegate.swift in Sources */, + 35C376E81D5CF9400069D7A1 /* JCColorHandle.swift in Sources */, + 35C376EB1D5CF9400069D7A1 /* UIColor+Utilities.swift in Sources */, + 35C376E71D5CF9400069D7A1 /* JCColorAddButton.swift in Sources */, + 35C376EA1D5CF9400069D7A1 /* JCShadeSlider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -249,6 +278,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; INFOPLIST_FILE = "JCColorPicker-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.jonathancardasis.JCColorPicker-Demo"; @@ -260,6 +290,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; INFOPLIST_FILE = "JCColorPicker-Demo/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.jonathancardasis.JCColorPicker-Demo"; @@ -286,6 +317,7 @@ 35C376E11D5CF5300069D7A1 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/JCColorPicker-Demo.xcodeproj/xcuserdata/jcardasi.xcuserdatad/xcschemes/JCColorPicker-Demo.xcscheme b/JCColorPicker-Demo.xcodeproj/xcuserdata/jcardasi.xcuserdatad/xcschemes/JCColorPicker-Demo.xcscheme index 9a1df9b..e4f6e2e 100644 --- a/JCColorPicker-Demo.xcodeproj/xcuserdata/jcardasi.xcuserdatad/xcschemes/JCColorPicker-Demo.xcscheme +++ b/JCColorPicker-Demo.xcodeproj/xcuserdata/jcardasi.xcuserdatad/xcschemes/JCColorPicker-Demo.xcscheme @@ -5,6 +5,22 @@ + + + + + + + + + + @@ -26,6 +51,16 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + @@ -35,6 +70,16 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> + + + + diff --git a/JCColorPicker-Demo/Base.lproj/Main.storyboard b/JCColorPicker-Demo/Base.lproj/Main.storyboard index 3a2a49b..bce22ab 100644 --- a/JCColorPicker-Demo/Base.lproj/Main.storyboard +++ b/JCColorPicker-Demo/Base.lproj/Main.storyboard @@ -1,25 +1,66 @@ - + - + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JCColorPicker/JCColorAddButton.swift b/JCColorPicker/JCColorAddButton.swift new file mode 100644 index 0000000..65a3f99 --- /dev/null +++ b/JCColorPicker/JCColorAddButton.swift @@ -0,0 +1,77 @@ +// +// JCColorAddButton.swift +// +// Created by Jonathan Cardasis on 5/16/16. +// Copyright © 2016 Jonathan Cardasis. All rights reserved. +// + +import UIKit + +class JCColorAddButton: UIButton { + var color = UIColor.grayColor(){ + didSet{ + if let layer = circleLayer{ + layer.fillColor = color.CGColor + layer.strokeColor = color.darkerColor(0.075).CGColor + } + } + } + override var frame: CGRect{ //update on frame change + didSet{ + self.layoutCircleLayer() + self.layoutPlusIconLayer() + } + } + var circleLayer: CAShapeLayer? + var plusIconLayer: CAShapeLayer? + + + override init(frame: CGRect) { + super.init(frame: frame) + self.createGraphics() + } + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.createGraphics() + } + + func createGraphics(){ + circleLayer = CAShapeLayer() + self.layoutCircleLayer() + circleLayer!.fillColor = color.CGColor + self.layer.addSublayer(circleLayer!) + + /* Create Plus Icon */ + let plusPath = UIBezierPath() + plusPath.moveToPoint(CGPointMake(self.bounds.width/2 - self.bounds.width/8, self.bounds.height/2)) + plusPath.addLineToPoint(CGPointMake(self.bounds.width/2 + self.bounds.width/8, self.bounds.height/2)) + plusPath.moveToPoint(CGPointMake(self.bounds.width/2, self.bounds.height/2 + self.bounds.height/8)) + plusPath.addLineToPoint(CGPointMake(self.bounds.width/2, self.bounds.height/2 - self.bounds.height/8)) + + plusIconLayer = CAShapeLayer() + self.layoutPlusIconLayer() + plusIconLayer!.strokeColor = UIColor.whiteColor().CGColor + self.layer.addSublayer(plusIconLayer!) + } + + func layoutCircleLayer(){ + if let layer = circleLayer{ + layer.path = UIBezierPath(ovalInRect: self.bounds).CGPath + layer.lineWidth = frame.width * 0.04 //4 for size (100,100) + } + } + + func layoutPlusIconLayer(){ + if let layer = plusIconLayer{ + let plusPath = UIBezierPath() + plusPath.moveToPoint(CGPointMake(self.bounds.width/2 - self.bounds.width/8, self.bounds.height/2)) + plusPath.addLineToPoint(CGPointMake(self.bounds.width/2 + self.bounds.width/8, self.bounds.height/2)) + plusPath.moveToPoint(CGPointMake(self.bounds.width/2, self.bounds.height/2 + self.bounds.height/8)) + plusPath.addLineToPoint(CGPointMake(self.bounds.width/2, self.bounds.height/2 - self.bounds.height/8)) + + layer.path = plusPath.CGPath + layer.lineWidth = frame.width * 0.03 + } + } + +} \ No newline at end of file diff --git a/JCColorPicker/JCColorHandle.swift b/JCColorPicker/JCColorHandle.swift new file mode 100644 index 0000000..3998cf1 --- /dev/null +++ b/JCColorPicker/JCColorHandle.swift @@ -0,0 +1,57 @@ +// +// JCColorHandle.swift +// +// Created by Jonathan Cardasis on 5/16/16. +// Copyright © 2016 Jonathan Cardasis. All rights reserved. +// + +import UIKit + +class JCColorHandle: UIView { + var color = UIColor.blackColor() { + didSet{ + circleLayer.fillColor = color.CGColor + } + } + override var frame: CGRect{ + didSet { self.layoutCircleLayer() } + } + var circleLayer = CAShapeLayer() + + var shadowOffset: CGSize?{ + set{ + if let offset = newValue { + circleLayer.shadowColor = UIColor.blackColor().CGColor + circleLayer.shadowRadius = 3 + circleLayer.shadowOpacity = 0.3 + circleLayer.shadowOffset = offset + } + } + get{ + return circleLayer.shadowOffset + } + } + + override init(frame: CGRect) { + super.init(frame:frame) + self.backgroundColor = UIColor.clearColor() + + /* Add Shape Layer */ + //circleLayer.shouldRasterize = true + self.layoutCircleLayer() + circleLayer.strokeColor = UIColor.whiteColor().CGColor + circleLayer.fillColor = color.CGColor + + self.layer.addSublayer(circleLayer) + } + + func layoutCircleLayer(){ + circleLayer.path = UIBezierPath(ovalInRect: self.bounds).CGPath + circleLayer.strokeColor = UIColor.whiteColor().CGColor + circleLayer.lineWidth = frame.width/8.75 //4 + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/JCColorPicker/JCColorPicker.swift b/JCColorPicker/JCColorPicker.swift new file mode 100644 index 0000000..043e94b --- /dev/null +++ b/JCColorPicker/JCColorPicker.swift @@ -0,0 +1,359 @@ +// +// JCColorPicker.swift +// +// Created by Jonathan Cardasis on 5/9/16. +// Copyright © 2016 Jonathan Cardasis. All rights reserved. +// + +import UIKit + +protocol JCColorPickerDelegate { + /* Called when the user tapps the add button in the center */ + func colorPickerDidChooseColor(colorPicker: JCColorPicker, color: UIColor) +} + +class JCColorPicker: UIControl { + var hexLabel: UILabel! + var shadeSlider: JCShadeSlider! + var handleView: JCColorHandle! + var handleLine: CAShapeLayer! + var addButton: JCColorAddButton! + + var currentColor = UIColor() + var delegate: JCColorPickerDelegate? + var currentAngle: Float = 0 + var radius: CGFloat = 0 + var stroke: CGFloat = 1 + var padding: CGFloat = 15 + var handleSize: CGSize{ + get{ return CGSizeMake(self.bounds.width * 0.1, self.bounds.height * 0.1) } + } + + //MARK: - Initialization + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.commonInit() + } + + private func commonInit(){ + self.backgroundColor = UIColor.clearColor() + + let minDimension = min(self.bounds.size.width, self.bounds.size.height) + radius = minDimension/2 - handleSize.width/2 + + /* Setup Handle */ + handleView = JCColorHandle(frame: CGRectMake(0,0, handleSize.width, handleSize.height)) + handleView.shadowOffset = CGSizeMake(0,2) + + /* Setup pan gesture for handle */ + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(JCColorPicker.handleWasMoved(_:))) + handleView.addGestureRecognizer(panRecognizer) + + /* Setup Add Button */ + addButton = JCColorAddButton() + self.layoutAddButton() //layout frame + addButton.addTarget(self, action: #selector(JCColorPicker.addButtonPressed(_:)), forControlEvents: .TouchUpInside) + + /* Setup Handle Line */ + handleLine = CAShapeLayer() + handleLine.lineWidth = 2 + handleLine.strokeColor = UIColor.whiteColor().colorWithAlphaComponent(0.2).CGColor + + /* Setup Color Hex Label */ + hexLabel = UILabel() + self.layoutHexLabel() //layout frame + hexLabel.layer.cornerRadius = 2 + hexLabel.adjustsFontSizeToFitWidth = true + hexLabel.textAlignment = .Center + hexLabel.textColor = UIColor(red: 51/255.0, green:51/255.0, blue: 51/255.0, alpha: 0.65) + + /* Setup Shade Slider */ + shadeSlider = JCShadeSlider() + shadeSlider.delegate = self + self.layoutShadeSlider() + + + /* Add components to view */ + self.layer.addSublayer(handleLine) + self.addSubview(shadeSlider) + self.addSubview(hexLabel) + self.addSubview(handleView) + self.addSubview(addButton) + } + + override func willMoveToSuperview(newSuperview: UIView?) { + /* Get the starting color */ + currentColor = colorOnWheelFromAngle(currentAngle) + handleView.center = positionOnWheelFromAngle(currentAngle) //update pos for angle + self.layoutHandleLine() //layout the lines positioning + + handleView.color = currentColor + addButton.color = currentColor + shadeSlider.primaryColor = currentColor + self.updateHexLabel() //update for hex value + } + + + //MARK: - Handle Touches + override func touchesMoved(touches: Set, withEvent event: UIEvent?){ + //Overriden to prevent uicontrolevents being called from the super + } + override func touchesBegan(touches: Set, withEvent event: UIEvent?){ + let touchPoint = touches.first!.locationInView(self) + if CGRectContainsPoint(handleView.frame, touchPoint) { + self.sendActionsForControlEvents(.TouchDown) + + /* Enlarge Animation */ + UIView.animateWithDuration(0.15, delay: 0, options: .CurveEaseIn, animations: { () -> Void in + self.handleView.transform = CGAffineTransformMakeScale(1.45, 1.45) + }, completion: nil) + } + } + override func touchesEnded(touches: Set, withEvent event: UIEvent?) { + //Run this animation after a pan or here if touches are released + if handleView.transform.d > 1 { //if scale is larger than 1 (already animated) + self.executeHandleShrinkAnimation() + } + } + + func handleWasMoved(recognizer: UIPanGestureRecognizer) { + switch(recognizer.state){ + + case UIGestureRecognizerState.Changed: + let touchPosition = recognizer.locationInView(self) + self.moveHandleTowardPoint(touchPosition) + self.sendActionsForControlEvents(.TouchDragInside) + break + + case UIGestureRecognizerState.Ended: + /* Shrink Animation */ + self.executeHandleShrinkAnimation() + break + + default: + break + } + } + + private func executeHandleShrinkAnimation(){ + self.sendActionsForControlEvents(.TouchUpInside) + UIView.animateWithDuration(0.15, delay: 0, options: .CurveEaseOut, animations: { () -> Void in + self.handleView.transform = CGAffineTransformMakeScale(1, 1) + }, completion: nil) + } + + private func moveHandleTowardPoint(point: CGPoint){ + currentAngle = angleToCenterFromPoint(point) //Find the angle of point to the frames center + + //Layout Handle + self.layoutHandle() + + //Layout Line + self.layoutHandleLine() + + //Update color for shade slider + shadeSlider.primaryColor = handleView.color//currentColor + + //Update color for add button if a shade isnt selected + if shadeSlider.currentValue == 0 { + self.updateCurrentColor(shadeSlider.currentColor) + } + + //Update Text Field display value + self.updateHexLabel() + } + + + func addButtonPressed(sender: JCColorAddButton){ + //Do a 'bob' animation + UIView.animateWithDuration(0.2, + delay: 0, + options: .CurveEaseIn, + animations: { () -> Void in + sender.transform = CGAffineTransformMakeScale(1.1, 1.1) + }, completion: { (done) -> Void in + UIView.animateWithDuration(0.1, animations: { () -> Void in + sender.transform = CGAffineTransformMakeScale(1, 1) + }) + }) + + delegate?.colorPickerDidChooseColor(self, color: sender.color) //Delegate call + } + + + //MARK: - Drawing + override func drawRect(rect: CGRect) { + super.drawRect(rect) + let ctx = UIGraphicsGetCurrentContext() + drawRainbowCircle(in: ctx, outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1) + } + + /* + Resolution should be between 0.1 and 1 + */ + func drawRainbowCircle(in context: CGContextRef?, outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float){ + CGContextSaveGState(context) + CGContextTranslateCTM(context, CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) //Move context to center + + let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512 + + let innerHeight = (CGFloat(M_PI)*innerRadius)/subdivisions //height of the inner wall for each segment + let outterHeight = (CGFloat(M_PI)*outerRadius)/subdivisions + + let segment = UIBezierPath() + segment.moveToPoint(CGPointMake(innerRadius, -innerHeight/2)) + segment.addLineToPoint(CGPointMake(innerRadius, innerHeight/2)) + segment.addLineToPoint(CGPointMake(outerRadius, outterHeight/2)) + segment.addLineToPoint(CGPointMake(outerRadius, -outterHeight/2)) + segment.closePath() + + + //Draw each segment and rotate around the center + for i in 0 ..< Int(ceil(subdivisions)) { + UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set() + segment.fill() + let lineTailSpace = CGFloat(M_PI*2)*outerRadius/subdivisions //The amount of space between the tails of each segment + segment.lineWidth = lineTailSpace //allows for seemless scaling + segment.stroke() + + //Rotate to correct location + let rotate = CGAffineTransformMakeRotation(-(CGFloat(M_PI*2)/subdivisions)) //rotates each segment + segment.applyTransform(rotate) + } + + CGContextTranslateCTM(context, -CGRectGetMidX(self.bounds), -CGRectGetMidY(self.bounds)) //Move context back to original position + CGContextRestoreGState(context) + } + + + //MARK: - Layout Updates + /* re-layout view and all its subview and drawings */ + func layout() { + self.setNeedsDisplay() //mark view as dirty + + let minDimension = min(self.bounds.size.width, self.bounds.size.height) + radius = minDimension/2 - handleSize.width/2 //create radius for new size + + self.layoutAddButton() + + //Update handle's size + handleView.frame = CGRect(origin: .zero, size: handleSize) + self.layoutHandle() + + //Ensure colors are updated + self.updateCurrentColor(handleView.color) + shadeSlider.primaryColor = handleView.color + + self.layoutShadeSlider() + self.layoutHandleLine() + self.layoutHexLabel() + } + + func layoutAddButton(){ + let addButtonSize = CGSize(width: self.bounds.width/5, height: self.bounds.height/5) + addButton.frame = CGRect(x: CGRectGetMidX(self.bounds) - addButtonSize.width/2, y: CGRectGetMidY(self.bounds) - addButtonSize.height/2, width: addButtonSize.width, height: addButtonSize.height) + } + + /* + Update the handleView's position and color for the currentAngle + */ + func layoutHandle(){ + let angle = currentAngle //Preserve value in case it changes + let newPosition = positionOnWheelFromAngle(angle) //find the correct position on the color wheel + + //Update handle position + handleView.center = newPosition + + //Update color for the movement + handleView.color = colorOnWheelFromAngle(angle) + } + + /* + Updates the line view's position for the current angle + Pre: dependant on addButtons position + */ + func layoutHandleLine(){ + let linePath = UIBezierPath() + linePath.moveToPoint(addButton.center) + linePath.addLineToPoint(positionOnWheelFromAngle(currentAngle)) + handleLine.path = linePath.CGPath + } + + /* + Pre: dependant on addButtons position + */ + func layoutHexLabel(){ + hexLabel.frame = CGRect(x: 0, y: 0, width: addButton.bounds.width*1.5, height: addButton.bounds.height/3) + hexLabel.center = CGPointMake(CGRectGetMidX(self.bounds), (addButton.frame.origin.y + (padding + handleView.frame.height/2 + stroke/2))/1.75) //Divided by 1.75 not 2 to make it a bit lower + hexLabel.font = UIFont(name: "Menlo-Regular", size: hexLabel.bounds.height) + } + + /* + Pre: dependant on radius + */ + func layoutShadeSlider(){ + /* Calculate proper length for slider */ + let centerPoint = CGPoint(x: CGRectGetMidX(bounds), y: CGRectGetMidY(bounds)) + let insideRadius = radius - padding + + let pointLeft = CGPoint(x: centerPoint.x + insideRadius*CGFloat(cos(7*M_PI/6)), y: centerPoint.y - insideRadius*CGFloat(sin(7*M_PI/6))) + let pointRight = CGPoint(x: centerPoint.x + insideRadius*CGFloat(cos(11*M_PI/6)), y: centerPoint.y - insideRadius*CGFloat(sin(11*M_PI/6))) + let deltaX = pointRight.x - pointLeft.x //distance on circle between points at 7pi/6 and 11pi/6 + + + let sliderSize = CGSize(width: deltaX * 0.75, height: 0.08 * (bounds.height - padding*2))//bounds.height + shadeSlider.frame = CGRect(x: bounds.midX - sliderSize.width/2, y: pointLeft.y - sliderSize.height/2, width: sliderSize.width, height: sliderSize.height) + shadeSlider.handleCenterX = shadeSlider.bounds.width/2 //set handle starting position + shadeSlider.layoutLayerFrames() //call sliders' layout function + } + + func updateHexLabel(){ + hexLabel.text = "#" + currentColor.hexCode + } + + func updateCurrentColor(color: UIColor){ + currentColor = color + addButton.color = color + self.sendActionsForControlEvents(.ValueChanged) + } + + + //MARK: - Helper Methods + private func angleToCenterFromPoint(point: CGPoint) -> Float { + let deltaX = Float(CGRectGetMidX(self.bounds) - point.x) + let deltaY = Float(CGRectGetMidY(self.bounds) - point.y) + let angle = atan2f(deltaX, deltaY) + + // Convert the angle to be between 0 and 2PI + var adjustedAngle = angle + Float(M_PI/2) + if (adjustedAngle < 0){ //Left side (Q2 and Q3) + adjustedAngle += Float(M_PI*2) + } + + return adjustedAngle + } + + /* Find the angle relative to the center of the frame and uses the angle to find what color lies there */ + private func colorOnWheelFromAngle(angle: Float) -> UIColor { + return UIColor(hue: CGFloat(Double(angle)/(2*M_PI)), saturation: 1, brightness: 1, alpha: 1) + } + + /* Returns a position centered on the wheel for a given angle */ + private func positionOnWheelFromAngle(angle: Float) -> CGPoint{ + let buffer = padding + stroke/2 + return CGPoint(x: CGRectGetMidX(self.bounds) + ((radius - buffer) * CGFloat(cos(-angle))), y: CGRectGetMidY(self.bounds) + ((radius - buffer) * CGFloat(sin(-angle)))) + } +} + + +extension JCColorPicker: JCShadeSliderDelegate{ + func shadeSliderChoseColor(slider: JCShadeSlider, color: UIColor) { + self.updateCurrentColor(color) //update main controller for selected color + self.updateHexLabel() + } +} diff --git a/JCColorPicker/JCShadeSlider.swift b/JCColorPicker/JCShadeSlider.swift new file mode 100644 index 0000000..4060e0e --- /dev/null +++ b/JCColorPicker/JCShadeSlider.swift @@ -0,0 +1,184 @@ +// +// JCShadeSlider.swift +// +// Created by Jonathan Cardasis on 7/7/16. +// Copyright © 2016 Jonathan Cardasis. All rights reserved. +// + +import UIKit + +class JCSliderTrackLayer: CALayer{ + let gradient = CAGradientLayer() + + override init() { + super.init() + gradient.actions = ["position" : NSNull(), "bounds" : NSNull(), "path" : NSNull()] + self.addSublayer(gradient) + } + override init(layer: AnyObject) { + super.init(layer: layer) + } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +protocol JCShadeSliderDelegate { + func shadeSliderChoseColor(slider: JCShadeSlider, color: UIColor) +} + +class JCShadeSlider: UIControl { + var currentValue: CGFloat = 0.0 //range of {-1,1} + + let trackLayer = JCSliderTrackLayer() + let handleView = JCColorHandle() + var handleWidth: CGFloat{ return self.bounds.height } + var handleCenterX: CGFloat = 0.0 + var delegate: JCShadeSliderDelegate? + + var primaryColor = UIColor.grayColor(){ + didSet{ + self.changeColorHue(to: currentColor) + self.updateGradientTrack(for: primaryColor) + } + } + /* The computed color of the primary color with shading based on the currentValue */ + var currentColor: UIColor{ + get{ + if currentValue < 0 {//darken + return primaryColor.darkerColor(-currentValue) + } + else{ //lighten + return primaryColor.lighterColor(currentValue) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.commonInit() + } + + private func commonInit(){ + self.backgroundColor = nil + handleCenterX = self.bounds.width/2 + + trackLayer.backgroundColor = UIColor.blueColor().CGColor + trackLayer.masksToBounds = true + trackLayer.actions = ["position" : NSNull(), "bounds" : NSNull(), "path" : NSNull()] //disable implicit animations + self.layer.addSublayer(trackLayer) + + handleView.color = UIColor.blueColor() + handleView.circleLayer.borderWidth = 3 + handleView.userInteractionEnabled = false //disable interaction for touch events + self.layer.addSublayer(handleView.layer) + + let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(resetHandleToCenter)) + doubleTapGesture.numberOfTapsRequired = 2 + self.addGestureRecognizer(doubleTapGesture) + + self.layoutLayerFrames() + self.changeColorHue(to: currentColor) + self.updateGradientTrack(for: primaryColor) + } + + override func didMoveToSuperview() { + self.updateGradientTrack(for: primaryColor) + } + + func layoutLayerFrames(){ + trackLayer.frame = self.bounds.insetBy(dx: handleWidth/2, dy: self.bounds.height/4) //Make half the height of the bounds + trackLayer.cornerRadius = trackLayer.bounds.height/2 + + self.updateGradientTrack(for: primaryColor) + handleCenterX = (currentValue+1)/2 * (bounds.width - handleView.bounds.width) + handleView.bounds.width/2 //Update where the handles center should be + self.layoutHandleFrame() + } + + //Lays out handle according to the currentValue on slider + func layoutHandleFrame(){ + handleView.frame = CGRect(x: handleCenterX - handleWidth/2, y: self.bounds.height/2 - handleWidth/2, width: handleWidth, height: handleWidth) + } + + func changeColorHue(to newColor: UIColor){ + handleView.color = newColor + if currentValue != 0 { //Don't call delegate if the color hasnt changed + self.delegate?.shadeSliderChoseColor(self, color: newColor) + } + } + + func updateGradientTrack(for color: UIColor){ + trackLayer.gradient.frame = trackLayer.bounds + trackLayer.gradient.startPoint = CGPoint(x: 0, y: 0.5) + trackLayer.gradient.endPoint = CGPoint(x: 1, y: 0.5) + + //Gradient is for astetics - the slider is actually between black and white + trackLayer.gradient.colors = [color.darkerColor(0.65).CGColor, color.CGColor, color.lighterColor(0.65).CGColor] + } + + + override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool { + let location = touch.locationInView(self) + + if handleView.frame.contains(location) { + return true + } + return false + } + + override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool { + let location = touch.locationInView(self) + + //Update for center point + handleCenterX = location.x + handleCenterX = fittedValueInBounds(handleCenterX) //adjust value to fit in bounds if needed + + + //Update current value + currentValue = ((handleCenterX - handleWidth/2)/trackLayer.bounds.width - 0.5) * 2 //find current value between {-1,1} of the slider + + //Update handle color + self.changeColorHue(to: currentColor) + + //Update layers frames + CATransaction.begin() + CATransaction.setDisableActions(true) + self.layoutHandleFrame() + CATransaction.commit() + + self.sendActionsForControlEvents(.ValueChanged) + return true + } + + func resetHandleToCenter(recognizer: UITapGestureRecognizer){ + let location = recognizer.locationInView(self) + guard handleView.frame.contains(location) else { + return + } + + //tap is on handle + handleCenterX = self.bounds.width/2 + self.layoutHandleFrame() + handleView.color = primaryColor + currentValue = 0.0 + + self.sendActionsForControlEvents(.ValueChanged) + self.delegate?.shadeSliderChoseColor(self, color: currentColor) + } + + /* Helper Methods */ + //Returns a CGFloat for the highest/lowest possble value such that it is inside the views bounds + private func fittedValueInBounds(value: CGFloat) -> CGFloat { + return min(max(value, trackLayer.frame.minX), trackLayer.frame.maxX) + } + +} + + + + diff --git a/JCColorPicker/UIColor+Utilities.swift b/JCColorPicker/UIColor+Utilities.swift new file mode 100644 index 0000000..2b21141 --- /dev/null +++ b/JCColorPicker/UIColor+Utilities.swift @@ -0,0 +1,59 @@ +// +// UIColor+Utilities.swift +// JCUIElements +// +// Created by Jonathan Cardasis on 8/17/15. +// Copyright (c) 2015 Jonathan Cardasis. All rights reserved. +// + +import UIKit +extension UIColor{ + var hexCode: String { + get{ + let colorComponents = CGColorGetComponents(self.CGColor) + return String(format: "%02x%02x%02x", Int(colorComponents[0]*255.0), Int(colorComponents[1]*255.0),Int(colorComponents[2]*255.0)).uppercaseString + } + } + + //Amount should be between 0 and 1 + func lighterColor(amount: CGFloat) -> UIColor{ + return UIColor.blendColors(color: self, destinationColor: UIColor.whiteColor(), amount: amount) + } + + func darkerColor(amount: CGFloat) -> UIColor{ + return UIColor.blendColors(color: self, destinationColor: UIColor.blackColor(), amount: amount) + } + + static func blendColors(color color: UIColor, destinationColor: UIColor, amount : CGFloat) -> UIColor{ + var amountToBlend = amount; + if amountToBlend > 1{ + amountToBlend = 1.0 + } + else if amountToBlend < 0{ + amountToBlend = 0 + } + + var r,g,b, alpha : CGFloat + r = 0 + g = 0 + b = 0 + alpha = 0 + color.getRed(&r, green: &g, blue: &b, alpha: &alpha) //gets the rgba values (0-1) + + //Get the destination rgba values + var dest_r, dest_g, dest_b, dest_alpha : CGFloat + dest_r = 0 + dest_g = 0 + dest_b = 0 + dest_alpha = 0 + destinationColor.getRed(&dest_r, green: &dest_g, blue: &dest_b, alpha: &dest_alpha) + + r = amountToBlend * (dest_r * 255) + (1 - amountToBlend) * (r * 255) + g = amountToBlend * (dest_g * 255) + (1 - amountToBlend) * (g * 255) + b = amountToBlend * (dest_b * 255) + (1 - amountToBlend) * (b * 255) + alpha = fabs(alpha / dest_alpha) + + return UIColor(red: r/255.0, green: g/255.0, blue: b/255.0, alpha: alpha) + } + +} \ No newline at end of file