PresentrController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. //
  2. // PresentrPresentationController.swift
  3. // OneUP
  4. //
  5. // Created by Daniel Lozano on 4/27/16.
  6. // Copyright © 2016 Icalia Labs. All rights reserved.
  7. //
  8. import UIKit
  9. /// Presentr's custom presentation controller. Handles the position and sizing for the view controller's.
  10. class PresentrController: UIPresentationController, UIAdaptivePresentationControllerDelegate {
  11. /// Presentation type must be passed in to make all the sizing and position decisions.
  12. let presentationType: PresentationType
  13. /// Should the presented controller dismiss on background tap.
  14. let dismissOnTap: Bool
  15. /// Should the presented controller dismiss on background Swipe.
  16. let dismissOnSwipe: Bool
  17. /// DismissSwipe direction
  18. let dismissOnSwipeDirection: DismissSwipeDirection
  19. /// Should the presented controller use animation when dismiss on background tap.
  20. let dismissAnimated: Bool
  21. /// How the presented view controller should respond in response to keyboard presentation.
  22. let keyboardTranslationType: KeyboardTranslationType
  23. /// The frame used for a current context presentation. If nil, normal presentation.
  24. let contextFrameForPresentation: CGRect?
  25. /// If contextFrameForPresentation is set, this handles what happens when tap outside context frame.
  26. let shouldIgnoreTapOutsideContext: Bool
  27. /// A custom background view to be added on top of the regular background view.
  28. let customBackgroundView: UIView?
  29. fileprivate var conformingPresentedController: PresentrDelegate? {
  30. return presentedViewController as? PresentrDelegate
  31. }
  32. fileprivate var shouldObserveKeyboard: Bool {
  33. return conformingPresentedController != nil ||
  34. (keyboardTranslationType != .none && presentationType == .popup) // TODO: Work w/other types?
  35. }
  36. fileprivate var containerFrame: CGRect {
  37. return contextFrameForPresentation ?? containerView?.bounds ?? CGRect()
  38. }
  39. fileprivate var keyboardIsShowing: Bool = false
  40. // MARK: Background Views
  41. fileprivate var chromeView = UIView()
  42. fileprivate var backgroundView = PassthroughBackgroundView()
  43. fileprivate var visualEffect: UIVisualEffect?
  44. // MARK: Swipe gesture
  45. fileprivate var presentedViewIsBeingDissmissed: Bool = false
  46. fileprivate var presentedViewFrame: CGRect = .zero
  47. fileprivate var presentedViewCenter: CGPoint = .zero
  48. fileprivate var latestShouldDismiss: Bool = true
  49. fileprivate lazy var shouldSwipeBottom: Bool = {
  50. return self.dismissOnSwipeDirection == .default ? self.presentationType != .topHalf : self.dismissOnSwipeDirection == .bottom
  51. }()
  52. fileprivate lazy var shouldSwipeTop: Bool = {
  53. return self.dismissOnSwipeDirection == .default ? self.presentationType == .topHalf : self.dismissOnSwipeDirection == .top
  54. }()
  55. // MARK: - Init
  56. init(presentedViewController: UIViewController,
  57. presentingViewController: UIViewController?,
  58. presentationType: PresentationType,
  59. roundCorners: Bool?,
  60. cornerRadius: CGFloat,
  61. dropShadow: PresentrShadow?,
  62. dismissOnTap: Bool,
  63. dismissOnSwipe: Bool,
  64. dismissOnSwipeDirection: DismissSwipeDirection,
  65. backgroundColor: UIColor,
  66. backgroundOpacity: Float,
  67. blurBackground: Bool,
  68. blurStyle: UIBlurEffect.Style,
  69. customBackgroundView: UIView?,
  70. keyboardTranslationType: KeyboardTranslationType,
  71. dismissAnimated: Bool,
  72. contextFrameForPresentation: CGRect?,
  73. shouldIgnoreTapOutsideContext: Bool) {
  74. self.presentationType = presentationType
  75. self.dismissOnTap = dismissOnTap
  76. self.dismissOnSwipe = dismissOnSwipe
  77. self.dismissOnSwipeDirection = dismissOnSwipeDirection
  78. self.keyboardTranslationType = keyboardTranslationType
  79. self.dismissAnimated = dismissAnimated
  80. self.contextFrameForPresentation = contextFrameForPresentation
  81. self.shouldIgnoreTapOutsideContext = shouldIgnoreTapOutsideContext
  82. self.customBackgroundView = customBackgroundView
  83. super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
  84. setupBackground(backgroundColor, backgroundOpacity: backgroundOpacity, blurBackground: blurBackground, blurStyle: blurStyle)
  85. setupCornerRadius(roundCorners: roundCorners, cornerRadius: cornerRadius)
  86. addDropShadow(shadow: dropShadow)
  87. if dismissOnSwipe {
  88. setupDismissOnSwipe()
  89. }
  90. if shouldObserveKeyboard {
  91. registerKeyboardObserver()
  92. }
  93. }
  94. // MARK: - Setup
  95. private func setupDismissOnSwipe() {
  96. let swipe = UIPanGestureRecognizer(target: self, action: #selector(presentedViewSwipe))
  97. presentedViewController.view.addGestureRecognizer(swipe)
  98. }
  99. private func setupBackground(_ backgroundColor: UIColor, backgroundOpacity: Float, blurBackground: Bool, blurStyle: UIBlurEffect.Style) {
  100. let tap = UITapGestureRecognizer(target: self, action: #selector(chromeViewTapped))
  101. chromeView.addGestureRecognizer(tap)
  102. if !shouldIgnoreTapOutsideContext {
  103. let tap = UITapGestureRecognizer(target: self, action: #selector(chromeViewTapped))
  104. backgroundView.addGestureRecognizer(tap)
  105. }
  106. if blurBackground {
  107. visualEffect = UIBlurEffect(style: blurStyle)
  108. } else {
  109. chromeView.backgroundColor = backgroundColor.withAlphaComponent(CGFloat(backgroundOpacity))
  110. }
  111. }
  112. private func setupCornerRadius(roundCorners: Bool?, cornerRadius: CGFloat) {
  113. let shouldRoundCorners = roundCorners ?? presentationType.shouldRoundCorners
  114. if shouldRoundCorners {
  115. presentedViewController.view.layer.cornerRadius = cornerRadius
  116. presentedViewController.view.layer.masksToBounds = true
  117. } else {
  118. presentedViewController.view.layer.cornerRadius = 0
  119. }
  120. }
  121. private func addDropShadow(shadow: PresentrShadow?) {
  122. guard let shadow = shadow else {
  123. presentedViewController.view.layer.masksToBounds = true
  124. presentedViewController.view.layer.shadowOpacity = 0
  125. return
  126. }
  127. presentedViewController.view.layer.masksToBounds = false
  128. if let shadowColor = shadow.shadowColor?.cgColor {
  129. presentedViewController.view.layer.shadowColor = shadowColor
  130. }
  131. if let shadowOpacity = shadow.shadowOpacity {
  132. presentedViewController.view.layer.shadowOpacity = shadowOpacity
  133. }
  134. if let shadowOffset = shadow.shadowOffset {
  135. presentedViewController.view.layer.shadowOffset = shadowOffset
  136. }
  137. if let shadowRadius = shadow.shadowRadius {
  138. presentedViewController.view.layer.shadowRadius = shadowRadius
  139. }
  140. }
  141. fileprivate func registerKeyboardObserver() {
  142. NotificationCenter.default.addObserver(self, selector: #selector(PresentrController.keyboardWasShown(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  143. NotificationCenter.default.addObserver(self, selector: #selector(PresentrController.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
  144. }
  145. fileprivate func removeObservers() {
  146. NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
  147. NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
  148. }
  149. }
  150. // MARK: - UIPresentationController
  151. extension PresentrController {
  152. // MARK: Presentation
  153. override var frameOfPresentedViewInContainerView: CGRect {
  154. var presentedViewFrame = CGRect.zero
  155. let containerBounds = containerFrame
  156. let size = self.size(forChildContentContainer: presentedViewController, withParentContainerSize: containerBounds.size)
  157. let origin: CGPoint
  158. // If the Presentation Type's calculate center point returns nil
  159. // this means that the user provided the origin, not a center point.
  160. if let center = getCenterPointFromType() {
  161. origin = calculateOrigin(center, size: size)
  162. } else {
  163. origin = getOriginFromType() ?? CGPoint(x: 0, y: 0)
  164. }
  165. presentedViewFrame.size = size
  166. presentedViewFrame.origin = origin
  167. return presentedViewFrame
  168. }
  169. override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
  170. let width = getWidthFromType(parentSize)
  171. let height = getHeightFromType(parentSize)
  172. return CGSize(width: CGFloat(width), height: CGFloat(height))
  173. }
  174. override func containerViewWillLayoutSubviews() {
  175. guard !keyboardIsShowing else {
  176. return // prevent resetting of presented frame when the frame is being translated
  177. }
  178. chromeView.frame = containerFrame
  179. presentedView!.frame = frameOfPresentedViewInContainerView
  180. }
  181. // MARK: Animation
  182. override func presentationTransitionWillBegin() {
  183. guard let containerView = containerView else {
  184. return
  185. }
  186. setupBackgroundView()
  187. backgroundView.frame = containerView.bounds
  188. chromeView.frame = containerFrame
  189. containerView.insertSubview(backgroundView, at: 0)
  190. containerView.insertSubview(chromeView, at: 1)
  191. if let customBackgroundView = customBackgroundView {
  192. chromeView.addSubview(customBackgroundView)
  193. }
  194. var blurEffectView: UIVisualEffectView?
  195. if visualEffect != nil {
  196. let view = UIVisualEffectView()
  197. view.frame = chromeView.bounds
  198. view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  199. chromeView.insertSubview(view, at: 0)
  200. blurEffectView = view
  201. } else {
  202. chromeView.alpha = 0.0
  203. }
  204. guard let coordinator = presentedViewController.transitionCoordinator else {
  205. chromeView.alpha = 1.0
  206. return
  207. }
  208. coordinator.animate(alongsideTransition: { context in
  209. blurEffectView?.effect = self.visualEffect
  210. self.chromeView.alpha = 1.0
  211. }, completion: nil)
  212. }
  213. override func dismissalTransitionWillBegin() {
  214. guard let coordinator = presentedViewController.transitionCoordinator else {
  215. chromeView.alpha = 0.0
  216. return
  217. }
  218. coordinator.animate(alongsideTransition: { context in
  219. self.chromeView.alpha = 0.0
  220. }, completion: nil)
  221. }
  222. // MARK: - Animation Helper's
  223. func setupBackgroundView() {
  224. if shouldIgnoreTapOutsideContext {
  225. backgroundView.shouldPassthrough = true
  226. backgroundView.passthroughViews = presentingViewController.view.subviews
  227. } else {
  228. backgroundView.shouldPassthrough = false
  229. backgroundView.passthroughViews = []
  230. }
  231. }
  232. }
  233. // MARK: - Sizing, Position
  234. fileprivate extension PresentrController {
  235. func getWidthFromType(_ parentSize: CGSize) -> Float {
  236. guard let size = presentationType.size() else {
  237. if case .dynamic = presentationType {
  238. return Float(presentedViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width)
  239. }
  240. return 0
  241. }
  242. return size.width.calculateWidth(parentSize)
  243. }
  244. func getHeightFromType(_ parentSize: CGSize) -> Float {
  245. guard let size = presentationType.size() else {
  246. if case .dynamic = presentationType {
  247. return Float(presentedViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height)
  248. }
  249. return 0
  250. }
  251. return size.height.calculateHeight(parentSize)
  252. }
  253. func getCenterPointFromType() -> CGPoint? {
  254. let containerBounds = containerFrame
  255. let position = presentationType.position()
  256. return position.calculateCenterPoint(containerBounds)
  257. }
  258. func getOriginFromType() -> CGPoint? {
  259. let position = presentationType.position()
  260. return position.calculateOrigin()
  261. }
  262. func calculateOrigin(_ center: CGPoint, size: CGSize) -> CGPoint {
  263. let x: CGFloat = center.x - size.width / 2
  264. let y: CGFloat = center.y - size.height / 2
  265. return CGPoint(x: x, y: y)
  266. }
  267. }
  268. // MARK: - Gesture Handling
  269. extension PresentrController {
  270. @objc func chromeViewTapped(gesture: UIGestureRecognizer) {
  271. guard dismissOnTap else {
  272. return
  273. }
  274. guard conformingPresentedController?.presentrShouldDismiss?(keyboardShowing: keyboardIsShowing) ?? true else {
  275. return
  276. }
  277. if gesture.state == .ended {
  278. if shouldObserveKeyboard {
  279. removeObservers()
  280. }
  281. presentingViewController.dismiss(animated: dismissAnimated, completion: nil)
  282. }
  283. }
  284. @objc func presentedViewSwipe(gesture: UIPanGestureRecognizer) {
  285. guard dismissOnSwipe else {
  286. return
  287. }
  288. if gesture.state == .began {
  289. presentedViewFrame = presentedViewController.view.frame
  290. presentedViewCenter = presentedViewController.view.center
  291. let directionDown = gesture.translation(in: presentedViewController.view).y > 0
  292. if (shouldSwipeBottom && directionDown) || (shouldSwipeTop && !directionDown) {
  293. latestShouldDismiss = conformingPresentedController?.presentrShouldDismiss?(keyboardShowing: keyboardIsShowing) ?? true
  294. }
  295. } else if gesture.state == .changed {
  296. swipeGestureChanged(gesture: gesture)
  297. } else if gesture.state == .ended || gesture.state == .cancelled {
  298. swipeGestureEnded()
  299. }
  300. }
  301. // MARK: Helper's
  302. func swipeGestureChanged(gesture: UIPanGestureRecognizer) {
  303. let amount = gesture.translation(in: presentedViewController.view)
  304. if shouldSwipeTop && amount.y > 0 {
  305. return
  306. } else if shouldSwipeBottom && amount.y < 0 {
  307. return
  308. }
  309. var swipeLimit: CGFloat = 100
  310. if shouldSwipeTop {
  311. swipeLimit = -swipeLimit
  312. }
  313. presentedViewController.view.center = CGPoint(x: presentedViewCenter.x, y: presentedViewCenter.y + amount.y)
  314. let dismiss = shouldSwipeTop ? (amount.y < swipeLimit) : ( amount.y > swipeLimit)
  315. if dismiss && latestShouldDismiss {
  316. presentedViewIsBeingDissmissed = true
  317. presentedViewController.dismiss(animated: dismissAnimated, completion: nil)
  318. }
  319. }
  320. func swipeGestureEnded() {
  321. guard !presentedViewIsBeingDissmissed else {
  322. return
  323. }
  324. UIView.animate(withDuration: 0.5,
  325. delay: 0,
  326. usingSpringWithDamping: 0.5,
  327. initialSpringVelocity: 1,
  328. options: [],
  329. animations: {
  330. self.presentedViewController.view.frame = self.presentedViewFrame
  331. }, completion: nil)
  332. }
  333. }
  334. // MARK: - Keyboard Handling
  335. extension PresentrController {
  336. @objc func keyboardWasShown(notification: Notification) {
  337. if let keyboardFrame = notification.keyboardEndFrame() {
  338. let presentedFrame = frameOfPresentedViewInContainerView
  339. let translatedFrame = keyboardTranslationType.getTranslationFrame(keyboardFrame: keyboardFrame, presentedFrame: presentedFrame)
  340. if translatedFrame != presentedFrame {
  341. UIView.animate(withDuration: notification.keyboardAnimationDuration() ?? 0.5, animations: {
  342. self.presentedView?.frame = translatedFrame
  343. })
  344. }
  345. keyboardIsShowing = true
  346. }
  347. }
  348. @objc func keyboardWillHide (notification: Notification) {
  349. if keyboardIsShowing {
  350. let presentedFrame = frameOfPresentedViewInContainerView
  351. if self.presentedView?.frame != presentedFrame {
  352. UIView.animate(withDuration: notification.keyboardAnimationDuration() ?? 0.5, animations: {
  353. self.presentedView?.frame = presentedFrame
  354. })
  355. }
  356. keyboardIsShowing = false
  357. }
  358. }
  359. }