mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-02-09 20:54:51 +00:00
329 lines
13 KiB
Swift
329 lines
13 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import ComponentFlow
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import AlertComponent
|
|
import MultilineTextComponent
|
|
import AccountContext
|
|
import TextFormat
|
|
import PlainButtonComponent
|
|
import BundleIconComponent
|
|
|
|
public final class AlertInputFieldComponent: Component {
|
|
public typealias EnvironmentType = AlertComponentEnvironment
|
|
|
|
public class ExternalState {
|
|
public fileprivate(set) var value: String = ""
|
|
public fileprivate(set) var animateError: () -> Void = {}
|
|
public fileprivate(set) var activateInput: () -> Void = {}
|
|
fileprivate let valuePromise = ValuePromise<String>("")
|
|
public var valueSignal: Signal<String, NoError> {
|
|
return self.valuePromise.get()
|
|
}
|
|
|
|
public init() {
|
|
}
|
|
}
|
|
|
|
let context: AccountContext
|
|
let initialValue: String?
|
|
let placeholder: String
|
|
let characterLimit: Int?
|
|
let hasClearButton: Bool
|
|
let isSecureTextEntry: Bool
|
|
let returnKeyType: UIReturnKeyType
|
|
let keyboardType: UIKeyboardType
|
|
let autocapitalizationType: UITextAutocapitalizationType
|
|
let autocorrectionType: UITextAutocorrectionType
|
|
let isInitiallyFocused: Bool
|
|
let externalState: ExternalState
|
|
let shouldChangeText: ((String) -> Bool)?
|
|
let returnKeyAction: (() -> Void)?
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
initialValue: String? = nil,
|
|
placeholder: String,
|
|
characterLimit: Int? = nil,
|
|
hasClearButton: Bool = false,
|
|
isSecureTextEntry: Bool = false,
|
|
returnKeyType: UIReturnKeyType = .done,
|
|
keyboardType: UIKeyboardType = .default,
|
|
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
|
autocorrectionType: UITextAutocorrectionType = .default,
|
|
isInitiallyFocused: Bool = false,
|
|
externalState: ExternalState,
|
|
shouldChangeText: ((String) -> Bool)? = nil,
|
|
returnKeyAction: (() -> Void)? = nil
|
|
) {
|
|
self.context = context
|
|
self.initialValue = initialValue
|
|
self.placeholder = placeholder
|
|
self.characterLimit = characterLimit
|
|
self.hasClearButton = hasClearButton
|
|
self.isSecureTextEntry = isSecureTextEntry
|
|
self.returnKeyType = returnKeyType
|
|
self.keyboardType = keyboardType
|
|
self.autocapitalizationType = autocapitalizationType
|
|
self.autocorrectionType = autocorrectionType
|
|
self.isInitiallyFocused = isInitiallyFocused
|
|
self.externalState = externalState
|
|
self.shouldChangeText = shouldChangeText
|
|
self.returnKeyAction = returnKeyAction
|
|
}
|
|
|
|
public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.initialValue != rhs.initialValue {
|
|
return false
|
|
}
|
|
if lhs.placeholder != rhs.placeholder {
|
|
return false
|
|
}
|
|
if lhs.characterLimit != rhs.characterLimit {
|
|
return false
|
|
}
|
|
if lhs.hasClearButton != rhs.hasClearButton {
|
|
return false
|
|
}
|
|
if lhs.isSecureTextEntry != rhs.isSecureTextEntry {
|
|
return false
|
|
}
|
|
if lhs.returnKeyType != rhs.returnKeyType {
|
|
return false
|
|
}
|
|
if lhs.keyboardType != rhs.keyboardType {
|
|
return false
|
|
}
|
|
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
|
return false
|
|
}
|
|
if lhs.autocorrectionType != rhs.autocorrectionType {
|
|
return false
|
|
}
|
|
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private final class TextField: UITextField {
|
|
var sideInset: CGFloat = 0.0
|
|
|
|
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
|
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
|
|
}
|
|
|
|
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
|
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
|
|
}
|
|
}
|
|
|
|
public final class View: UIView, UITextFieldDelegate {
|
|
private let background = ComponentView<Empty>()
|
|
private let textField = TextField()
|
|
private let placeholder = ComponentView<Empty>()
|
|
private let clearButton = ComponentView<Empty>()
|
|
|
|
private var component: AlertInputFieldComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
private var isUpdating = false
|
|
|
|
var currentText: String {
|
|
return self.textField.text ?? ""
|
|
}
|
|
|
|
func activateInput() {
|
|
self.textField.becomeFirstResponder()
|
|
}
|
|
|
|
func animateError() {
|
|
self.textField.layer.addShakeAnimation()
|
|
|
|
HapticFeedback().error()
|
|
}
|
|
|
|
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
self.component?.returnKeyAction?()
|
|
return false
|
|
}
|
|
|
|
@objc private func textDidChange() {
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
self.clearButton.view?.isHidden = self.currentText.isEmpty
|
|
}
|
|
|
|
public func textFieldDidEndEditing(_ textField: UITextField) {
|
|
self.clearButton.view?.isHidden = true
|
|
}
|
|
|
|
public func setText(text: String) {
|
|
self.textField.text = text
|
|
if !self.isUpdating {
|
|
self.state?.updated(transition: .immediate, isLocal: true)
|
|
}
|
|
}
|
|
|
|
func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
var resetText: String?
|
|
if self.component == nil {
|
|
resetText = component.initialValue
|
|
component.externalState.animateError = { [weak self] in
|
|
self?.animateError()
|
|
}
|
|
component.externalState.activateInput = { [weak self] in
|
|
self?.activateInput()
|
|
}
|
|
}
|
|
|
|
let isFirstTime = self.component == nil
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let environment = environment[AlertComponentEnvironment.self]
|
|
|
|
let topInset: CGFloat = 15.0
|
|
|
|
if self.textField.superview == nil {
|
|
self.addSubview(self.textField)
|
|
self.textField.delegate = self
|
|
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
|
|
}
|
|
if self.textField.autocapitalizationType != component.autocapitalizationType {
|
|
self.textField.autocapitalizationType = component.autocapitalizationType
|
|
}
|
|
if self.textField.autocorrectionType != component.autocorrectionType {
|
|
self.textField.autocorrectionType = component.autocorrectionType
|
|
}
|
|
if self.textField.isSecureTextEntry != component.isSecureTextEntry {
|
|
self.textField.isSecureTextEntry = component.isSecureTextEntry
|
|
}
|
|
if self.textField.returnKeyType != component.returnKeyType {
|
|
self.textField.returnKeyType = component.returnKeyType
|
|
}
|
|
self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light
|
|
if let resetText {
|
|
self.textField.text = resetText
|
|
}
|
|
|
|
self.textField.font = Font.regular(17.0)
|
|
self.textField.textColor = environment.theme.actionSheet.primaryTextColor
|
|
self.textField.tintColor = environment.theme.actionSheet.controlAccentColor
|
|
self.textField.sideInset = 16.0
|
|
|
|
let backgroundPadding: CGFloat = 14.0
|
|
let size = CGSize(width: availableSize.width, height: 50.0)
|
|
|
|
let backgroundSize = self.background.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
|
|
)
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
|
|
if let backgroundView = self.background.view {
|
|
if backgroundView.superview == nil {
|
|
self.addSubview(backgroundView)
|
|
}
|
|
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
|
}
|
|
|
|
let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0)
|
|
let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize)
|
|
transition.setFrame(view: self.textField, frame: textFieldFrame)
|
|
|
|
let placeholderSize = self.placeholder.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(
|
|
string: component.placeholder,
|
|
font: Font.regular(17.0),
|
|
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
|
|
)))
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: size.width, height: 50.0)
|
|
)
|
|
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
|
|
if let placeholderView = self.placeholder.view {
|
|
if placeholderView.superview == nil {
|
|
placeholderView.isUserInteractionEnabled = false
|
|
self.addSubview(placeholderView)
|
|
}
|
|
placeholderView.frame = placeholderFrame
|
|
placeholderView.isHidden = !self.currentText.isEmpty
|
|
}
|
|
|
|
if component.hasClearButton {
|
|
let clearButtonSize = self.clearButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(PlainButtonComponent(
|
|
content: AnyComponent(BundleIconComponent(
|
|
name: "Components/Search Bar/Clear",
|
|
tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
|
|
)),
|
|
effectAlignment: .center,
|
|
minSize: CGSize(width: 44.0, height: 44.0),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.setText(text: "")
|
|
},
|
|
animateAlpha: false,
|
|
animateScale: true
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 44.0, height: 44.0)
|
|
)
|
|
if let clearButtonView = self.clearButton.view {
|
|
if clearButtonView.superview == nil {
|
|
self.addSubview(clearButtonView)
|
|
}
|
|
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize))
|
|
clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder
|
|
}
|
|
} else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil {
|
|
clearButtonView.removeFromSuperview()
|
|
}
|
|
|
|
if isFirstTime && component.isInitiallyFocused {
|
|
self.activateInput()
|
|
}
|
|
|
|
component.externalState.value = self.currentText
|
|
component.externalState.valuePromise.set(self.currentText)
|
|
|
|
return CGSize(width: availableSize.width, height: size.height + topInset)
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|