NCPopupViewController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. //
  2. // NCPopupViewController.swift
  3. // EzPopup
  4. //
  5. // Created by Huy Nguyen on 6/4/18.
  6. //
  7. // Modified by Marino Faggiana for Nextcloud progect.
  8. //
  9. // Author Huy Nguyen
  10. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  11. //
  12. // This program is free software: you can redistribute it and/or modify
  13. // it under the terms of the GNU General Public License as published by
  14. // the Free Software Foundation, either version 3 of the License, or
  15. // (at your option) any later version.
  16. //
  17. // This program is distributed in the hope that it will be useful,
  18. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. // GNU General Public License for more details.
  21. //
  22. // You should have received a copy of the GNU General Public License
  23. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. //
  25. import UIKit
  26. public protocol NCPopupViewControllerDelegate: class {
  27. /// It is called when pop up is dismissed by tap outside
  28. func popupViewControllerDidDismissByTapGesture(_ sender: NCPopupViewController)
  29. }
  30. // optional func
  31. public extension NCPopupViewControllerDelegate {
  32. func popupViewControllerDidDismissByTapGesture(_ sender: NCPopupViewController) {}
  33. }
  34. public class NCPopupViewController: UIViewController {
  35. public enum PopupPosition {
  36. /// Align center X, center Y with offset param
  37. case center(CGPoint?)
  38. /// Top left anchor point with offset param
  39. case topLeft(CGPoint?)
  40. /// Top right anchor point with offset param
  41. case topRight(CGPoint?)
  42. /// Bottom left anchor point with offset param
  43. case bottomLeft(CGPoint?)
  44. /// Bottom right anchor point with offset param
  45. case bottomRight(CGPoint?)
  46. /// Top anchor, align center X with top padding param
  47. case top(CGFloat)
  48. /// Left anchor, align center Y with left padding param
  49. case left(CGFloat)
  50. /// Bottom anchor, align center X with bottom padding param
  51. case bottom(CGFloat)
  52. /// Right anchor, align center Y with right padding param
  53. case right(CGFloat)
  54. }
  55. /// Popup width, it's nil if width is determined by view's intrinsic size
  56. private(set) public var popupWidth: CGFloat?
  57. /// Popup height, it's nil if width is determined by view's intrinsic size
  58. private(set) public var popupHeight: CGFloat?
  59. /// Popup position, default is center
  60. private(set) public var position: PopupPosition = .center(nil)
  61. /// Background alpha, default is 0.5
  62. public var backgroundAlpha: CGFloat = 0.5
  63. /// Background color, default is black
  64. public var backgroundColor = UIColor.black
  65. /// Allow tap outside popup to dismiss, default is true
  66. public var canTapOutsideToDismiss = true
  67. /// Corner radius, default is 0 (no rounded corner)
  68. public var cornerRadius: CGFloat = 0
  69. /// Shadow enabled, default is true
  70. public var shadowEnabled = true
  71. /// The pop up view controller. It's not mandatory.
  72. private(set) public var contentController: UIViewController?
  73. /// The pop up view
  74. private(set) public var contentView: UIView?
  75. /// The delegate to receive pop up event
  76. public weak var delegate: NCPopupViewControllerDelegate?
  77. private var containerView = UIView()
  78. // MARK: -
  79. /// NOTE: Don't use this init method
  80. required public init?(coder aDecoder: NSCoder) {
  81. super.init(coder: aDecoder)
  82. }
  83. /**
  84. Init with content view controller. Your pop up content is a view controller (easiest way to design it is using storyboard)
  85. - Parameters:
  86. - contentController: Popup content view controller
  87. - position: Position of popup content, default is center
  88. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  89. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  90. */
  91. public init(contentController: UIViewController, position: PopupPosition = .center(nil), popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  92. super.init(nibName: nil, bundle: nil)
  93. self.contentController = contentController
  94. self.contentView = contentController.view
  95. self.popupWidth = popupWidth
  96. self.popupHeight = popupHeight
  97. self.position = position
  98. commonInit()
  99. }
  100. /**
  101. Init with content view
  102. - Parameters:
  103. - contentView: Popup content view
  104. - position: Position of popup content, default is center
  105. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  106. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  107. */
  108. public init(contentView: UIView, position: PopupPosition = .center(nil), popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  109. super.init(nibName: nil, bundle: nil)
  110. self.contentView = contentView
  111. self.popupWidth = popupWidth
  112. self.popupHeight = popupHeight
  113. self.position = position
  114. commonInit()
  115. }
  116. private func commonInit() {
  117. modalPresentationStyle = .overFullScreen
  118. modalTransitionStyle = .crossDissolve
  119. }
  120. override public func viewDidLoad() {
  121. super.viewDidLoad()
  122. setupUI()
  123. setupViews()
  124. addDismissGesture()
  125. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  126. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil);
  127. }
  128. // MARK: -
  129. @objc internal func keyboardWillShow(_ notification : Notification?) {
  130. var keyboardSize: CGSize!
  131. if let info = notification?.userInfo {
  132. let frameEndUserInfoKey = UIResponder.keyboardFrameEndUserInfoKey
  133. // Getting UIKeyboardSize.
  134. if let keyboardFrame = info[frameEndUserInfoKey] as? CGRect {
  135. let screenSize = UIScreen.main.bounds
  136. //Calculating actual keyboard displayed size, keyboard frame may be different when hardware keyboard is attached (Bug ID: #469) (Bug ID: #381)
  137. let intersectRect = keyboardFrame.intersection(screenSize)
  138. if intersectRect.isNull {
  139. keyboardSize = CGSize(width: screenSize.size.width, height: 0)
  140. } else {
  141. keyboardSize = intersectRect.size
  142. }
  143. }
  144. }
  145. }
  146. @objc func keyboardWillHide(_ notification: Notification) {
  147. print("")
  148. }
  149. // MARK: - Setup
  150. private func addDismissGesture() {
  151. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissTapGesture(gesture:)))
  152. tapGesture.delegate = self
  153. view.addGestureRecognizer(tapGesture)
  154. }
  155. private func setupUI() {
  156. containerView.translatesAutoresizingMaskIntoConstraints = false
  157. contentView?.translatesAutoresizingMaskIntoConstraints = false
  158. view.backgroundColor = backgroundColor.withAlphaComponent(backgroundAlpha)
  159. if cornerRadius > 0 {
  160. contentView?.layer.cornerRadius = cornerRadius
  161. contentView?.layer.masksToBounds = true
  162. }
  163. if shadowEnabled {
  164. containerView.layer.shadowOpacity = 0.5
  165. containerView.layer.shadowColor = UIColor.black.cgColor
  166. containerView.layer.shadowOffset = CGSize(width: 5, height: 5)
  167. containerView.layer.shadowRadius = 5
  168. }
  169. }
  170. private func setupViews() {
  171. if let contentController = contentController {
  172. addChild(contentController)
  173. }
  174. addViews()
  175. addSizeConstraints()
  176. addPositionConstraints()
  177. }
  178. private func addViews() {
  179. view.addSubview(containerView)
  180. if let contentView = contentView {
  181. containerView.addSubview(contentView)
  182. let topConstraint = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0)
  183. let leftConstraint = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: containerView, attribute: .left, multiplier: 1, constant: 0)
  184. let bottomConstraint = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0)
  185. let rightConstraint = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: 0)
  186. NSLayoutConstraint.activate([topConstraint, leftConstraint, bottomConstraint, rightConstraint])
  187. }
  188. }
  189. // MARK: - Add constraints
  190. private func addSizeConstraints() {
  191. if let popupWidth = popupWidth {
  192. let widthConstraint = NSLayoutConstraint(item: containerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupWidth)
  193. NSLayoutConstraint.activate([widthConstraint])
  194. }
  195. if let popupHeight = popupHeight {
  196. let heightConstraint = NSLayoutConstraint(item: containerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupHeight)
  197. NSLayoutConstraint.activate([heightConstraint])
  198. }
  199. }
  200. private func addPositionConstraints() {
  201. switch position {
  202. case .center(let offset):
  203. addCenterPositionConstraints(offset: offset)
  204. case .topLeft(let offset):
  205. addTopLeftPositionConstraints(offset: offset)
  206. case .topRight(let offset):
  207. addTopRightPositionConstraints(offset: offset)
  208. case .bottomLeft(let offset):
  209. addBottomLeftPositionConstraints(offset: offset)
  210. case .bottomRight(let offset):
  211. addBottomRightPositionConstraints(offset: offset)
  212. case .top(let offset):
  213. addTopPositionConstraints(offset: offset)
  214. case .left(let offset):
  215. addLeftPositionConstraints(offset: offset)
  216. case .bottom(let offset):
  217. addBottomPositionConstraints(offset: offset)
  218. case .right(let offset):
  219. addRightPositionConstraints(offset: offset)
  220. }
  221. }
  222. private func addCenterPositionConstraints(offset: CGPoint?) {
  223. let centerXConstraint = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: offset?.x ?? 0)
  224. let centerYConstraint = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: offset?.y ?? 0)
  225. NSLayoutConstraint.activate([centerXConstraint, centerYConstraint])
  226. }
  227. private func addTopLeftPositionConstraints(offset: CGPoint?) {
  228. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset?.y ?? 0)
  229. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset?.x ?? 0)
  230. NSLayoutConstraint.activate([topConstraint, leftConstraint])
  231. }
  232. private func addTopRightPositionConstraints(offset: CGPoint?) {
  233. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset?.y ?? 0)
  234. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset?.x ?? 0)
  235. NSLayoutConstraint.activate([topConstraint, rightConstraint])
  236. }
  237. private func addBottomLeftPositionConstraints(offset: CGPoint?) {
  238. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset?.y ?? 0)
  239. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset?.x ?? 0)
  240. NSLayoutConstraint.activate([bottomConstraint, leftConstraint])
  241. }
  242. private func addBottomRightPositionConstraints(offset: CGPoint?) {
  243. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset?.y ?? 0)
  244. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset?.x ?? 0)
  245. NSLayoutConstraint.activate([bottomConstraint, rightConstraint])
  246. }
  247. private func addTopPositionConstraints(offset: CGFloat) {
  248. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset)
  249. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerX, relatedBy: .equal, toItem: containerView, attribute: .centerX, multiplier: 1, constant: 0)
  250. NSLayoutConstraint.activate([topConstraint, centerXConstraint])
  251. }
  252. private func addLeftPositionConstraints(offset: CGFloat) {
  253. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset)
  254. let centerYConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerY, relatedBy: .equal, toItem: containerView, attribute: .centerY, multiplier: 1, constant: 0)
  255. NSLayoutConstraint.activate([leftConstraint, centerYConstraint])
  256. }
  257. private func addBottomPositionConstraints(offset: CGFloat) {
  258. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset)
  259. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerX, relatedBy: .equal, toItem: containerView, attribute: .centerX, multiplier: 1, constant: 0)
  260. NSLayoutConstraint.activate([bottomConstraint, centerXConstraint])
  261. }
  262. private func addRightPositionConstraints(offset: CGFloat) {
  263. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset)
  264. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerY, relatedBy: .equal, toItem: containerView, attribute: .centerY, multiplier: 1, constant: 0)
  265. NSLayoutConstraint.activate([rightConstraint, centerXConstraint])
  266. }
  267. // MARK: - Actions
  268. @objc func dismissTapGesture(gesture: UIGestureRecognizer) {
  269. dismiss(animated: true) {
  270. self.delegate?.popupViewControllerDidDismissByTapGesture(self)
  271. }
  272. }
  273. }
  274. // MARK: - UIGestureRecognizerDelegate
  275. extension NCPopupViewController: UIGestureRecognizerDelegate {
  276. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
  277. guard let touchView = touch.view, canTapOutsideToDismiss else {
  278. return false
  279. }
  280. return !touchView.isDescendant(of: containerView)
  281. }
  282. }