Interacting with a ViewController that has Multiple child ViewControllers - ios

I have a parent viewController and a child viewController. The childViewController is like a card and functions similar to Apple's stock or map app. I can expand or collapse it and interact with the buttons within it. The only problem is that I can't interact with any buttons within the parent viewController. Anyone know what's the problem.
class BaseViewController: UIViewController {
enum CardState {
case expanded
case collapsed
}
var cardViewController: CardViewController!
var visualEffectView: UIVisualEffectView!
lazy var deviceCardHeight: CGFloat = view.frame.height - (view.frame.height / 6)
lazy var cardHeight: CGFloat = deviceCardHeight
let cardHandleAreaHeight: CGFloat = 65
var cardVisible = false
var nextState: CardState {
return cardVisible ? .collapsed : .expanded
}
override func viewDidLoad() {
super.viewDidLoad()
setupCard()
}
#IBAction func expandCard(_ sender: Any) {
print("Button Pressed")
}
func setupCard() {
cardViewController = CardViewController(nibName: "CardViewController", bundle: nil)
self.addChild(cardViewController)
self.view.addSubview(cardViewController.view)
//Set up frame of cardView
cardViewController.view.frame = CGRect(x: 0, y: view.frame.height - cardHandleAreaHeight, width: view.frame.width, height: cardHeight)
cardViewController.view.clipsToBounds = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCardTap(recognizer:)))
cardViewController.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleCardTap(recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
animationTransitionIfNeeded(state: nextState, duration: 0.9)
default:
break
}
}
func animationTransitionIfNeeded(state: CardState, duration: TimeInterval) {
if runningAnimations.isEmpty {
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
switch state {
case .expanded:
self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardHeight
case .collapsed:
self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardHandleAreaHeight
}
}
frameAnimator.addCompletion { _ in
//if true set to false, if false set to true
self.cardVisible = !self.cardVisible
self.runningAnimations.removeAll()
}
frameAnimator.startAnimation()
runningAnimations.append(frameAnimator)
let cornerRadiusAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
switch state {
case .expanded:
self.cardViewController.view.layer.cornerRadius = 12
case .collapsed:
self.cardViewController.view.layer.cornerRadius = 0
}
}
cornerRadiusAnimator.startAnimation()
}
}

'Can't interact' should mean that you can't press. If that is the case the most likely cause is that the button is covered with something (it could be transparent so ensure when testing that there is no transparent backgrounds until you resolve this). The other possible reason would be that you have set some property of the button that would cause this behavior, but you would probably know that so it is almost certainly the former.

I solve it. This code should work perfectly. The reason why it didn't before was because I added in a visualEffectView. It was covering the parent.

Related

Is there a way to have interactive modal dismissal for a .fullscreen modally presented view controller?

