-
-
Save Amzd/62160c84a29ae93d565b20f63b9e3247 to your computer and use it in GitHub Desktop.
import SwiftUI | |
@available(iOS 14.0, *) | |
public struct ColorPickerWithoutLabel: UIViewRepresentable { | |
@Binding var selection: Color | |
var supportsAlpha: Bool = true | |
public init(selection: Binding<Color>, supportsAlpha: Bool = true) { | |
self._selection = selection | |
self.supportsAlpha = supportsAlpha | |
} | |
public func makeUIView(context: Context) -> UIColorWell { | |
let well = UIColorWell() | |
well.supportsAlpha = supportsAlpha | |
return well | |
} | |
public func updateUIView(_ uiView: UIColorWell, context: Context) { | |
uiView.selectedColor = UIColor(selection) | |
} | |
} | |
extension View { | |
@available(iOS 14.0, *) | |
public func colorPickerSheet(isPresented: Binding<Bool>, selection: Binding<Color>, supportsAlpha: Bool = true, title: String? = nil) -> some View { | |
self.background(ColorPickerSheet(isPresented: isPresented, selection: selection, supportsAlpha: supportsAlpha, title: title)) | |
} | |
} | |
@available(iOS 14.0, *) | |
private struct ColorPickerSheet: UIViewRepresentable { | |
@Binding var isPresented: Bool | |
@Binding var selection: Color | |
var supportsAlpha: Bool | |
var title: String? | |
func makeCoordinator() -> Coordinator { | |
Coordinator(selection: $selection, isPresented: $isPresented) | |
} | |
class Coordinator: NSObject, UIColorPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate { | |
@Binding var selection: Color | |
@Binding var isPresented: Bool | |
var didPresent = false | |
init(selection: Binding<Color>, isPresented: Binding<Bool>) { | |
self._selection = selection | |
self._isPresented = isPresented | |
} | |
func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { | |
selection = Color(viewController.selectedColor) | |
} | |
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { | |
isPresented = false | |
didPresent = false | |
} | |
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { | |
isPresented = false | |
didPresent = false | |
} | |
} | |
func getTopViewController(from view: UIView) -> UIViewController? { | |
guard var top = view.window?.rootViewController else { | |
return nil | |
} | |
while let next = top.presentedViewController { | |
top = next | |
} | |
return top | |
} | |
func makeUIView(context: Context) -> UIView { | |
let view = UIView() | |
view.isHidden = true | |
return view | |
} | |
func updateUIView(_ uiView: UIView, context: Context) { | |
if isPresented && !context.coordinator.didPresent { | |
let modal = UIColorPickerViewController() | |
modal.selectedColor = UIColor(selection) | |
modal.supportsAlpha = supportsAlpha | |
modal.title = title | |
modal.delegate = context.coordinator | |
modal.presentationController?.delegate = context.coordinator | |
let top = getTopViewController(from: uiView) | |
top?.present(modal, animated: true) | |
context.coordinator.didPresent = true | |
} | |
} | |
} |
Thank you! This helped tremendously. Some changes I made to ColorPickerSheet
which may help others:
import SwiftUI
extension View {
@available(iOS 15.0, *)
public func colorPickerSheet(isPresented: Binding<Bool>, selection: Binding<Color>, supportsAlpha: Bool = false, title: String? = nil, action: @escaping () -> Void) -> some View {
self.background(ColorPickerSheet(isPresented: isPresented, selection: selection, supportsAlpha: supportsAlpha, title: title, action: action))
}
}
@available(iOS 15.0, *)
private struct ColorPickerSheet: UIViewRepresentable {
@Binding var isPresented: Bool
@Binding var selection: Color
var supportsAlpha: Bool
var title: String?
var action: () -> Void
func makeCoordinator() -> Coordinator {
Coordinator(selection: $selection, isPresented: $isPresented, action: action)
}
class Coordinator: NSObject, UIColorPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
@Binding var selection: Color
@Binding var isPresented: Bool
var didPresent = false
var action: () -> Void
init(selection: Binding<Color>, isPresented: Binding<Bool>, action: @escaping () -> Void) {
self._selection = selection
self._isPresented = isPresented
self.action = action
}
func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) {
selection = Color(viewController.selectedColor)
action()
}
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
isPresented = false
didPresent = false
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
isPresented = false
didPresent = false
}
}
func getTopViewController(from view: UIView) -> UIViewController? {
guard var top = view.window?.rootViewController else {
return nil
}
while let next = top.presentedViewController {
top = next
}
return top
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.isHidden = true
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if isPresented && !context.coordinator.didPresent {
let modal = UIColorPickerViewController()
modal.selectedColor = UIColor(selection)
modal.supportsAlpha = supportsAlpha
modal.title = title
modal.delegate = context.coordinator
modal.presentationController?.delegate = context.coordinator
let top = getTopViewController(from: uiView)
top?.present(modal, animated: true)
context.coordinator.didPresent = true
}
}
}
@JTostitos Seems you added a callback for selection changes?
You could just use SwiftUIs onChange?
From the top of my head it could be something like this
.colorPickerSheet(..., selection: $color, ...)
.onChange(of: color) { newColor in
// do whatever you wanted to do in the action block
}
@Amzd Sorry, I didn't realize that my explanation got removed when I updated my code in my comment. What I was trying to do was make it so that when you tap a color, it automatically dismisses but that caused a few other issues with buttons that are displayed on the view behind the Color Picker and so it wasn't worth it for me to try and fix it. I ended up leaving the callback though because 3 less lines of code in my view if it is part of the .colorPickerSheet()
itself instead of using .onChange()
.
Thank you, this is still very helpful! But is there a way to display the picker inside a popover instead of a sheet?
@Amzd thank you for this!
I've noticed that on Catalyst, it first opens up a sheet with just a tiny color selection box in the top left corner. Then pressing the "show colors" button, we get the regular Mac color picker.
@pianostringquartet hmm yea I didnt write this with crossplatform code in mind
On iOS, this works perfectly, just with a renaming of the supportsOpacity
to supportsAlpha
argument.
I posted a Stackoverflow question and answer, in case anyone else might find this useful, and linked to this post, @Amzd .
It fixed my issue that had me scratching my head wondering why I couldn't align my color pickers, due to the blank label actually taking up space (can see below in before vs. after screenshots of implementing this ColorPickerWithoutLabel
:
Before:

