This is my custom transitioningDelegate:
enum CameraState {
case On
case Off
}
class CameraTransitioning: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var state: CameraState
init(state: CameraState) {
self.state = state
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
var toViewInitialFrame = transitionContext.initialFrameForViewController(toVC!)
var fromViewFinalFrame = transitionContext.finalFrameForViewController(fromVC!)
switch self.state {
case .On:
toViewInitialFrame.origin.y = containerView!.frame.height
case .Off:
fromViewFinalFrame.origin.y = -containerView!.frame.height
}
containerView?.addSubview(toView!)
toView?.frame = toViewInitialFrame
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
fromView?.frame = fromViewFinalFrame
}) {
finished in
transitionContext.completeTransition(true)
}
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 10
}
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
And this is how I use it:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "Home -> Camera" {
let cameraVC = segue.destinationViewController as! CameraViewController
cameraVC.delegate = self
cameraVC.transitioningDelegate = CameraTransitioning(state: .On)
}
}
As you can see, I use this transitioning because I don't like the default UIViewAnimationCurveEaseInOut, and I have tried to set the duration to 10 to make this change clear. But this doesn't work. Where is the problem?
The transitioningDelegate property is weak, and you are creating no other strong references to it. Something else needs to own that object for it to stick around long enough to be used to animate the transition.
Related
I am facing an incomprehensible problem I have a CollectionViewController and I want to make a custom animation.
My collection is a gallery and I want to switch from collection gallery. to fullscreen gallery.
So I have ControllerTransitionDelegate
extension NavigationGalleryViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return DimmingPresentationController(presentedViewController: presented, presenting: presenting)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let selectedCellFrame = self.collectionView?.cellForItem(at: IndexPath(item: index, section: 0))?.frame else { return nil }
return PresentingAnimator(pageIndex: index, originFrame: selectedCellFrame)
}
My DimmingPresentationController
class DimmingPresentationController: UIPresentationController {
lazy var background = UIView(frame: .zero)
override var shouldRemovePresentersView: Bool {
return false
}
override func presentationTransitionWillBegin() {
setupBackground()
// Grabing the coordinator responsible for the presentation so that the background can be animated at the same rate
if let coordinator = presentedViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { (_) in
self.background.alpha = 1
}, completion: nil)
}
}
private func setupBackground() {
background.backgroundColor = UIColor.black
background.autoresizingMask = [.flexibleWidth, .flexibleHeight]
background.frame = containerView!.bounds
containerView!.insertSubview(background, at: 0)
background.alpha = 0
}
}
And my presenting animator
class PresentingAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private let indexPath: IndexPath
private let originFrame: CGRect
private let duration: TimeInterval = 0.5
init(pageIndex: Int, originFrame: CGRect) {
self.indexPath = IndexPath(item: pageIndex, section: 0)
self.originFrame = originFrame
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from) as? NavigationGalleryViewController, // The problem is here !
let fromView = fromVC.collectionView?.cellForItem(at: indexPath) as? InstallationViewCell
else {
transitionContext.completeTransition(true)
return
}
// All the animation things
}
My BIG problem is that my execution go inside the else because he can't find the FromVC from the transitionContext.viewController.
And here is how I call my Gallery
gallery = SwiftPhotoGallery(delegate: self, dataSource: self)
// Gallery visual colours stuff
gallery.modalPresentationStyle = .custom
gallery.transitioningDelegate = self
present(gallery, animated: true, completion: { () -> Void in
self.gallery.currentPage = self.index
})
}
This is what I receive from the transitionContext :
Why the transitionContext won't give me the right VC ?
Well, I checked and noticed that transitionContext.viewController(forKey: .from) is NavigationController.
In line: let fromVC = transitionContext.viewController(forKey: .from) as? NavigationGalleryViewController should be nil, because it is not NavigationGalleryViewController but NavigationController.
If you want you can make smth like this: let fromVC = transitionContext.viewController(forKey: .from).childViewControllers.first as? NavigationGalleryViewController
I made a custom UIViewControllerAnimatedTransitioning for the dismissal of my detail view controller as so:
class DismissAnimator : NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)?.childViewControllers.first as? MainController,
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? DetailViewController
else {
return
}
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
fromVC.view.isHidden = true
let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false)
containerView.insertSubview(snapshot!, aboveSubview: toVC.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
snapshot!.center.y += UIScreen.main.bounds.height
},
completion: { _ in
fromVC.view.isHidden = false
snapshot?.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
My MainViewController has the following functions:
extension MainController: UIViewControllerTransitioningDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "openDetailView" {
let cell = sender as! PopularCell
let indexPath = popularCollectionView.indexPath(for: cell)
let destinationViewController = segue.destination as! DetailViewController
destinationViewController.transitioningDelegate = self
destinationViewController.event = events[indexPath!.row]
destinationViewController.interactor = interactor
}
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return OpeningAnimator()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimator()
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
and this is how my pan interaction is being handled:
#IBAction func handleGesture(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
let translation = sender.translation(in: view)
let progress = progressAlongAxis(pointOnAxis: translation.y, axisLength: view.bounds.height)
guard let interactor = interactor,
let originView = sender.view else { return }
switch originView {
case view:
break
case tableView:
if tableView.contentOffset.y > 0 {
return
}
default:
break
}
switch sender.state {
case .began:
interactor.hasStarted = true
dismiss(animated: true, completion: nil)
case .changed:
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
interactor.shouldFinish
? interactor.finish()
: interactor.cancel()
default:
break
}
}
Unfortunately, as soon as I barely drag the modal view controller, it disappears automatically, with no interactivity. Putting a breakpoint in the handleGesture(sender:) shows that the function is being called so I'm confused as to what I should do now.
Any help?
Swift 3 - IOS 10 - Xcode 8
I want create an interactive transition. I have two view controller :
RootViewController (Source - SVC)
PlayerViewController (Destination - DVC)
But, when my DVC is presented, it never execute my animation and related methods. It execute the system animation.
My RootViewController :
class RootViewController: UIViewController {
var transitionManager = PlayerTransitionManager()
override func viewDidLoad() {
super.viewDidLoad()
self.transitionManager.sourceViewController = self
// ...
}
}
My TransitionManager :
class PlayerTransitionManager: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = false
private var interactive = false
private var enterPanGesture: UIPanGestureRecognizer!
private var exitPanGesture: UIPanGestureRecognizer!
var sourceViewController: RootViewController! {
didSet {
self.enterPanGesture = UIPanGestureRecognizer()
self.enterPanGesture.addTarget(self, action:#selector(PlayerTransitionManager.handleOnstagePan(_:)))
self.sourceViewController.playerV.addGestureRecognizer(self.enterPanGesture)
}
}
var destinationViewController: UIViewController! {
didSet {
self.exitPanGesture = UIPanGestureRecognizer()
self.exitPanGesture.addTarget(self, action:#selector(PlayerTransitionManager.handleOffstagePan(_:)))
self.destinationViewController.view.addGestureRecognizer(self.exitPanGesture)
}
}
func handleOnstagePan(_ pan: UIPanGestureRecognizer){
let translation = pan.translation(in: pan.view!)
let d = translation.y / pan.view!.bounds.height * -0.5
switch (pan.state) {
case .began:
self.interactive = true
let storyboard = UIStoryboard(name: "Main", bundle: nil) // OK
let controller = storyboard.instantiateViewController(withIdentifier: "PlayerVC") // OK
controller.transitioningDelegate = self
controller.modalPresentationStyle = .custom
self.destinationViewController = controller
self.sourceViewController.present(controller, animated: true, completion: nil) // present with system annimation
break
case .changed:
// Never Executed
self.update(d)
break
default: // .Ended, .Cancelled, .Failed ...
// Finish or cancel.
// No impact because annimation never started
break
}
}
func handleOffstagePan(_ pan: UIPanGestureRecognizer){
// For dismiss
// Same problem
}
// MARK: UIViewControllerAnimatedTransitioning protocol methods
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Nerver Executed
// ...
}
func offStage(amount: CGFloat) -> CGAffineTransform {
// Nerver Executed
// ...
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
// Nerver Executed
// ...
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// Nerver Executed
// ...
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// Nerver Executed
// ...
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// Nerver Executed
// ...
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// Nerver Executed
// ...
}
}
I want to use interactive transitions in my app. I have two view controllers. And when user touches a button in first view controller I am presenting second view controller modally. My custom animation is working well but interactive transition is not working. I added a gesture to left edge of screen and when I pan from left edge second view controller is presenting but not interactive it is working as same as touching to button for presenting.
My class:
class MenuTransitionManager: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {
private var interactive = false
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
let containerView = transitionContext.containerView()
let bounds = UIScreen.mainScreen().bounds
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
containerView!.addSubview(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
fromViewController.view.alpha = 0.5
toViewController.view.frame = finalFrameForVC
}, completion: {
finished in
transitionContext.completeTransition(true)
fromViewController.view.alpha = 1.0
})
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// if our interactive flag is true, return the transition manager object
// otherwise return nil
return self.interactive ? self : nil
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
private var enterPanGesture: UIScreenEdgePanGestureRecognizer!
var sourceViewController: UIViewController! {
didSet {
self.enterPanGesture = UIScreenEdgePanGestureRecognizer()
self.enterPanGesture.addTarget(self, action:"handleOnstagePan:")
self.enterPanGesture.edges = UIRectEdge.Left
self.sourceViewController.view.addGestureRecognizer(self.enterPanGesture)
}
}
func handleOnstagePan(pan: UIPanGestureRecognizer){
// how much distance have we panned in reference to the parent view?
let translation = pan.translationInView(pan.view!)
// do some math to translate this to a percentage based value
let d = translation.x / CGRectGetWidth(pan.view!.bounds) * 0.5
// now lets deal with different states that the gesture recognizer sends
switch (pan.state) {
case UIGestureRecognizerState.Began:
// set our interactive flag to true
self.interactive = true
// trigger the start of the transition
self.sourceViewController.performSegueWithIdentifier("showAction", sender: self)
break
case UIGestureRecognizerState.Changed:
// update progress of the transition
self.updateInteractiveTransition(d)
break
default: // .Ended, .Cancelled, .Failed ...
// return flag to false and finish the transition
self.interactive = false
self.finishInteractiveTransition()
}
}
}
My first view controller:
override func viewDidLoad() {
super.viewDidLoad()
self.transitionManager.sourceViewController = self
}
var transitionManager = MenuTransitionManager()
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transitionManager
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAction" {
let toViewController = segue.destinationViewController as UIViewController
toViewController.transitioningDelegate = self
toViewController.modalPresentationStyle = .Custom
}
}
How can I fix it?
You also need to implement interactionControllerForPresentation(_:) on the view controller and vend an instance of UIViewControllerInteractiveTransitioning (which is a sub-protocol of UIPercentDrivenInteractiveTransition that your MenuTransitionManager class already implements).
The documentation around this subject is actually pretty good.
I'm lost in the universe of the transitions. I want an interactive transition with a push segue. The following code works with a modal segue, but not with a push one :
(With a push segue, the animation is not interactive and is reversed)
FirstViewController.swift
let transitionManager = TransitionManager()
override func viewDidLoad() {
super.viewDidLoad()
transitionManager.sourceViewController = self
// Do any additional setup after loading the view.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let dest = segue.destinationViewController as UIViewController
dest.transitioningDelegate = transitionManager
transitionManager.destViewController = dest
}
TransitionManager.swift
class TransitionManager: UIPercentDrivenInteractiveTransition,UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate,UIViewControllerInteractiveTransitioning {
var interactive = false
var presenting = false
var panGesture : UIPanGestureRecognizer!
var destViewController : UIViewController!
var sourceViewController : UIViewController! {
didSet {
panGesture = UIPanGestureRecognizer(target: self, action: "gestureHandler:")
sourceViewController.view.addGestureRecognizer(panGesture)
}
}
func gestureHandler(pan : UIPanGestureRecognizer) {
let translation = pan.translationInView(pan.view!)
let velocity = pan.velocityInView(pan.view!)
let d = translation.x / pan.view!.bounds.width * 0.5
switch pan.state {
case UIGestureRecognizerState.Began :
interactive = true
sourceViewController.performSegueWithIdentifier("1to2", sender: self)
case UIGestureRecognizerState.Changed :
self.updateInteractiveTransition(d)
default :
interactive = false
if d > 0.2 || velocity.x > 0 {
self.finishInteractiveTransition()
}
else {
self.cancelInteractiveTransition()
}
}
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// create a tuple of our screens
let screens : (from:UIViewController, to:UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)
let container = transitionContext.containerView()
let toView = screens.to.view
let fromView = screens.from.view
toView.frame = CGRectMake(-320, 0, container.frame.size.width, container.frame.size.height)
container.addSubview(toView)
container.addSubview(fromView)
let duration = self.transitionDuration(transitionContext)
// perform the animation!
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
toView.frame.origin = container.frame.origin
fromView.frame.origin = CGPointMake(320, 0)
}, completion: { finished in
if(transitionContext.transitionWasCancelled()){
transitionContext.completeTransition(false)
UIApplication.sharedApplication().keyWindow.addSubview(screens.from.view)
}
else {
transitionContext.completeTransition(true)
UIApplication.sharedApplication().keyWindow.addSubview(screens.to.view)
}
})
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 1
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = true
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
}
Storyboard
The segue is from the FirstViewController to the SecondViewController.
Identifier : "1to2"
Segue : Push
Destination : Current
Thanks for your help