Skip to content

Instantly share code, notes, and snippets.

@rmnblm
Created July 3, 2020 07:45
Show Gist options
  • Save rmnblm/8417ba5c2d084e7af30ddb5389b5757f to your computer and use it in GitHub Desktop.
Save rmnblm/8417ba5c2d084e7af30ddb5389b5757f to your computer and use it in GitHub Desktop.
import UIKit
private class Line {
var path = UIBezierPath()
var layer = CAShapeLayer()
}
private enum AnimationKey {
static let activeStart = "ActiveLineStartAnimation"
static let activeEnd = "ActiveLineEndAnimation"
}
class UnderlineTextField: UITextField {
private var line = Line()
private var activeLine = Line()
var borderOffset: CGPoint = .init(x: 0, y: 3)
var lineColor: UIColor {
get {
if let strokeColor = line.layer.strokeColor {
return UIColor(cgColor: strokeColor)
}
return .clear
} set {
line.layer.strokeColor = newValue.cgColor
}
}
var lineWidth: CGFloat {
get {
line.layer.lineWidth
} set {
line.layer.lineWidth = newValue
}
}
var activeLineColor: UIColor {
get {
if let strokeColor = activeLine.layer.strokeColor {
return UIColor(cgColor: strokeColor)
}
return .clear
} set {
activeLine.layer.strokeColor = newValue.cgColor
}
}
var activeLineWidth: CGFloat {
get {
activeLine.layer.lineWidth
} set {
activeLine.layer.lineWidth = newValue
}
}
var animationDuration: Double = 0.3
// MARK: Methods
override public init(frame: CGRect) {
super.init(frame: frame)
initializeSetup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSetup()
}
override open func layoutSubviews() {
super.layoutSubviews()
calculateLine(line)
if isEditing {
calculateLine(activeLine)
}
}
private func initializeSetup() {
observe()
configureBottomLine()
configureActiveLine()
}
private func configureBottomLine() {
line.layer.fillColor = UIColor.clear.cgColor
layer.addSublayer(line.layer)
}
private func configureActiveLine() {
activeLine.layer.fillColor = UIColor.clear.cgColor
layer.addSublayer(activeLine.layer)
}
private func calculateLine(_ line: Line) {
// Path
line.path = UIBezierPath()
let yOffset = frame.height - (line.layer.lineWidth * 0.5) + borderOffset.y
let startPoint = CGPoint(x: .zero, y: yOffset)
line.path.move(to: startPoint)
let endPoint = CGPoint(x: frame.width + borderOffset.x, y: yOffset)
line.path.addLine(to: endPoint)
// Layer
let interfaceDirection = UIView.userInterfaceLayoutDirection(for: semanticContentAttribute)
let path = interfaceDirection == .rightToLeft ? line.path.reversing() : line.path
line.layer.path = path.cgPath
}
private func observe() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(
self,
selector: #selector(showLineAnimation),
name: UITextField.textDidBeginEditingNotification,
object: self
)
notificationCenter.addObserver(
self,
selector: #selector(hideLineAnimation),
name: UITextField.textDidEndEditingNotification,
object: self
)
}
@objc private func showLineAnimation() {
calculateLine(activeLine)
let animation = CABasicAnimation(
path: #keyPath(CAShapeLayer.strokeEnd),
fromValue: CGFloat.zero,
toValue: CGFloat(1),
duration: animationDuration
)
activeLine.layer.add(animation, forKey: AnimationKey.activeStart)
}
@objc private func hideLineAnimation() {
let animation = CABasicAnimation(
path: #keyPath(CAShapeLayer.strokeEnd),
fromValue: nil,
toValue: CGFloat.zero,
duration: animationDuration
)
activeLine.layer.add(animation, forKey: AnimationKey.activeEnd)
}
}
private extension CABasicAnimation {
convenience init(path: String, fromValue: Any?, toValue: Any?, duration: CFTimeInterval) {
self.init(keyPath: path)
self.fromValue = fromValue
self.toValue = toValue
self.duration = duration
isRemovedOnCompletion = false
fillMode = CAMediaTimingFillMode.forwards
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment