Files

293 lines
11 KiB
Swift

//
// 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<Character> {
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 }
}