I've been trying this for awhile. The code below is my UIPresentationController. When a button is pressed, I add a dimmed UIView and a second modal (presentedViewController) pops up halfway.
I added the tap gesture recognizer in the method presentationTransitionWillBegin()
I don't know why the tap gesture is not being registered when I click on the dimmed UIView.
I've tried changing the "target" and adding the gesture in a different place. Also looked at other posts, but nothing has worked for me.
Thanks
import UIKit
class PanModalPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: containerView!.bounds.size)
frame.origin.y = containerView!.frame.height * (1.0 / 2.0)
print("frameOfPresentedViewInContainerView")
return frame
}
private lazy var dimView: UIView! = {
print("dimView")
guard let container = containerView else { return nil }
let dimmedView = UIView(frame: container.bounds)
dimmedView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
dimmedView.isUserInteractionEnabled = true
return dimmedView
}()
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
print("init presentation controller")
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override func presentationTransitionWillBegin() {
guard let container = containerView else { return }
print("presentation transition will begin")
container.addSubview(dimView)
dimView.translatesAutoresizingMaskIntoConstraints = false
dimView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
dimView.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
dimView.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
dimView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
dimView.isUserInteractionEnabled = true
let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
dimView.addGestureRecognizer(recognizer)
container.addSubview(presentedViewController.view)
presentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
presentedViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
presentedViewController.view.widthAnchor.constraint(equalTo: container.widthAnchor).isActive = true
presentedViewController.view.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true
guard let coordinator = presentingViewController.transitionCoordinator else { return }
coordinator.animate(alongsideTransition: { _ in
self.dimView.alpha = 1.0
})
print(dimView.alpha)
}
override func dismissalTransitionWillBegin() {
guard let coordinator = presentedViewController.transitionCoordinator else {
print("dismissal coordinator")
self.dimView.alpha = 0.0
return
}
print("dismissal transition begin")
coordinator.animate(alongsideTransition: { _ in
self.dimView.alpha = 0.0
})
}
override func containerViewDidLayoutSubviews() {
print("containerViewDidLayoutSubviews")
presentedView?.frame = frameOfPresentedViewInContainerView
// presentedViewController.dismiss(animated: true, completion: nil)
}
override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
print("size")
return CGSize(width: parentSize.width, height: parentSize.height * (1.0 / 2.0))
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
print("tapped")
// presentingViewController.dismiss(animated: true, completion: nil)
presentedViewController.dismiss(animated: true, completion: nil)
}
}
I can't tell what the frame/bounds of your presentedViewController.view is but even if it's top half has an alpha of 0 it could be covering your dimView and receiving the tap events instead of the dimView - since presentedViewController.view is added as a subview on top of dimView.
You may have to wait until after the controller is presented and add the gesture to its superview's first subview. I've used this before to dismiss a custom alert controller with a background tap. You could probably do something similar:
viewController.present(alertController, animated: true) {
// Enabling Interaction for Transparent Full Screen Overlay
alertController.view.superview?.subviews.first?.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: alertController, action: #selector(alertController.dismissSelf))
alertController.view.superview?.subviews.first?.addGestureRecognizer(tapGesture)
}
Hmm, try using this instead. Let me know how it goes. It works for me.
class PC: UIPresentationController {
/*
We'll have a dimming view behind.
We want to be able to tap anywhere on the dimming view to do a dismissal.
*/
override var frameOfPresentedViewInContainerView: CGRect {
let f = super.frameOfPresentedViewInContainerView
var new = f
new.size.height /= 2
new.origin.y = f.midY
return new
}
override func presentationTransitionWillBegin() {
let con = self.containerView!
let v = UIView(frame: con.bounds)
v.backgroundColor = UIColor.black
v.alpha = 0
con.insertSubview(v, at: 0)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
v.addGestureRecognizer(tap)
let tc = self.presentedViewController.transitionCoordinator!
tc.animate(alongsideTransition: { _ in
v.alpha = 1
}, completion: nil)
}
#objc func handleTap() {
print("tapped")
self.presentedViewController.dismiss(animated: true, completion: nil)
}
override func dismissalTransitionWillBegin() {
let con = self.containerView!
let v = con.subviews[0]
let tc = self.presentedViewController.transitionCoordinator!
tc.animate(alongsideTransition: { _ in
v.alpha = 0
}, completion: nil)
}
}
I took a look at your project just now. The problem is in your animation controller. If you comment out the functions in your transition delegate object that vend animation controllers, everything works fine.
But just looking at your animation controller, what you wanted to achieve was to have your new vc slide up / slide down. And in fact, you don't even need a custom animation controller for this; the modalTransitionStyle property of a view controller has a default value of coverVertical, which is just what you want I think.
In any case though, you can still use the presentation controller class I posted before, as it has same semantics from your class, just without unnecessary overrides.
Optional
Also just a tip if you'd like, you have these files right now in your project:
PanModalPresentationDelegate.swift
PanModalPresentationController.swift
PanModalPresentationAnimator.swift
TaskViewController.swift
HomeViewController.swift
What I normally do is abbreviate some of those long phrases, so that the name of the file and class conveys the essence of its nature without long un-needed boilerplate.
So HomeViewController and TaskViewController would be Home_VC and Task_VC. Those other 3 files are all for the presentation of one VC; it can get out of hand very quickly. So what I normally do there is call my presentation controller just PC and nest its declaration inside the VC class that will use it (in this case that's Task_VC). Until the time comes where it needs to be used by some other VC too; then it's more appropriate to put it in its own file and call it Something_PC but I've never actually needed to do that yet lol. And the same for any animation controllers ex. Fade_AC, Slide_AC etc. I tend to call transition delegate a TransitionManager and nest it in the presented VC's class. Makes it easier for me to think of it as just a thing that vends AC's / a PC.
Then your project simply becomes:
Home_VC.swift
Task_VC.swift
And if you go inside Task_VC, you'll see a nested TransitionManager and PC.
But yeah up to you 😃.
The dimmedView is behind presented view. You have a couple options to correct that.
First, is allow touches to pass through the top view, it must override pointInside:
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *subview in self.subviews) {
if ([subview hitTest:[self convertPoint:point toView:subview] withEvent:event]) {
return TRUE;
}
}
return FALSE;
}
Another options is to instead add the gesture recognizer to the presentedViewController.view, instead of the dimmedView. And, if you allow PanModalPresentationController to adopt the UIGestureRecognizerDelegate, and it as the delegate to the recognizer, you can determine if you should respond to touches, by implementing shouldReceive touch:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if (touch.view == presentedViewController.view) {
return true
}
return false
}
If you use the second option, don't forget to remove the gesture recognizer in dismissalTransitionWillBegin or dismissalTransitionDidEnd!
Related
iOS 10 added a new function for custom animated view controller transitions called
interruptibleAnimator(using:)
Lots of people appear to be using the new function, however by simply implementing their old animateTransition(using:) within the animation block of a UIViewPropertyAnimator in interruptibleAnimator(using:) (see Session 216 from 2016)
However I can't find a single example of someone actually using the interruptible animator for creating interruptible transitions. Everyone seems to support it, but no one actually uses it.
For example, I created a custom transition between two UIViewControllers using a UIPanGestureRecognizer. Both view controllers have a backgroundColor set, and a UIButton in the middle that changes the backgroundColour on touchUpInside.
Now I've implemented the animation simply as:
Setup the toViewController.view to be positioned to the
left/right (depending on the direction needed) of the
fromViewController.view
In the UIViewPropertyAnimator animation block, I slide the
toViewController.view into view, and the fromViewController.view out
of view (off screen).
Now, during transition, I want to be able to press that UIButton. However, the button press was not called. Odd, this is how the session implied things should work, I setup a custom UIView to be the view of both of my UIViewControllers as follows:
class HitTestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view is UIButton {
print("hit button, point: \(point)")
}
return view
}
}
class ViewController: UIViewController {
let button = UIButton(type: .custom)
override func loadView() {
self.view = HitTestView(frame: UIScreen.main.bounds)
}
<...>
}
and logged out the func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? results. The UIButton is being hitTested, however, the buttons action is not called.
Has anyone gotten this working?
Am I thinking about this wrong and are interruptible transitions just to pausing/resuming a transition animation, and not for interaction?
Almost all of iOS11 uses what I believe are interruptible transitions, allowing you to, for example, pull up control centre 50% of the way and interact with it without releasing the control centre pane then sliding it back down. This is exactly what I wish to do.
Thanks in advance! Spent way to long this summer trying to get this working, or finding someone else trying to do the same.
I have published sample code and a reusable framework that demonstrates interruptible view controller animation transitions. It's called PullTransition and it makes it easy to either dismiss or pop a view controller simply by swiping downward. Please let me know if the documentation needs improvement. I hope this helps!
Here you go! A short example of an interruptible transition. Add your own animations in the addAnimation block to get things going.
class ViewController: UIViewController {
var dismissAnimation: DismissalObject?
override func viewDidLoad() {
super.viewDidLoad()
self.modalPresentationStyle = .custom
self.transitioningDelegate = self
dismissAnimation = DismissalObject(viewController: self)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return dismissAnimation
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? DismissalObject else { return nil }
return animator
}
}
class DismissalObject: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning {
fileprivate var shouldCompleteTransition = false
var panGestureRecongnizer: UIPanGestureRecognizer!
weak var viewController: UIViewController!
fileprivate var propertyAnimator: UIViewPropertyAnimator?
var startProgress: CGFloat = 0.0
var initiallyInteractive = false
var wantsInteractiveStart: Bool {
return initiallyInteractive
}
init(viewController: UIViewController) {
self.viewController = viewController
super.init()
panGestureRecongnizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
viewController.view.addGestureRecognizer(panGestureRecongnizer)
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 8.0 // slow animation for debugging
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
let animator = interruptibleAnimator(using: transitionContext)
if transitionContext.isInteractive {
animator.pauseAnimation()
} else {
animator.startAnimation()
}
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// as per documentation, we need to return existing animator
// for ongoing transition
if let propertyAnimator = propertyAnimator {
return propertyAnimator
}
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else { fatalError("fromVC or toVC not found") }
let containerView = transitionContext.containerView
// Do prep work for animations
let duration = transitionDuration(using: transitionContext)
let timingParameters = UICubicTimingParameters(animationCurve: .easeOut)
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timingParameters)
animator.addAnimations {
// animations
}
animator.addCompletion { [weak self] (position) in
let didComplete = position == .end
if !didComplete {
// transition was cancelled
}
transitionContext.completeTransition(didComplete)
self?.startProgress = 0
self?.propertyAnimator = nil
self?.initiallyInteractive = false
}
self.propertyAnimator = animator
return animator
}
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
initiallyInteractive = true
if !viewController.isBeingDismissed {
viewController.dismiss(animated: true, completion: nil)
} else {
propertyAnimator?.pauseAnimation()
propertyAnimator?.isReversed = false
startProgress = propertyAnimator?.fractionComplete ?? 0.0
}
break
case .changed:
let translation = gestureRecognizer.translation(in: nil)
var progress: CGFloat = translation.y / UIScreen.main.bounds.height
progress = CGFloat(fminf(fmaxf(Float(progress), -1.0), 1.0))
let velocity = gestureRecognizer.velocity(in: nil)
shouldCompleteTransition = progress > 0.3 || velocity.y > 450
propertyAnimator?.fractionComplete = progress + startProgress
break
case .ended:
if shouldCompleteTransition {
propertyAnimator?.startAnimation()
} else {
propertyAnimator?.isReversed = true
propertyAnimator?.startAnimation()
}
break
case .cancelled:
propertyAnimator?.isReversed = true
propertyAnimator?.startAnimation()
break
default:
break
}
}
}
As you can see in the image below I have a video which covers almost the entire screen. I want the video to play/stop whenever one taps on it. The code works almost perfectly fine, the thing is the class in which I call the defInteractions function also contains the booked and comments subview which can also be found below. Consequently, the video also plays/stops when one taps these areas which I don't want.
The UITapGestureRecognizer triggering the function to play/pause the video:
//set interactions
func defInteractions (){
//singletap
let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapDetected(_:)))
singleTap.numberOfTapsRequired = 1
//singleTap.cancelsTouchesInView = false
//controlsContainerView
controlsContainerView.addGestureRecognizer(singleTap)
}
//define type
var player: AVPlayer?
//set playing to false
var isPlaying: Bool = false
func singleTapDetected(_ sender: UITapGestureRecognizer) {
//play or pause
if(!isPlaying){
//play
player?.play()
isPlaying = true
}
else{
//pause
player?.pause()
isPlaying = false
}
}
Each subview looks basically like this:
//create controls container view
let comments: UIView = {
//set properties of controls container view
let commentrect = CGRect(x: viewWidth / 2, y: viewHeight - 110, width: viewWidth / 2, height: 50)
let entireCommentView = UILabel(frame: commentrect)
entireCommentView.translatesAutoresizingMaskIntoConstraints = true
entireCommentView.backgroundColor = .white
entireCommentView.font = UIFont(name: "HelveticaNeue", size: 20)
entireCommentView.text = "3 comments"
entireCommentView.textColor = .black
entireCommentView.textAlignment = .center
return entireCommentView
}()
In the override they are added as subviews. I tried setting isUserInteractionEnabled to false in the individual subviews (e.g. comments: entireCommentView.isUserInteractionEnabled = false) which didn't work and don't know how to achieve my goal. Can someone help me? Can I exclude these subviews from my target in the UITapGestureRecognizer recognizer.
[
EDIT (Result of first answer):
Modify the singleTapDetected to return if the tap is in any of the top or bottom UIViews.
func singleTapDetected(_ sender: UITapGestureRecognizer) {
let view = sender.view
let loc = sender.location(in: controlsContainerView)
if let subview = view?.hitTest(loc, with: nil) {
if subview == entireCommentView || subview == bookedView {
return
}
}
//play or pause
player?.rate != 0 && player?.error == nil ? player?.pause() : player?.play
}
Alternatively, you can have your UIViewController adopt the UIGestureRecognizerDelegate protocol and call the below method for checking the view. Inside this method, check your view against touch.view and return the appropriate bool (Yes/No). Something like this:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return !touch.view?.isDescendant(of: controlsContainerView)
}
You should define the likes and booked variables as property of the UIView subclass itself, to make them visible to your singleTapDetected(_:) method
What I am trying to do is a custom animation of pushing ViewController from the left side.
I have created my custom transitioning delegate and I provide my custom animation, and everything works fine (new view slides from the left side).
The only problem is that push animation in iOS isn't only about sliding a view from the right side. The VC being obscured is also slightly moving in the same directions as the VC being pushed. Also, navigation bar kinda blinks. I can of course try to imitate this behaviour by guessing what the parameters should be (for example how much the VC being obscured moves on different iPhones), but maybe it is possible to find the values somewhere?
Help greatly appreciated.
I would create a UIViewControllerAnimatedTransitioning protocol abiding object
class CustomHorizontalSlideTransition: NSObject, UIViewControllerAnimatedTransitioning {
var operation: UINavigationControllerOperation = .Push
convenience init(operation: UINavigationControllerOperation) {
self.init()
self.operation = operation
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let disappearingVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let appearingVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let bounds = UIScreen.mainScreen().bounds
if self.operation == .Push {
appearingVC.view.frame = CGRectOffset(bounds, -bounds.size.height, 0)
containerView!.addSubview(disappearingVC.view)
containerView!.addSubview(appearingVC.view)
} else {
appearingVC.view.frame = bounds
disappearingVC.view.frame = bounds
containerView!.addSubview(appearingVC.view)
containerView!.addSubview(disappearingVC.view)
}
UIView.animateWithDuration(transitionDuration(transitionContext),
delay: 0.0,
options: UIViewAnimationOptions.CurveEaseInOut,
animations: { () -> Void in
if self.operation == .Push {
appearingVC.view.frame = bounds
} else {
disappearingVC.view.frame = CGRectOffset(bounds, -bounds.size.width, 0)
}
}) { (complete) -> Void in
transitionContext.completeTransition(true)
}
}
}
Then in your "From" and "To" view controllers, set the navigationController's delegate to self in view ViewDidAppear
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.delegate = self
}
The in both view controllers, override the following to provide a transitionAnimatedTransition delegate method and return the protocol abiding instance for your animation
override func transitionAnimatedTransition(operation: UINavigationControllerOperation) -> UIViewControllerAnimatedTransitioning? {
return CustomHorizontalSlideTransition(operation: operation)
}
Right now I have a scrollView that takes up the entire view controller. The code below is able to move the scrollView around but I want to move the whole view controller around. How would I do that?
override func viewDidLoad() {
pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
self.scrollview.addGestureRecognizer(pan)
}
func handlePan(recognizer:UIPanGestureRecognizer!) {
switch recognizer.state {
case .Changed:
handlePanChanged(recognizer); break
case .Ended:
handlePanTerminated(recognizer); break
case .Cancelled:
handlePanTerminated(recognizer); break
case .Failed:
handlePanTerminated(recognizer); break
default: break
}
}
func handlePanChanged(recognizer:UIPanGestureRecognizer!) {
if let view = recognizer.view {
var translation = recognizer.translationInView(self.view)
println("moving")
view.center = CGPointMake(view.center.x, view.center.y + translation.y);
recognizer.setTranslation(CGPointZero, inView: self.view)
}
}
I've tried different variations of "self.view.center ...." "UIApplication.sharedApplication.rootViewController.view.center.." etc.
I infer from your other question that you want to a gesture to dismiss this view controller. Rather than manipulating the view yourself in the gesture, I'd suggest you use custom transition with a UIPercentDrivenInteractiveTransition interaction controller, and have the gesture just manipulate the interaction controller. This achieves the same UX, but in a manner consistent with Apple's custom transitions paradigm.
The interesting question here is how do you want to delineate between the custom dismiss transition gesture and the scroll view gesture. What you want is some gesture that is constrained in some fashion. There are tons of options here:
If the scroll view is left-right only, have a custom pan gesture subclass that fails if you use it horizontally;
If the scroll view is up-down, too, then have a top "screen edge gesture recognizer" or add some visual element that is a "grab bar" to which you tie a pan gesture
But however you design this gesture to work, have the scroll view's gestures require that your own gesture fails before they trigger.
For example, if you wanted a screen edge gesture recognizer, that would look like:
class SecondViewController: UIViewController, UIViewControllerTransitioningDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var interactionController: UIPercentDrivenInteractiveTransition?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .Custom
transitioningDelegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
let edge = UIScreenEdgePanGestureRecognizer(target: self, action: "handleScreenEdgeGesture:")
edge.edges = UIRectEdge.Top
view.addGestureRecognizer(edge)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(edge)
}
}
// because we're using top edge gesture, hide status bar
override func prefersStatusBarHidden() -> Bool {
return true
}
func handleScreenEdgeGesture(gesture: UIScreenEdgePanGestureRecognizer) {
switch gesture.state {
case .Began:
interactionController = UIPercentDrivenInteractiveTransition()
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
let percent = gesture.translationInView(gesture.view).y / gesture.view!.frame.size.height
interactionController?.updateInteractiveTransition(percent)
case .Cancelled:
fallthrough
case .Ended:
if gesture.velocityInView(gesture.view).y < 0 || gesture.state == .Cancelled || (gesture.velocityInView(gesture.view).y == 0 && gesture.translationInView(gesture.view).y < view.frame.size.height / 2.0) {
interactionController?.cancelInteractiveTransition()
} else {
interactionController?.finishInteractiveTransition()
}
interactionController = nil
default: ()
}
}
#IBAction func didTapDismissButton(sender: UIButton) {
dismissViewControllerAnimated(true, completion: nil)
}
// MARK: UIViewControllerTransitioningDelegate
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimation()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
class DismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.25
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let container = transitionContext.containerView()!
let height = container.bounds.size.height
UIView.animateWithDuration(transitionDuration(transitionContext), animations:
{
from.view.transform = CGAffineTransformMakeTranslation(0, height)
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
Personally, I find the notion of having top and bottom screen edge gestures to be a bad UX, so I'd personally change this modal presentation to slide in from the right, and then swiping from left edge to the right feels logical, and doesn't interfere with the built in top pull down (for iOS notifications). Or if the scroll view only scrolls horizontally, then you can just have your own vertical pan gesture that fails if it's not a vertical pan.
Or, if the scroll view only scrolls left and right, you can add your own pan gesture that is only recognized when you pull down by (a) using UIGestureRecognizerDelegate to recognize downward pans only; and (b) again setting the scroll view gestures to only recognize gestures if our pull-down gesture fails:
override func viewDidLoad() {
super.viewDidLoad()
// ...
let pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
pan.delegate = self
view.addGestureRecognizer(pan)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(pan)
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let gesture = gestureRecognizer as? UIPanGestureRecognizer {
let translation = gesture.translationInView(gesture.view)
let angle = atan2(translation.x, translation.y)
return abs(angle) < CGFloat(M_PI_4 / 2.0)
}
return true
}
func handlePan(gesture: UIPanGestureRecognizer) {
// the same as the `handleScreenEdgeGesture` above
}
Like I said, tons of options here. But you haven't shared enough of your design for us to advise you further on that.
But the above illustrates the basic idea, that you shouldn't be moving the view around yourself, but rather use custom transition with your own animators and your own interactive controller.
For more information, see WWDC 2013 Custom Transitions Using View Controllers (and also WWDC 2014 A Look Inside Presentation Controllers, if you want a little more information on the evolution of custom transitions).
Right now I have a scrollView that takes up the entire view controller. The code below is able to move the scrollView around but I want to move the whole view controller around. How would I do that?
override func viewDidLoad() {
pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
self.scrollview.addGestureRecognizer(pan)
}
func handlePan(recognizer:UIPanGestureRecognizer!) {
switch recognizer.state {
case .Changed:
handlePanChanged(recognizer); break
case .Ended:
handlePanTerminated(recognizer); break
case .Cancelled:
handlePanTerminated(recognizer); break
case .Failed:
handlePanTerminated(recognizer); break
default: break
}
}
func handlePanChanged(recognizer:UIPanGestureRecognizer!) {
if let view = recognizer.view {
var translation = recognizer.translationInView(self.view)
println("moving")
view.center = CGPointMake(view.center.x, view.center.y + translation.y);
recognizer.setTranslation(CGPointZero, inView: self.view)
}
}
I've tried different variations of "self.view.center ...." "UIApplication.sharedApplication.rootViewController.view.center.." etc.
I infer from your other question that you want to a gesture to dismiss this view controller. Rather than manipulating the view yourself in the gesture, I'd suggest you use custom transition with a UIPercentDrivenInteractiveTransition interaction controller, and have the gesture just manipulate the interaction controller. This achieves the same UX, but in a manner consistent with Apple's custom transitions paradigm.
The interesting question here is how do you want to delineate between the custom dismiss transition gesture and the scroll view gesture. What you want is some gesture that is constrained in some fashion. There are tons of options here:
If the scroll view is left-right only, have a custom pan gesture subclass that fails if you use it horizontally;
If the scroll view is up-down, too, then have a top "screen edge gesture recognizer" or add some visual element that is a "grab bar" to which you tie a pan gesture
But however you design this gesture to work, have the scroll view's gestures require that your own gesture fails before they trigger.
For example, if you wanted a screen edge gesture recognizer, that would look like:
class SecondViewController: UIViewController, UIViewControllerTransitioningDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var interactionController: UIPercentDrivenInteractiveTransition?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .Custom
transitioningDelegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
let edge = UIScreenEdgePanGestureRecognizer(target: self, action: "handleScreenEdgeGesture:")
edge.edges = UIRectEdge.Top
view.addGestureRecognizer(edge)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(edge)
}
}
// because we're using top edge gesture, hide status bar
override func prefersStatusBarHidden() -> Bool {
return true
}
func handleScreenEdgeGesture(gesture: UIScreenEdgePanGestureRecognizer) {
switch gesture.state {
case .Began:
interactionController = UIPercentDrivenInteractiveTransition()
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
let percent = gesture.translationInView(gesture.view).y / gesture.view!.frame.size.height
interactionController?.updateInteractiveTransition(percent)
case .Cancelled:
fallthrough
case .Ended:
if gesture.velocityInView(gesture.view).y < 0 || gesture.state == .Cancelled || (gesture.velocityInView(gesture.view).y == 0 && gesture.translationInView(gesture.view).y < view.frame.size.height / 2.0) {
interactionController?.cancelInteractiveTransition()
} else {
interactionController?.finishInteractiveTransition()
}
interactionController = nil
default: ()
}
}
#IBAction func didTapDismissButton(sender: UIButton) {
dismissViewControllerAnimated(true, completion: nil)
}
// MARK: UIViewControllerTransitioningDelegate
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimation()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
class DismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.25
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let container = transitionContext.containerView()!
let height = container.bounds.size.height
UIView.animateWithDuration(transitionDuration(transitionContext), animations:
{
from.view.transform = CGAffineTransformMakeTranslation(0, height)
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
Personally, I find the notion of having top and bottom screen edge gestures to be a bad UX, so I'd personally change this modal presentation to slide in from the right, and then swiping from left edge to the right feels logical, and doesn't interfere with the built in top pull down (for iOS notifications). Or if the scroll view only scrolls horizontally, then you can just have your own vertical pan gesture that fails if it's not a vertical pan.
Or, if the scroll view only scrolls left and right, you can add your own pan gesture that is only recognized when you pull down by (a) using UIGestureRecognizerDelegate to recognize downward pans only; and (b) again setting the scroll view gestures to only recognize gestures if our pull-down gesture fails:
override func viewDidLoad() {
super.viewDidLoad()
// ...
let pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
pan.delegate = self
view.addGestureRecognizer(pan)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(pan)
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let gesture = gestureRecognizer as? UIPanGestureRecognizer {
let translation = gesture.translationInView(gesture.view)
let angle = atan2(translation.x, translation.y)
return abs(angle) < CGFloat(M_PI_4 / 2.0)
}
return true
}
func handlePan(gesture: UIPanGestureRecognizer) {
// the same as the `handleScreenEdgeGesture` above
}
Like I said, tons of options here. But you haven't shared enough of your design for us to advise you further on that.
But the above illustrates the basic idea, that you shouldn't be moving the view around yourself, but rather use custom transition with your own animators and your own interactive controller.
For more information, see WWDC 2013 Custom Transitions Using View Controllers (and also WWDC 2014 A Look Inside Presentation Controllers, if you want a little more information on the evolution of custom transitions).