PopupViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. //
  2. // PopupViewController.swift
  3. // EzPopup
  4. //
  5. // Created by Huy Nguyen on 6/4/18.
  6. //
  7. import UIKit
  8. public protocol PopupViewControllerDelegate: class {
  9. /// It is called when pop up is dismissed by tap outside
  10. func popupViewControllerDidDismissByTapGesture(_ sender: PopupViewController)
  11. }
  12. // optional func
  13. public extension PopupViewControllerDelegate {
  14. func popupViewControllerDidDismissByTapGesture(_ sender: PopupViewController) {}
  15. }
  16. public class PopupViewController: UIViewController {
  17. public enum PopupPosition {
  18. /// Align center X, center Y with offset param
  19. case center(CGPoint?)
  20. /// Top left anchor point with offset param
  21. case topLeft(CGPoint?)
  22. /// Top right anchor point with offset param
  23. case topRight(CGPoint?)
  24. /// Bottom left anchor point with offset param
  25. case bottomLeft(CGPoint?)
  26. /// Bottom right anchor point with offset param
  27. case bottomRight(CGPoint?)
  28. /// Top anchor, align center X with top padding param
  29. case top(CGFloat)
  30. /// Left anchor, align center Y with left padding param
  31. case left(CGFloat)
  32. /// Bottom anchor, align center X with bottom padding param
  33. case bottom(CGFloat)
  34. /// Right anchor, align center Y with right padding param
  35. case right(CGFloat)
  36. }
  37. /// Popup width, it's nil if width is determined by view's intrinsic size
  38. private(set) public var popupWidth: CGFloat?
  39. /// Popup height, it's nil if width is determined by view's intrinsic size
  40. private(set) public var popupHeight: CGFloat?
  41. /// Popup position, default is center
  42. private(set) public var position: PopupPosition = .center(nil)
  43. /// Background alpha, default is 0.5
  44. public var backgroundAlpha: CGFloat = 0.5
  45. /// Background color, default is black
  46. public var backgroundColor = UIColor.black
  47. /// Allow tap outside popup to dismiss, default is true
  48. public var canTapOutsideToDismiss = true
  49. /// Corner radius, default is 0 (no rounded corner)
  50. public var cornerRadius: CGFloat = 0
  51. /// Shadow enabled, default is true
  52. public var shadowEnabled = true
  53. /// The pop up view controller. It's not mandatory.
  54. private(set) public var contentController: UIViewController?
  55. /// The pop up view
  56. private(set) public var contentView: UIView?
  57. /// The delegate to receive pop up event
  58. public weak var delegate: PopupViewControllerDelegate?
  59. private var containerView = UIView()
  60. // MARK: -
  61. /// NOTE: Don't use this init method
  62. required public init?(coder aDecoder: NSCoder) {
  63. super.init(coder: aDecoder)
  64. }
  65. /**
  66. Init with content view controller. Your pop up content is a view controller (easiest way to design it is using storyboard)
  67. - Parameters:
  68. - contentController: Popup content view controller
  69. - position: Position of popup content, default is center
  70. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  71. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  72. */
  73. public init(contentController: UIViewController, position: PopupPosition = .center(nil), popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  74. super.init(nibName: nil, bundle: nil)
  75. self.contentController = contentController
  76. self.contentView = contentController.view
  77. self.popupWidth = popupWidth
  78. self.popupHeight = popupHeight
  79. self.position = position
  80. commonInit()
  81. }
  82. /**
  83. Init with content view
  84. - Parameters:
  85. - contentView: Popup content view
  86. - position: Position of popup content, default is center
  87. - popupWidth: Width of popup content. If it isn't set, width will be determine by popup content view intrinsic size.
  88. - popupHeight: Height of popup content. If it isn't set, height will be determine by popup content view intrinsic size.
  89. */
  90. public init(contentView: UIView, position: PopupPosition = .center(nil), popupWidth: CGFloat? = nil, popupHeight: CGFloat? = nil) {
  91. super.init(nibName: nil, bundle: nil)
  92. self.contentView = contentView
  93. self.popupWidth = popupWidth
  94. self.popupHeight = popupHeight
  95. self.position = position
  96. commonInit()
  97. }
  98. private func commonInit() {
  99. modalPresentationStyle = .overFullScreen
  100. modalTransitionStyle = .crossDissolve
  101. }
  102. override public func viewDidLoad() {
  103. super.viewDidLoad()
  104. setupUI()
  105. setupViews()
  106. addDismissGesture()
  107. }
  108. // MARK: - Setup
  109. private func addDismissGesture() {
  110. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissTapGesture(gesture:)))
  111. tapGesture.delegate = self
  112. view.addGestureRecognizer(tapGesture)
  113. }
  114. private func setupUI() {
  115. containerView.translatesAutoresizingMaskIntoConstraints = false
  116. contentView?.translatesAutoresizingMaskIntoConstraints = false
  117. view.backgroundColor = backgroundColor.withAlphaComponent(backgroundAlpha)
  118. if cornerRadius > 0 {
  119. contentView?.layer.cornerRadius = cornerRadius
  120. contentView?.layer.masksToBounds = true
  121. }
  122. if shadowEnabled {
  123. containerView.layer.shadowOpacity = 0.5
  124. containerView.layer.shadowColor = UIColor.black.cgColor
  125. containerView.layer.shadowOffset = CGSize(width: 5, height: 5)
  126. containerView.layer.shadowRadius = 5
  127. }
  128. }
  129. private func setupViews() {
  130. if let contentController = contentController {
  131. addChild(contentController)
  132. }
  133. addViews()
  134. addSizeConstraints()
  135. addPositionConstraints()
  136. }
  137. private func addViews() {
  138. view.addSubview(containerView)
  139. if let contentView = contentView {
  140. containerView.addSubview(contentView)
  141. let topConstraint = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0)
  142. let leftConstraint = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: containerView, attribute: .left, multiplier: 1, constant: 0)
  143. let bottomConstraint = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0)
  144. let rightConstraint = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: 0)
  145. NSLayoutConstraint.activate([topConstraint, leftConstraint, bottomConstraint, rightConstraint])
  146. }
  147. }
  148. // MARK: - Add constraints
  149. private func addSizeConstraints() {
  150. if let popupWidth = popupWidth {
  151. let widthConstraint = NSLayoutConstraint(item: containerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupWidth)
  152. NSLayoutConstraint.activate([widthConstraint])
  153. }
  154. if let popupHeight = popupHeight {
  155. let heightConstraint = NSLayoutConstraint(item: containerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: popupHeight)
  156. NSLayoutConstraint.activate([heightConstraint])
  157. }
  158. }
  159. private func addPositionConstraints() {
  160. switch position {
  161. case .center(let offset):
  162. addCenterPositionConstraints(offset: offset)
  163. case .topLeft(let offset):
  164. addTopLeftPositionConstraints(offset: offset)
  165. case .topRight(let offset):
  166. addTopRightPositionConstraints(offset: offset)
  167. case .bottomLeft(let offset):
  168. addBottomLeftPositionConstraints(offset: offset)
  169. case .bottomRight(let offset):
  170. addBottomRightPositionConstraints(offset: offset)
  171. case .top(let offset):
  172. addTopPositionConstraints(offset: offset)
  173. case .left(let offset):
  174. addLeftPositionConstraints(offset: offset)
  175. case .bottom(let offset):
  176. addBottomPositionConstraints(offset: offset)
  177. case .right(let offset):
  178. addRightPositionConstraints(offset: offset)
  179. }
  180. }
  181. private func addCenterPositionConstraints(offset: CGPoint?) {
  182. let centerXConstraint = NSLayoutConstraint(item: containerView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: offset?.x ?? 0)
  183. let centerYConstraint = NSLayoutConstraint(item: containerView, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: offset?.y ?? 0)
  184. NSLayoutConstraint.activate([centerXConstraint, centerYConstraint])
  185. }
  186. private func addTopLeftPositionConstraints(offset: CGPoint?) {
  187. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset?.y ?? 0)
  188. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset?.x ?? 0)
  189. NSLayoutConstraint.activate([topConstraint, leftConstraint])
  190. }
  191. private func addTopRightPositionConstraints(offset: CGPoint?) {
  192. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset?.y ?? 0)
  193. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset?.x ?? 0)
  194. NSLayoutConstraint.activate([topConstraint, rightConstraint])
  195. }
  196. private func addBottomLeftPositionConstraints(offset: CGPoint?) {
  197. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset?.y ?? 0)
  198. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset?.x ?? 0)
  199. NSLayoutConstraint.activate([bottomConstraint, leftConstraint])
  200. }
  201. private func addBottomRightPositionConstraints(offset: CGPoint?) {
  202. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset?.y ?? 0)
  203. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset?.x ?? 0)
  204. NSLayoutConstraint.activate([bottomConstraint, rightConstraint])
  205. }
  206. private func addTopPositionConstraints(offset: CGFloat) {
  207. let topConstraint = NSLayoutConstraint(item: containerView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: offset)
  208. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerX, relatedBy: .equal, toItem: containerView, attribute: .centerX, multiplier: 1, constant: 0)
  209. NSLayoutConstraint.activate([topConstraint, centerXConstraint])
  210. }
  211. private func addLeftPositionConstraints(offset: CGFloat) {
  212. let leftConstraint = NSLayoutConstraint(item: containerView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: offset)
  213. let centerYConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerY, relatedBy: .equal, toItem: containerView, attribute: .centerY, multiplier: 1, constant: 0)
  214. NSLayoutConstraint.activate([leftConstraint, centerYConstraint])
  215. }
  216. private func addBottomPositionConstraints(offset: CGFloat) {
  217. let bottomConstraint = NSLayoutConstraint(item: view as Any, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: offset)
  218. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerX, relatedBy: .equal, toItem: containerView, attribute: .centerX, multiplier: 1, constant: 0)
  219. NSLayoutConstraint.activate([bottomConstraint, centerXConstraint])
  220. }
  221. private func addRightPositionConstraints(offset: CGFloat) {
  222. let rightConstraint = NSLayoutConstraint(item: view as Any, attribute: .right, relatedBy: .equal, toItem: containerView, attribute: .right, multiplier: 1, constant: offset)
  223. let centerXConstraint = NSLayoutConstraint(item: view as Any, attribute: .centerY, relatedBy: .equal, toItem: containerView, attribute: .centerY, multiplier: 1, constant: 0)
  224. NSLayoutConstraint.activate([rightConstraint, centerXConstraint])
  225. }
  226. // MARK: - Actions
  227. @objc func dismissTapGesture(gesture: UIGestureRecognizer) {
  228. dismiss(animated: true) {
  229. self.delegate?.popupViewControllerDidDismissByTapGesture(self)
  230. }
  231. }
  232. }
  233. // MARK: - UIGestureRecognizerDelegate
  234. extension PopupViewController: UIGestureRecognizerDelegate {
  235. public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
  236. guard let touchView = touch.view, canTapOutsideToDismiss else {
  237. return false
  238. }
  239. return !touchView.isDescendant(of: containerView)
  240. }
  241. }