Skip to content

Instantly share code, notes, and snippets.

@elmodos
Last active November 12, 2019 15:07
Show Gist options
  • Save elmodos/6062b1496e5f27d485456799fc784f27 to your computer and use it in GitHub Desktop.
Save elmodos/6062b1496e5f27d485456799fc784f27 to your computer and use it in GitHub Desktop.
iOS UIViewController modal Interactive dismisser
class EmailContentsViewController: UIViewController {
private var interactiveDismisser: InteractiveContentDismisser?
init(...) {
super.init(nibName: "EmailContentsViewController", bundle: nil)
self.modalPresentationStyle = .custom
self.transitioningDelegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.transitioningDelegate = self.interactiveDismisser
}
override func viewDidLoad() {
super.viewDidLoad()
let dismisser = self.createInteractiveDismisser()
self.view.addGestureRecognizer(dismisser.gestureRecognizer)
self.interactiveDismisser = dismisser
}
private func createInteractiveDismisser() -> InteractiveContentDismisser {
let dismisser = InteractiveContentDismisser(direction: .down)
dismisser.getDismissableDimension = { [weak self] in
return self?.view.frame.height ?? 0
}
dismisser.handlerDismissAnimated = { [weak self] in
self?.coordinator?.emailContentsSceneClose(self!)
}
dismisser.handlerDismissFinish = nil
dismisser.handlerDismissCancel = { [weak self] in
self?.layoutContentOnscreen()
}
return dismisser
}
}
extension EmailContentsViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DynamicContentTransitionPresenter()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DynamicContentTransitionDismisser()
}
}
extension EmailContentsViewController: DynamicContentTransitinable {
func layoutContentOnscreen() {
self.viewDimmBackground?.alpha = 1
make lyaout when everything is at finalized onscreen state
}
func layoutContentOffscreen() {
self.viewDimmBackground?.alpha = 0
self.attachView(toBeVisible: false)
make lyaout when everything is at finalized offscreen state
}
}
import UIKit
public protocol DynamicContentTransitinable {
func layoutContentOnscreen()
func layoutContentOffscreen()
}
public class DynamicContentTransitioner: NSObject, UIViewControllerAnimatedTransitioning {
var animationDuration: TimeInterval
private(set) var animationOptions: UIView.AnimationOptions
public init(animationDuration: TimeInterval = 0.4, animationOptions: UIView.AnimationOptions) {
self.animationDuration = animationDuration
self.animationOptions = animationOptions
super.init()
}
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return self.animationDuration
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
transitionContext.completeTransition(false)
assert(false, "Override required")
}
}
public class DynamicContentTransitionPresenter: DynamicContentTransitioner {
public init(animationDuration: TimeInterval = 0.4) {
super.init(animationDuration: animationDuration, animationOptions: .curveLinear)
}
public override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let toViewController = transitionContext.viewController(forKey: .to)
else {
transitionContext.completeTransition(false)
return
}
if let toView = transitionContext.view(forKey: .to) {
toView.frame = transitionContext.finalFrame(for: toViewController)
transitionContext.containerView.addSubview(toView)
}
let fromViewController = transitionContext.viewController(forKey: .from) as? DynamicContentTransitinable
let initializeState = {
if transitionContext.presentationStyle == .fullScreen {
fromViewController?.layoutContentOnscreen()
}
(toViewController as? DynamicContentTransitinable)?.layoutContentOffscreen()
}
let finalizeState = {
if transitionContext.presentationStyle == .fullScreen {
fromViewController?.layoutContentOffscreen()
}
(toViewController as? DynamicContentTransitinable)?.layoutContentOnscreen()
}
guard transitionContext.isAnimated else {
finalizeState()
transitionContext.completeTransition(true)
return
}
initializeState()
UIView.animate(
withDuration: self.animationDuration,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: [self.animationOptions],
animations: {
finalizeState()
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
public class DynamicContentTransitionDismisser: DynamicContentTransitioner {
public init(animationDuration: TimeInterval = 0.4) {
super.init(animationDuration: animationDuration, animationOptions: .curveLinear)
}
public override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from)
else {
transitionContext.completeTransition(false)
return
}
if let toView = transitionContext.view(forKey: .to) {
toView.frame = transitionContext.finalFrame(for: toViewController)
transitionContext.containerView.addSubview(toView)
}
if let fromView = transitionContext.view(forKey: .from) {
fromView.frame = transitionContext.initialFrame(for: fromViewController)
transitionContext.containerView.addSubview(fromView)
}
let initializeState = {
(fromViewController as? DynamicContentTransitinable)?.layoutContentOnscreen()
if transitionContext.presentationStyle == .fullScreen {
(toViewController as? DynamicContentTransitinable)?.layoutContentOffscreen()
}
}
let finalizeState = {
(fromViewController as? DynamicContentTransitinable)?.layoutContentOffscreen()
if transitionContext.presentationStyle == .fullScreen {
(toViewController as? DynamicContentTransitinable)?.layoutContentOnscreen()
}
}
guard transitionContext.isAnimated else {
finalizeState()
transitionContext.completeTransition(true)
return
}
initializeState()
UIView.animate(
withDuration: self.animationDuration,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: [self.animationOptions],
animations: {
finalizeState()
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
import Foundation
public class InteractiveContentDismisser: NSObject {
public enum Direction {
// swiftlint:disable identifier_name
case up
// swiftlint:enable identifier_name
case down
case left
case right
func directionalMultiplier() -> CGFloat {
return [.up, .left].contains(self)
? -1
: 1
}
func isHorizontal() -> Bool {
return [.left, .right].contains(self)
}
}
public let direction: Direction
public var getDismissableDimension: (() -> CGFloat)?
public var handlerDismissAnimated: (() -> Void)?
public var handlerDismissFinish: (() -> Void)?
public var handlerDismissCancel: (() -> Void)?
private var interactionController: UIPercentDrivenInteractiveTransition?
public private(set) lazy var gestureRecognizer: UIGestureRecognizer = {
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
gestureRecognizer.maximumNumberOfTouches = 1
return gestureRecognizer
}()
public init(direction: Direction) {
self.direction = direction
super.init()
}
}
extension InteractiveContentDismisser {
@objc private func onPan(_ recognizer: UIPanGestureRecognizer) {
let translationPoint = recognizer.translation(in: recognizer.view)
let velocityPoint = recognizer.velocity(in: recognizer.view)
let isHor = self.direction.isHorizontal()
let multiplier = self.direction.directionalMultiplier()
let translation = multiplier * (isHor ? translationPoint.x : translationPoint.y)
let velocity = multiplier * (isHor ? velocityPoint.x : velocityPoint.y)
if self.getDismissableDimension == nil {
Log.error("Not set: getDismissableDimension")
}
var percent = translation / max(1, (self.getDismissableDimension?() ?? 1))
if percent < 0 { percent = 0 }
if percent > 1 { percent = 1 }
Log.debug("Percent: \(percent), velocity \(velocity)")
switch recognizer.state {
case .began:
Log.debug("began")
self.interactionController = UIPercentDrivenInteractiveTransition()
self.handlerDismissAnimated?()
if self.handlerDismissAnimated == nil {
Log.error("Not set: handlerDismissAnimated")
}
case .changed:
Log.debug("changed")
self.interactionController?.update(percent)
case .ended:
Log.debug("ended")
if percent > 0.5 || velocity > 0 {
Log.debug("finish")
self.interactionController?.finish()
self.handlerDismissFinish?()
} else {
Log.debug("cancel")
self.interactionController?.cancel()
self.handlerDismissCancel?()
}
self.interactionController = nil
default: ()
}
}
}
extension InteractiveContentDismisser: UIViewControllerTransitioningDelegate {
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DynamicContentTransitionDismisser()
}
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactionController
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment