LNPopupView.swift 5.4 KB

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