Let’s see how we would make this text field in Swift
First create a UITextField subclass. I called mine UnderlinedTextField and annotated it as @IBDesignable. Now create a few properties.
These are annotated as @IBInspectable so they can be set in interface builder. We also need a property of hasError, which when set changes the colour of the text field:
When this property is set, we set the colour of our placeholderView. This is just a UILabel that we add and we animate it up when the user starts typing. Then if hasError is true, the text colour turns red and the view redraws to turn the underline colour to red. If it’s set again and is true again, the label will shake using a CAKeyframeAnimation.
Now we just override draw(_ rect: CGRect) and setup the placeholderView in didMoveToWindow(). That’s it, in about 100 lines of code. You can grab everything down below to look at in a bit more detail.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
@IBDesignable | |
class UnderlinedTextField: UITextField { | |
@IBInspectable var borderColor: UIColor = Colors.darkText { | |
didSet { setNeedsDisplay() } | |
} | |
@IBInspectable var borderedWidth: CGFloat = 1 { | |
didSet { setNeedsDisplay() } | |
} | |
@IBInspectable var padding: CGFloat = 0 { | |
didSet { setNeedsDisplay() } | |
} | |
@IBInspectable var errorColor: UIColor = Colors.errorBackground { | |
didSet { setNeedsDisplay() } | |
} | |
var hasError: Bool = false { | |
didSet { | |
if hasError && hasError == oldValue { | |
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") | |
animation.duration = 0.3 | |
animation.repeatCount = 1 | |
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) | |
animation.values = [-10.0, 10.0, -5.0, 5.0, -2.0, 0.0] | |
placeholderView.layer.add(animation, forKey: "shake") | |
return | |
} | |
setNeedsDisplay() | |
placeholderView.textColor = hasError ? errorColor : borderColor | |
} | |
} | |
override var intrinsicContentSize: CGSize { | |
let size = super.intrinsicContentSize | |
return CGSize(width: size.width, height: size.height + 30) | |
} | |
lazy var placeholderView: UILabel = { | |
let placeholderView = UILabel() | |
placeholderView.textColor = borderColor | |
placeholderView.font = Font.semiBold.withSize(10) | |
placeholderView.text = placeholder ?? "No text" | |
placeholderView.alpha = 0.0 | |
return placeholderView | |
}() | |
var placeholderViewTopConstraint: NSLayoutConstraint? | |
override func draw(_ rect: CGRect) { | |
let path = UIBezierPath() | |
path.move(to: CGPoint(x: rect.minX + padding, y: rect.maxY)) | |
path.addLine(to: CGPoint(x: rect.maxX – padding, y: rect.maxY)) | |
path.lineWidth = borderedWidth | |
if hasError { | |
errorColor.setStroke() | |
} else { | |
borderColor.setStroke() | |
} | |
path.stroke() | |
} | |
override func didMoveToWindow() { | |
super.didMoveToWindow() | |
font = Font.semiBold.withSize(17) | |
addSubview(placeholderView) | |
placeholderView.translatesAutoresizingMaskIntoConstraints = false | |
placeholderView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true | |
placeholderViewTopConstraint = placeholderView.topAnchor.constraint(equalTo: topAnchor, constant: 5) | |
placeholderViewTopConstraint?.priority = UILayoutPriority(999) | |
placeholderViewTopConstraint?.isActive = true | |
addTarget(self, action: #selector(textDidChange), for: .editingChanged) | |
} | |
@objc func textDidChange() { | |
if text != "" { | |
placeholderViewTopConstraint?.constant = 0 | |
UIView.animate(withDuration: 0.25) { | |
self.placeholderView.alpha = 1.0 | |
self.layoutIfNeeded() | |
} | |
} else { | |
placeholderViewTopConstraint?.constant = 5 | |
UIView.animate(withDuration: 0.25) { | |
self.placeholderView.alpha = 0.0 | |
self.layoutIfNeeded() | |
} | |
} | |
} | |
} |