I want to enable interactive modal dismissal that pans along with a users finger on a fullscreen modally presented view controller .fullscreen.
I've seen that it's fairly trivial to do so on the .pageSheet and the .formSheet which have it built in but have not seen a clear example for the full screen.
I'm guessing I'd need to have a pan gesture added to my vc within the body of it's code and then adjust for the states myself but wondering if anyone knows what exactly needs to be done / if there's a simpler way to do it as it seems much more complicated for the .fullscreen case
It can be done with creating your custom UIPresentationController and UIViewControllerTransitioningDelegate. Lets say we have TestViewController and we want to present SecondViewController with total presentedHeight of 1.0 (fullScreen). Presentation will be triggered with #IBAction func buttonPressed and can be dismissed by dragging controller down (as we are used to it). It would be also nice to add some backgroundEffect to be gradually changed while user is sliding down the SecondViewController (especially when used only presentedHeight of 0.6).
Firstly we define OverlayViewController which will be later superclass of presented SecondViewControllerand will contain UIPanGestureRecognizer.
class OverlayViewController: UIViewController {
var hasSetPointOrigin = false
var pointOrigin: CGPoint?
var delegate: OverlayViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction))
view.addGestureRecognizer(panGesture)
}
override func viewDidLayoutSubviews() {
if !hasSetPointOrigin {
hasSetPointOrigin = true
pointOrigin = self.view.frame.origin
}
}
#objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
// Not allowing the user to drag the view upward
guard translation.y >= 0 else { return }
let currentPosition = translation.y
let originPos = self.pointOrigin
delegate?.userDragged(draggedPercentage: translation.y/originPos!.y)
// setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down
view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
if sender.state == .ended {
let dragVelocity = sender.velocity(in: view)
if dragVelocity.y >= 1100 {
self.dismiss(animated: true, completion: nil)
} else {
// Set back to original position of the view controller
UIView.animate(withDuration: 0.3) {
self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
self.delegate?.animateBlurBack(seconds: 0.3)
}
}
}
}
}
protocol OverlayViewDelegate: AnyObject {
func userDragged(draggedPercentage: CGFloat)
func animateBlurBack(seconds: TimeInterval)
}
Next we define custom PresentationController
class PresentationController: UIPresentationController {
private var backgroundEffectView: UIView?
private var backgroundEffect: BackgroundEffect?
private var viewHeight: CGFloat?
private let maxDim:CGFloat = 0.6
private var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
convenience init(presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?,
backgroundEffect: BackgroundEffect = .blur,
viewHeight: CGFloat = 0.6)
{
self.init(presentedViewController: presentedViewController, presenting: presentingViewController)
self.backgroundEffect = backgroundEffect
self.backgroundEffectView = returnCorrectEffectView(backgroundEffect)
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
self.backgroundEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.backgroundEffectView?.isUserInteractionEnabled = true
self.backgroundEffectView?.addGestureRecognizer(tapGestureRecognizer)
self.viewHeight = viewHeight
}
private override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * (1-viewHeight!)),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
viewHeight!))
}
override func presentationTransitionWillBegin() {
self.backgroundEffectView?.alpha = 0
self.containerView?.addSubview(backgroundEffectView!)
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.alpha = 0
}, completion: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
backgroundEffectView?.frame = containerView!.bounds
}
#objc func dismissController(){
self.presentedViewController.dismiss(animated: true, completion: nil)
}
func graduallyChangeOpacity(withPercentage: CGFloat) {
self.backgroundEffectView?.alpha = withPercentage
}
func returnCorrectEffectView(_ effect: BackgroundEffect) -> UIView {
switch effect {
case .blur:
var blurEffect = UIBlurEffect(style: .dark)
if self.traitCollection.userInterfaceStyle == .dark {
blurEffect = UIBlurEffect(style: .light)
}
return UIVisualEffectView(effect: blurEffect)
case .dim:
var dimView = UIView()
dimView.backgroundColor = .black
if self.traitCollection.userInterfaceStyle == .dark {
dimView.backgroundColor = .gray
}
dimView.alpha = maxDim
return dimView
case .none:
let clearView = UIView()
clearView.backgroundColor = .clear
return clearView
}
}
}
extension PresentationController: OverlayViewDelegate {
func userDragged(draggedPercentage: CGFloat) {
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
switch self.backgroundEffect! {
case .blur:
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
case .dim:
graduallyChangeOpacity(withPercentage: maxDim-draggedPercentage)
case .none:
self.backgroundEffectView?.alpha = 0
}
}
func animateBlurBack(seconds: TimeInterval) {
UIView.animate(withDuration: seconds) {
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}
}
}
enum BackgroundEffect {
case blur
case dim
case none
}
Create SecondViewController subclassing OverlayViewController:
class SecondViewController: OverlayViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
addSlider()
}
func addSlider() {
let sliderWidth:CGFloat = 100
let centerOfScreen = self.view.frame.size.width / 2
let rect = CGRect(x: centerOfScreen - sliderWidth/2, y: 80, width: sliderWidth, height: 10)
let slider = UIView(frame: rect)
slider.backgroundColor = .black
self.view.addSubview(slider)
}
Add showOverlay() function that will be triggered after buttonPressed and conform your presenting UIViewController (TestViewController) to UIViewControllerTransitioningDelegate :
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func buttonPressed(_ sender: Any) {
showOverlay()
}
func showOverlay() {
let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondVC") as! SecondViewController
secondVC.modalPresentationStyle = .custom
secondVC.transitioningDelegate = self
self.present(secondVC, animated: true, completion: nil)
}
}
extension TestViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController?
{
let presentedHeight: CGFloat = 1.0
let controller = PresentationController(presentedViewController: presented,
presenting: presenting,
backgroundEffect: .dim,
viewHeight: presentedHeight)
if let vc = presented as? OverlayViewController {
vc.delegate = controller
}
return controller
}
}
Now we should be able to present SecondViewController with showOverlay() function setting its presentedHeight to 1.0 and .dim background effect. We can dismiss SecondViewController similar to another modal presentations.

UIViewPropertyAnimator: Hide both TabBar and StatusBar simultaneously (iOS 13)

