clearlight
clearlight

Reputation: 12625

Make custom-triggered UIColorPickerViewController hug bottom of screen?

I am using custom control to trigger the presentation of a UIColorPickerViewController. It triggers the color picker, but not as expected.

I was using UIColorWell previously, which does present the picker correctly (but it is an opaque implementation so I don't know how it does so). I don't want to use UIColorWell, because the shape and appearance aren't right for where I'm invoking the picker from.

Everything works exactly as desired except I can't get the color picker to present at the bottom of the screen like UIColorWell would do it. Instead, whatever I've tried to present (or show), UIColorPickerController appears near the top of the screen.

Note: Currently, to present the color picker controller, I'm using the latest iOS 15 trick with sheet controller / detents to try to anchor the picker to the bottom of the screen, but it's not working as expected (it's the technique is from the Apple Docs example, and as I've seen documented online).

What might be happening? What can I do to get the picker at the bottom of the screen, so that it doesn't obstruct my interface?

The Extension:

extension UIView {
    func findViewController() -> UIViewController? {
        if let nextResponder = self.next as? UIViewController {
            return nextResponder
        } else if let nextResponder = self.next as? UIView {
            return nextResponder.findViewController()
        } else {
            return nil
        }
     }
}

Reproducible example (almost. It needs to be implemented/invoked from whatever VC [not shown here]):

This is custom control that that creates a rectangular color picker view (I'm using it to replace UIColorWell). It attempts to locate the VC which contains the view of which this custom control is a subview.

import UIKit
import QuartzCore

class RectangularColorWell : UIControl {

    var colorPickerController = UIColorPickerViewController()

    var selectedColor : UIColor = UIColor.clear

    var lineWidth : CGFloat = 4.0 {
        didSet {
            self.setNeedsDisplay()
        }
    }
      
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        colorPickerController.supportsAlpha = false
        colorPickerController.delegate = self
        selectedColor = backgroundColor ?? UIColor.clear
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(CustomToggleControl.controlTapped(_:)))
        tapGestureRecognizer.numberOfTapsRequired = 1;
        tapGestureRecognizer.numberOfTouchesRequired = 1;
        self.addGestureRecognizer(tapGestureRecognizer)
        self.setNeedsDisplay()
        
    }
    override func draw(_ rect: CGRect) {
        guard let ctx = UIGraphicsGetCurrentContext() else { return }
        ctx.setLineWidth(lineWidth)
        ctx.setFillColor(backgroundColor!.cgColor)
        ctx.fill(rect)
        drawGradientBorder(rect, context: ctx)
    }
    
    
    func drawGradientBorder(_ rect: CGRect, context: CGContext) {
        context.saveGState()
        context.setLineWidth(lineWidth)
        let path = UIBezierPath(rect: rect)
        context.addPath(path.cgPath)
        context.replacePathWithStrokedPath()
        context.clip()
        
        let rainbowColors : [UIColor] = [ .red, .orange, .yellow, .green, .blue, .purple ]
        let colorDistribution : [CGFloat] =  [  0.0, 0.2, 0.4, 0.6, 0.8, 1.0 ]
        let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: rainbowColors.map { $0.cgColor } as CFArray, locations: colorDistribution)!
        context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: rect.width, y: rect.height), options: [])
        context.restoreGState()
    }

    
    @objc func controlTapped(_ gestureRecognizer :UIGestureRecognizer) {
        UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
            self.alpha = 0.5
        }, completion: { (finished: Bool) -> Void in
            UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseOut, animations: {
                self.alpha = 1.0
            }, completion: { (finished: Bool) -> Void in
            })
        })

        setNeedsDisplay()
        let vc = self.findViewController()
        if let sheet = vc?.sheetPresentationController {
             sheet.detents = [.medium()]
             sheet.largestUndimmedDetentIdentifier = .medium
             sheet.prefersScrollingExpandsWhenScrolledToEdge = false
             sheet.prefersEdgeAttachedInCompactHeight = true
             sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
            
        }
        vc?.present(colorPickerController, animated: true, completion: nil)
        sendActions(for: .touchDown)
    }
}

extension RectangularColorWell : UIColorPickerViewControllerDelegate {
    func colorPickerViewControllerDidFinish(_ controller : UIColorPickerViewController) {
        self.selectedColor = controller.selectedColor
    }
    
    func colorPickerViewController(_ controller : UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
        self.backgroundColor = color
        self.setNeedsDisplay()
    }
}

enter image description here enter image description here

Upvotes: 0

Views: 882

Answers (1)

clearlight
clearlight

Reputation: 12625

@JordanH's final comment caused me to read up on the show/present methods on UIViewController docs. The default presentation style was what I'd been using (e.g. I hadn't set any of the presentation options on the color picker's view controller when I filed the question.

Surprisingly after reading that I could control the style, setting the picker's modalPresentationStyle property to '.pageSheet' which is the one that looks like it would work, to do a partial covering, did not work!.

The solution turned out to be adding colorPickerController.modalPresentationStyle = .popover before calling present()in the custom RectangularColorWellView). It now behaves as if it was presented from a UIColorWell control view.

Upvotes: 0

Related Questions