LNPopupView.swift 5.7 KB

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