Trying to hide both TabBar and StatusBar simultaneously and inside the same animation block, I came across an incomprehensible layout behavior. Starting to hide TabBar in the usual way with tabbar item viewcontroller:
import UIKit
class TestViewController: UIViewController {
var mainViewController: UITabBarController {
get {
return UIApplication.shared.windows.first {$0.rootViewController != nil}?.rootViewController as! UITabBarController
}
}
var offset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
offset = mainViewController.tabBar.frame.height
}
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator.startAnimation()
}
}
So far so good:
Now let's add animation for StatusBar:
import UIKit
class TestViewController: UIViewController {
var mainViewController: UITabBarController {
get {
return UIApplication.shared.windows.first {$0.rootViewController != nil}?.rootViewController as! UITabBarController
}
}
var isTabBarHidden = false {
didSet(newValue) {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
get {
return isTabBarHidden
}
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
get {
return .slide
}
}
var offset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
offset = mainViewController.tabBar.frame.height
}
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
self.isTabBarHidden = true
}
animator.startAnimation()
}
}
Now StatusBar is sliding, bur TabBar froze (I don't know why):
Any attempts to update layout using layoutIfNeeded(), setNeedsLayout() etc. were unsuccessful. Now let's swap animations for TabBar and StatusBar:
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.isTabBarHidden = true
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator.startAnimation()
}
Both are sliding now, but TabBar started to jump at the begining of animation:
I found that when adding directives for StatusBar to an animation block, a ViewDidLayoutSubviews() starts to be called additionally. Actually, you can fix the initial position of TabBar inside ViewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if isTabBarHidden {
let tabBar = self.mainViewController.tabBar
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
}
The disadvantage of this method is that TabBar can twitch during the movement, depending on the speed of movement and other factors.
Another way (without using ViewDidLayoutSubviews()) is contrary to logic but works in practice. Namely, you can put one animation in a completion block of another one:
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator1 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.isTabBarHidden = !self.isTabBarHidden
}
animator1.addCompletion({_ in
let animator2 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator2.startAnimation()
})
animator1.startAnimation()
}
Following the logic, we have two consecutive animations. And TabBar animation should begin after StatusBar animation ends. However, in practice:
The disadvantage of this method is that if you want to reverse the animation (for example, the user tapped the screen while TabBar is moving), the variable animator1.isRunning will be false, although physically the StatusBar will still move around the screen (I also don't know why).
Looking forward to reading your comments, suggestions, explanations.
The logic is that setNeedsStatusBarAppearanceUpdate() is animated asyncroniusly. I.e. StatusBar animation ends immediately after start and then runs in a thread that cannot be paused or reversed. It’s a pity that iOS SDK doesn't provide StatusBar animation control.
How to prevent the effect of setNeedsStatusBarAppearanceUpdate() animation on the layout, I still do not know.

Pass UILongPressGestureRecognizer between UIViewControllers

I've looked around on SO and other places online and haven't been able to find a solution yet. I have a UILongPressGestureRecognizer on one view controller which performs an animation on a UIView then presents another view controller (if the user does not move their finger during the animation). I would like to dismiss the second view controller when the user lifts their finger, but I am having trouble.
My current approach is adding another UILongPressGestureRecognizer to the second view controller and dismissing the view controller when its state is .ended; however I don't know how to tie the two gesture recognizers together so that I can start the touch in one view controller and end in another.
I have attached the relevant code below
In the first View Controller
#objc private func pinAnimation(sender: UILongPressGestureRecognizer) {
if let headerView = projectView.projectCollectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(row: 0, section: 0)) as? ProjectHeaderView {
switch sender.state {
case .began:
let frame = headerView.pinButton.frame
UIView.setAnimationCurve(.linear)
UIView.animate(withDuration: 1, animations: {
headerView.pinProgressView.frame = frame
}, completion: { (_) in
if (headerView.pinProgressView.frame == headerView.pinButton.frame) {
let notification = UINotificationFeedbackGenerator()
notification.notificationOccurred(.success)
let homeVC = HomeViewController()
self.present(homeVC, animated: true, completion: {
homeVC.longPress(sender)
let height = headerView.pinButton.frame.height
let minX = headerView.pinButton.frame.minX
let minY = headerView.pinButton.frame.minY
headerView.pinProgressView.frame = CGRect(x: minX, y: minY, width: 0, height: height)
})
}
})
case .ended:
//cancel current animation
let height = headerView.pinProgressView.frame.height
let minX = headerView.pinProgressView.frame.minX
let minY = headerView.pinProgressView.frame.minY
UIView.setAnimationCurve(.linear)
UIView.animate(withDuration: 0.5) {
headerView.pinProgressView.frame = CGRect(x: minX, y: minY, width: 0, height: height)
}
default:
break
}
}
}
In the second View Controller
override func viewDidLoad() {
super.viewDidLoad()
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
view.addGestureRecognizer(longPressRecognizer)
}
#objc func longPress(_ sender: UILongPressGestureRecognizer) {
switch sender.state {
case .ended:
self.dismiss(animated: true, completion: nil)
default:
break
}
}
Any help / guidance will be much appreciated!
I would recommend you using delegate pattern:
Create delegate for your first ViewController
protocol FirstViewControllerDelegate {
func longPressEnded()
}
then add delegate variable to your first ViewController
class FirstViewController {
var delegate: FirstViewControllerDelegate?
...
}
next call method on delegate when long press ended
#objc private func pinAnimation(sender: UILongPressGestureRecognizer) {
...
switch sender.state {
case .began:
...
case .ended:
...
delegate?.longPressEnded()
default:
break
}
}
after that when you're presenting second ViewController, set first ViewController's delegate as homeVC
let homeVC = HomeViewController()
self.delegate = homeVC
then implement this delegate to second ViewController and define what should happen when long press ended
class HomeViewController: UIViewController, FirstViewControllerDelegate {
...
func longPressEnded() {
dismiss(animated: true, completion: nil)
}
}

