Created
July 3, 2020 07:45
-
-
Save rmnblm/8417ba5c2d084e7af30ddb5389b5757f to your computer and use it in GitHub Desktop.
This file contains hidden or 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 | |
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