├── .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 | 
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 |
--------------------------------------------------------------------------------