How do you really hide and show a tab bar when tapping on part of your view? (no buttons but any location of the screen)

override func viewDidLoad() {
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
tabBarController?.hideTabBarAnimated(hide: true)
}
extension UITabBarController {
func hideTabBarAnimated(hide:Bool) {
UIView.animate(withDuration: 2, animations: {
if hide {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: 100)
} else {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: -100)
}
})
}
}
I can only hide the tab bar but I can't make it show when you tap again. I tried to look for answers on stack overflow but the answers seems to only work if you're using a button or a storyboard.
Have a variable isTabBarHidden in class which stores if the tabBar has been animated to hide. (You could have used tabBar.isHidden, but that would complicate the logic a little bit when animate hiding and showing)
class ViewController {
var isTabBarHidden = false // set the default value as required
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
guard let tabBarControllerFound = tabBarController else {
return
}
tabBarController?.hideTabBarAnimated(hide: !isTabBarHidden)
isTabBarHidden = !isTabBarHidden
}
}
Generalised solution with protocol which will work in all the screens
Create UIViewController named BaseViewController and make it base class of all of your view controllers
Now Define protocol
protocol ProtocolHideTabbar:class {
func hideTabbar ()
}
protocol ProtocolShowTabbar:class {
func showTabbar ()
}
extension ProtocolHideTabbar where Self : UIViewController {
func hideTabbar () {
self.tabBarController?.tabBar.isHidden = true
}
}
extension ProtocolShowTabbar where Self : UIViewController {
func showTabbar () {
self.tabBarController?.tabBar.isHidden = false
}
}
By default we want show tabbar in every view controller so
extension UIViewController : ProtocolShowTabbar {}
In your BaseView Controller
in view will appear method add following code to show hide based on protocol
if self is ProtocolHideTabbar {
( self as! ProtocolHideTabbar).hideTabbar()
} else if self is ProtocolShowTabbar{
( self as ProtocolShowTabbar).showTabbar()
}
How to use
Simply
class YourViewControllerWithTabBarHidden:BaseViewController,ProtocolHideTabbar {
}
Hope it is helpful
Tested 100% working
Please try below code for that in UITabBarController subclass
var isTabBarHidden:Bool = false
func setTabBarHidden(_ tabBarHidden: Bool, animated: Bool,completion:((Void) -> Void)? = nil) {
if tabBarHidden == isTabBarHidden {
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
//check tab bar is visible and view and window height is same then it should be 49 + window Heigth
if (tabBarHidden == true && UIScreen.main.bounds.height == self.view.frame.height) {
let offset = self.tabBar.frame.size.height
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset)
}
if let block = completion {
block()
}
return
}
let offset: CGFloat? = tabBarHidden ? self.tabBar.frame.size.height : -self.tabBar.frame.size.height
UIView.animate(withDuration: animated ? 0.250 : 0.0, delay: 0.1, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [.curveEaseIn, .layoutSubviews], animations: {() -> Void in
self.tabBar.center = CGPoint(x: CGFloat(self.tabBar.center.x), y: CGFloat(self.tabBar.center.y + offset!))
//Check if View is already at bottom so we don't want to move view more up (it will show black screen on bottom ) Scnario : When present mail app
if (Int(offset!) <= 0 && UIScreen.main.bounds.height == self.view.frame.height) == false {
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset!)
}
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}, completion: { _ in
if let block = completion {
block()
}
})
isTabBarHidden = tabBarHidden
}
Hope it is helpful

