I have view controller which as UIPageViewController inside.
Using pageviewcontroller I can swipt left, right in order to go to other VCs. It works!
So, after I added sideBarMenu. When adding this menu I use this code to add gesture recognizer:
var menuViewController: UIViewController! {
didSet {
self.exitPanGesture = UIPanGestureRecognizer()
self.exitPanGesture.addTarget(self, action:"handleOffstagePan:")
self.sourceViewController.view.addGestureRecognizer(self.exitPanGesture)
}
Here the sourceViewController is my original VC.
The problem is when I try to swipe (in order to close menu), the pageViewController swipe works.
I want to disable pageViewController swipe and enable new swipe function when menu is opened. And do oppositely when menu is closed.
Additional code:
func handleOffstagePan(pan: UIPanGestureRecognizer){
println("dismiss pan gesture recognizer")
let translation = pan.translationInView(pan.view!)
let d = translation.x / CGRectGetWidth(pan.view!.bounds) * -0.5
switch (pan.state) {
case UIGestureRecognizerState.Began:
self.interactive = true
self.menuViewController.performSegueWithIdentifier("dismisMenu", sender: self)
break
case UIGestureRecognizerState.Changed:
self.updateInteractiveTransition(d)
break
default:
self.interactive = false
if d > 0.1 {
self.finishInteractiveTransition()
}else {
isMenuVisible = false
self.cancelInteractiveTransition()
}
}
}
Guys!
SO, the solution is instead of setting PageViewController to the sourceVC of your TransitionManager, set pageContentViewController to the sourceVC. PageContentViewControler is :
func resetToMainPage(index: Int!) {
/* Getting the page View controller */
mainPageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("MainPageViewController") as UIPageViewController
self.mainPageViewController.dataSource = self
self.mainPageViewController.delegate = self
let pageContentViewController = self.viewControllerAtIndex(index)
self.transtionManger.sourceViewController = pageContentViewController // adding swipe to the pageContentViewControlle in order to close menu
self.mainPageViewController.setViewControllers([pageContentViewController!], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: nil)
self.mainPageViewController.view.frame = CGRectMake(0, 102, self.view.frame.width, self.view.frame.height)
self.addChildViewController(mainPageViewController)
self.view.addSubview(mainPageViewController.view)
self.mainPageViewController.didMoveToParentViewController(self)
}
Here I set pageContentVC to the sourveVS of transitionManageClass. NExt how to choose the right GestureRecognizer. By default when you add new gesture recognizer the old one doesnt work. When you disable the new gesture recognizer the old one starts to work! I added new gesture recognizer using code:
var menuViewController: UIViewController! {
didSet {
self.exitPanGesture = UIPanGestureRecognizer()
self.exitPanGesture.addTarget(self, action:"handleOffstagePan:")
// self.exitPanGesture.view?.userInteractionEnabled = false
self.sourceViewController.view.addGestureRecognizer(self.exitPanGesture)
}
}
Before setting menuViewController I set sourceViewController. So, here I am adding new gesture recognizer to my sourceViewController. Next, step is to disable this gesture recognize. When you close the menu disable it using this code:
var presentingP:Bool!{
didSet{
if presentingP == true {
// enable the gesture recognizer only when the view of menucontroller is presented
self.exitPanGesture.view?.userInteractionEnabled = true
}else{
// disable gesture recognizer when menu is not presented
self.exitPanGesture.view?.userInteractionEnabled = false
isMenuVisible = false
}
}
}
PresentingP is boolean value which shows when menu is opened and closed!
Related
I'm working on a game prototype in Swift using UIKit and SpriteKit. The inventory (at the bottom of this screenshot) is a UIView with UIImageView subviews for the individual items. In this example, a single acorn.
I have the acorn recognizing the "pan" gesture so I can drag it around. However, it renders it below the other views in the hierarchy. I want it to pop out of the inventory and be on top of everything (even above its parent view) so I can drop it onto other views elsewhere in the game.
This is what I have as my panHandler on the acorn view:
#objc func panHandler(gesture: UIPanGestureRecognizer){
switch (gesture.state) {
case .began:
removeFromSuperview()
controller.view.addSubview(self)
case .changed:
let translation = gesture.translation(in: controller.view)
if let v = gesture.view {
v.center = CGPoint(x: v.center.x + translation.x, y: v.center.y + translation.y)
}
gesture.setTranslation(CGPoint.zero, in: controller.view)
default:
return
}
}
The problem is in the .began case, when I remove it from the superview, the pan gesture immediately cancels. Is it possible to remove the view from a superview and add it as a subview elsewhere while maintaining the pan gesture?
Or, if my approach is completely wrong, could you give me pointers how to accomplish my goal with another method?
The small answer is you can keep the gesture working if you don't call removeFromSuperview() on your view and add it as a subview right away to your controller view, but that's not the right way to do this because if the user cancels the drag you will have to re add to your main view again and if that view your dragging is heavy somehow it gets laggy and messy quickly
The long answer, and in my opinion is the right way to do it and what apple actually does in all drag and drop apis is
you can actually make a snapshot of the view you want to drag and add it as a subview of the controller view that's holding all your views and then call bringSubviewToFront(_ view: UIView) to make sure it's the top most view in the hierarchy and pass in the snapshot you took of the dragging view
in the .began you can hide the original view and in the .ended you can show it again
and also on .ended you can either take that snapshot and add to the dropping view or do anything else with it's dropping coordinates
I made a sample project to apply this
Here is the storyboard design and view hierarchy
Here is the ViewController code
class ViewController: UIViewController {
#IBOutlet var topView: UIView!
#IBOutlet var bottomView: UIView!
#IBOutlet var smallView: UIView!
var snapshotView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let pan = UIPanGestureRecognizer(target: self, action: #selector(panning(_:)))
smallView.addGestureRecognizer(pan)
}
#objc private func panning(_ pan: UIPanGestureRecognizer) {
switch pan.state {
case .began:
smallView.backgroundColor = .systemYellow
snapshotView = smallView.snapshotView(afterScreenUpdates: true)
smallView.backgroundColor = .white
if let snapshotView = self.snapshotView {
self.snapshotView = snapshotView
view.addSubview(snapshotView)
view.bringSubviewToFront(snapshotView)
snapshotView.backgroundColor = .blue
snapshotView.center = bottomView.convert(smallView.center, to: view)
}
case .changed:
guard let snapshotView = snapshotView else {
fallthrough
}
smallView.alpha = 0
let translation = pan.translation(in: view)
snapshotView.center = CGPoint(x: snapshotView.center.x + translation.x, y: snapshotView.center.y + translation.y)
pan.setTranslation(.zero, in: view)
case .ended:
if let snapshotView = snapshotView {
let frame = view.convert(snapshotView.frame, to: topView)
if topView.frame.contains(frame) {
topView.addSubview(snapshotView)
snapshotView.frame = frame
smallView.alpha = 1
} else {
bottomView.addSubview(snapshotView)
let newFrame = view.convert(snapshotView.frame, to: bottomView)
snapshotView.frame = newFrame
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
snapshotView.frame = self.smallView.frame
} completion: { _ in
self.snapshotView?.removeFromSuperview()
self.snapshotView = nil
self.smallView.alpha = 1
}
}
}
default: break
}
}
}
Here is how it ended up
I am trying to recreate the bottom drawer functionality seen in Maps or Siri Shortcuts by using a UIPresentationController by having it recognise user input and updating the frameOfPresentedViewInContainerView accordingly. However I want this mechanism to work independently of the presented UIViewController as much as possible so I'm trying to have the presentation controller add a handle area above the view. Ideally the view of the presented controller and the handle are should both recognise user input.
This works for the presented view, however any view I add to it responds to no UIGestureRecognizer at all. Am I missing something?
class PresentationController: UIPresentationController {
private let handleArea: UIView = UIView()
override var frameOfPresentedViewInContainerView: CGRect {
// Return some frame for now
return CGRect(x: 0, y: 250, width: containerView!.frame.width, height: 500)
}
override func presentationTransitionWillBegin() {
// Unwrap presented view
guard let presentedView = self.presentedView else {
return
}
// Set color
self.handleArea.backgroundColor = UIColor.green
// Add to view hierachy
presentedView.addSubview(self.handleArea)
// Set constraints
self.handleArea.trailingAnchor.constraint(equalTo: presentedView.trailingAnchor).isActive = true
self.handleArea.leadingAnchor.constraint(equalTo: presentedView.leadingAnchor).isActive = true
self.handleArea.bottomAnchor.constraint(equalTo: presentedView.topAnchor).isActive = true
self.handleArea.heightAnchor.constraint(equalToConstant: 56).isActive = true
self.handleArea.translatesAutoresizingMaskIntoConstraints = false
// These don't help
self.handleArea.isUserInteractionEnabled = true
presentedView.isUserInteractionEnabled = true
presentedView.bringSubviewToFront(self.handleArea)
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if completed {
// Add gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onHandleAreaTapped(sender:)))
self.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// Remove subview
self.handleArea.removeFromSuperview()
}
// MARK: - Responder
#objc private func onHandleAreaTapped(sender: UITapGestureRecognizer) {
print("tap") // No output
}
}
I managed to solve it by adding both the handle area and the view of the presentedViewController to a custom view and then overriding the presentedView property and returning my custom view.
I'm trying to implement a custom pan gesture to interactively transition to a new view controller. The way it works is that I have a button (labeled "Template Editor", see below) on which you can start a pan to move the current view controller to the right, revealing the new view controller next to it (I've recorded my problem, see below).
Everything is working but there is a bug that I don't understand at all:
Sometimes, when I just swipe over the button (triggering a pan gesture) then lift my finger again (touch down -> fast, short swipe to the right -> touch up) the interactive transition glitches out. It starts to very slowly complete the transition and afterwards, I cannot dismiss the presented view controller, nor can I present anything on that presented view controller.
I have no idea why. Here's my code:
First, the UIViewControllerAnimatedTransitioning class. It's implemented using UIViewPropertyAnimator and just adds the animation using transform:
class MovingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
enum Direction {
case left, right
}
// MARK: - Properties
// ========== PROPERTIES ==========
private var animator: UIViewImplicitlyAnimating?
var duration = 0.6
var presenting = true
var shouldAnimateInteractively: Bool = false
public var direction: Direction = .left
private var movingMultiplicator: CGFloat {
return direction == .left ? -1 : 1
}
// ====================
// MARK: - Initializers
// ========== INITIALIZERS ==========
// ====================
// MARK: - Overrides
// ========== OVERRIDES ==========
// ====================
// MARK: - Functions
// ========== FUNCTIONS ==========
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let animator = interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// If the animator already exists, return it (important, see documentation!)
if let animator = self.animator {
return animator
}
// Otherwise, create the animator
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
if presenting {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: -movingMultiplicator * toView.frame.width, y: 0)
}
containerView.addSubview(toView)
let animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.9, animations: nil)
animator.addAnimations {
if self.presenting {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: -self.movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: self.movingMultiplicator * toView.frame.width, y: 0)
}
}
animator.addCompletion { (position) in
// Important to set frame above (device rotation will otherwise mess things up)
toView.transform = .identity
fromView.transform = .identity
if !transitionContext.transitionWasCancelled {
self.shouldAnimateInteractively = false
}
self.animator = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
self.animator = animator
return animator
}
// ====================
}
Here's the part that adds the interactivity. It's a method that's being called by a UIPanGestureRecognizer I added to the button.
public lazy var transitionAnimator: MovingTransitionAnimator = MovingTransitionAnimator()
public lazy var interactionController = UIPercentDrivenInteractiveTransition()
...
#objc private func handlePan(pan: UIPanGestureRecognizer) {
let translation = pan.translation(in: utilityView)
var progress = (translation.x / utilityView.frame.width)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch pan.state {
case .began:
// This is a flag that helps me distinguish between when a user taps on the button and when he starts a pan
transitionAnimator.shouldAnimateInteractively = true
// Just a dummy view controller that's dismissing as soon as its been presented (the problem occurs with every view controller I use here)
let vc = UIViewController()
vc.view.backgroundColor = .red
vc.transitioningDelegate = self
present(vc, animated: true, completion: {
self.transitionAnimator.shouldAnimateInteractively = false
vc.dismiss(animated: true, completion: nil)
})
case .changed:
interactionController.update(progress)
case .cancelled:
interactionController.cancel()
case .ended:
if progress > 0.55 || pan.velocity(in: utilityView).x > 600
interactionController.completionSpeed = 0.8
interactionController.finish()
} else {
interactionController.completionSpeed = 0.8
interactionController.cancel()
}
default:
break
}
}
I also implemented all the necessary delegate methods:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = true
return transitionAnimator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = false
return transitionAnimator
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
That's it. There's no more logic behind it (I think; if you need more information, please tell me), but it still has this bug. Here's a recording of the bug. You can't really see my touch but all I'm doing is touching down -> fast, shortly swiping to the right -> touching up. And after this really slow transition has finished, I can't dismiss the red view controller. It's stuck there:
Here's what's even stranger:
Neither interactionController.finish() nor interactionController.cancel() is being called when this occurs (at least not from within my handlePan(_:)method).
I checked the view hierarchy in Xcode after this bug occurred and I got this:
First, it's seemingly stuck in the transition (everything is still inside UITransitionView).
Second, on the left hand side you see the views of the first view controller(the one I start the transition from). However, on the image there only is the red view controller visible, the one that was about to be presented.
Do you have any idea what's going on? I've been trying to figure this out for the past 3 hours but I can't get get it to work properly. I'd appreciate any help
Thank you!
EDIT
Okay, I found a way to reproduce it 100% of the time. I also created an isolated project demonstrating the problem (it's a little differently structured because I tried many things but the result is still exactly the same)
Here's the project: https://github.com/d3mueller/InteractiveTransitionDemo2
How to reproduce the problem:
Swipe from right to left and then quickly from left to right. This will trigger the bug.
Also, a similar bug will appear, when you swipe from right to left very fast multiple times. Then it will actually run the transition and finish it correctly (but it shouldn't even start because moving from right to left keeps the progress at 0.0)
You might try setting:
/// Set this to NO in order to start an interruptible transition non
/// interactively. By default this is YES, which is consistent with the behavior
/// before 10.0.
#property (nonatomic) BOOL wantsInteractiveStart NS_AVAILABLE_IOS(10_0);
to NO on your interactionController
Good luck and curious to hear if you figure it out.
I'm trying to create a feature similar to tableView swipe to show delete button, the only difference is that I show multiple buttons and have it implemented on a collectionView within a collectionViewCell. I want to be able to slide the inner collectionView to the right and have multiple options buttons snap into view on the left.
Something like this:
I understand that I'll probably need to use UIPanGestureRecognizer, the problem for me is that the collectionView to pan is nested within another collectionView, and I'm not certain as to how to use the UIGestureRecognizer correctly so that the cells slide together and the buttons snap into view.
Any suggestions are very much appreciated.
set delegate in collection view for get action
protocol ColumnBookCellDelegate: class {
func deleteBook(_ book: Book)
}
class ColumnBookCell: AZCollectionViewCell{
weak var deleteDelegate: ColumnBookCellDelegate?
var canBeRemove: Bool = false{
didSet{
if self.canBeRemove{
let swipeL = UISwipeGestureRecognizer(target: self, action: #selector(self.showDelete))
swipeL.numberOfTouchesRequired = 1
swipeL.direction = .left
self.addGestureRecognizer(swipeL)
let swipeR = UISwipeGestureRecognizer(target: self, action: #selector(self.hideDelete))
swipeR.numberOfTouchesRequired = 1
swipeR.direction = .right
self.addGestureRecognizer(swipeR)
}
}
}
// Show Delete Button
func showDelete(){
// unhidden button here
// self.button.isHidden = false
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
}
}
// Hide Delete Button
func hideDelete(){
// hidden button here
// self.button.isHidden = true
// self.deleteButton.aZConstraints.width?.constant = 0
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
}
}
// Delete Action
func deleteAction(){
self.deleteDelegate?.deleteBook(self.book)
self.hideDelete()
}
// blob blob blob
}
the UIView in question is headerView:
if isShown {
stack.alpha = 1.0
self.isStackShown = true
DispatchQueue.main.async {
self.headerView.isHidden = !isShown
self.stack.addArrangedSubview(self.headerView)
// add touch gesture recognizer to stack view header
let tapFind = UIGestureRecognizer(target: self, action: #selector(self.handleFindTap))
self.headerView.addGestureRecognizer(tapFind)
}
} else {
stack.alpha = 0.0
self.isStackShown = false
DispatchQueue.main.async {
self.headerView.isHidden = isShown
self.stack.removeArrangedSubview(self.headerView)
}
}
The tap gesture recognizer is not registering any taps
self.stack is the stack view which contains the headerView
The condition for either showing or hiding the headerView is being handled in a different method and just passes the boolean self.isStackShown to this method to show/hide accordingly.
You are using a UIGestureRecognizer. UIGestureRecognizer is a polymorphic base class and should really be subclassed. UITapGestureRecognizer is the concrete subclass for handling taps. Use it instead.
let tapFind = UITapGestureRecognizer(target: self, action: #selector(self.handleFindTap))
self.headerView.addGestureRecognizer(tapFind)
Your action is never getting called because UIGestureRecognizer has no inherent information about what kind of gesture to recognize. Only a concrete subclass of it does.
Looks like you are adding multiple gesture recognizers when changing back the alpha to 1.0 and they aren't able to recognize simultaneously. Remove all gesture recognizers when hiding and removing the headerView since you don't need one anymore and add one back when adding back the headerView and it should work. Or you can also let the gesture recognizer be when hiding the headerView since it won't work anyway and check if one exists before adding another.
if isShown {
stack.alpha = 1.0
self.isStackShown = true
DispatchQueue.main.async {
self.headerView.isHidden = !isShown
self.stack.addArrangedSubview(self.headerView)
// add touch gesture recognizer to stack view header
let tapFind = UIGestureRecognizer(target: self, action: #selector(self.handleFindTap))
self.headerView.addGestureRecognizer(tapFind)
}
} else {
stack.alpha = 0.0
self.isStackShown = false
DispatchQueue.main.async {
self.headerView.isHidden = isShown
self.headerView.gestureRecognizers?.forEach({self.headerView.removeGestureRecognizer($0)})
self.stack.removeArrangedSubview(self.headerView)
}
}
or
if isShown {
stack.alpha = 1.0
self.isStackShown = true
DispatchQueue.main.async {
self.headerView.isHidden = !isShown
self.stack.addArrangedSubview(self.headerView)
if self.headerView.gestureRecognizers?.isEmpty != false{
// add touch gesture recognizer to stack view header
let tapFind = UIGestureRecognizer(target: self, action: #selector(self.handleFindTap))
self.headerView.addGestureRecognizer(tapFind)
}
}
} else {
stack.alpha = 0.0
self.isStackShown = false
DispatchQueue.main.async {
self.headerView.isHidden = isShown
self.stack.removeArrangedSubview(self.headerView)
}
}