NCPopupViewController.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. //
  2. // NCPopupViewController.swift
  3. //
  4. // Based on EzPopup by Huy Nguyen
  5. // Modified by Marino Faggiana for Nextcloud progect.
  6. //
  7. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU General Public License as published by
  11. // the Free Software Foundation, either version 3 of the License, or
  12. // (at your option) any later version.
  13. //
  14. // This program is distributed in the hope that it will be useful,
  15. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. // GNU General Public License for more details.
  18. //
  19. // You should have received a copy of the GNU General Public License
  20. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. //
  22. import UIKit
  23. public protocol NCPopupViewControllerDelegate: class {
  24. // It is called when pop up is dismissed by tap outside
  25. func popupViewControllerDidDismissByTapGesture(_ sender: NCPopupViewController)
  26. }
  27. // optional func
  28. public extension NCPopupViewControllerDelegate {
  29. func popupViewControllerDidDismissByTapGesture(_ sender: NCPopupViewController) {}
  30. }
  31. public class NCPopupViewController: UIViewController {
  32. private var centerYConstraint: NSLayoutConstraint?
  33. // Popup width, it's nil if width is determined by view's intrinsic size
  34. private(set) public var popupWidth: CGFloat?
  35. // Popup height, it's nil if width is determined by view's intrinsic size
  36. private(set) public var popupHeight: CGFloat?
  37. // Background alpha, default is 0.3
  38. public var backgroundAlpha: CGFloat = 0.3
  39. // Background color, default is black
  40. public var backgroundColor = UIColor.black
  41. // Allow tap outside popup to dismiss, default is true
  42. public var canTapOutsideToDismiss = true
  43. // Corner radius, default is 10 (0 no rounded corner)
  44. public var cornerRadius: CGFloat = 10
  45. // Shadow enabled, default is true
  46. public var shadowEnabled = true
  47. // Move the popup position H when show/hide keyboard
  48. public var keyboardPosizionEnabled = true
  49. // The pop up view controller. It's not mandatory.
  50. private(set) public var contentController: UIViewController?
  51. // The pop up view
  52. private(set) public var contentView: UIView?
  53. // The delegate to receive pop up event
  54. public weak var delegate: NCPopupViewControllerDelegate?
  55. private var containerView = UIView()
  56. // MARK: - Life Cycle
  57. // NOTE: Don't use this init method
  58. required public init?(coder aDecoder: NSCoder) {
  59. super.init(coder: aDecoder)
  60. }
  61. /**
  62. Init with content view controller. Your pop up content is a view controller (easiest way to design it is using storyboard)
  63. - Parameters:
  64. - contentController: Popup content view controller
  65. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  66. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  67. */
  68. public init(contentController: UIViewController, popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  69. super.init(nibName: nil, bundle: nil)
  70. self.contentController = contentController
  71. self.contentView = contentController.view
  72. self.popupWidth = popupWidth
  73. self.popupHeight = popupHeight
  74. modalPresentationStyle = .overFullScreen
  75. modalTransitionStyle = .crossDissolve
  76. }
  77. /**
  78. Init with content view
  79. - Parameters:
  80. - contentView: Popup content view
  81. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  82. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  83. */
  84. public init(contentView: UIView, popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  85. super.init(nibName: nil, bundle: nil)
  86. self.contentView = contentView
  87. self.popupWidth = popupWidth
  88. self.popupHeight = popupHeight
  89. modalPresentationStyle = .overFullScreen
  90. modalTransitionStyle = .crossDissolve
  91. }
  92. override public func viewDidLoad() {
  93. super.viewDidLoad()
  94. setupUI()
  95. setupViews()
  96. addDismissGesture()
  97. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  98. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil);
  99. }
  100. // MARK: - Setup
  101. private func addDismissGesture() {
  102. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissTapGesture(gesture:)))
  103. tapGesture.delegate = self
  104. view.addGestureRecognizer(tapGesture)
  105. }
  106. private func setupUI() {
  107. containerView.translatesAutoresizingMaskIntoConstraints = false
  108. contentView?.translatesAutoresizingMaskIntoConstraints = false
  109. view.backgroundColor = backgroundColor.withAlphaComponent(backgroundAlpha)
  110. if cornerRadius > 0 {
  111. contentView?.layer.cornerRadius = cornerRadius
  112. contentView?.layer.masksToBounds = true
  113. }
  114. if shadowEnabled {
  115. containerView.layer.shadowOpacity = 0.5
  116. containerView.layer.shadowColor = UIColor.black.cgColor
  117. containerView.layer.shadowOffset = CGSize(width: 5, height: 5)
  118. containerView.layer.shadowRadius = 5
  119. }
  120. }
  121. private func setupViews() {
  122. if let contentController = contentController {
  123. addChild(contentController)
  124. }
  125. addViews()
  126. addSizeConstraints()
  127. addCenterPositionConstraints()
  128. }
  129. private func addViews() {
  130. view.addSubview(containerView)
  131. if let contentView = contentView {
  132. containerView.addSubview(contentView)
  133. let topConstraint = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0)
  134. let leftConstraint = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: containerView, attribute: .left, multiplier: 1, constant: 0)
  135. let bottomConstraint = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0)
  136. let rightConstraint = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: 0)
  137. NSLayoutConstraint.activate([topConstraint, leftConstraint, bottomConstraint, rightConstraint])
  138. }
  139. }
  140. // MARK: - Add constraints
  141. private func addSizeConstraints() {
  142. if let popupWidth = popupWidth {
  143. let widthConstraint = NSLayoutConstraint(item: containerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupWidth)
  144. NSLayoutConstraint.activate([widthConstraint])
  145. }
  146. if let popupHeight = popupHeight {
  147. let heightConstraint = NSLayoutConstraint(item: containerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupHeight)
  148. NSLayoutConstraint.activate([heightConstraint])
  149. }
  150. }
  151. private func addCenterPositionConstraints() {
  152. let centerXConstraint = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0)
  153. centerYConstraint = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)
  154. NSLayoutConstraint.activate([centerXConstraint, centerYConstraint!])
  155. }
  156. // MARK: - Actions
  157. @objc func dismissTapGesture(gesture: UIGestureRecognizer) {
  158. dismiss(animated: true) {
  159. self.delegate?.popupViewControllerDidDismissByTapGesture(self)
  160. }
  161. }
  162. // MARK: - Keyboard notification
  163. @objc internal func keyboardWillShow(_ notification : Notification?) {
  164. var keyboardSize = CGSize.zero
  165. if let info = notification?.userInfo {
  166. let frameEndUserInfoKey = UIResponder.keyboardFrameEndUserInfoKey
  167. // Getting UIKeyboardSize.
  168. if let keyboardFrame = info[frameEndUserInfoKey] as? CGRect {
  169. let screenSize = UIScreen.main.bounds
  170. //Calculating actual keyboard displayed size, keyboard frame may be different when hardware keyboard is attached (Bug ID: #469) (Bug ID: #381)
  171. let intersectRect = keyboardFrame.intersection(screenSize)
  172. if intersectRect.isNull {
  173. keyboardSize = CGSize(width: screenSize.size.width, height: 0)
  174. } else {
  175. keyboardSize = intersectRect.size
  176. }
  177. if keyboardPosizionEnabled {
  178. let popupDiff = screenSize.height - ((screenSize.height - (popupHeight ?? 0)) / 2)
  179. let keyboardDiff = screenSize.height - keyboardSize.height
  180. let diff = popupDiff - keyboardDiff
  181. if centerYConstraint != nil && diff > 0 {
  182. centerYConstraint?.constant = -(diff + 10)
  183. }
  184. }
  185. }
  186. }
  187. }
  188. @objc func keyboardWillHide(_ notification: Notification) {
  189. if keyboardPosizionEnabled {
  190. if centerYConstraint != nil {
  191. centerYConstraint?.constant = 0
  192. }
  193. }
  194. }
  195. }
  196. // MARK: - UIGestureRecognizerDelegate
  197. extension NCPopupViewController: UIGestureRecognizerDelegate {
  198. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
  199. guard let touchView = touch.view, canTapOutsideToDismiss else {
  200. return false
  201. }
  202. return !touchView.isDescendant(of: containerView)
  203. }
  204. }