Skip to content

Instantly share code, notes, and snippets.

@Amzd
Last active Aug 31, 2022
Embed
What would you like to do?
What SwiftUI's ColorPicker should have been.
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
}
}
}
@Amzd
Copy link
Author

Amzd commented Jan 27, 2021

I hated working with the ColorPicker SwiftUI implementation so I wrote two fixes.

  1. ColorPickerWithoutLabel which speaks for itself; it doesn't have a label.
  2. 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.

@JTostitos
Copy link

JTostitos commented Aug 30, 2022

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
        }
    }
}

@Amzd
Copy link
Author

Amzd commented Aug 31, 2022

@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
}

@JTostitos
Copy link

JTostitos commented Aug 31, 2022

@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().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment