NCPopupViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. private var centerYConstraint: NSLayoutConstraint?
  56. /// Popup width, it's nil if width is determined by view's intrinsic size
  57. private(set) public var popupWidth: CGFloat?
  58. /// Popup height, it's nil if width is determined by view's intrinsic size
  59. private(set) public var popupHeight: CGFloat?
  60. /// Popup position, default is center
  61. private(set) public var position: PopupPosition = .center(nil)
  62. /// Background alpha, default is 0.3
  63. public var backgroundAlpha: CGFloat = 0.3
  64. /// Background color, default is black
  65. public var backgroundColor = UIColor.black
  66. /// Allow tap outside popup to dismiss, default is true
  67. public var canTapOutsideToDismiss = true
  68. /// Corner radius, default is 10 (0 no rounded corner)
  69. public var cornerRadius: CGFloat = 10
  70. /// Shadow enabled, default is true
  71. public var shadowEnabled = true
  72. /// Move the popup position H when show/hide keyboard
  73. public var keyboardPosizionEnabled = true
  74. /// The pop up view controller. It's not mandatory.
  75. private(set) public var contentController: UIViewController?
  76. /// The pop up view
  77. private(set) public var contentView: UIView?
  78. /// The delegate to receive pop up event
  79. public weak var delegate: NCPopupViewControllerDelegate?
  80. private var containerView = UIView()
  81. // MARK: -
  82. /// NOTE: Don't use this init method
  83. required public init?(coder aDecoder: NSCoder) {
  84. super.init(coder: aDecoder)
  85. }
  86. /**
  87. Init with content view controller. Your pop up content is a view controller (easiest way to design it is using storyboard)
  88. - Parameters:
  89. - contentController: Popup content view controller
  90. - position: Position of popup content, default is center
  91. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  92. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  93. */
  94. public init(contentController: UIViewController, position: PopupPosition = .center(nil), popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  95. super.init(nibName: nil, bundle: nil)
  96. self.contentController = contentController
  97. self.contentView = contentController.view
  98. self.popupWidth = popupWidth
  99. self.popupHeight = popupHeight
  100. self.position = position
  101. commonInit()
  102. }
  103. /**
  104. Init with content view
  105. - Parameters:
  106. - contentView: Popup content view
  107. - position: Position of popup content, default is center
  108. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  109. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  110. */
  111. public init(contentView: UIView, position: PopupPosition = .center(nil), popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  112. super.init(nibName: nil, bundle: nil)
  113. self.contentView = contentView
  114. self.popupWidth = popupWidth
  115. self.popupHeight = popupHeight
  116. self.position = position
  117. commonInit()
  118. }
  119. private func commonInit() {
  120. modalPresentationStyle = .overFullScreen
  121. modalTransitionStyle = .crossDissolve
  122. }
  123. override public func viewDidLoad() {
  124. super.viewDidLoad()
  125. setupUI()
  126. setupViews()
  127. addDismissGesture()
  128. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  129. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil);
  130. }
  131. // MARK: -
  132. @objc internal func keyboardWillShow(_ notification : Notification?) {
  133. var keyboardSize = CGSize.zero
  134. if let info = notification?.userInfo {
  135. let frameEndUserInfoKey = UIResponder.keyboardFrameEndUserInfoKey
  136. // Getting UIKeyboardSize.
  137. if let keyboardFrame = info[frameEndUserInfoKey] as? CGRect {
  138. let screenSize = UIScreen.main.bounds
  139. //Calculating actual keyboard displayed size, keyboard frame may be different when hardware keyboard is attached (Bug ID: #469) (Bug ID: #381)
  140. let intersectRect = keyboardFrame.intersection(screenSize)
  141. if intersectRect.isNull {
  142. keyboardSize = CGSize(width: screenSize.size.width, height: 0)
  143. } else {
  144. keyboardSize = intersectRect.size
  145. }
  146. if keyboardPosizionEnabled {
  147. let popupDiffScreen = screenSize.height - ((screenSize.height - (popupHeight ?? 0)) / 2)
  148. let keyboardDiffScreen = screenSize.height - keyboardSize.height
  149. let diff = popupDiffScreen - keyboardDiffScreen
  150. if centerYConstraint != nil {
  151. centerYConstraint?.constant = -(diff + 10)
  152. }
  153. }
  154. }
  155. }
  156. }
  157. @objc func keyboardWillHide(_ notification: Notification) {
  158. if keyboardPosizionEnabled {
  159. if centerYConstraint != nil {
  160. centerYConstraint?.constant = 0
  161. }
  162. }
  163. }
  164. // MARK: - Setup
  165. private func addDismissGesture() {
  166. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissTapGesture(gesture:)))
  167. tapGesture.delegate = self
  168. view.addGestureRecognizer(tapGesture)
  169. }
  170. private func setupUI() {
  171. containerView.translatesAutoresizingMaskIntoConstraints = false
  172. contentView?.translatesAutoresizingMaskIntoConstraints = false
  173. view.backgroundColor = backgroundColor.withAlphaComponent(backgroundAlpha)
  174. if cornerRadius > 0 {
  175. contentView?.layer.cornerRadius = cornerRadius
  176. contentView?.layer.masksToBounds = true
  177. }
  178. if shadowEnabled {
  179. containerView.layer.shadowOpacity = 0.5
  180. containerView.layer.shadowColor = UIColor.black.cgColor
  181. containerView.layer.shadowOffset = CGSize(width: 5, height: 5)
  182. containerView.layer.shadowRadius = 5
  183. }
  184. }
  185. private func setupViews() {
  186. if let contentController = contentController {
  187. addChild(contentController)
  188. }
  189. addViews()
  190. addSizeConstraints()
  191. addPositionConstraints()
  192. }
  193. private func addViews() {
  194. view.addSubview(containerView)
  195. if let contentView = contentView {
  196. containerView.addSubview(contentView)
  197. let topConstraint = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0)
  198. let leftConstraint = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: containerView, attribute: .left, multiplier: 1, constant: 0)
  199. let bottomConstraint = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0)
  200. let rightConstraint = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: 0)
  201. NSLayoutConstraint.activate([topConstraint, leftConstraint, bottomConstraint, rightConstraint])
  202. }
  203. }
  204. // MARK: - Add constraints
  205. private func addSizeConstraints() {
  206. if let popupWidth = popupWidth {
  207. let widthConstraint = NSLayoutConstraint(item: containerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupWidth)
  208. NSLayoutConstraint.activate([widthConstraint])
  209. }
  210. if let popupHeight = popupHeight {
  211. let heightConstraint = NSLayoutConstraint(item: containerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupHeight)
  212. NSLayoutConstraint.activate([heightConstraint])
  213. }
  214. }
  215. private func addPositionConstraints() {
  216. switch position {
  217. case .center(let offset):
  218. addCenterPositionConstraints(offset: offset)
  219. case .topLeft(let offset):
  220. addTopLeftPositionConstraints(offset: offset)
  221. case .topRight(let offset):
  222. addTopRightPositionConstraints(offset: offset)
  223. case .bottomLeft(let offset):
  224. addBottomLeftPositionConstraints(offset: offset)
  225. case .bottomRight(let offset):
  226. addBottomRightPositionConstraints(offset: offset)
  227. case .top(let offset):
  228. addTopPositionConstraints(offset: offset)
  229. case .left(let offset):
  230. addLeftPositionConstraints(offset: offset)
  231. case .bottom(let offset):
  232. addBottomPositionConstraints(offset: offset)
  233. case .right(let offset):
  234. addRightPositionConstraints(offset: offset)
  235. }
  236. }
  237. private func addCenterPositionConstraints(offset: CGPoint?) {
  238. let centerXConstraint = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: offset?.x ?? 0)
  239. centerYConstraint = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: offset?.y ?? 0)
  240. NSLayoutConstraint.activate([centerXConstraint, centerYConstraint!])
  241. }
  242. private func addTopLeftPositionConstraints(offset: CGPoint?) {
  243. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset?.y ?? 0)
  244. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset?.x ?? 0)
  245. NSLayoutConstraint.activate([topConstraint, leftConstraint])
  246. }
  247. private func addTopRightPositionConstraints(offset: CGPoint?) {
  248. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset?.y ?? 0)
  249. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset?.x ?? 0)
  250. NSLayoutConstraint.activate([topConstraint, rightConstraint])
  251. }
  252. private func addBottomLeftPositionConstraints(offset: CGPoint?) {
  253. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset?.y ?? 0)
  254. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset?.x ?? 0)
  255. NSLayoutConstraint.activate([bottomConstraint, leftConstraint])
  256. }
  257. private func addBottomRightPositionConstraints(offset: CGPoint?) {
  258. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset?.y ?? 0)
  259. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset?.x ?? 0)
  260. NSLayoutConstraint.activate([bottomConstraint, rightConstraint])
  261. }
  262. private func addTopPositionConstraints(offset: CGFloat) {
  263. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset)
  264. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerX, relatedBy: .equal, toItem: containerView, attribute: .centerX, multiplier: 1, constant: 0)
  265. NSLayoutConstraint.activate([topConstraint, centerXConstraint])
  266. }
  267. private func addLeftPositionConstraints(offset: CGFloat) {
  268. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset)
  269. let centerYConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerY, relatedBy: .equal, toItem: containerView, attribute: .centerY, multiplier: 1, constant: 0)
  270. NSLayoutConstraint.activate([leftConstraint, centerYConstraint])
  271. }
  272. private func addBottomPositionConstraints(offset: CGFloat) {
  273. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset)
  274. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerX, relatedBy: .equal, toItem: containerView, attribute: .centerX, multiplier: 1, constant: 0)
  275. NSLayoutConstraint.activate([bottomConstraint, centerXConstraint])
  276. }
  277. private func addRightPositionConstraints(offset: CGFloat) {
  278. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset)
  279. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerY, relatedBy: .equal, toItem: containerView, attribute: .centerY, multiplier: 1, constant: 0)
  280. NSLayoutConstraint.activate([rightConstraint, centerXConstraint])
  281. }
  282. // MARK: - Actions
  283. @objc func dismissTapGesture(gesture: UIGestureRecognizer) {
  284. dismiss(animated: true) {
  285. self.delegate?.popupViewControllerDidDismissByTapGesture(self)
  286. }
  287. }
  288. }
  289. // MARK: - UIGestureRecognizerDelegate
  290. extension NCPopupViewController: UIGestureRecognizerDelegate {
  291. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
  292. guard let touchView = touch.view, canTapOutsideToDismiss else {
  293. return false
  294. }
  295. return !touchView.isDescendant(of: containerView)
  296. }
  297. }