├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── PresentableColorPicker │ ├── Notification.Name+Extensions.swift │ ├── PresentableColorPicker+Modifiers.swift │ ├── PresentableColorPicker.swift │ ├── Resources │ ├── en-US.lproj │ │ └── Localizable.strings │ └── en.lproj │ │ └── Localizable.strings │ └── UIKitColorPicker.swift └── Tests ├── LinuxMain.swift └── PresentableColorPickerTests ├── PresentableColorPickerTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Franklyn Weber 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.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "HalfASheet", 6 | "repositoryURL": "https://github.com/franklynw/HalfASheet.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "2f1959c5e8a74cb078b96942d46388b7e4bebcdb", 10 | "version": "1.0.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "PresentableColorPicker", 8 | defaultLocalization: "en", 9 | platforms: [ 10 | .iOS(.v14) 11 | ], 12 | products: [ 13 | .library( 14 | name: "PresentableColorPicker", 15 | targets: ["PresentableColorPicker"]), 16 | ], 17 | dependencies: [ 18 | .package(name: "HalfASheet", url: "https://github.com/franklynw/HalfASheet.git", .upToNextMajor(from: "1.0.0")) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "PresentableColorPicker", 23 | dependencies: ["HalfASheet"], 24 | resources: [.process("Resources")]), 25 | .testTarget( 26 | name: "PresentableColorPickerTests", 27 | dependencies: ["PresentableColorPicker"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PresentableColorPicker 2 | 3 | A Colour Picker pretty much identical to Apple's ColorPicker (in that it uses the UIColorPickerViewController), except that this one can be presented in the way common to sheets, etc, with a bound 'isPresented' boolean var. 4 | 5 | 6 | ## Installation 7 | 8 | ### Swift Package Manager 9 | 10 | In Xcode: 11 | * File ⭢ Swift Packages ⭢ Add Package Dependency... 12 | * Use the URL https://github.com/franklynw/PresentableColorPicker 13 | 14 | 15 | ## Example 16 | 17 | > **NB:** All examples require `import PresentableColorPicker` at the top of the source file 18 | 19 | It can be used directly as a view, which offers the full range of customisation options - 20 | 21 | ```swift 22 | var body: some View { 23 | 24 | PresentableColorPicker(isPresented: $isStandaloneColorPickerPresented) { 25 | viewModel.paintColor = $0 26 | } 27 | .backgroundColor(viewModel.backgroundColor) 28 | .disableDismissOnSelection 29 | } 30 | ``` 31 | 32 | or as a modifier, which presents the default colour picker (with no customisation options) - 33 | 34 | ```swift 35 | var body: some View { 36 | 37 | MyView { 38 | 39 | } 40 | .presentableColorPicker(isPresented: $isStandaloneColorPickerPresented, Binding: $viewModel.paintColor) 41 | } 42 | ``` 43 | 44 | Both of these methods allow you to specify either a binding to a Color var, or use a 'colorSelected' closure which is invoked when the colour is picked. 45 | 46 | 47 | ### Set the picker's title 48 | 49 | ```swift 50 | PresentableColorPicker(isPresented: $isStandaloneColorPickerPresented, selected: $viewModel.paintColor) 51 | .title("Pick a colour") 52 | ``` 53 | 54 | If not used, the title will default to localised "Colour" (ie, if you have "Colour" in your Localizable.strings file, it will use that, otherwise just "Colour") 55 | 56 | 57 | ### Disable the automatic "dismiss on selection" functionality 58 | 59 | This might be necessary if you have (eg) a preview visible above the picker, where you can see how your selected colour looks - the user can then decide when to dismiss the picker 60 | 61 | ```swift 62 | PresentableColorPicker(isPresented: $isStandaloneColorPickerPresented, selected: $viewModel.paintColor) 63 | .disableDismissOnSelection 64 | ``` 65 | 66 | ### Set the picker's background colour 67 | 68 | ```swift 69 | PresentableColorPicker(isPresented: $isStandaloneColorPickerPresented, selected: $viewModel.paintColor) 70 | .backgroundColor(.lightGray) 71 | ``` 72 | 73 | ### Set the height of the picker as either a fixed height or as a proportion of the containing view's height 74 | 75 | ```swift 76 | PresentableColorPicker(isPresented: $isStandaloneColorPickerPresented, selected: $viewModel.paintColor) 77 | .height(.fixed(400)) 78 | ``` 79 | 80 | or 81 | 82 | ```swift 83 | PresentableColorPicker(isPresented: $isStandaloneColorPickerPresented, selected: $viewModel.paintColor) 84 | .height(.proportional(0.6)) 85 | ``` 86 | 87 | ## Additionally... 88 | 89 | There are two NotificationCenter notifications which are sent, which are defined as static vars on Notification.Name - 90 | 91 | * presentableColorPickerAppeared ("PresentableColorPickerAppearedNotification") 92 | * presentableColorPickerDisappeared ("PresentableColorPickerDisappearedNotification") 93 | 94 | These are sent as their names suggest, and there is no additional userInfo 95 | 96 | 97 | ## Dependencies 98 | 99 | Requires HalfASheet, which is linked. Take a look at it [here](https://github.com/franklynw/HalfASheet) 100 | 101 | 102 | ## Licence 103 | 104 | `PresentableColorPicker` is available under the MIT licence 105 | -------------------------------------------------------------------------------- /Sources/PresentableColorPicker/Notification.Name+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.Name+Extensions.swift 3 | // 4 | // 5 | // Created by Franklyn Weber on 04/02/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension Notification.Name { 12 | 13 | public static let presentableColorPickerAppeared = Notification.Name("PresentableColorPickerAppearedNotification") 14 | public static let presentableColorPickerDisappeared = Notification.Name("PresentableColorPickerDisappearedNotification") 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PresentableColorPicker/PresentableColorPicker+Modifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentableColorPicker+Modifiers.swift 3 | // 4 | // 5 | // Created by Franklyn Weber on 10/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | import HalfASheet 10 | 11 | 12 | extension PresentableColorPicker { 13 | 14 | /// The title to use for the picker 15 | /// - Parameter title: a String 16 | public func title(_ title: String) -> Self { 17 | var copy = self 18 | copy.title = title 19 | return copy 20 | } 21 | 22 | /// The color to use for the background of the picker 23 | /// - Parameter backgroundColor: a UIColor 24 | public func backgroundColor(_ backgroundColor: UIColor) -> Self { 25 | var copy = self 26 | copy.backgroundColor = backgroundColor 27 | return copy 28 | } 29 | 30 | /// Use this for more precise control over the picker's height 31 | /// - Parameter height: a HalfASheetHeight case 32 | public func height(_ height: HalfASheetHeight) -> Self { 33 | var copy = self 34 | copy.height = height 35 | return copy 36 | } 37 | 38 | /// Normally, selecting a colour will dismiss the picker. Use this to disable that functionality (eg if you have a preview which shows how the colour will look in context) 39 | public var disableDismissOnSelection: Self { 40 | var copy = self 41 | copy.dismissOnSelection = false 42 | return copy 43 | } 44 | } 45 | 46 | 47 | extension View { 48 | 49 | /// View extension in the style of .sheet - lacks a couple of customisation options. If more flexibility is required, use PresentableColorPicker(...) directly, and apply the required modifiers 50 | /// - Parameters: 51 | /// - isPresented: binding to a Bool which controls whether or not to show the picker 52 | /// - selection: binding to a Color var for the selected colour 53 | public func presentableColorPicker(isPresented: Binding, selection: Binding) -> some View { 54 | modifier(ColorPickerPresentationModifier(content: { PresentableColorPicker(isPresented: isPresented, selection: selection)})) 55 | } 56 | 57 | /// View extension in the style of .sheet - lacks a couple of customisation options. If more flexibility is required, use PresentableColorPicker(...) directly, and apply the required modifiers 58 | /// - Parameters: 59 | /// - isPresented: binding to a Bool which controls whether or not to show the picker 60 | /// - colorSelected: closure invoked with the selected colour when the user picks a colour 61 | public func presentableColorPicker(isPresented: Binding, colorSelected: @escaping (Color) -> ()) -> some View { 62 | modifier(ColorPickerPresentationModifier(content: { PresentableColorPicker(isPresented: isPresented, colorSelected: colorSelected)})) 63 | } 64 | } 65 | 66 | 67 | struct ColorPickerPresentationModifier: ViewModifier { 68 | 69 | var content: () -> PresentableColorPicker 70 | 71 | init(@ViewBuilder content: @escaping () -> PresentableColorPicker) { 72 | self.content = content 73 | } 74 | 75 | func body(content: Content) -> some View { 76 | ZStack { 77 | content 78 | self.content() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/PresentableColorPicker/PresentableColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentableColorPicker.swift 3 | // Simplist 4 | // 5 | // Created by Franklyn Weber on 10/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | import HalfASheet 10 | 11 | 12 | public struct PresentableColorPicker: View { 13 | 14 | @Binding private var isPresented: Bool 15 | @Binding private var selection: Color 16 | private var colorSelected: ((Color) -> ())? 17 | 18 | internal var title: String? 19 | internal var backgroundColor: UIColor = .systemBackground 20 | internal var dismissOnSelection = true 21 | internal var height: HalfASheetHeight? 22 | 23 | 24 | public init(isPresented: Binding, selection: Binding) { 25 | _isPresented = isPresented 26 | _selection = selection 27 | colorSelected = nil 28 | } 29 | 30 | public init(isPresented: Binding, colorSelected: @escaping (Color) -> ()) { 31 | _isPresented = isPresented 32 | _selection = Binding(get: { .black }, set: { _ in }) 33 | self.colorSelected = colorSelected 34 | } 35 | 36 | public var body: some View { 37 | 38 | HalfASheet(isPresented: $isPresented) { 39 | presentColorPicker() 40 | .background(Color(backgroundColor)) 41 | .cornerRadius(15) 42 | .onAppear { 43 | NotificationCenter.default.post(name: .presentableColorPickerAppeared, object: self) 44 | } 45 | .onDisappear { 46 | NotificationCenter.default.post(name: .presentableColorPickerDisappeared, object: self) 47 | } 48 | } 49 | .closeButtonColor(UIColor.gray.withAlphaComponent(0.4)) 50 | .height(height ?? .fixed(544)) 51 | } 52 | 53 | private func presentColorPicker() -> UIKitColorPicker { 54 | 55 | var picker: UIKitColorPicker 56 | 57 | if let colorSelected = colorSelected { 58 | picker = UIKitColorPicker(isPresented: _isPresented, colorSelected: colorSelected) 59 | } else { 60 | picker = UIKitColorPicker(isPresented: _isPresented, selection: _selection) 61 | } 62 | 63 | picker.dismissOnSelection = dismissOnSelection 64 | picker.title = title 65 | 66 | return picker 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/PresentableColorPicker/Resources/en-US.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | PresentableColorPicker 4 | 5 | Created by Franklyn Weber on 15/01/2021. 6 | 7 | */ 8 | 9 | "Colour" = "Color"; 10 | -------------------------------------------------------------------------------- /Sources/PresentableColorPicker/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | PresentableColorPicker 4 | 5 | Created by Franklyn Weber on 15/01/2021. 6 | 7 | */ 8 | 9 | "Colour" = "Colour"; 10 | -------------------------------------------------------------------------------- /Sources/PresentableColorPicker/UIKitColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitColorPicker.swift 3 | // 4 | // 5 | // Created by Franklyn Weber on 10/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct UIKitColorPicker: UIViewControllerRepresentable { 12 | 13 | typealias UIViewControllerType = UIColorPickerViewController 14 | 15 | @Binding private var isPresented: Bool 16 | @Binding private var selection: Color 17 | private var colorSelected: ((Color) -> ())? 18 | 19 | var dismissOnSelection = true 20 | var title: String? 21 | 22 | 23 | init(isPresented: Binding, selection: Binding) { 24 | _isPresented = isPresented 25 | _selection = selection 26 | } 27 | 28 | init(isPresented: Binding, colorSelected: @escaping (Color) -> ()) { 29 | _isPresented = isPresented 30 | _selection = Binding(get: { .black }, set: { _ in }) 31 | self.colorSelected = colorSelected 32 | } 33 | 34 | 35 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewControllerType { 36 | 37 | let controller = UIColorPickerViewController() 38 | controller.selectedColor = UIColor(selection) 39 | controller.supportsAlpha = false 40 | controller.delegate = context.coordinator 41 | controller.title = title 42 | 43 | return controller 44 | } 45 | 46 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext) { 47 | 48 | } 49 | 50 | func makeCoordinator() -> UIKitColorPicker.Coordinator { 51 | Coordinator(self) 52 | } 53 | 54 | class Coordinator: NSObject, UIColorPickerViewControllerDelegate { 55 | 56 | let parent: UIKitColorPicker 57 | 58 | init(_ parent: UIKitColorPicker) { 59 | self.parent = parent 60 | } 61 | 62 | func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { 63 | 64 | let color = Color(viewController.selectedColor) 65 | 66 | parent.colorSelected?(color) 67 | parent.selection = color 68 | 69 | if parent.dismissOnSelection { 70 | parent.isPresented = false 71 | } 72 | } 73 | 74 | func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { 75 | parent.isPresented = false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PresentableColorPickerTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PresentableColorPickerTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/PresentableColorPickerTests/PresentableColorPickerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PresentableColorPicker 3 | 4 | final class PresentableColorPickerTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/PresentableColorPickerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(PresentableColorPickerTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------