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

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. //
  2. // DevikTextField.swift
  3. // GMETextFIeld
  4. //
  5. // Created by InKwon Devik Kim on 16/04/2019.
  6. // Copyright © 2019 devikkim. All rights reserved.
  7. //
  8. import UIKit
  9. import RxSwift
  10. @available(iOS 9.0, *)
  11. private enum ValidateType: String {
  12. case success
  13. case error
  14. case ready
  15. var image: UIImage? {
  16. let bundle = Bundle(for: ValidationTextField.self)
  17. let image = UIImage(named: self.rawValue, in: bundle,compatibleWith: nil)
  18. return image
  19. }
  20. }
  21. @available(iOS 9.0, *)
  22. @IBDesignable
  23. open class ValidationTextField: UITextField {
  24. // MARK: Left Flag View
  25. private var leftViewPadding: CGFloat = 8
  26. private var widthOfImageInLeftView: CGFloat = 35
  27. private var leftFlagImageWidth: CGFloat = 0
  28. // MARK: Heights
  29. private var lineHeight: CGFloat = 0
  30. private var selectedLineHeight: CGFloat = 0
  31. // MARK: IBInspectable
  32. @IBInspectable
  33. open var isShowTitle: Bool = true {
  34. didSet {
  35. update()
  36. }
  37. }
  38. @IBInspectable
  39. open var isUseTitle: Bool = true {
  40. didSet {
  41. update()
  42. }
  43. }
  44. @IBInspectable
  45. open var isLineHidden: Bool = false {
  46. didSet {
  47. update()
  48. }
  49. }
  50. @IBInspectable
  51. open var titleText: String = "TITLE" {
  52. didSet {
  53. update()
  54. statusImageView.heightAnchor.constraint(
  55. equalToConstant: max(titleFont.lineHeight, titleLabel.intrinsicContentSize.height)
  56. ).isActive = true
  57. statusImageView.widthAnchor.constraint(
  58. equalToConstant: max(titleFont.lineHeight, titleLabel.intrinsicContentSize.height)
  59. ).isActive = true
  60. statusImageView.layoutIfNeeded()
  61. }
  62. }
  63. @IBInspectable
  64. open var titleFont: UIFont = .sanfrancisco(.regular, size: 12) {
  65. didSet {
  66. update()
  67. createTitleLabel()
  68. }
  69. }
  70. @IBInspectable
  71. open var titleColor: UIColor = .themeTitleTextColor {
  72. didSet {
  73. update()
  74. }
  75. }
  76. @IBInspectable
  77. open var errorColor: UIColor = .themeRed {
  78. didSet {
  79. update()
  80. }
  81. }
  82. @IBInspectable
  83. open var errorFont: UIFont = .sanfrancisco(.regular, size: 12) {
  84. didSet {
  85. update()
  86. createTitleLabel()
  87. }
  88. }
  89. @IBInspectable
  90. open var errorMessage: String? {
  91. didSet {
  92. update()
  93. }
  94. }
  95. @IBInspectable
  96. open var disabledColor: UIColor = UIColor(white: 0.88, alpha: 1.0) {
  97. didSet {
  98. update()
  99. updatePlaceholder()
  100. }
  101. }
  102. @IBInspectable
  103. open override var placeholder: String? {
  104. didSet {
  105. setNeedsDisplay()
  106. update()
  107. }
  108. }
  109. @IBInspectable
  110. open var placeholderColor: UIColor = UIColor(red:0.49, green:0.49, blue:0.49, alpha:1.0) {
  111. didSet {
  112. updatePlaceholder()
  113. }
  114. }
  115. @IBInspectable
  116. open var leftImage: UIImage? {
  117. didSet {
  118. leftViewMode = .always
  119. let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
  120. imageView.image = leftImage
  121. leftView = imageView
  122. }
  123. }
  124. // MARK: Properties
  125. open var validCondition: ((String) -> Bool)? {
  126. didSet {
  127. editingChanged()
  128. }
  129. }
  130. open var mobileNumberText: String? {
  131. set {
  132. text = newValue?.replacingOccurrences(of: countryCode?.phoneCode ?? "", with: "")
  133. }
  134. get {
  135. return "\(countryCode?.phoneCode ?? "")\(text ?? "")"
  136. }
  137. }
  138. private var countryCode: CountryEnum? {
  139. didSet {
  140. mobileNumberText = text
  141. }
  142. }
  143. private var bankCode: BankEnum?
  144. open var isValid = false
  145. open var valid = BehaviorSubject<Bool>(value: false)
  146. open var successImage: UIImage?
  147. open var errorImage: UIImage?
  148. open lazy var statusImageView = UIImageView()
  149. private lazy var titleLabel = UILabel()
  150. private lazy var errorLabel = UILabel()
  151. private lazy var lineView = UIView()
  152. private lazy var containerView = UIStackView()
  153. private var validateStatus: ValidateType = .ready {
  154. didSet {
  155. switch validateStatus {
  156. case .success:
  157. guard let successImage = successImage else {
  158. statusImageView.image = validateStatus.image
  159. return
  160. }
  161. statusImageView.image = successImage
  162. case .error:
  163. guard let errorImage = errorImage else {
  164. statusImageView.image = validateStatus.image
  165. return
  166. }
  167. statusImageView.image = errorImage
  168. case .ready:
  169. statusImageView.image = errorImage
  170. }
  171. }
  172. }
  173. private var titleFadeInDuration: TimeInterval = 0.3
  174. private var titleFadeOutDuration: TimeInterval = 0.5
  175. private var placeholderFont: UIFont? {
  176. didSet {
  177. updatePlaceholder()
  178. }
  179. }
  180. private var isTitleVisible: Bool {
  181. return hasText
  182. }
  183. private var editingOrSelected: Bool {
  184. return super.isEditing || isSelected
  185. }
  186. // MARK: Initialize
  187. override init(frame: CGRect) {
  188. super.init(frame: frame)
  189. initValidationTextField()
  190. }
  191. required public init?(coder aDecoder: NSCoder) {
  192. super.init(coder: aDecoder)
  193. initValidationTextField()
  194. }
  195. private func initValidationTextField() {
  196. borderStyle = .none
  197. createTitleLabel()
  198. createLineView()
  199. createErrorLabel()
  200. addTarget(self, action: #selector(editingChanged), for: [.editingChanged])
  201. }
  202. @objc
  203. private func editingChanged() {
  204. if let isValid = validCondition?(self.text ?? ""), !isValid {
  205. self.isValid = isValid
  206. valid.onNext(isValid)
  207. validateStatus = .error
  208. } else {
  209. isValid = true
  210. valid.onNext(isValid)
  211. validateStatus = .success
  212. }
  213. update()
  214. updateTitleVisibility(true)
  215. }
  216. private func createTitleLabel() {
  217. titleLabel = UILabel()
  218. titleLabel.autoresizingMask = [.flexibleWidth,. flexibleHeight]
  219. titleLabel.font = titleFont
  220. titleLabel.textColor = titleColor
  221. statusImageView.translatesAutoresizingMaskIntoConstraints = false
  222. statusImageView.contentMode = .scaleAspectFill
  223. containerView = UIStackView()
  224. containerView.axis = .horizontal
  225. containerView.spacing = 5
  226. containerView.addArrangedSubview(titleLabel)
  227. containerView.addArrangedSubview(statusImageView)
  228. containerView.addArrangedSubview(UILabel())
  229. containerView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
  230. containerView.translatesAutoresizingMaskIntoConstraints = false
  231. addSubview(containerView)
  232. }
  233. private func createLineView() {
  234. lineView.isUserInteractionEnabled = false
  235. lineView.backgroundColor = titleColor
  236. configureDefaultLineHeight()
  237. lineView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
  238. addSubview(lineView)
  239. }
  240. private func createErrorLabel() {
  241. errorLabel.minimumScaleFactor = 0.5
  242. errorLabel.adjustsFontSizeToFitWidth = true
  243. errorLabel.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
  244. addSubview(errorLabel)
  245. }
  246. private func titleHeight() -> CGFloat {
  247. return titleLabel.font.lineHeight
  248. }
  249. private func errorHeight() -> CGFloat {
  250. return errorLabel.font.lineHeight
  251. }
  252. private func configureDefaultLineHeight() {
  253. let pixel: CGFloat = 1.0 / UIScreen.main.scale
  254. lineHeight = 2.0 * pixel
  255. selectedLineHeight = 2.0 * lineHeight
  256. }
  257. private func titleRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect {
  258. if isShowTitle {
  259. return CGRect(
  260. x: 0,
  261. y: 0,
  262. width: bounds.size.width,
  263. height: titleHeight()
  264. )
  265. } else {
  266. if editing {
  267. return CGRect(
  268. x: 0,
  269. y: 0,
  270. width: bounds.size.width,
  271. height: titleHeight()
  272. )
  273. } else {
  274. return CGRect(
  275. x: 0,
  276. y: titleHeight(),
  277. width: bounds.size.width,
  278. height: titleHeight()
  279. )
  280. }
  281. }
  282. }
  283. private func lineViewRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect {
  284. let height = editing ? selectedLineHeight : lineHeight
  285. return CGRect(
  286. x: 0,
  287. y: bounds.size.height - height,
  288. width: bounds.size.width,
  289. height: height
  290. )
  291. }
  292. private func errorLabelRectForBounds(_ bounds: CGRect) -> CGRect {
  293. if isValid {
  294. return CGRect(
  295. x: 0,
  296. y: bounds.height + errorHeight(),
  297. width: 0,
  298. height: 0
  299. )
  300. } else {
  301. return CGRect(
  302. x: 0,
  303. y: bounds.height,
  304. width: bounds.size.width,
  305. height: errorHeight()
  306. )
  307. }
  308. }
  309. private func textHeight() -> CGFloat {
  310. guard let font = self.font else { return 0.0 }
  311. return font.lineHeight + 3.0
  312. }
  313. private func updateTitleVisibility(_ animated: Bool = false) {
  314. let alpha: CGFloat
  315. if isShowTitle {
  316. alpha = 1.0
  317. } else {
  318. alpha = isTitleVisible ? 1.0 : 0.0
  319. }
  320. let frame = titleRectForBounds(bounds, editing: isTitleVisible)
  321. let errorAlpha: CGFloat = isValid || text == "" ? 0.0 : 1.0
  322. let errorLabelFrame = errorLabelRectForBounds(bounds)
  323. let updateBlock = {() -> Void in
  324. if !self.isShowTitle {
  325. self.containerView.alpha = alpha
  326. self.containerView.frame = frame
  327. }
  328. self.errorLabel.alpha = errorAlpha
  329. self.errorLabel.frame = errorLabelFrame
  330. }
  331. if animated {
  332. #if swift(>=4.2)
  333. let animationOptions: UIView.AnimationOptions = .curveEaseOut
  334. #else
  335. let animationOptions: UIViewAnimationOptions = .curveEaseOut
  336. #endif
  337. let duration = isTitleVisible ? titleFadeInDuration : titleFadeOutDuration
  338. UIView.animate(
  339. withDuration: duration,
  340. delay: 0,
  341. options: animationOptions,
  342. animations: { () -> Void in
  343. updateBlock()
  344. },
  345. completion: nil)
  346. }
  347. }
  348. private func update() {
  349. lineView.isHidden = isLineHidden
  350. if !isValid && text != "" {
  351. errorLabel.text = errorMessage
  352. errorLabel.textColor = errorColor
  353. errorLabel.font = errorFont
  354. lineView.backgroundColor = errorColor
  355. } else {
  356. lineView.backgroundColor = titleColor
  357. }
  358. if isUseTitle {
  359. containerView.isHidden = false
  360. containerView.alpha = 1.0
  361. } else {
  362. containerView.isHidden = true
  363. containerView.alpha = 0.0
  364. }
  365. titleLabel.text = titleText
  366. titleLabel.textColor = titleColor
  367. titleLabel.font = titleFont
  368. updateTitleVisibility(true)
  369. if !isEnabled {
  370. lineView.backgroundColor = disabledColor
  371. }
  372. }
  373. private func updatePlaceholder() {
  374. guard let placeholder = placeholder, let font = placeholderFont ?? font else {
  375. return
  376. }
  377. let color = isEnabled ? placeholderColor : disabledColor
  378. #if swift(>=4.2)
  379. attributedPlaceholder = NSAttributedString(
  380. string: placeholder,
  381. attributes: [
  382. NSAttributedString.Key.foregroundColor: color, NSAttributedString.Key.font: font
  383. ]
  384. )
  385. #elseif swift(>=4.0)
  386. attributedPlaceholder = NSAttributedString(
  387. string: placeholder,
  388. attributes: [
  389. NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font
  390. ]
  391. )
  392. #else
  393. attributedPlaceholder = NSAttributedString(
  394. string: placeholder,
  395. attributes: [NSForegroundColorAttributeName: color, NSFontAttributeName: font]
  396. )
  397. #endif
  398. }
  399. override open func textRect(forBounds bounds: CGRect) -> CGRect {
  400. let superRect = super.textRect(forBounds: bounds)
  401. let titleHeight = self.titleHeight()
  402. return CGRect(
  403. x: superRect.origin.x,
  404. y: titleHeight,
  405. width: superRect.size.width,
  406. height: superRect.size.height - titleHeight - selectedLineHeight
  407. )
  408. }
  409. override open func editingRect(forBounds bounds: CGRect) -> CGRect {
  410. let superRect = super.editingRect(forBounds: bounds)
  411. let titleHeight = self.titleHeight()
  412. let padding: CGFloat = leftImage == nil ? 0 : 10
  413. return CGRect(
  414. x: superRect.origin.x + padding,
  415. y: titleHeight,
  416. width: superRect.size.width,
  417. height: superRect.size.height - titleHeight - selectedLineHeight
  418. )
  419. }
  420. override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
  421. let superRect = super.editingRect(forBounds: bounds)
  422. let titleHeight = self.titleHeight()
  423. let padding: CGFloat = leftImage == nil ? 0 : 10
  424. let rect = CGRect(
  425. x: superRect.origin.x + padding,
  426. y: titleHeight,
  427. width: bounds.size.width - leftFlagImageWidth - padding,
  428. height: bounds.size.height - titleHeight - selectedLineHeight
  429. )
  430. return rect
  431. }
  432. override open func leftViewRect(forBounds bounds: CGRect) -> CGRect {
  433. let titleHeight = self.titleHeight()
  434. let rect = CGRect (
  435. x: 0,
  436. y: titleHeight,
  437. width: leftFlagImageWidth,
  438. height: bounds.size.height - titleHeight - selectedLineHeight
  439. )
  440. return rect
  441. }
  442. override open func prepareForInterfaceBuilder() {
  443. super.prepareForInterfaceBuilder()
  444. borderStyle = .none
  445. isSelected = true
  446. invalidateIntrinsicContentSize()
  447. }
  448. override open func layoutSubviews() {
  449. super.layoutSubviews()
  450. self.containerView.frame = titleRectForBounds(
  451. bounds,
  452. editing: isTitleVisible
  453. )
  454. self.lineView.frame = lineViewRectForBounds(
  455. bounds,
  456. editing: editingOrSelected
  457. )
  458. self.errorLabel.frame = errorLabelRectForBounds(bounds)
  459. }
  460. override open var intrinsicContentSize: CGSize {
  461. return CGSize(width: bounds.size.width, height: titleHeight() + textHeight() + 10)
  462. }
  463. }
  464. extension ValidationTextField {
  465. func setCountry(with code: String?, isShowCode: Bool = true) {
  466. guard
  467. let code = code?.lowercased(),
  468. let countryCode = CountryEnum(rawValue: code) else {
  469. leftView = nil
  470. return
  471. }
  472. setCountry(with: countryCode, isShowCode: isShowCode)
  473. }
  474. func setCountry(with code: CountryEnum, isShowCode: Bool = true) {
  475. countryCode = code
  476. widthOfImageInLeftView = 35
  477. let textArea = textRect(forBounds: bounds)
  478. let imageView = UIImageView(
  479. frame: CGRect(
  480. x: leftViewPadding,
  481. y: 0,
  482. width: widthOfImageInLeftView,
  483. height: textArea.height
  484. )
  485. )
  486. imageView.contentMode = .scaleAspectFit
  487. imageView.image = code.flag
  488. let label = UILabel()
  489. label.font = font ?? UIFont.systemFont(ofSize: 14)
  490. label.textColor = textColor ?? .black
  491. label.text = code.phoneCode
  492. label.frame = CGRect(
  493. x: leftViewPadding + widthOfImageInLeftView + leftViewPadding,
  494. y: 0,
  495. width: label.intrinsicContentSize.width,
  496. height: textArea.height
  497. )
  498. var width: CGFloat = leftViewPadding +
  499. widthOfImageInLeftView +
  500. leftViewPadding +
  501. label.intrinsicContentSize.width +
  502. leftViewPadding
  503. if !isShowCode {
  504. width = leftViewPadding + widthOfImageInLeftView + leftViewPadding
  505. label.isHidden = !isShowCode
  506. }
  507. let paddingView = UIView(
  508. frame: CGRect(
  509. x: 0,
  510. y: textArea.origin.y,
  511. width: width,
  512. height: textArea.height
  513. )
  514. )
  515. paddingView.addSubview(imageView)
  516. paddingView.addSubview(label)
  517. paddingView.backgroundColor = .clear
  518. leftFlagImageWidth = paddingView.frame.width
  519. leftViewMode = .always
  520. leftView = paddingView
  521. }
  522. func setBank(with code: String?, isShowCode: Bool = true) {
  523. guard
  524. let code = code?.lowercased(),
  525. let bankCode = BankEnum(rawValue: code) else {
  526. leftView = nil
  527. return
  528. }
  529. setBank(with: bankCode, isShowCode: isShowCode)
  530. }
  531. func setBank(with code: BankEnum, isShowCode: Bool = true) {
  532. bankCode = code
  533. widthOfImageInLeftView = 35
  534. let textArea = textRect(forBounds: bounds)
  535. let imageView = UIImageView(
  536. frame: CGRect(
  537. x: leftViewPadding,
  538. y: 0,
  539. width: widthOfImageInLeftView,
  540. height: textArea.height
  541. )
  542. )
  543. imageView.contentMode = .scaleAspectFit
  544. imageView.image = code.ciImage
  545. let label = UILabel()
  546. label.font = font ?? UIFont.systemFont(ofSize: 14)
  547. label.textColor = textColor ?? .black
  548. label.text = ""
  549. label.frame = CGRect(
  550. x: leftViewPadding + widthOfImageInLeftView + leftViewPadding,
  551. y: 0,
  552. width: label.intrinsicContentSize.width,
  553. height: textArea.height
  554. )
  555. var width: CGFloat = leftViewPadding +
  556. widthOfImageInLeftView +
  557. leftViewPadding +
  558. label.intrinsicContentSize.width +
  559. leftViewPadding
  560. if !isShowCode {
  561. width = leftViewPadding + widthOfImageInLeftView + leftViewPadding
  562. label.isHidden = !isShowCode
  563. }
  564. let paddingView = UIView(
  565. frame: CGRect(
  566. x: 0,
  567. y: textArea.origin.y,
  568. width: width,
  569. height: textArea.height
  570. )
  571. )
  572. paddingView.addSubview(imageView)
  573. paddingView.addSubview(label)
  574. paddingView.backgroundColor = .clear
  575. leftFlagImageWidth = paddingView.frame.width
  576. leftViewMode = .always
  577. leftView = paddingView
  578. }
  579. }