// // CommonViewTextField.swift // NavigationAnimationTest // // Created by Igor on 10.02.2021. // import UIKit @objc protocol CommonTextFieldDelegate: AnyObject { @objc optional func textFieldShouldBeginEditing(_ textField: CommonTextField) -> Bool @objc optional func textFieldDidBeginEditing(_ textField: CommonTextField) @objc optional func textFieldShouldEndEditing(_ textField: CommonTextField) -> Bool @objc optional func textFieldDidEndEditing(_ textField: CommonTextField) @objc optional func textFieldDidChange(_ textField: CommonTextField) @objc optional func textField(_ textField: CommonTextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool @objc optional func textFieldShouldClear(_ textField: CommonTextField) -> Bool @objc optional func textFieldShouldSubmit(_ textField: CommonTextField) -> Bool @objc optional func textFieldDidSubmit(_ textField: CommonTextField) } class CommonTextField: CommonControlCustom, CommonMenuSender { @IBOutlet private weak var titleLbl: UILabel! @IBOutlet weak var textField: UITextField! @IBOutlet private weak var placeholderLbl: UILabel! @IBOutlet weak var line1View: UIView! @IBOutlet weak var line2View: UIView! @IBOutlet private weak var textLbl: UILabel! @IBOutlet private weak var titleOffset: NSLayoutConstraint! @IBOutlet private weak var textOffset: NSLayoutConstraint! @IBOutlet private weak var lineOffset1: NSLayoutConstraint! @IBOutlet private weak var lineOffset2: NSLayoutConstraint! @IBOutlet weak var delegate: CommonTextFieldDelegate? private let fontSize: CGFloat = 20 private var _value = "" { didSet { updateText() } } var value: String { get { _value } set { set(value: newValue, animated: false) } } func set(value: String, animated: Bool) { _value = value update(animated: animated) } var suffix = "" { didSet { updateText() } } var symbolsToBeRemovedOnDuplication: Set { self.keyboardType == .decimalPad ? [",", "."] : [] } /// Updating text and **cursor** to evaluate text position [SHOULD BE REFACTORED WITH THIS CLASS] private func updateText() { let value = action?.menu?.title ?? _value guard !value.isEmpty else { self.textField.text = .init() return } guard textField.text != value else { return } let oldOffset = (textField.selectedTextRange?.start ?? textField.endOfDocument) let shiftDirection = (textField.text?.count ?? 0) < value.count ? UITextLayoutDirection.right : .left textField.text = value.isEmpty ? nil : (isFirstResponder ? value : value.appending(suffix)) if isFirstResponder { let newPosition = textField.position(from: oldOffset, in: shiftDirection, offset: 1) textField.selectedTextRange = textField.textRange(from: newPosition ?? textField.endOfDocument, to: newPosition ?? textField.endOfDocument) } } @IBInspectable var lzTitle: String? { didSet { update(animated: false) } } @IBInspectable var lzPlaceholder: String? { didSet { update(animated: false) } } private var _lzText: String? @IBInspectable var lzText: String? { get { _lzText } set { set(text: newValue, animated: false) } } private var _error: String? @IBInspectable var error: String? { get { _error } set { set(error: newValue, animated: false) } } var precision: Int? @discardableResult override func resignFirstResponder() -> Bool { textField.resignFirstResponder() } override var isFirstResponder: Bool { textField.isFirstResponder } @discardableResult override func becomeFirstResponder() -> Bool { textField.becomeFirstResponder() } var keyboardType: UIKeyboardType { get { textField.keyboardType } set { textField.keyboardType = newValue } } override open var isEnabled: Bool { get { textField.isEnabled } set { textField.isEnabled = newValue } } var autocorrectionType: UITextAutocorrectionType { get { textField.autocorrectionType } set { textField.autocorrectionType = newValue } } var returnKeyType: UIReturnKeyType { get { textField.returnKeyType } set { textField.returnKeyType = newValue } } var autocapitalizationType: UITextAutocapitalizationType { get { textField.autocapitalizationType } set { textField.autocapitalizationType = newValue } } var isSecure: Bool { get { textField.isSecureTextEntry } set { textField.isSecureTextEntry = newValue } } var accessoryView: UIView? { didSet { update(animated: false) } } var action: CommonMenuAction? { didSet { update(animated: false) } } var isLoading: Bool = false { didSet { update(animated: true) } } override func setup() { super.setup() backgroundColor = .clear textField.clearButtonMode = .never textField.autocorrectionType = .no textField.rightViewMode = .always textField.returnKeyType = .done textField.autocapitalizationType = .none textField.font = FontFamily.GolosUI.regular.font(size: fontSize) textField.textColor = Asset.textCoal.color line2View.backgroundColor = Asset.deepWater.color update(animated: false) } func update(animated: Bool) { let frame = self.frame if isLoading { let indicator = UIActivityIndicatorView(style: .medium) indicator.startAnimating() textField.rightView = indicator layoutIfNeeded() } else if isUserInteractionEnabled && ((isFirstResponder && !value.isEmpty) || !(error?.isEmpty ?? true)) { let btn = UIButton(type: .custom) btn.setImage(Asset.commonTextfieldClear.image, for: .normal) btn.frame.size = Asset.commonTextfieldClear.image.size btn.addTarget(self, action: #selector(onClear), for: .touchUpInside) textField.rightView = btn layoutIfNeeded() } else if let view = accessoryView { if let button = view as? UIButton { textField.rightView = button } else { textField.rightView = view } layoutIfNeeded() } else if let icon = action?.icon { textField.rightView = UIImageView(image: icon) layoutIfNeeded() } else { textField.rightView = nil } updateText() let value = action?.menu?.title ?? _value titleOffset.constant = value.isEmpty ? 10 : 0 titleLbl.attributedText = lzTitle?.localized.attributed(style: .regular, size: 14, color: Asset.textGranite.color) placeholderLbl.attributedText = lzPlaceholder?.localized.attributed(style: .regular, size: fontSize, color: Asset.textPebble.color) if let error = error, !error.isEmpty { textOffset.constant = 4 textLbl.attributedText = error.localized.attributed(style: .regular, size: 12, color: Asset.textBrick.color) line1View.backgroundColor = Asset.brick.color } else { textLbl.attributedText = (action?.menu?.text ?? lzText)?.localized.attributed(style: .regular, size: 12, color: Asset.textGranite.color) line1View.backgroundColor = Asset.pebble.color textOffset.constant = (action?.menu?.text ?? lzText ?? "").isEmpty ? 0 : 4 } lineOffset1.constant = isFirstResponder ? 0 : frame.width/2 lineOffset2.constant = isFirstResponder ? 0 : frame.width/2 placeholderLbl.isHidden = !value.isEmpty animateIfNeeded(duration: animated ? .slow : .none) { [weak self] in guard let self else { return } self.titleLbl.alpha = value.isEmpty ? 0 : 1 self.line1View.alpha = self.isFirstResponder ? 0 : 1 } } @objc private func onClear() { _error = nil set(value: "", animated: true) delegate?.textFieldDidChange?(self) becomeFirstResponder() } func set(text: String?, animated: Bool) { _lzText = text update(animated: animated) } func set(error: String?, animated: Bool) { _error = error update(animated: animated) } } extension CommonTextField: UITextFieldDelegate { func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { condition( action == nil, true: { self.delegate?.textFieldShouldBeginEditing?(self) ?? true }, false: { self.action?.perform(sender: self);return false } ) ?? false } func textFieldDidBeginEditing(_ textField: UITextField) { set(error: nil, animated: true) delegate?.textFieldDidBeginEditing?(self) } func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { delegate?.textFieldShouldEndEditing?(self) ?? true } func textFieldDidEndEditing(_ textField: UITextField) { delegate?.textFieldDidEndEditing?(self) update(animated: true) } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { condition( delegate?.textField?(self, shouldChangeCharactersIn: range, replacementString: string) ?? true, true: { self._error = nil if self.keyboardType == .decimalPad, let precision = self.precision { self.set(value: self.value.replacingCharacters(in: range, with: string) .removeDuplication(of: self.symbolsToBeRemovedOnDuplication) .removingWhitespaceAndNewlines() .normalized(precision: precision), animated: true) } else if self.keyboardType == .decimalPad { self.set(value: self.value.replacingCharacters(in: range, with: string) .removeDuplication(of: self.symbolsToBeRemovedOnDuplication) .removingWhitespaceAndNewlines(), animated: true) } else { self.set(value: self.value.replacingCharacters(in: range, with: string) .removeDuplication(of: self.symbolsToBeRemovedOnDuplication), animated: true) } self.delegate?.textFieldDidChange?(self) return false } ) ?? false } func textFieldShouldReturn(_ textField: UITextField) -> Bool { condition( delegate?.textFieldShouldSubmit?(self) ?? true, true: { self.resignFirstResponder() self.delegate?.textFieldDidSubmit?(self) return false } ) ?? false } func textFieldShouldClear(_ textField: UITextField) -> Bool { delegate?.textFieldShouldClear?(self) ?? true } }