Interactive sliding menu goes to completion as soon as pan gesture starts [Swift]

I am trying to implement a sliding menu that can be interactively dismissed by horizontal panning, same as the ones in Uber and Google apps. Everything works as expected except that, as soon as I start panning horizontally, dismiss goes to completion without following my finger. Any suggestion of where the problem may lie is very appreciated.
I subclassed UIPresentationController to define the presented width of my menu controller. I have custom presentation animator and dismiss animator, and a UIViewControllerTransitioningDelegate object to return them all to UIKit. I also implemented gestureRecognizerShouldBegin(_ gestureRecognizer:) method in my menu controller to allow vertical scrolling.
SlideDismissAnimator
class SlideDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let interactionController: SlideInteractionController?
init(interactionController: SlideInteractionController?) {
self.interactionController = interactionController
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromCV = transitionContext.viewController(forKey: .from)!
let initialFrame = transitionContext.finalFrame(for: fromCV)
var finalFrame = initialFrame
finalFrame.origin.x = transitionContext.containerView.frame.width // My menu slides in from right
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
fromCV.view.frame = finalFrame
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
SlideInteractionController
class SlideInteractionController: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
private var shouldCompleteTransition = false
private weak var collectionViewController: UICollectionViewController!
init(collectionViewController: UICollectionViewController) {
super.init()
self.collectionViewController = collectionViewController
if let menuController = collectionViewController as? MenuController {
let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
menuController.collectionView?.addGestureRecognizer(gesture)
gesture.delegate = menuController
}
}
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
var progress = (translation.x / 100)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
collectionViewController.dismiss(animated: true, completion: nil)
case .changed:
shouldCompleteTransition = progress > 0.5
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
}
MenuController
class MenuController: UICollectionViewController, UIGestureRecognizerDelegate {
var slideInteractionController: SlideInteractionController?
override func viewDidLoad() {
super.viewDidLoad()
setupView()
slideInteractionController = SlideInteractionController(collectionViewController: self)
}
...
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let translation = panGestureRecognizer.translation(in: collectionView)
if translation.x > fabs(translation.y) {
return true
}
}
return false
}
}
I made a sample Project that Use a tableView inside a View that acts as A side Menu drawer Presented over Current Context
my pan Handler
//MARK: Pan gesture Handler
#objc func handlePanGesture(panGesture: UIPanGestureRecognizer)
{
///Get the changes
let translation = panGesture.translation(in: self.view)
///Make View move to left side of Frame
if CGFloat(round(Double((panGesture.view?.frame.origin.x)!))) <= 0
{
panGesture.view!.center = CGPoint(x: panGesture.view!.center.x + translation.x, y: panGesture.view!.center.y)
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
///Do not let View go beyond origin as 0
if CGFloat(round(Double((panGesture.view?.frame.origin.x)!))) > 0
{
panGesture.view?.frame.origin.x = 0
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
///States When Dragging
switch panGesture.state
{
case .changed:
self.setAlphaOfBlurView(origin: (panGesture.view?.frame.maxX)!)
case .ended:
if CGFloat(round(Double((panGesture.view?.frame.maxX)!))) >= self.view.frame.size.width*0.35
{
UIView.animate(withDuration: 0.7, animations: {
panGesture.view?.frame.origin.x = 0
panGesture.setTranslation(CGPoint.zero, in: self.view)
})
}
else
{
UIView.animate(withDuration: 0.4, animations: {
panGesture.view?.frame.origin.x -= self.maximum_x
panGesture.setTranslation(CGPoint.zero, in: self.view)
}, completion: { (success) in
if (success)
{
self.remove(asChildViewController: self.sideMenuVCObject, baseView: self.baseView)
self.baseView.removeFromSuperview()
self.blurView.removeFromSuperview()
//Remove Notification observer
NotificationCenter.default.removeObserver(self,name: NSNotification.Name(rawValue: "hideMenu"),object: nil)
}
})
}
break
default:
print("Default Case")
}
}
Repository Link at GiHub
https://github.com/RockinGarg/Slide-Menu-Drawer.git
Working Video :
https://drive.google.com/open?id=13Q-bBkVlAX7uEweDyQGvNct-dXkBSveT

Resources