LNPopupView.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //
  2. // LNPopupView.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/14.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. enum LNPopupViewHeight {
  11. case auto
  12. case percent(CGFloat)
  13. case height(CGFloat)
  14. }
  15. class LNPopupView: UIView {
  16. let backgroundView = UIView()
  17. let container = UIView()
  18. var containerHeight: LNPopupViewHeight = .auto {
  19. didSet {
  20. container.snp.makeConstraints { make in
  21. switch containerHeight {
  22. case .percent(let percent):
  23. make.height.equalToSuperview().multipliedBy(percent)
  24. case .height(let height):
  25. make.height.equalTo(height)
  26. case .auto: break
  27. }
  28. }
  29. }
  30. }
  31. var touchOutsideToCancel = true
  32. var ignoreKeyboardToDismiss = false
  33. var onTouchOutside: (() -> Void)?
  34. private var bottomConstraint: Constraint?
  35. override init(frame: CGRect) {
  36. super.init(frame: frame)
  37. setupViews()
  38. }
  39. func popup(_ targetView: UIView? = nil, animated: Bool = true) {
  40. let parentView: UIView? = if let window = targetView as? UIWindow {
  41. window
  42. } else if let view = targetView?.viewController?.view {
  43. view
  44. } else if let window = UIView.appKeyWindow {
  45. window
  46. } else {
  47. nil
  48. }
  49. guard let parentView else { return }
  50. frame = parentView.bounds
  51. parentView.addSubview(self)
  52. snp.makeConstraints { make in
  53. make.edges.equalToSuperview()
  54. }
  55. layoutIfNeeded()
  56. moveToShowupPosition()
  57. window?.endEditing(true)
  58. if animated {
  59. backgroundView.backgroundColor = .clear
  60. UIView.animate(withDuration: 0.25) {
  61. self.backgroundView.backgroundColor = .black.withAlphaComponent(0.4)
  62. self.layoutIfNeeded()
  63. }
  64. } else {
  65. backgroundView.backgroundColor = .black.withAlphaComponent(0.4)
  66. layoutIfNeeded()
  67. }
  68. }
  69. func dismiss(animated: Bool = true) {
  70. endEditing(true)
  71. moveToHiddenPosition()
  72. if animated {
  73. UIView.animate(withDuration: 0.25) {
  74. self.backgroundView.backgroundColor = .clear
  75. self.layoutIfNeeded()
  76. } completion: { [weak self] _ in
  77. guard let self else { return }
  78. self.removeFromSuperview()
  79. }
  80. } else {
  81. removeFromSuperview()
  82. }
  83. }
  84. required init?(coder: NSCoder) {
  85. fatalError("init(coder:) has not been implemented")
  86. }
  87. }
  88. extension LNPopupView {
  89. private func setupViews() {
  90. backgroundView.onTap { [weak self] in
  91. guard let self else { return }
  92. if !ignoreKeyboardToDismiss,
  93. LNKeyboardManager.shared.isEditing {
  94. endEditing(true)
  95. } else if touchOutsideToCancel {
  96. if let onTouchOutside {
  97. onTouchOutside()
  98. } else {
  99. dismiss()
  100. }
  101. } else {
  102. endEditing(true)
  103. }
  104. }
  105. insertSubview(backgroundView, at: 0)
  106. backgroundView.snp.makeConstraints { make in
  107. make.horizontalEdges.equalToSuperview()
  108. make.bottom.equalToSuperview()
  109. make.top.equalToSuperview().offset(-(UIView.navigationBarHeight + UIView.statusBarHeight))
  110. }
  111. container.backgroundColor = .white
  112. container.layer.cornerRadius = 20
  113. container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
  114. addSubview(container)
  115. container.snp.makeConstraints { make in
  116. make.horizontalEdges.equalToSuperview()
  117. make.top.equalTo(snp.bottom).priority(.medium)
  118. bottomConstraint = make.bottom.equalToSuperview().priority(.low).constraint
  119. }
  120. let dismissView = UIView()
  121. container.addSubview(dismissView)
  122. dismissView.snp.makeConstraints { make in
  123. make.edges.equalToSuperview()
  124. }
  125. dismissView.onTap { [weak self] in
  126. guard let self else { return }
  127. endEditing(true)
  128. }
  129. LNEventDeliver.addObserver(self)
  130. }
  131. private func moveToHiddenPosition() {
  132. bottomConstraint?.update(priority: .low).update(offset: 0)
  133. }
  134. private func moveToShowupPosition() {
  135. bottomConstraint?.update(priority: .high).update(offset: 0)
  136. }
  137. private func onKeyboardShowup(_ keyboardHeight: CGFloat) {
  138. bottomConstraint?.update(offset: -keyboardHeight)
  139. }
  140. }
  141. extension LNPopupView: LNKeyboardNotify {
  142. func onKeyboardWillShow(curInput: UIView?, keyboardHeight: CGFloat) {
  143. guard let curInput, curInput.isDescendant(of: self) == true else { return }
  144. let containerY = bounds.height - container.bounds.height
  145. let editViewY = curInput.convert(curInput.bounds, to: container).maxY
  146. let offset = bounds.height - (containerY + editViewY) - keyboardHeight - 16
  147. if offset < 0 {
  148. onKeyboardShowup(-offset)
  149. }
  150. }
  151. func onKeyboardShow(curInput: UIView?, keyboardHeight: CGFloat) {
  152. guard curInput?.isDescendant(of: self) == true else { return }
  153. layoutIfNeeded()
  154. }
  155. func onKeyboardWillHide(curInput: UIView?) {
  156. guard curInput?.isDescendant(of: self) == true else { return }
  157. moveToShowupPosition()
  158. }
  159. func onKeyboardHide(curInput: UIView?) {
  160. guard curInput?.isDescendant(of: self) == true else { return }
  161. layoutIfNeeded()
  162. }
  163. }