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