2022-01-11 23:34:51 +04:00

316 lines
12 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import PhoneNumberFormat
public final class CodeInputView: ASDisplayNode, UITextFieldDelegate {
public struct Theme: Equatable {
public var inactiveBorder: UInt32
public var activeBorder: UInt32
public var foreground: UInt32
public var isDark: Bool
public init(
inactiveBorder: UInt32,
activeBorder: UInt32,
foreground: UInt32,
isDark: Bool
) {
self.inactiveBorder = inactiveBorder
self.activeBorder = activeBorder
self.foreground = foreground
self.isDark = isDark
}
}
private final class ItemView: ASDisplayNode {
private let backgroundView: UIView
private let textNode: ImmediateTextNode
private var borderColorValue: UInt32?
private var text: String = ""
override init() {
self.backgroundView = UIView()
self.textNode = ImmediateTextNode()
super.init()
self.addSubnode(self.textNode)
self.view.addSubview(self.backgroundView)
self.clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(borderColor: UInt32, isHighlighted: Bool) {
if self.borderColorValue != borderColor {
self.borderColorValue = borderColor
let previousColor = self.backgroundView.layer.borderColor
self.backgroundView.layer.cornerRadius = 5.0
self.backgroundView.layer.borderColor = UIColor(argb: borderColor).cgColor
self.backgroundView.layer.borderWidth = 1.0
if let previousColor = previousColor {
self.backgroundView.layer.animate(from: previousColor, to: UIColor(argb: borderColor).cgColor, keyPath: "borderColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15)
}
}
}
func update(textColor: UInt32, text: String, size: CGSize, animated: Bool) {
let previousText = self.text
self.text = text
if animated && previousText.isEmpty != text.isEmpty {
if !text.isEmpty {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.textNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: size.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, additive: true)
} else {
if let copyView = self.textNode.view.snapshotContentTree() {
self.view.insertSubview(copyView, at: 0)
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: size.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true)
}
}
}
let fontSize: CGFloat = floor(21.0 * size.height / 28.0)
if #available(iOS 13.0, *) {
self.textNode.attributedText = NSAttributedString(string: text, font: UIFont.monospacedSystemFont(ofSize: fontSize, weight: .regular), textColor: UIColor(argb: textColor))
} else {
self.textNode.attributedText = NSAttributedString(string: text, font: Font.monospace(fontSize), textColor: UIColor(argb: textColor))
}
let textSize = self.textNode.updateLayout(size)
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
self.backgroundView.frame = CGRect(origin: CGPoint(), size: size)
}
}
private let prefixLabel: ImmediateTextNode
public let textField: UITextField
private var focusIndex: Int? = 0
private var itemViews: [ItemView] = []
public var updated: (() -> Void)?
private var theme: Theme?
private var count: Int?
private var textValue: String = ""
public var text: String {
get {
return self.textValue
} set(value) {
self.textValue = value
self.textField.text = value
}
}
override public init() {
self.prefixLabel = ImmediateTextNode()
self.textField = UITextField()
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.textField.keyboardType = .asciiCapableNumberPad
} else {
self.textField.keyboardType = .numberPad
}
if #available(iOSApplicationExtension 12.0, iOS 12.0, *) {
self.textField.textContentType = .oneTimeCode
}
self.textField.returnKeyType = .done
self.textField.disableAutomaticKeyboardHandling = [.forward, .backward]
super.init()
self.addSubnode(self.prefixLabel)
self.view.addSubview(self.textField)
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.textField.delegate = self
self.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.textField.becomeFirstResponder()
}
}
@objc func textFieldChanged(_ textField: UITextField) {
self.textValue = textField.text ?? ""
self.updateItemViews(animated: true)
self.updated?()
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let count = self.count else {
return false
}
var text = textField.text ?? ""
guard let stringRange = Range(range, in: text) else {
return false
}
text.replaceSubrange(stringRange, with: string)
if !text.allSatisfy({ $0.isNumber && $0.isASCII }) {
return false
}
if text.count > count {
return false
}
return true
}
private func currentCaretIndex() -> Int? {
if let selectedTextRange = self.textField.selectedTextRange {
let index = self.textField.offset(from: self.textField.beginningOfDocument, to: selectedTextRange.end)
return index
} else {
return nil
}
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.focusIndex = self.currentCaretIndex()
self.updateItemViews(animated: true)
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.focusIndex = textField.text?.count ?? 0
self.updateItemViews(animated: true)
}
public func textFieldDidChangeSelection(_ textField: UITextField) {
self.focusIndex = self.currentCaretIndex()
self.updateItemViews(animated: true)
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return false
}
private func updateItemViews(animated: Bool) {
guard let theme = self.theme else {
return
}
for i in 0 ..< self.itemViews.count {
let itemView = self.itemViews[i]
let itemSize = itemView.bounds.size
itemView.update(borderColor: self.focusIndex == i ? theme.activeBorder : theme.inactiveBorder, isHighlighted: self.focusIndex == i)
let itemText: String
if i < self.textValue.count {
itemText = String(self.textValue[self.textValue.index(self.textValue.startIndex, offsetBy: i)])
} else {
itemText = ""
}
itemView.update(textColor: theme.foreground, text: itemText, size: itemSize, animated: animated)
}
}
public func update(theme: Theme, prefix: String, count: Int, width: CGFloat) -> CGSize {
self.theme = theme
self.count = count
if theme.isDark {
self.textField.keyboardAppearance = .dark
} else {
self.textField.keyboardAppearance = .light
}
let height: CGFloat
if prefix.isEmpty {
height = 40.0
} else {
height = 28.0
}
if #available(iOS 13.0, *) {
self.prefixLabel.attributedText = NSAttributedString(string: prefix, font: UIFont.monospacedSystemFont(ofSize: 21.0, weight: .regular), textColor: UIColor(argb: theme.foreground))
} else {
self.prefixLabel.attributedText = NSAttributedString(string: prefix, font: Font.monospace(21.0), textColor: UIColor(argb: theme.foreground))
}
let prefixSize = self.prefixLabel.updateLayout(CGSize(width: width, height: 100.0))
let prefixSpacing: CGFloat = prefix.isEmpty ? 0.0 : 8.0
let itemSize = CGSize(width: floor(25.0 * height / 28.0), height: height)
let itemSpacing: CGFloat = 5.0
let itemsWidth: CGFloat = itemSize.width * CGFloat(count) + itemSpacing * CGFloat(count - 1)
let contentWidth: CGFloat = prefixSize.width + prefixSpacing + itemsWidth
let contentOriginX: CGFloat = floor((width - contentWidth) / 2.0)
self.prefixLabel.frame = CGRect(origin: CGPoint(x: contentOriginX, y: floorToScreenPixels((height - prefixSize.height) / 2.0)), size: prefixSize)
for i in 0 ..< count {
let itemView: ItemView
if self.itemViews.count > i {
itemView = self.itemViews[i]
} else {
itemView = ItemView()
self.itemViews.append(itemView)
self.addSubnode(itemView)
}
itemView.update(borderColor: self.focusIndex == i ? theme.activeBorder : theme.inactiveBorder, isHighlighted: self.focusIndex == i)
let itemText: String
if i < self.textValue.count {
itemText = String(self.textValue[self.textValue.index(self.textValue.startIndex, offsetBy: i)])
} else {
itemText = ""
}
itemView.update(textColor: theme.foreground, text: itemText, size: itemSize, animated: false)
itemView.frame = CGRect(origin: CGPoint(x: contentOriginX + prefixSize.width + prefixSpacing + CGFloat(i) * (itemSize.width + itemSpacing), y: 0.0), size: itemSize)
}
if self.itemViews.count > count {
for i in count ..< self.itemViews.count {
self.itemViews[i].removeFromSupernode()
}
self.itemViews.removeSubrange(count...)
}
return CGSize(width: width, height: height)
}
public override func becomeFirstResponder() -> Bool {
return self.textField.becomeFirstResponder()
}
public override func canBecomeFirstResponder() -> Bool {
return self.textField.canBecomeFirstResponder
}
public override func resignFirstResponder() -> Bool {
return self.textField.resignFirstResponder()
}
public override func canResignFirstResponder() -> Bool {
return self.textField.canResignFirstResponder
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
return self.view
} else {
return nil
}
}
}