├── .github └── stale.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ └── hendriku.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Package.resolved ├── LICENSE ├── Package.swift ├── Sources └── ColorPickerRing │ ├── ColorExtension.swift │ └── ColorPickerRing.swift └── README.md /.github/stale.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm/xcode/package.xcworkspace/xcuserdata 6 | .swiftpm/xcode/xcuserdata 7 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DynamicColor", 6 | "repositoryURL": "https://github.com/yannickl/DynamicColor.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "1e85f9461f44bbf8c3ffba9101b8d202f864cdc6", 10 | "version": "5.0.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/hendriku.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ColorPicker.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | ColorPicker 16 | 17 | primary 18 | 19 | 20 | ColorPickerTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hendrik Ulbrich 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.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ColorPickerRing", 8 | platforms: [ 9 | .macOS(SupportedPlatform.MacOSVersion.v10_15), 10 | .iOS(SupportedPlatform.IOSVersion.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "ColorPickerRing", 16 | targets: ["ColorPickerRing"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | .package(url: "https://github.com/yannickl/DynamicColor.git", from: "5.0.0") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .target( 27 | name: "ColorPickerRing", 28 | dependencies: ["DynamicColor"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /Sources/ColorPickerRing/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtension.swift 3 | // ColorPickerRing 4 | // 5 | // Created by Hendrik Ulbrich on 16.07.19. 6 | // 7 | 8 | import SwiftUI 9 | import DynamicColor 10 | 11 | extension Angle { 12 | var color: DynamicColor { 13 | DynamicColor(hue: CGFloat(self.radians / (2 * .pi)), saturation: 1, brightness: 1, alpha: 1) 14 | } 15 | 16 | var colorView: Color { 17 | Color(hue: self.radians / (2 * .pi), saturation: 1, brightness: 1) 18 | } 19 | } 20 | 21 | extension DynamicColor { 22 | var angle: Angle { 23 | Angle(radians: Double(2 * .pi * self.hueComponent)) 24 | } 25 | } 26 | 27 | extension AngularGradient { 28 | static let conic = AngularGradient(gradient: Gradient.colorWheelSpectrum, center: .center, angle: .degrees(-90)) 29 | } 30 | 31 | extension Gradient { 32 | static let colorWheelSpectrum: Gradient = Gradient(colors: [ 33 | Angle(radians: 3/6 * .pi).colorView, 34 | 35 | Angle(radians: 2/6 * .pi).colorView, 36 | Angle(radians: 1/6 * .pi).colorView, 37 | Angle(radians: 12/6 * .pi).colorView, 38 | 39 | Angle(radians: 11/6 * .pi).colorView, 40 | 41 | Angle(radians: 10/6 * .pi).colorView, 42 | Angle(radians: 9/6 * .pi).colorView, 43 | Angle(radians: 8/6 * .pi).colorView, 44 | 45 | Angle(radians: 7/6 * .pi).colorView, 46 | 47 | Angle(radians: 6/6 * .pi).colorView, 48 | Angle(radians: 5/6 * .pi).colorView, 49 | Angle(radians: 4/6 * .pi).colorView, 50 | 51 | Angle(radians: 3/6 * .pi).colorView, 52 | ]) 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ColorPickerRing 2 | 3 | A color picker implementation with color wheel appearance written in plain SwiftUI. It is compatible with UIColor and NSColor. 4 | 5 | ![ColorPickerDemo-2](https://user-images.githubusercontent.com/8998518/78367183-f6531780-75c1-11ea-8829-f288241c0d49.gif) 6 | 7 | ## Usage 8 | 9 | Add this repository as a Swift Package Dependency to your project. You find the option in Xcode unter "File > Swift Packages > Add Package Dependency...". Paste the HTTPS reference to this repo and you're done! 10 | 11 | After importing the module: Simply use the `ColorPickerRing` structure which is a regular SwiftUI `View`. 12 | This project uses the [DynamicColor](https://github.com/yannickl/DynamicColor) dependency which lets you write plattform independant color classes. No matter if iOS or macOS. Internally it uses `UIColor` and `NSColor`. 13 | 14 | ``` 15 | import SwiftUI 16 | import ColorPickerRing 17 | 18 | struct ContentView: View { 19 | @State var color = UIColor.red 20 | 21 | var body: some View { 22 | ColorPickerRing(color: $color, strokeWidth: 30) 23 | .frame(width: 300, height: 300, alignment: .center) 24 | } 25 | } 26 | ``` 27 | 28 | The color wheel will take all the space it can get unless you frame it to a custom size. You are also able to specify the `strokeWidth` of the color wheel over the given property. 29 | 30 | ## Usage with SwiftUI Color 31 | 32 | As the native SwiftUI `Color` is implemented as a view rather than a model data structure it won't be used as data structure in the future. This is conforming with the indices provided by Apple: They implemented a @frozen property for their `Color` class which could be interpreted that they won't provide any further interfaces for data extraction to their `Color` view class. 33 | 34 | ## License 35 | 36 | You can use this software under the terms and conditions of the MIT License. 37 | 38 | Hendrik Ulbrich © 2020 39 | -------------------------------------------------------------------------------- /Sources/ColorPickerRing/ColorPickerRing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPickerRing.swift 3 | // ColorPickerRing 4 | // 5 | // Created by Hendrik Ulbrich on 15.07.19. 6 | // 7 | // This code uses: 8 | // https://developer.apple.com/documentation/swiftui/gestures/composing_swiftui_gestures 9 | // and 10 | // https://developer.apple.com/wwdc19/237 11 | 12 | import SwiftUI 13 | import DynamicColor 14 | 15 | public struct ColorPickerRing : View { 16 | public var color: Binding 17 | public var strokeWidth: CGFloat = 30 18 | 19 | public var body: some View { 20 | GeometryReader { 21 | ColorWheel(color: self.color, frame: $0.frame(in: .local), strokeWidth: self.strokeWidth) 22 | } 23 | .aspectRatio(1, contentMode: .fit) 24 | } 25 | 26 | public init(color: Binding, strokeWidth: CGFloat) { 27 | self.color = color 28 | self.strokeWidth = strokeWidth 29 | } 30 | } 31 | 32 | public struct ColorWheel: View { 33 | public var color: Binding 34 | public var frame: CGRect 35 | public var strokeWidth: CGFloat 36 | 37 | public var body: some View { 38 | let indicatorOffset = CGSize( 39 | width: cos(color.wrappedValue.angle.radians) * Double(frame.midX - strokeWidth / 2), 40 | height: -sin(color.wrappedValue.angle.radians) * Double(frame.midY - strokeWidth / 2)) 41 | return ZStack(alignment: .center) { 42 | // Color Gradient 43 | Circle() 44 | .strokeBorder(AngularGradient.conic, lineWidth: strokeWidth) 45 | .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local) 46 | .onChanged(self.update(value:)) 47 | ) 48 | // Color Selection Indicator 49 | Circle() 50 | .fill(Color(color.wrappedValue)) 51 | .frame(width: strokeWidth, height: strokeWidth, alignment: .center) 52 | .fixedSize() 53 | .offset(indicatorOffset) 54 | .allowsHitTesting(false) 55 | .overlay( 56 | Circle() 57 | .stroke(Color.white, lineWidth: 3) 58 | .offset(indicatorOffset) 59 | .allowsHitTesting(false) 60 | ) 61 | } 62 | } 63 | 64 | public init(color: Binding, frame: CGRect, strokeWidth: CGFloat) { 65 | self.frame = frame 66 | self.color = color 67 | self.strokeWidth = strokeWidth 68 | } 69 | 70 | func update(value: DragGesture.Value) { 71 | self.color.wrappedValue = Angle(radians: radCenterPoint(value.location, frame: self.frame)).color 72 | } 73 | 74 | func radCenterPoint(_ point: CGPoint, frame: CGRect) -> Double { 75 | let adjustedAngle = atan2f(Float(frame.midX - point.x), Float(frame.midY - point.y)) + .pi / 2 76 | return Double(adjustedAngle < 0 ? adjustedAngle + .pi * 2 : adjustedAngle) 77 | } 78 | } 79 | --------------------------------------------------------------------------------