mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
302 lines
11 KiB
Swift
302 lines
11 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: UIImageView
|
|
private let textNode: ImmediateTextNode
|
|
|
|
private var borderColorValue: UInt32?
|
|
|
|
private var text: String = ""
|
|
|
|
override init() {
|
|
self.backgroundView = UIImageView()
|
|
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) {
|
|
if self.borderColorValue != borderColor {
|
|
self.borderColorValue = borderColor
|
|
|
|
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: 10.0, color: nil, strokeColor: UIColor(argb: borderColor), strokeWidth: 1.0, backgroundColor: nil)
|
|
}
|
|
}
|
|
|
|
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.animatePosition(from: CGPoint(x: 0.0, y: size.height / 2.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, 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)
|
|
|
|
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
|
|
private let textField: UITextField
|
|
|
|
private var focusIndex: Int?
|
|
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 = nil
|
|
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)
|
|
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
|
|
}
|
|
|
|
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)
|
|
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
|
|
}
|
|
}
|
|
}
|