Update to correct version, since have got the answer.
I want to do fade in/fade out animation when navigation view controller pop up /push, then I implement a BaseViewController:
class BaseViewController: UIViewController, UINavigationControllerDelegate {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if (operation == UINavigationControllerOperation.Push) {
return FadeInAnimator()
}
if (operation == UINavigationControllerOperation.Pop) {
return FadeOutAnimator()
}
return nil;
}
}
and FadeInAnimator, FadeOutAnimator:
class FadeInAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
transitionContext.containerView()?.addSubview(toViewController!.view)
toViewController?.view.alpha = 0.0
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { () -> Void in
toViewController?.view.alpha = 1.0
}) { (finished) -> Void in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
}
class FadeOutAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
transitionContext.containerView()?.insertSubview((toViewController?.view)!, belowSubview: (fromViewController?.view)!)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { () -> Void in
fromViewController?.view.alpha = 0.0
}) { (finished) -> Void in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
Then, I add 2 view controllers and embed navigation view controller, ViewControllerA and ViewControllerB:
class ViewControllerA: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func pressButton(sender: UIButton) {
if let vc = create(ViewControllerB) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
class ViewControllerB: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
press button on ViewControllerA will create and jump to ViewControllerB, then press navigation bar item "back" on ViewControllerB will back to ViewControllerA.
But it doesn't work, when press button, the ViewControllerA show ViewControllerB, then show ViewControllerA again.
any suggestion? my xcode is 7.1 and run on iPad Air 2 simulator.
in your FadeInAnimator at animation completion block
change this
transitionContext.completeTransition(transitionContext.transitionWasCancelled())
to
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
Related
I learn the book iOS Animations by Tutorials
But I don't use Storyboard. I have several ViewControllers created programmatically. I have added RootVC in AppDelegate.swift. This application is working without navigation to the RootVC (going to the beginning) and the screens looks like that:
My question is about how to create such a navigation between different screens (ViewControllers) in Swift 4 (Xcode 10.2.1). It looks like there is an issue with looping... when the last ViewController instantiates the first RootVC and so on...
At the end I would like to have different custom navigation transitions on one ViewController (with .present() and with .navigationController?.pushViewController()
import UIKit
class FadePresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
var dismissCompletion: (()->Void)?
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
//Setting the transition’s context
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//Adding a fade transition
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animate(withDuration: duration,
animations: {
toView.alpha = 1.0
},
completion: { _ in
transitionContext.completeTransition(true)
}
)
}
}
import UIKit
//UIViewControllerTransitioningDelegate for self.present(self.nextScreen, animated: true, completion: nil)
//UINavigationControllerDelegate for self.navigationController?.pushViewController(self.nextScreen, animated: true)
class Screen3: UIViewController, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
let nextScreen = RootVC() //4th Screen //<----- EXEC ERRROR
let transition = FadePresentAnimator()
let btnSize:CGFloat = 56.0
let btn1 = ClickableButton()
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Screen 3"
view.backgroundColor = HexColor.Named.BabyBlue
self.navigationController?.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupLayout()
}
private func setupLayout() {
//btn1:
view.addSubview(btn1)
btn1.setDefaultTitle(title: "▶") // ⏹ "▶" "■"
btn1.apply(height: btnSize)
btn1.apply(width: btnSize)
btn1.applyDefaultStyle()
if #available(iOS 11.0, *) {
btn1.alignXCenter(to: view.safeAreaLayoutGuide.centerXAnchor)
} else {
// Fallback on earlier versions
}
if #available(iOS 11.0, *) {
btn1.alignYCenter(to: view.safeAreaLayoutGuide.centerYAnchor)
} else {
// Fallback on earlier versions
}
btn1.clickHandler {
self.nextScreen.transitioningDelegate = self
//self.present(self.nextScreen, animated: true, completion: nil)
self.navigationController?.pushViewController(self.nextScreen, animated: true)
}
}
//forward
func animationController(forPresented presented: UIViewController,
presenting: UIViewController, source: UIViewController) ->
UIViewControllerAnimatedTransitioning? {
return transition
}
//backward
func animationController(forDismissed dismissed: UIViewController) ->
UIViewControllerAnimatedTransitioning? {
return nil
}
}
//
// AppDelegate.swift
// Anime-Control-01
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//No Storyboards!
window = UIWindow(frame:UIScreen.main.bounds)
window?.makeKeyAndVisible()
let rootVC = RootVC() //RootVC.swift
let rootController = UINavigationController(rootViewController: rootVC)
window?.rootViewController = rootController
return true
}
}
It looks like there is an issue with looping... when the last ViewController instantiates the first RootVC and so on...
Yeah, you're totally right. The thing is you're creating next ViewController right when current ViewController is initialized.
The simplest way to fix this is to make nextScreen initialized lazily "on demand" by replacing this line
let nextScreen = RootVC()
by this one lazy var nextScreen = RootVC()
Or to create nextScreen variable right before the transition:
btn1.clickHandler {
let nextScreen = RootVC()
nextScreen.transitioningDelegate = self
navigationController?.pushViewController(nextScreen, animated: true)
}
I have a UITabBarController which has 9 view controllers and I am custom handling the navigation with a forward and back button. I have created a UIViewControllerAnimatedTransitioning object to handle the animation (simple left to right) and it gets returned in the delegate method:
tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
However, on and after selecting the fifth index, this function is no longer called (as it is now handled by the UIMoreNavigationController. Is there an animation delegate or some way I should be handling it for the UIMoreNavigationController instance?
You would need to specify the delegate of your moreNavigationController. So in your UITabBarController class you would need to:
self.moreNavigationController.delegate = strongDelegate // where strongDelegate is a local instance of MyControllerDelegate as defined below
The delegate should then implement the navigationController function which will call the applicable UIViewControllerAnimatedTransitioning instance:
class MyControllerDelegate: NSObject, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return MyPresentationAnimationController(isPresenting: true)
case .pop:
return MyPresentationAnimationController(isPresenting: false)
default:
return nil
}
}
}
Your animateTransition function contains the logic where the magic happens:
class MyPresentationAnimationController : NSObject, UIViewControllerAnimatedTransitioning {
private let isPresenting: Bool
init(isPresenting: Bool) {
self.isPresenting = isPresenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from)
else {
return
}
let containerView = transitionContext.containerView
// TODO: Implement the logic with UIView.animateKeyframes here ...
}
}
Can somebody explain why my fromViewController is nil? I am trying to understand custom transition animations in iOS going back and forth between two different view controllers.
Removing the guard part of my code and modifying it a little bit, my present animation works, but I cannot figure out how to implement the dismiss animation. Any suggestions?
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
let containerView = transitionContext.containerView
guard
let fromViewController = transitionContext.view(forKey: .from),
let toViewController = transitionContext.view(forKey: .to)
else
{
print("Nope")
return
}
containerView.addSubview(toViewController)
if (presentationMode == .Present)
{
toViewController.alpha = 0.0
UIView.animate(withDuration: 1.0,
animations: {
toViewController.alpha = 1.0
},
completion: {
_ in transitionContext.completeTransition(true)
})
}
Edit: This is where I use the animation
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
let secondVC = segue.destination as! SignUpViewController
secondVC.transitioningDelegate = self
}
extension LoginViewController: UIViewControllerTransitioningDelegate
{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
print("Presenting")
presentationTransition.presentationMode = .Present
return presentationTransition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
print("Dismissing")
presentationTransition.presentationMode = .Dismiss
return presentationTransition
}
}`
I have followed this tutorial and watched the WWDC video on this subject but I couldn't find my answer.
I have almost the same transition in my code. It is working pretty well when I am doing it as a presented view, but not as a pushed view.
It is supposed to animate a snapshot of the pushed view from a CGRect to the full screen and vice versa when popped.
Here is the code of my UIViewControllerAnimatedTransitioning class:
class ZoomingTransitionController: NSObject, UIViewControllerAnimatedTransitioning {
let originFrame: CGRect
let isDismissing: Bool
init(originFrame: CGRect, isDismissing: Bool) {
self.originFrame = originFrame
self.isDismissing = isDismissing
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return Constant.Animation.VeryShort
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
return
}
let finalFrame = transitionContext.finalFrame(for: toVC)
toVC.view.frame = finalFrame
let snapshot = self.isDismissing ? fromVC.view.snapshotView(afterScreenUpdates: true) : toVC.view.snapshotView(afterScreenUpdates: true)
snapshot?.frame = self.isDismissing ? finalFrame : self.originFrame
snapshot?.layer.cornerRadius = Constant.FakeButton.CornerRadius
snapshot?.layer.masksToBounds = true
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot!)
if self.isDismissing {
fromVC.view.isHidden = true
} else {
toVC.view.isHidden = true
}
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration,
animations: {
snapshot?.frame = self.isDismissing ? self.originFrame : finalFrame
},
completion: { _ in
if self.isDismissing {
fromVC.view.isHidden = false
} else {
toVC.view.isHidden = false
}
snapshot?.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Then, I tried to show a new View Controller using 2 ways: by presenting it and by pushing it.
My FromViewController is subclassing both UINavigationControllerDelegate and UIViewControllerTransitioningDelegate.
FromViewController class presenting the ToViewController (which works fine):
func buttonAction(_ sender: AnyObject) {
self.tappedButtonFrame = sender.frame
let toVC = self.storyboard!.instantiateViewController(withIdentifier: "ToViewController")
toVC.transitioningDelegate = self
self.present(toVC, animated: true, completion: nil)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transitionController = ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: false)
return transitionController
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transitionController = ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: true)
return transitionController
}
FromViewController class pushing the ToViewController (which doesn't work):
func buttonAction(_ sender: AnyObject) {
self.tappedButtonFrame = sender.frame
let toVC = self.storyboard!.instantiateViewController(withIdentifier: "ToViewController")
self.navigationController?.pushViewController(toVC, animated: true)
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: false)
case .pop:
return ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: true)
default:
return nil
}
}
When pushing, the delegate method is called and the ZoomingTransitionController performs its code fine (going in animateTransition until the end without notable issue). But on the screen, the snapshot view isn't display at any moment. The ToVC appears after the transition duration, but without anything else meanwhile.
I am running out of idea on how to debug this... Do you have any idea?
Thanks!!
I found my answer by replacing the snapshot element (which was causing the problem) by a CGAffineTransform of the toVC.
Code is almost the same than here.
I encountered a strange bug. I am just using iOS's custom transitioning method for UIViewControllers using UIViewControllerTransitioningDelegate together with an implementation of UIViewControllerAnimatedTransitioning. It all seems to work fine, until I do exactly the following:
open the app
present another view controller with my custom transition
rotate to landscape
dismiss the just presented view controller
That's all! What happens now is the following: I see a large black bar on the right side of the initial view controller (as if that controller's view wasn't rotated to landscape).
The funny thing is this only goes wrong in iOS 9, in iOS 8 everything seems to work just fine. Did anything change with custom transition API I don't know of? Or is this simply a really nasty iOS 9 bug? If anyone can tell me what I did wrong or if anyone can provide me with a workaround I would really appreciate that!
These classes reproduce the problem:
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
let controller = ModalViewController()
controller.transitioningDelegate = self
presentViewController(controller, animated: true, completion: nil)
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
}
The presented view controller:
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.redColor()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
dismissViewControllerAnimated(true, completion: nil)
}
}
And finally the UIViewControllerAnimatedTransitioning implementation:
import UIKit
class Transitioning: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let containerView = transitionContext.containerView()
if let fromView = fromView, toView = toView {
containerView?.addSubview(fromView)
containerView?.addSubview(toView)
toView.alpha = 0
UIView.animateWithDuration(0.5, animations: {
toView.alpha = 1
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
}
}
I generally use the following in animateTransition:
toView.frame = fromView.frame
FYI, you don't have to add fromView to the hierarchy, as it's already there.