alexfu
5/10/2016 - 6:47 PM

A UITextField with error text support. Draws error text underneath UITextField.

A UITextField with error text support. Draws error text underneath UITextField.

import Foundation
import UIKit

class UIErrorTextField : UITextField {
  private let bgView = UIView()
  private var textFieldHeight = CGFloat(40)
  private var errorTextHeight = CGFloat(12)
  private let errorTextPadding = CGFloat(2)

  var errorFont = UIFont.systemFontOfSize(12) {
    didSet {
      invalidateErrorTextContentSize()
    }
  }
  var errorText: String? {
    didSet {
      setNeedsDisplay()
    }
  }
  var errorTextColor = UIColor.blackColor() {
    didSet {
      setNeedsDisplay()
    }
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
  }

  private func setup() {
    clipsToBounds = true
    borderStyle = .None
    let errorTextBounds = measureText("TEST", attributes: [NSFontAttributeName: errorFont])
    errorTextHeight = errorTextBounds.height
    setupBackgroundView()
  }

  override func intrinsicContentSize() -> CGSize {
    return CGSize(width: self.frame.width, height: calculateHeight())
  }

  override func editingRectForBounds(bounds: CGRect) -> CGRect {
    return offsetRectForEditing(super.editingRectForBounds(bounds))
  }

  override func textRectForBounds(bounds: CGRect) -> CGRect {
    return offsetRectForEditing(super.textRectForBounds(bounds))
  }

  override func leftViewRectForBounds(bounds: CGRect) -> CGRect {
    return offsetRectForEdgeView(super.leftViewRectForBounds(bounds))
  }

  override func rightViewRectForBounds(bounds: CGRect) -> CGRect {
    return offsetRectForEdgeView(super.rightViewRectForBounds(bounds))
  }

  override func drawRect(rect: CGRect) {
    bgView.frame.size.width = rect.width
    bgView.frame.size.height = textFieldHeight

    drawErrorText(rect)
  }

  private func invalidateErrorTextContentSize() {
    guard let text = errorText else { return }

    let bounds = measureText(text, attributes: [NSFontAttributeName: errorFont])
    errorTextHeight = bounds.height
    invalidateIntrinsicContentSize()
  }

  private func drawErrorText(rect: CGRect) {
    guard let text = errorText else { return }

    let errorTextBounds = CGRect(x: bgView.frame.origin.x,
                                 y: bgView.frame.height + errorTextPadding,
                                 width: bgView.frame.width,
                                 height: errorTextHeight)

    text.drawInRect(errorTextBounds,
                    withAttributes: [
                      NSFontAttributeName: errorFont,
                      NSForegroundColorAttributeName: errorTextColor
                    ])
  }

  private func offsetRectForEditing(rect: CGRect) -> CGRect {
    return UIEdgeInsetsInsetRect(rect, UIEdgeInsets(top: 0, left: 5, bottom: errorTextHeight, right: 5))
  }

  private func offsetRectForEdgeView(rect: CGRect) -> CGRect {
    let x = CGFloat(5)
    let y = (bgView.frame.height/2) - (rect.height/2)
    return CGRect(x: x, y: y, width: rect.width, height: rect.height)
  }

  private func setupBackgroundView() {
    bgView.layer.cornerRadius = 5
    bgView.layer.borderWidth = 1
    bgView.layer.borderColor = UIColor.blackColor().colorWithAlphaComponent(0.1).CGColor
    bgView.backgroundColor = UIColor.clearColor()
    bgView.userInteractionEnabled = false // Let touch events fall through
    addSubview(bgView)
  }

  private func measureText(text: String, attributes: [String:AnyObject]) -> CGRect {
    let nsString = NSString(string: text)
    return nsString.boundingRectWithSize(self.frame.size,
                                  options: .UsesLineFragmentOrigin,
                                  attributes: attributes,
                                  context: nil)
  }

  private func calculateHeight() -> CGFloat {
    return errorTextHeight + textFieldHeight + errorTextPadding
  }
}