NCPopupViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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. // Border enabled, default is false
  48. public var borderEnabled = false
  49. // Move the popup position H when show/hide keyboard
  50. public var keyboardPosizionEnabled = true
  51. // The pop up view controller. It's not mandatory.
  52. private(set) public var contentController: UIViewController?
  53. // The pop up view
  54. private(set) public var contentView: UIView?
  55. // The delegate to receive pop up event
  56. public weak var delegate: NCPopupViewControllerDelegate?
  57. private var containerView = UIView()
  58. // MARK: - Life Cycle
  59. // NOTE: Don't use this init method
  60. required public init?(coder aDecoder: NSCoder) {
  61. super.init(coder: aDecoder)
  62. }
  63. /**
  64. Init with content view controller. Your pop up content is a view controller (easiest way to design it is using storyboard)
  65. - Parameters:
  66. - contentController: Popup content view controller
  67. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  68. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  69. */
  70. public init(contentController: UIViewController, popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  71. super.init(nibName: nil, bundle: nil)
  72. self.contentController = contentController
  73. self.contentView = contentController.view
  74. self.popupWidth = popupWidth
  75. self.popupHeight = popupHeight
  76. modalPresentationStyle = .overFullScreen
  77. modalTransitionStyle = .crossDissolve
  78. }
  79. /**
  80. Init with content view
  81. - Parameters:
  82. - contentView: Popup content view
  83. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  84. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  85. */
  86. public init(contentView: UIView, popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  87. super.init(nibName: nil, bundle: nil)
  88. self.contentView = contentView
  89. self.popupWidth = popupWidth
  90. self.popupHeight = popupHeight
  91. modalPresentationStyle = .overFullScreen
  92. modalTransitionStyle = .crossDissolve
  93. }
  94. override public func viewDidLoad() {
  95. super.viewDidLoad()
  96. setupUI()
  97. setupViews()
  98. addDismissGesture()
  99. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  100. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil);
  101. }
  102. // MARK: - Setup
  103. private func addDismissGesture() {
  104. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissTapGesture(gesture:)))
  105. tapGesture.delegate = self
  106. view.addGestureRecognizer(tapGesture)
  107. }
  108. private func setupUI() {
  109. containerView.translatesAutoresizingMaskIntoConstraints = false
  110. contentView?.translatesAutoresizingMaskIntoConstraints = false
  111. view.backgroundColor = backgroundColor.withAlphaComponent(backgroundAlpha)
  112. if cornerRadius > 0 {
  113. contentView?.layer.cornerRadius = cornerRadius
  114. contentView?.layer.masksToBounds = true
  115. }
  116. if shadowEnabled {
  117. containerView.layer.shadowOpacity = 0.5
  118. containerView.layer.shadowColor = UIColor.black.cgColor
  119. containerView.layer.shadowOffset = CGSize(width: 5, height: 5)
  120. containerView.layer.shadowRadius = 5
  121. }
  122. if borderEnabled {
  123. containerView.layer.cornerRadius = cornerRadius
  124. containerView.layer.borderWidth = 0.3
  125. containerView.layer.borderColor = UIColor.gray.cgColor
  126. }
  127. }
  128. private func setupViews() {
  129. if let contentController = contentController {
  130. addChild(contentController)
  131. }
  132. addViews()
  133. addSizeConstraints()
  134. addCenterPositionConstraints()
  135. }
  136. private func addViews() {
  137. view.addSubview(containerView)
  138. if let contentView = contentView {
  139. containerView.addSubview(contentView)
  140. let topConstraint = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0)
  141. let leftConstraint = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: containerView, attribute: .left, multiplier: 1, constant: 0)
  142. let bottomConstraint = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0)
  143. let rightConstraint = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: 0)
  144. NSLayoutConstraint.activate([topConstraint, leftConstraint, bottomConstraint, rightConstraint])
  145. }
  146. }
  147. // MARK: - Add constraints
  148. private func addSizeConstraints() {
  149. if let popupWidth = popupWidth {
  150. let widthConstraint = NSLayoutConstraint(item: containerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupWidth)
  151. NSLayoutConstraint.activate([widthConstraint])
  152. }
  153. if let popupHeight = popupHeight {
  154. let heightConstraint = NSLayoutConstraint(item: containerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupHeight)
  155. NSLayoutConstraint.activate([heightConstraint])
  156. }
  157. }
  158. private func addCenterPositionConstraints() {
  159. let centerXConstraint = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0)
  160. centerYConstraint = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)
  161. NSLayoutConstraint.activate([centerXConstraint, centerYConstraint!])
  162. }
  163. // MARK: -
  164. func addPath() {
  165. let balloon = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 250))
  166. balloon.backgroundColor = UIColor.clear
  167. let path = UIBezierPath()
  168. path.move(to: CGPoint(x: 0, y: 0))
  169. path.addLine(to: CGPoint(x: 200, y: 0))
  170. path.addLine(to: CGPoint(x: 200, y: 200))
  171. // Draw arrow
  172. path.addLine(to: CGPoint(x: 120, y: 200))
  173. path.addLine(to: CGPoint(x: 100, y: 250))
  174. path.addLine(to: CGPoint(x: 80, y: 200))
  175. path.addLine(to: CGPoint(x: 0, y: 200))
  176. path.close()
  177. let shape = CAShapeLayer()
  178. //shape.backgroundColor = UIColor.blue.cgColor
  179. shape.fillColor = UIColor.blue.cgColor
  180. shape.path = path.cgPath
  181. balloon.layer.addSublayer(shape)
  182. // [self.view addSubview:balloonView];
  183. }
  184. // MARK: - Actions
  185. @objc func dismissTapGesture(gesture: UIGestureRecognizer) {
  186. dismiss(animated: true) {
  187. self.delegate?.popupViewControllerDidDismissByTapGesture(self)
  188. }
  189. }
  190. // MARK: - Keyboard notification
  191. @objc internal func keyboardWillShow(_ notification : Notification?) {
  192. var keyboardSize = CGSize.zero
  193. if let info = notification?.userInfo {
  194. let frameEndUserInfoKey = UIResponder.keyboardFrameEndUserInfoKey
  195. // Getting UIKeyboardSize.
  196. if let keyboardFrame = info[frameEndUserInfoKey] as? CGRect {
  197. let screenSize = UIScreen.main.bounds
  198. //Calculating actual keyboard displayed size, keyboard frame may be different when hardware keyboard is attached (Bug ID: #469) (Bug ID: #381)
  199. let intersectRect = keyboardFrame.intersection(screenSize)
  200. if intersectRect.isNull {
  201. keyboardSize = CGSize(width: screenSize.size.width, height: 0)
  202. } else {
  203. keyboardSize = intersectRect.size
  204. }
  205. if keyboardPosizionEnabled {
  206. let popupDiff = screenSize.height - ((screenSize.height - (popupHeight ?? 0)) / 2)
  207. let keyboardDiff = screenSize.height - keyboardSize.height
  208. let diff = popupDiff - keyboardDiff
  209. if centerYConstraint != nil && diff > 0 {
  210. centerYConstraint?.constant = -(diff + 10)
  211. }
  212. }
  213. }
  214. }
  215. }
  216. @objc func keyboardWillHide(_ notification: Notification) {
  217. if keyboardPosizionEnabled {
  218. if centerYConstraint != nil {
  219. centerYConstraint?.constant = 0
  220. }
  221. }
  222. }
  223. }
  224. // MARK: - UIGestureRecognizerDelegate
  225. extension NCPopupViewController: UIGestureRecognizerDelegate {
  226. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
  227. guard let touchView = touch.view, canTapOutsideToDismiss else {
  228. return false
  229. }
  230. return !touchView.isDescendant(of: containerView)
  231. }
  232. }