mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Updated code input
This commit is contained in:
21
submodules/CodeInputView/BUILD
Normal file
21
submodules/CodeInputView/BUILD
Normal file
@@ -0,0 +1,21 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "CodeInputView",
|
||||
module_name = "CodeInputView",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
293
submodules/CodeInputView/Sources/CodeInputView.swift
Normal file
293
submodules/CodeInputView/Sources/CodeInputView.swift
Normal file
@@ -0,0 +1,293 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.monospace(21.0), 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 = 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: 25.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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user