You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

711 lines
17 KiB

//
// DevikTextField.swift
// GMETextFIeld
//
// Created by InKwon Devik Kim on 16/04/2019.
// Copyright © 2019 devikkim. All rights reserved.
//
import UIKit
import RxSwift
@available(iOS 9.0, *)
private enum ValidateType: String {
case success
case error
case ready
var image: UIImage? {
let bundle = Bundle(for: ValidationTextField.self)
let image = UIImage(named: self.rawValue, in: bundle,compatibleWith: nil)
return image
}
}
@available(iOS 9.0, *)
@IBDesignable
open class ValidationTextField: UITextField {
// 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()
statusImageView.heightAnchor.constraint(
equalToConstant: max(titleFont.lineHeight, titleLabel.intrinsicContentSize.height)
).isActive = true
statusImageView.widthAnchor.constraint(
equalToConstant: max(titleFont.lineHeight, titleLabel.intrinsicContentSize.height)
).isActive = true
statusImageView.layoutIfNeeded()
}
}
@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 {
leftViewMode = .always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.image = leftImage
leftView = imageView
}
}
// 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
}
}
private var bankCode: BankEnum?
open var isValid = false
open var valid = BehaviorSubject<Bool>(value: false)
open var successImage: UIImage?
open var errorImage: UIImage?
open lazy var statusImageView = UIImageView()
private lazy var titleLabel = UILabel()
private lazy var errorLabel = UILabel()
private lazy var lineView = UIView()
private lazy var containerView = UIStackView()
private var validateStatus: ValidateType = .ready {
didSet {
switch validateStatus {
case .success:
guard let successImage = successImage else {
statusImageView.image = validateStatus.image
return
}
statusImageView.image = successImage
case .error:
guard let errorImage = errorImage else {
statusImageView.image = validateStatus.image
return
}
statusImageView.image = errorImage
case .ready:
statusImageView.image = errorImage
}
}
}
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: 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(editingChanged), for: [.editingChanged])
}
@objc
private func editingChanged() {
if let isValid = validCondition?(self.text ?? ""), !isValid {
self.isValid = isValid
valid.onNext(isValid)
validateStatus = .error
} else {
isValid = true
valid.onNext(isValid)
validateStatus = .success
}
update()
updateTitleVisibility(true)
}
private func createTitleLabel() {
titleLabel = UILabel()
titleLabel.autoresizingMask = [.flexibleWidth,. flexibleHeight]
titleLabel.font = titleFont
titleLabel.textColor = titleColor
statusImageView.translatesAutoresizingMaskIntoConstraints = false
statusImageView.contentMode = .scaleAspectFill
containerView = UIStackView()
containerView.axis = .horizontal
containerView.spacing = 5
containerView.addArrangedSubview(titleLabel)
containerView.addArrangedSubview(statusImageView)
containerView.addArrangedSubview(UILabel())
containerView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
containerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
}
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.containerView.alpha = alpha
self.containerView.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 {
containerView.isHidden = false
containerView.alpha = 1.0
} else {
containerView.isHidden = true
containerView.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()
return CGRect(
x: superRect.origin.x,
y: titleHeight,
width: superRect.size.width,
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 prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
borderStyle = .none
isSelected = true
invalidateIntrinsicContentSize()
}
override open func layoutSubviews() {
super.layoutSubviews()
self.containerView.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: leftViewPadding,
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: leftViewPadding + widthOfImageInLeftView + leftViewPadding,
y: 0,
width: label.intrinsicContentSize.width,
height: textArea.height
)
var width: CGFloat = leftViewPadding +
widthOfImageInLeftView +
leftViewPadding +
label.intrinsicContentSize.width +
leftViewPadding
if !isShowCode {
width = leftViewPadding + 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?, isShowCode: Bool = true) {
guard
let code = code?.lowercased(),
let bankCode = BankEnum(rawValue: code) else {
leftView = nil
return
}
setBank(with: bankCode, isShowCode: isShowCode)
}
func setBank(with code: BankEnum, isShowCode: Bool = true) {
bankCode = code
widthOfImageInLeftView = 35
let textArea = textRect(forBounds: bounds)
let imageView = UIImageView(
frame: CGRect(
x: leftViewPadding,
y: 0,
width: widthOfImageInLeftView,
height: textArea.height
)
)
imageView.contentMode = .scaleAspectFit
imageView.image = code.ciImage
let label = UILabel()
label.font = font ?? UIFont.systemFont(ofSize: 14)
label.textColor = textColor ?? .black
label.text = ""
label.frame = CGRect(
x: leftViewPadding + widthOfImageInLeftView + leftViewPadding,
y: 0,
width: label.intrinsicContentSize.width,
height: textArea.height
)
var width: CGFloat = leftViewPadding +
widthOfImageInLeftView +
leftViewPadding +
label.intrinsicContentSize.width +
leftViewPadding
if !isShowCode {
width = leftViewPadding + 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
}
}