// // DevikTextField.swift // GMETextFIeld // // Created by InKwon Devik Kim on 16/04/2019. // Copyright © 2019 devikkim. All rights reserved. // import UIKit import RxSwift @IBDesignable open class ValidationTextField: UITextField { @objc public enum ValidateTextFieldType: Int { case dropdown case `default` } // MARK: Left Flag View private var leftViewPadding: CGFloat = 8 private var widthOfImageInLeftView: CGFloat = 35 private var leftFlagImageWidth: CGFloat = 0 // MARK: Heights private var lineHeight: CGFloat = 0 private var selectedLineHeight: CGFloat = 0 // MARK: IBInspectable @IBInspectable open var isShowTitle: Bool = true { didSet { update() } } @IBInspectable open var isUseTitle: Bool = true { didSet { update() } } @IBInspectable open var isLineHidden: Bool = false { didSet { update() } } @IBInspectable open var titleText: String = "TITLE" { didSet { update() } } @IBInspectable open var titleFont: UIFont = .sanfrancisco(.regular, size: 12) { didSet { update() createTitleLabel() } } @IBInspectable open var titleColor: UIColor = .themeTitleTextColor { didSet { update() } } @IBInspectable open var errorColor: UIColor = .themeRed { didSet { update() } } @IBInspectable open var errorFont: UIFont = .sanfrancisco(.regular, size: 12) { didSet { update() createTitleLabel() } } @IBInspectable open var errorMessage: String? { didSet { update() } } @IBInspectable open var disabledColor: UIColor = UIColor(white: 0.88, alpha: 1.0) { didSet { update() updatePlaceholder() } } @IBInspectable open override var placeholder: String? { didSet { setNeedsDisplay() update() } } @IBInspectable open var placeholderColor: UIColor = UIColor(red:0.49, green:0.49, blue:0.49, alpha:1.0) { didSet { updatePlaceholder() } } @IBInspectable open var leftImage: UIImage? { didSet { let width = textHeight() * 1.5 let height = textHeight() leftImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: width, height: height) ) let view = UIView() view.addSubview(leftImageView) leftImageView.image = leftImage leftImageView.translatesAutoresizingMaskIntoConstraints = false leftImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true leftImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true leftImageView.widthAnchor.constraint(equalToConstant: width).isActive = true leftImageView.heightAnchor.constraint(equalToConstant: height).isActive = true leftImageView.contentMode = .scaleAspectFit leftContainerView = view leftView = leftContainerView leftViewMode = .always leftFlagImageWidth = width } } @IBInspectable open var leftImageTintColor: UIColor? { didSet { guard let color = leftImageTintColor else { return } leftImageView.image = leftImage?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate) leftImageView.tintColor = color } } // MARK: Properties open var validCondition: ((String) -> Bool)? { didSet { editingChanged() } } open var mobileNumberText: String? { set { text = newValue?.replacingOccurrences(of: countryCode?.phoneCode ?? "", with: "") } get { return "\(countryCode?.phoneCode ?? "")\(text ?? "")" } } private var countryCode: CountryEnum? { didSet { mobileNumberText = text } } open var isValid = false open var valid = BehaviorSubject(value: false) private lazy var leftImageView = UIImageView() private var leftContainerView: UIView? private lazy var titleLabel = UILabel() private lazy var errorLabel = UILabel() private lazy var lineView = UIView() private var bankCode: BankEnum? private var titleFadeInDuration: TimeInterval = 0.3 private var titleFadeOutDuration: TimeInterval = 0.5 private var placeholderFont: UIFont? { didSet { updatePlaceholder() } } private var isTitleVisible: Bool { return hasText } private var editingOrSelected: Bool { return super.isEditing || isSelected } // MARK: DropDown private var type: ValidateTextFieldType = .default { didSet { switch type { case .default: inputView = nil tintColor = .themeText case .dropdown: inputView = UIView() tintColor = .clear let dropdownImage = UIImageView() dropdownImage.setDropDownImage(tintColor: .themeTextColor) rightView = dropdownImage rightViewMode = .always } } } private lazy var tablePresenterWireframe = TablePresenterWireframe() private var dropwDownItems: [TablePresenterProtocol]? private var configure: TablePresenterConfiguration? var selectedItem = BehaviorSubject(value: nil) // MARK: Initialize override init(frame: CGRect) { super.init(frame: frame) initValidationTextField() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) initValidationTextField() } private func initValidationTextField() { borderStyle = .none createTitleLabel() createLineView() createErrorLabel() addTarget(self, action: #selector(editingDidBegin), for: .editingDidBegin) addTarget(self, action: #selector(editingChanged), for: [.editingChanged, .valueChanged]) } @objc private func editingDidBegin() { switch type { case .dropdown: guard let parentVC = parentContainerViewController() else { return } if dropwDownItems != nil { keyboardToolbar.isHidden = true tablePresenterWireframe.openWith( delegate: self, model: dropwDownItems, source: parentVC ) } default: () } } @objc private func editingChanged() { if let isValid = validCondition?(self.text ?? ""), !isValid { self.isValid = isValid valid.onNext(isValid) } else { isValid = true valid.onNext(isValid) } update() updateTitleVisibility(true) } private func createTitleLabel() { titleLabel = UILabel() titleLabel.autoresizingMask = [.flexibleWidth,. flexibleHeight] titleLabel.font = titleFont titleLabel.textColor = titleColor addSubview(titleLabel) } private func createLineView() { lineView.isUserInteractionEnabled = false lineView.backgroundColor = titleColor configureDefaultLineHeight() lineView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] addSubview(lineView) } private func createErrorLabel() { errorLabel.minimumScaleFactor = 0.5 errorLabel.adjustsFontSizeToFitWidth = true errorLabel.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] addSubview(errorLabel) } private func titleHeight() -> CGFloat { return titleLabel.font.lineHeight } private func errorHeight() -> CGFloat { return errorLabel.font.lineHeight } private func configureDefaultLineHeight() { let pixel: CGFloat = 1.0 / UIScreen.main.scale lineHeight = 2.0 * pixel selectedLineHeight = 2.0 * lineHeight } private func titleRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect { if isShowTitle { return CGRect( x: 0, y: 0, width: bounds.size.width, height: titleHeight() ) } else { if editing { return CGRect( x: 0, y: 0, width: bounds.size.width, height: titleHeight() ) } else { return CGRect( x: 0, y: titleHeight(), width: bounds.size.width, height: titleHeight() ) } } } private func lineViewRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect { let height = editing ? selectedLineHeight : lineHeight return CGRect( x: 0, y: bounds.size.height - height, width: bounds.size.width, height: height ) } private func errorLabelRectForBounds(_ bounds: CGRect) -> CGRect { if isValid { return CGRect( x: 0, y: bounds.height + errorHeight(), width: 0, height: 0 ) } else { return CGRect( x: 0, y: bounds.height, width: bounds.size.width, height: errorHeight() ) } } private func textHeight() -> CGFloat { guard let font = self.font else { return 0.0 } return font.lineHeight + 3.0 } private func updateTitleVisibility(_ animated: Bool = false) { let alpha: CGFloat if isShowTitle { alpha = 1.0 } else { alpha = isTitleVisible ? 1.0 : 0.0 } let frame = titleRectForBounds(bounds, editing: isTitleVisible) let errorAlpha: CGFloat = isValid || text == "" ? 0.0 : 1.0 let errorLabelFrame = errorLabelRectForBounds(bounds) let updateBlock = {() -> Void in if !self.isShowTitle { self.titleLabel.alpha = alpha self.titleLabel.frame = frame } self.errorLabel.alpha = errorAlpha self.errorLabel.frame = errorLabelFrame } if animated { #if swift(>=4.2) let animationOptions: UIView.AnimationOptions = .curveEaseOut #else let animationOptions: UIViewAnimationOptions = .curveEaseOut #endif let duration = isTitleVisible ? titleFadeInDuration : titleFadeOutDuration UIView.animate( withDuration: duration, delay: 0, options: animationOptions, animations: { () -> Void in updateBlock() }, completion: nil) } } private func update() { lineView.isHidden = isLineHidden if !isValid && text != "" { errorLabel.text = errorMessage errorLabel.textColor = errorColor errorLabel.font = errorFont lineView.backgroundColor = errorColor } else { lineView.backgroundColor = titleColor } if isUseTitle { titleLabel.isHidden = false titleLabel.alpha = 1.0 } else { titleLabel.isHidden = true titleLabel.alpha = 0.0 } titleLabel.text = titleText titleLabel.textColor = titleColor titleLabel.font = titleFont updateTitleVisibility(true) if !isEnabled { lineView.backgroundColor = disabledColor } } private func updatePlaceholder() { guard let placeholder = placeholder, let font = placeholderFont ?? font else { return } let color = isEnabled ? placeholderColor : disabledColor #if swift(>=4.2) attributedPlaceholder = NSAttributedString( string: placeholder, attributes: [ NSAttributedString.Key.foregroundColor: color, NSAttributedString.Key.font: font ] ) #elseif swift(>=4.0) attributedPlaceholder = NSAttributedString( string: placeholder, attributes: [ NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font ] ) #else attributedPlaceholder = NSAttributedString( string: placeholder, attributes: [NSForegroundColorAttributeName: color, NSFontAttributeName: font] ) #endif } override open func textRect(forBounds bounds: CGRect) -> CGRect { let superRect = super.textRect(forBounds: bounds) let titleHeight = self.titleHeight() let padding: CGFloat = leftImage == nil ? 0 : 10 return CGRect( x: superRect.origin.x + padding, y: titleHeight, width: superRect.size.width - padding, height: superRect.size.height - titleHeight - selectedLineHeight ) } override open func editingRect(forBounds bounds: CGRect) -> CGRect { let superRect = super.editingRect(forBounds: bounds) let titleHeight = self.titleHeight() let padding: CGFloat = leftImage == nil ? 0 : 10 return CGRect( x: superRect.origin.x + padding, y: titleHeight, width: superRect.size.width, height: superRect.size.height - titleHeight - selectedLineHeight ) } override open func placeholderRect(forBounds bounds: CGRect) -> CGRect { let superRect = super.editingRect(forBounds: bounds) let titleHeight = self.titleHeight() let padding: CGFloat = leftImage == nil ? 0 : 10 let rect = CGRect( x: superRect.origin.x + padding, y: titleHeight, width: bounds.size.width - leftFlagImageWidth - padding, height: bounds.size.height - titleHeight - selectedLineHeight ) return rect } override open func leftViewRect(forBounds bounds: CGRect) -> CGRect { let titleHeight = self.titleHeight() let rect = CGRect ( x: 0, y: titleHeight, width: leftFlagImageWidth, height: bounds.size.height - titleHeight - selectedLineHeight ) return rect } override open func rightViewRect(forBounds bounds: CGRect) -> CGRect { let titleHeight = self.titleHeight() let size = bounds.size.height - titleHeight - selectedLineHeight return CGRect( x: bounds.width - size, y: titleHeight, width: size, height: size ) } override open func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() borderStyle = .none isSelected = true invalidateIntrinsicContentSize() } override open func layoutSubviews() { super.layoutSubviews() self.titleLabel.frame = titleRectForBounds( bounds, editing: isTitleVisible ) self.lineView.frame = lineViewRectForBounds( bounds, editing: editingOrSelected ) self.errorLabel.frame = errorLabelRectForBounds(bounds) } override open var intrinsicContentSize: CGSize { return CGSize(width: bounds.size.width, height: titleHeight() + textHeight() + 10) } } extension ValidationTextField { func setCountry(with code: String?, isShowCode: Bool = true) { guard let code = code?.lowercased(), let countryCode = CountryEnum(rawValue: code) else { leftView = nil return } setCountry(with: countryCode, isShowCode: isShowCode) } func setCountry(with code: CountryEnum, isShowCode: Bool = true) { countryCode = code widthOfImageInLeftView = 35 let textArea = textRect(forBounds: bounds) let imageView = UIImageView( frame: CGRect( x: 0, y: 0, width: widthOfImageInLeftView, height: textArea.height ) ) imageView.contentMode = .scaleAspectFit imageView.image = code.flag let label = UILabel() label.font = font ?? UIFont.systemFont(ofSize: 14) label.textColor = textColor ?? .black label.text = code.phoneCode label.frame = CGRect( x: widthOfImageInLeftView + leftViewPadding, y: 0, width: label.intrinsicContentSize.width, height: textArea.height ) var width: CGFloat = widthOfImageInLeftView + leftViewPadding + label.intrinsicContentSize.width + leftViewPadding if !isShowCode { width = widthOfImageInLeftView + leftViewPadding label.isHidden = !isShowCode } let paddingView = UIView( frame: CGRect( x: 0, y: textArea.origin.y, width: width, height: textArea.height ) ) paddingView.addSubview(imageView) paddingView.addSubview(label) paddingView.backgroundColor = .clear leftFlagImageWidth = paddingView.frame.width leftViewMode = .always leftView = paddingView } func setBank(with code: String?) { guard let code = code?.lowercased(), let bankCode = BankEnum(rawValue: code) else { leftView = nil return } setBank(with: bankCode) } func setBank(with code: BankEnum) { leftImageView.removeFromSuperview() leftImage = code.ciImage } } extension ValidationTextField { func useAsDropDown(with configure: TablePresenterConfiguration? = nil, items: [TablePresenterProtocol]?) { func isEqual(target1: [TablePresenterProtocol]?, target2: [TablePresenterProtocol]?) -> Bool { guard let model1 = target1, let model2 = target2 else { return false} if model1.count != model2.count { return false } for i in 0.. TablePresenterConfiguration { guard let configure = self.configure else { return TablePresenterConfiguration( presenterTitle: "DropDown", closeButtonTitle: "cancel_text".localized(), notFoundTitle: "no_result_found_text".localized(), searchBarPlaceHolder: "Search" ) } return configure } func tablePresenterView( _ viewController: TablePresenterViewController, didSelectModel model: TablePresenterProtocol? ) { didSelect(item: model) } }