Turns out, using your struct ColorPickerWithoutLabel
broke my code. I believe this is due to the fact that yours only supports Color
types, while Swift's also supports CGColor
s :
public struct ColorPicker<Label> : View where Label : View {
/// Creates an instance that selects a color.
///
/// - Parameters:
/// - selection: A ``Binding`` to the variable that displays the
/// selected ``Color``.
/// - supportsOpacity: A Boolean value that indicates whether the color
/// picker allows adjusting the selected color's opacity; the default
/// is `true`.
/// - label: A view that describes the use of the selected color.
/// The system color picker UI sets it's title using the text from
/// this view.
///
public init(selection: Binding<Color>, supportsOpacity: Bool = true, @ViewBuilder label: () -> Label)
/// Creates an instance that selects a color.
///
/// - Parameters:
/// - selection: A ``Binding`` to the variable that displays the
/// selected ``CGColor``.
/// - supportsOpacity: A Boolean value that indicates whether the color
/// picker allows adjusting the selected color's opacity; the default
/// is `true`.
/// - label: A view that describes the use of the selected color.
/// The system color picker UI sets it's title using the text from
/// this view.
///
public init(selection: Binding<CGColor>, supportsOpacity: Bool = true, @ViewBuilder label: () -> Label)
/// The content and behavior of the view.
///
/// When you implement a custom view, you must implement a computed
/// `body` property to provide the content for your view. Return a view
/// that's composed of built-in views that SwiftUI provides, plus other
/// composite views that you've already defined:
///
/// struct MyView: View {
/// var body: some View {
/// Text("Hello, World!")
/// }
/// }
///
/// For more information about composing views and a view hierarchy,
/// see <doc:Declaring-a-Custom-View>.
@MainActor public var body: some View { get }
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required ``View/body-swift.property`` property.
public typealias Body = some View
}
Thank you so much!
I hated working with the ColorPicker SwiftUI implementation so I wrote two fixes.
ColorPickerWithoutLabel
which speaks for itself; it doesn't have a label.View.colorPickerSheet
it boggles my mind that you cannot use ColorPicker as just a sheet by default, I must be missing something. Anyway, I wrote it myself with UIKit under the hood.