mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Refactor PasscodeUI
This commit is contained in:
396
submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift
Normal file
396
submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift
Normal file
@@ -0,0 +1,396 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import LocalAuth
|
||||
|
||||
private let titleFont = Font.regular(20.0)
|
||||
private let subtitleFont = Font.regular(15.0)
|
||||
private let buttonFont = Font.regular(17.0)
|
||||
|
||||
final class PasscodeEntryControllerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
private var wallpaper: TelegramWallpaper
|
||||
private let passcodeType: PasscodeEntryFieldType
|
||||
private let biometricsType: LocalAuthBiometricAuthentication?
|
||||
private let arguments: PasscodeEntryControllerPresentationArguments
|
||||
private var background: PasscodeBackground?
|
||||
|
||||
private let statusBar: StatusBar
|
||||
|
||||
private let backgroundNode: ASImageNode
|
||||
private let iconNode: PasscodeLockIconNode
|
||||
private let titleNode: PasscodeEntryLabelNode
|
||||
private let inputFieldNode: PasscodeEntryInputFieldNode
|
||||
private let subtitleNode: PasscodeEntryLabelNode
|
||||
private let keyboardNode: PasscodeEntryKeyboardNode
|
||||
private let cancelButtonNode: HighlightableButtonNode
|
||||
private let deleteButtonNode: HighlightableButtonNode
|
||||
private let biometricButtonNode: HighlightableButtonNode
|
||||
private let effectView: UIVisualEffectView
|
||||
|
||||
private var invalidAttempts: AccessChallengeAttempts?
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
var checkPasscode: ((String) -> Void)?
|
||||
var requestBiometrics: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, passcodeType: PasscodeEntryFieldType, biometricsType: LocalAuthBiometricAuthentication?, arguments: PasscodeEntryControllerPresentationArguments, statusBar: StatusBar) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.wallpaper = wallpaper
|
||||
self.passcodeType = passcodeType
|
||||
self.biometricsType = biometricsType
|
||||
self.arguments = arguments
|
||||
self.statusBar = statusBar
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.contentMode = .scaleToFill
|
||||
|
||||
self.iconNode = PasscodeLockIconNode()
|
||||
self.titleNode = PasscodeEntryLabelNode()
|
||||
self.inputFieldNode = PasscodeEntryInputFieldNode(color: .white, accentColor: .white, fieldType: passcodeType, keyboardAppearance: .dark, useCustomNumpad: true)
|
||||
self.subtitleNode = PasscodeEntryLabelNode()
|
||||
self.keyboardNode = PasscodeEntryKeyboardNode()
|
||||
self.cancelButtonNode = HighlightableButtonNode()
|
||||
self.deleteButtonNode = HighlightableButtonNode()
|
||||
self.biometricButtonNode = HighlightableButtonNode()
|
||||
self.effectView = UIVisualEffectView(effect: nil)
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return UITracingLayerView()
|
||||
})
|
||||
|
||||
self.backgroundColor = .clear
|
||||
self.iconNode.unlockedColor = theme.rootController.navigationBar.primaryTextColor
|
||||
|
||||
self.keyboardNode.charactedEntered = { [weak self] character in
|
||||
self?.inputFieldNode.append(character)
|
||||
}
|
||||
self.inputFieldNode.complete = { [weak self] passcode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.shouldWaitBeforeNextAttempt() {
|
||||
strongSelf.animateError()
|
||||
} else {
|
||||
strongSelf.checkPasscode?(passcode)
|
||||
}
|
||||
}
|
||||
|
||||
self.cancelButtonNode.setTitle(strings.Common_Cancel, with: buttonFont, with: .white, for: .normal)
|
||||
self.deleteButtonNode.setTitle(strings.Common_Delete, with: buttonFont, with: .white, for: .normal)
|
||||
|
||||
if let biometricsType = self.biometricsType {
|
||||
switch biometricsType {
|
||||
case .touchId:
|
||||
self.biometricButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/PasscodeTouchId"), color: .white), for: .normal)
|
||||
case .faceId:
|
||||
self.biometricButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/PasscodeFaceId"), color: .white), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
self.addSubnode(self.keyboardNode)
|
||||
self.addSubnode(self.deleteButtonNode)
|
||||
self.addSubnode(self.biometricButtonNode)
|
||||
|
||||
if self.arguments.cancel != nil {
|
||||
self.addSubnode(self.cancelButtonNode)
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.insertSubview(self.effectView, at: 0)
|
||||
|
||||
if self.arguments.cancel != nil {
|
||||
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
self.deleteButtonNode.addTarget(self, action: #selector(self.deletePressed), forControlEvents: .touchUpInside)
|
||||
self.biometricButtonNode.addTarget(self, action: #selector(self.biometricsPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.animateOut(down: true)
|
||||
self.arguments.cancel?()
|
||||
}
|
||||
|
||||
@objc private func deletePressed() {
|
||||
self.hapticFeedback.tap()
|
||||
self.inputFieldNode.delete()
|
||||
}
|
||||
|
||||
@objc private func biometricsPressed() {
|
||||
self.requestBiometrics?()
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.theme = presentationData.theme
|
||||
self.strings = presentationData.strings
|
||||
self.wallpaper = presentationData.chatWallpaper
|
||||
|
||||
self.deleteButtonNode.setTitle(self.strings.Common_Delete, with: buttonFont, with: .white, for: .normal)
|
||||
if let validLayout = self.validLayout {
|
||||
self.containerLayoutUpdated(validLayout, navigationBarHeight: 0.0, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func updateBackground() {
|
||||
guard let validLayout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var size = validLayout.size
|
||||
if case .compact = validLayout.metrics.widthClass, size.width > size.height {
|
||||
size = CGSize(width: size.height, height: size.width)
|
||||
}
|
||||
|
||||
if let background = self.background, background.size == size {
|
||||
return
|
||||
}
|
||||
|
||||
switch self.wallpaper {
|
||||
case .image, .file:
|
||||
if let image = chatControllerBackgroundImage(theme: self.theme, wallpaper: self.wallpaper, mediaBox: self.context.sharedContext.accountManager.mediaBox, composed: false, knockoutMode: false) {
|
||||
self.background = ImageBasedPasscodeBackground(image: image, size: size)
|
||||
} else {
|
||||
self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor)
|
||||
}
|
||||
default:
|
||||
self.background = GradientPasscodeBackground(size: size, backgroundColors: self.theme.passcode.backgroundColors.colors, buttonColor: self.theme.passcode.buttonColor)
|
||||
}
|
||||
|
||||
if let background = self.background {
|
||||
self.backgroundNode.image = background.backgroundImage
|
||||
self.keyboardNode.updateBackground(background)
|
||||
self.inputFieldNode.updateBackground(background)
|
||||
}
|
||||
}
|
||||
|
||||
private let waitInterval: Int32 = 60
|
||||
private func shouldWaitBeforeNextAttempt() -> Bool {
|
||||
if let attempts = self.invalidAttempts {
|
||||
if attempts.count >= 6 {
|
||||
if Int32(CFAbsoluteTimeGetCurrent()) - attempts.timestamp < waitInterval {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func updateInvalidAttempts(_ attempts: AccessChallengeAttempts?, animated: Bool = false) {
|
||||
self.invalidAttempts = attempts
|
||||
if let attempts = attempts {
|
||||
var text = NSAttributedString(string: "")
|
||||
if attempts.count >= 6 && self.shouldWaitBeforeNextAttempt() {
|
||||
text = NSAttributedString(string: self.strings.PasscodeSettings_TryAgainIn1Minute, font: subtitleFont, textColor: .white)
|
||||
|
||||
self.timer?.invalidate()
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double(attempts.timestamp + waitInterval - Int32(CFAbsoluteTimeGetCurrent())), repeat: false, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.timer = nil
|
||||
strongSelf.updateInvalidAttempts(strongSelf.invalidAttempts, animated: true)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.timer = timer
|
||||
timer.start()
|
||||
}
|
||||
self.subtitleNode.setAttributedText(text, animation: animated ? .crossFade : .none, completion: {})
|
||||
} else {
|
||||
self.subtitleNode.setAttributedText(NSAttributedString(string: ""), animation: animated ? .crossFade : .none, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
func hideBiometrics() {
|
||||
self.biometricButtonNode.layer.animateScale(from: 1.0, to: 0.00001, duration: 0.25, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
||||
self?.biometricButtonNode.isHidden = true
|
||||
})
|
||||
self.animateError()
|
||||
}
|
||||
|
||||
func initialAppearance(fadeIn: Bool = false) {
|
||||
if fadeIn {
|
||||
let effect = self.theme.overallDarkAppearance ? UIBlurEffect(style: .dark) : UIBlurEffect(style: .light)
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = effect
|
||||
} else {
|
||||
self.effectView.alpha = 1.0
|
||||
}
|
||||
})
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .none)
|
||||
}
|
||||
|
||||
func animateIn(iconFrame: CGRect, completion: @escaping () -> Void = {}) {
|
||||
let effect = self.theme.overallDarkAppearance ? UIBlurEffect(style: .dark) : UIBlurEffect(style: .light)
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = effect
|
||||
} else {
|
||||
self.effectView.alpha = 1.0
|
||||
}
|
||||
})
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
if !iconFrame.isEmpty {
|
||||
self.iconNode.animateIn(fromScale: 0.416)
|
||||
self.iconNode.layer.animatePosition(from: iconFrame.center.offsetBy(dx: 6.0, dy: 6.0), to: self.iconNode.layer.position, duration: 0.45)
|
||||
}
|
||||
|
||||
self.statusBar.layer.removeAnimation(forKey: "opacity")
|
||||
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
self.subtitleNode.isHidden = true
|
||||
self.inputFieldNode.isHidden = true
|
||||
self.keyboardNode.isHidden = true
|
||||
self.cancelButtonNode.isHidden = true
|
||||
self.deleteButtonNode.isHidden = true
|
||||
self.biometricButtonNode.isHidden = true
|
||||
|
||||
self.titleNode.setAttributedText(NSAttributedString(string: self.strings.Passcode_AppLockedAlert.replacingOccurrences(of: "\n", with: " "), font: titleFont, textColor: .white), animation: .slideIn, completion: {
|
||||
self.subtitleNode.isHidden = false
|
||||
self.inputFieldNode.isHidden = false
|
||||
self.keyboardNode.isHidden = false
|
||||
self.cancelButtonNode.isHidden = false
|
||||
self.deleteButtonNode.isHidden = false
|
||||
self.biometricButtonNode.isHidden = false
|
||||
|
||||
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
||||
self.inputFieldNode.animateIn()
|
||||
self.keyboardNode.animateIn()
|
||||
var biometricDelay = 0.3
|
||||
if case .alphanumeric = self.passcodeType {
|
||||
biometricDelay = 0.0
|
||||
} else {
|
||||
self.cancelButtonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||
self.deleteButtonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||
}
|
||||
self.biometricButtonNode.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.25, delay: biometricDelay, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||
|
||||
Queue.mainQueue().after(1.5, {
|
||||
self.titleNode.setAttributedText(NSAttributedString(string: self.strings.EnterPasscode_EnterPasscode, font: titleFont, textColor: .white), animation: .crossFade)
|
||||
})
|
||||
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(down: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: down ? self.bounds.size.height : -self.bounds.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func animateSuccess() {
|
||||
self.iconNode.animateUnlock()
|
||||
self.inputFieldNode.animateSuccess()
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.reset()
|
||||
self.inputFieldNode.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true)
|
||||
self.iconNode.layer.addShakeAnimation(amplitude: -8.0, duration: 0.5, count: 6, decay: true)
|
||||
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
self.updateBackground()
|
||||
|
||||
if layout.size.width == 320.0 {
|
||||
self.iconNode.alpha = 0.0
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: layout.size)
|
||||
transition.updateFrame(node: self.backgroundNode, frame: bounds)
|
||||
transition.updateFrame(view: self.effectView, frame: bounds)
|
||||
|
||||
let iconSize = CGSize(width: 35.0, height: 37.0)
|
||||
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + 6.0, y: layout.insets(options: .statusBar).top + 15.0), size: iconSize))
|
||||
|
||||
let passcodeLayout = PasscodeLayout(layout: layout)
|
||||
|
||||
let inputFieldFrame = self.inputFieldNode.updateLayout(layout: passcodeLayout, transition: transition)
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(layout: layout, transition: transition)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: passcodeLayout.titleOffset), size: titleSize))
|
||||
|
||||
var subtitleOffset = passcodeLayout.subtitleOffset
|
||||
if case .alphanumeric = self.passcodeType {
|
||||
subtitleOffset = 16.0
|
||||
}
|
||||
let subtitleSize = self.subtitleNode.updateLayout(layout: layout, transition: transition)
|
||||
transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: inputFieldFrame.maxY + subtitleOffset), size: subtitleSize))
|
||||
|
||||
let (keyboardFrame, keyboardButtonSize) = self.keyboardNode.updateLayout(layout: passcodeLayout, transition: transition)
|
||||
transition.updateFrame(node: self.keyboardNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
switch self.passcodeType {
|
||||
case .digits6, .digits4:
|
||||
self.keyboardNode.alpha = 1.0
|
||||
self.deleteButtonNode.alpha = 1.0
|
||||
case .alphanumeric:
|
||||
self.keyboardNode.alpha = 0.0
|
||||
self.deleteButtonNode.alpha = 0.0
|
||||
}
|
||||
|
||||
let bottomInset = layout.inputHeight ?? 0.0
|
||||
|
||||
let cancelSize = self.cancelButtonNode.measure(layout.size)
|
||||
var cancelY: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - cancelSize.height - passcodeLayout.keyboard.deleteOffset
|
||||
if bottomInset > 0 && self.keyboardNode.alpha < 1.0 {
|
||||
cancelY = layout.size.height - bottomInset - cancelSize.height - 20.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.minX + keyboardButtonSize.width / 2.0 - cancelSize.width / 2.0), y: cancelY), size: cancelSize))
|
||||
|
||||
let deleteSize = self.deleteButtonNode.measure(layout.size)
|
||||
transition.updateFrame(node: self.deleteButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.maxX - keyboardButtonSize.width / 2.0 - deleteSize.width / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - deleteSize.height - passcodeLayout.keyboard.deleteOffset), size: deleteSize))
|
||||
|
||||
if let biometricIcon = self.biometricButtonNode.image(for: .normal) {
|
||||
var biometricY: CGFloat = 0.0
|
||||
if bottomInset > 0 && self.keyboardNode.alpha < 1.0 {
|
||||
biometricY = inputFieldFrame.maxY + floor((layout.size.height - bottomInset - inputFieldFrame.maxY - biometricIcon.size.height) / 2.0)
|
||||
} else {
|
||||
biometricY = keyboardFrame.maxY + passcodeLayout.keyboard.biometricsOffset
|
||||
}
|
||||
transition.updateFrame(node: self.biometricButtonNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - biometricIcon.size.width) / 2.0), y: biometricY), size: biometricIcon.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user