Drawing a Pin Input view with Core Graphics


#1

This question came up on an episode, but I thought it was easier to post here for the sake of readability.

To draw a pin input view with core graphics, rather than layered UIViews is to override drawRect and compute the paths of the various shapes you want to draw.

I started by creating a DigitsView that held a couple of properties:

class DigitView: UIView {
    var numDigits: Int = 4 {
        didSet {
            setNeedsDisplay()
        }
    }
    
    var digitsFilled: Int = 2 {
        didSet {
            digitsFilled = max(0, min(digitsFilled, numDigits))
            setNeedsDisplay()
        }
    }
    
    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        UIColor(white: 0.81, alpha: 1.0).setFill()
        context.fill(rect)
    }
}

This allows me to clamp the number of digits that the user typed to the possible number of digits. It also lets me experiment with adding / removing the number of required digits.

The drawing code will use those values to determine how big to draw each rectangle.

We first start by declaring some constants for the height and margins, then we compute how much of the available space we can use for the width of each digit rectangle…

let digitHeight: CGFloat = 80
let margin: CGFloat = 20
let digitWidth = (bounds.size.width - (CGFloat(numDigits)+1)*margin) / CGFloat(numDigits)        

We then store some variables for where we are going to draw:

let y = (bounds.size.height - digitHeight) / 2
var x = margin        

Then we loop over the number of digits, computing the rect that we will draw with:

for d in 0..<numDigits {
    let digitRect = CGRect(x: x, y: y, width: digitWidth, height: digitHeight)
    let path = UIBezierPath(roundedRect: digitRect, cornerRadius: 8)
            
    context.addPath(path.cgPath)
    x += digitWidth + margin
        
    context.setLineWidth(2)
    UIColor.white.setFill()
    UIColor.lightGray.setStroke()
    context.fillPath()
    context.addPath(path.cgPath)
    context.strokePath()

    // fill in typed digits...
}

Then to fill in the digits with a symbol, we can draw a simple shape, or we can draw text with Core Graphics. The layout here takes a lot of tweaking, because you need a text rect and a font. It’s also worth noting that you may need to translate (and/or invert) the drawing depending on platform. On iOS the origin for text drawing is in the lower left, so you need to offset this when you calculate the position.

if d < digitsFilled {
    context.saveGState()
                
    let glyphRect = digitRect.insetBy(dx: 22, dy: 12)
    context.translateBy(x: 0, y: glyphRect.size.height)
                    
    ("*" as NSString).draw(with: glyphRect,
          options: [],
          attributes: [
              NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 48),
              NSAttributedStringKey.foregroundColor: UIColor.darkGray
          ], context: nil)
          
    context.restoreGState()

With the above code, you get this:

Hope this helps!

– Ben