Basic problem with Custom transition using Navigation controller - ios

In my work on an iOS multi-screen App, I am trying to get my first custom animated transaction to work. Instead of using standard animations when presenting a screen modal and fullscreen, I want to have a horizontal slide in animation.
I have made this work by overriding perform in UIStoryboardSegue, and make the animation and transaction be performed from here.
Overall, it works, except from one case, which is when a navigation controller embeds a view.
In that case, I have followed a tutorial showing precisely how custom transaction is achieved by implementing the UINavigationControllerDelegate protocol for the particular navigation controller.
import Foundation
import UIKit
class AnimatedNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return NavigationControllerTransition()
}
}
Unfortunately the delegate, which should return an animation object:
navigationController(_:animationControllerFor:from:to:)
is never called, and that is my primary problem.
In the tutorial mentioned, a button is used to perform a call to the navigation controller that pushes the destination view with animation set to true:
navigationController.pushViewController(view, animated:true)
In my case, I suspect that the problem is that the navigation controller pushes the view without having the animation parameter set to true, and that explains why the transaction delegate is never called.
(a confirmation off this theory will of course help a bit).
Consequently, I have tried to implement a custom segue that presents the view by calling the push function with animation set to true.
Sadly, I have not been able to get this to work.
When the navigationController.pushViewController(view, animated:true) is called, it raises an exception saying that view has already been pushed.
I have tried a lot without any luck. Code looks like this:
class NavigationControllerSegue: UIStoryboardSegue {
override func perform() {
let navigationController = self.destination as! UINavigationController
let view = navigationController.viewControllers.first!
navigationController.pushViewController(view, animated: true)
}
}

Hey first of all we are talking about two different animations... the first "UIStoryboardSegue" creates a custom subclass UIStoryboardSegue, and this is assigned to the following in question set an id to the following, below just call from the 'ViewController' a self.performSegue(withIdentifier: "YOUR-ID", sender: nil), Example of the subclass UIStoryboardSegue:
import UIKit
// Example =>
class ScaleSegue: UIStoryboardSegue {
override func perform() {
self.scale()
}
func scale() {
let toViewController = self.destination
let fromViewController = self.source
let containerView = fromViewController.view.superview
let originalCenter = fromViewController.view.center
//let originalCenter = self.posizioneClick
toViewController.view.transform = CGAffineTransform.init(scaleX: 0.05, y: 0.05)
toViewController.view.center = originalCenter
containerView?.addSubview(toViewController.view)
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseInOut, animations: {
toViewController.view.transform = CGAffineTransform.identity
}, completion: { success in
fromViewController.present(toViewController, animated: false, completion: nil)
})
}
}
Now, you have to create a segue from storyboard and set as custom class of the segue from the AttributeInspector:
Now to use this you just need to call from the view controller => self.performSegue(withIdentifier: "segue1", sender: nil). Different is the way to implement Transition with UIViewControllerTransitioningDelegate.
If you need to let me know, I can give you examples using this technique too, so long.

Related

How to make UIViewControllerContextTransitioning modular for UINavigationControllerDelegate?

I want to create a UIViewController which has always the same in and out transitions applied, without side-effects on all other UIViewController transitions.
Current state
I created a custom transition:
class FadeTransition: UIViewControllerAnimatedTransitioning {
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.45
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
transitionContext.completeTransition(false)
return
}
toVc.view.alpha = 0.0
transitionContext.containerView.addSubview(toVc.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
toVc.view.alpha = 1.0
}, completion: {_ in
transitionContext.containerView.addSubview(toVc.view)
fromVc.view.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
and then apply it to the given VC:
class CustomTransitionVc: UIViewController, UINavigationControllerDelegate {
let transition = FadeTransition()
init() {
// self.navigationController?.delegate = self // 1. cannot work
Coordinator.shared.navigationController.delegate = self // 2. workaround
}
// MARK: - UINavigationControllerDelegate
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transition
}
Problem
The transition is applied to any view controller pushed and popped in the navigation stack as long as the delegate (the CustomTransitionVc) is not deinit'd.
Questions
I would like the transition only be applied to the current view controller being pushed and popped. Making it more modular and avoiding overlaps of different view controller setting the navigationController.delegate. I can't figure out if this UIKit structure is a bit badly designed (forcing a centralized approach to something that should be modular) or i am using it wrong (or both). Do i have to take care of applying the correct transition to the correct view controller in a centralized place and removing r reapplying the delegate for each screen? Or is there a pattern to reset the delegate in the transition itself?
Is it normal that in order for the transition to be applied when the view controller is pushed (and not only when it pushes another VC) i have to set the delegate in the init() method and not in the viewDidLoad() (it does not work), but at the same time the self.navigationController is not yet available in the init() so i have to use an external reference?
(Bonus) If you could take a look at the implementation of the animation itself would also be great.
--
Docs UIViewcontrollerAnimatedTransitioning
Docs UINavigationDelegate

All view is disappear from view hierarchy after animate transitioning using UIViewControllerContextTransitioning

https://i.stack.imgur.com/kqKLf.gif
Problem:
When implementing the Present transition using UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning, if modalPresentationStyle is not .fullScreen, the return of the view(forKey: .from) method of UIViewControllerContextTransitioning is nil.
In the case of Dismiss, on the contrary, the return of view(forKey: .to) is nil. So, if I use the viewController view returned by viewController(forKey: .to) for animation, when the transition is complete, nothing remains in the view layer, and a black screen is displayed.
SomePresentingViewController.swift
let somePresentedViewController = SomePresentedViewController()
somePresentedViewController.transitioningDelegate = somePresentedViewController.transitionController
self.present(somePresentedViewController, animated: true, completion: nil)
SomePresentedViewController.swift
class SomePresentedViewController: UIViewController {
var transitionController = TransitionController()
#IBAction func closeButtonTapped(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
...
TransitionController.swift
class TransitionController: NSObject, UIViewControllerTransitioningDelegate {
let animator: SlideAnimator
override init() {
animator = SlideAnimator()
super.init()
}
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
animator.isPresenting = true
return animator
}
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
animator.isPresenting = false
return animator
}
}
SlideAnimator.swift
class SlideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting: Bool = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
print(transitionContext.containerView)
if isPresenting {
animateSlideUpTransition(using: transitionContext)
} else {
animateSlideDownTransition(using: transitionContext)
}
}
func animateSlideDownTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
let container = transitionContext.containerView
let screenOffDown = CGAffineTransform(translationX: 0, y: container.frame.height)
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0.0,
options: .curveEaseInOut,
animations: {
fromVC.view.transform = screenOffDown
}) { (success) in
fromVC.view.transform = .identity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
...
}
Information gathering:
iOS13 bug? In the link below, you can see that the radar for the issue is open to this day.
http://www.openradar.me/radar?id=4999313432248320
There have been many comments that this is a bug in iOS 13 on stackoverflow. But there have been some more compelling comments that this is Apple's intention and why:
https://stackoverflow.com/a/25901154/5705503
A possible cause:
If NO is returned from -shouldRemovePresentersView, the view associated
with UITransitionContextFromViewKey is nil during presentation. This
intended to be a hint that your animator should NOT be manipulating the
presenting view controller's view. For a dismissal, the -presentedView
is returned.
Why not allow the animator manipulate the presenting view controller's
view at all times? First of all, if the presenting view controller's
view is going to stay visible after the animation finishes during the
whole presentation life cycle there is no need to animate it at all — it
just stays where it is. Second, if the ownership for that view
controller is transferred to the presentation controller, the
presentation controller will most likely not know how to layout that
view controller's view when needed, for example when the orientation
changes, but the original owner of the presenting view controller does.
-Apple Documentation Archive-
https://developer.apple.com/library/archive/samplecode/CustomTransitions/Listings/CustomTransitions_Custom_Presentation_AAPLCustomPresentationController_m.html
Attempts to solve:
Set modalPresentationStyle to .fullScreen.
I don't pick this solve because the background of the present view controller must be translucent and the view controller behind it must be visible.
Create a new project to see if the problem recurs
Problem reproduced!
Build on iOS12
Problem reproduced
Subclass UIPresentationController to override shouldRemovePresentersView property to return false and adopt it as presentationController.
I tried the solution mentioned in the link below, but the results were not different.
https://stackoverflow.com/a/41314396/5705503
The role of shouldRemovePresentersView as I know it:
Indicates whether the view of the view controller being switched is removed from the window at the end of the presentation transition.
Substitute the view of the view controller returned by viewController(forKey:) to use it for animation and add the view to the key window.
I was able to achieve the desired result, but it was a bad approach and I did not adopt it as a workaround as it could cause various problems in the future.
Environments:
iOS 13.5 simulator, iOS 13.5.1 iPhoneXS
Xcode 11.5 (11E608c)
Addition)
I found an old Sample where the same problem occurs (black screen is visible when dismiss transition is complete) in the same situation as me.
Please, I hope someone can clone this Sample and run it and let me know how to fix this.
https://www.thorntech.com/2016/03/ios-tutorial-make-interactive-slide-menu-swift/

Presenting View controller disappears after animation

I've a TabBarController in which presents a view controller with custom animations with UIViewControllerAnimatedTransitioning;
The animations run normally without issues, but after the animationController(forPresented) function runs, the Presenting view controller disappears.
I've found a question around here with people having the same issues but none of those tries solved my issue.
I've read that there is a bug in iOS and we should had again the 'vanished' view controller to the stack, but adding this with UIApplication.shared.keyWindow?.addSubview(presentingView) makes the view added on top of the presentedView and I don't know it adding it again, adds another one to the stack, because it could only be a graphical bug and the view is still part of the container.
Here's some code:
// Global var
var transition = Animator()
// Present a VC modally using tab bar
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController is NewPostVC {
if let newVC = tabBarController.storyboard?.instantiateViewController(withIdentifier: "newPostVC") as? NewPostVC {
newVC.transitioningDelegate = self
newVC.interactor = interactor // new
tabBarController.present(newVC, animated: true)
return false
}
}
return true
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitioningMode = .Present
return transition
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitioningMode = .Dismiss
return transition
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
//*****************************
/// On the Animator class:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
// Animates to present
if transitioningMode == .Present {
// Get views
guard
let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to),
let presentingView = transitionContext.view(forKey: UITransitionContextViewKey.from)
else {
print("returning")
return
}
// Add the presenting view controller to the container view
containerView.addSubview(presentingView)
// Add the presented view controller to the container view
containerView.addSubview(presentedView)
// Animate
UIView.animate(withDuration: presentDuration, animations: {
presentingView.transform = CGAffineTransform.identity.scaledBy(x: 0.85, y: 0.85);
presentingView.layer.cornerRadius = 7.0
presentingView.layer.masksToBounds = true
presentedView.transform = CGAffineTransform.identity.scaledBy(x: 0.6, y: 0.6);
presentedView.layer.masksToBounds = true
}, completion: { _ in
// On completion, complete the transition
transitionContext.completeTransition(true)
//UIApplication.shared.keyWindow?.addSubview(presentingView)
})
}
// Animates to dismiss
else {
// TODO: - Implement reverse animation
}
}
Note that the animations itself are just tests I'm doing, just scaling them around.
Thx.
The idea is that you don't need to add subview .from during presentation and .to during dismissal to the contatiner's view hierarchy. And if you want to see background underneath, just set the contatiner's view background color to .clear.
After reading Apple's related documentation here I found that it's not a bug that the presentingViewController disappears from the screen, it's just how the API works.
Anyone using transmission animation read the documentation, which was updated and you'll find really interesting and solid explanations there.

Custom flip animation segue - Swift 3

My goal is to perform a flip-animation from view1 to view2 and vice versa.
I only want to flip the views not the whole viewController
Set up as follows:
i have a view controller (fromViewController) in my storyboard embedded in a navigationController. I drew a segue to a second view controller (toViewController) and made it a custom segue.
Now i want to add a flip animation to flip the views of the view controllers from left to right and right to left.
The forward animation already works (from fromViewController to toViewController) but he backward animation does not work.
I checked a lot of similar questions here but i couldn't find an answer that fit my needs.
I hope someone can help me out of this.
The class for the custom segue is FlipSegue.
here is my code:
import UIKit
extension UIViewController{
var isVisible: Bool{
return self.isViewLoaded && self.view.window != nil
}
}
class FlipSegue: UIStoryboardSegue {
override func perform() {
let fromViewController = self.source
let toViewController = self.destination
if fromViewController.isVisible{
UIView.transition(from: fromViewController.view, to: toViewController.view, duration: 1, options: .transitionFlipFromLeft, completion: nil)
}
else{
UIView.transition(from: toViewController.view, to: fromViewController.view, duration: 1, options: .transitionFlipFromRight, completion: nil)
}
}
}
FYI: The toViewController is not part of the NavigationController.
EDIT: I perform the segue by tapping on a UIBarButton on the left side of the navigation controller. The Button is calling the perform function in the FlipSegue class. Maybe the problem is that the toViewController is not part of the navigation stack? So i added it to the navigation stack and then i got a back button and i could return to the fromViewController with the standard animation, but this is not what i wanted.
I haven't tried this, but you should probably use from:fromViewcontroller.view in both cases, since in a back segue, from is still the starting VC.

Swift - UIPopoverController in iOS 8

I'm trying to add a simple popoverController to my iphone app, and I'm currently struggling with the classic "blank screen" which covers everything when I tap the button.
My code looks like this:
#IBAction func sendTapped(sender: UIBarButtonItem) {
var popView = PopViewController(nibName: "PopView", bundle: nil)
var popController = UIPopoverController(contentViewController: popView)
popController.popoverContentSize = CGSize(width: 3, height: 3)
popController.presentPopoverFromBarButtonItem(sendTappedOutl, permittedArrowDirections: UIPopoverArrowDirection.Up, animated: true)
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController!) -> UIModalPresentationStyle {
// Return no adaptive presentation style, use default presentation behaviour
return .None
}
}
The adaptivePresentationStyleForPresentationController-function was just something I added because I read somewhere that this is what you need to implement to get this function on the iphone. But still: there is still a blank image covering the whole screen, and I do not know how to fix it.
Any suggestions would be appreciated.
The solution I implemented for this is based on an example presented in the 2014 WWDC session View Controller Advancements in iOS 8 (see the slide notes). Note that you do have to implement the adaptivePresentationStyleForPresentationController function as a part of the UIPopoverPresentationControllerDelegate, but that function should be outside of your sendTapped function in your main view controller, and you must specify UIPopoverPresentationControllerDelegate in your class declaration line in that file to make sure that your code modifies that behaviour. I also took the liberty to separate out the logic to present a view controller in a popover into its own function and added a check to make sure the function does not present the request view controller if it is already presented in the current context.
So, your solution could look something like this:
// ViewController must implement UIPopoverPresentationControllerDelegate
class TheViewController: UIViewController, UIPopoverPresentationControllerDelegate {
// ...
// The contents of TheViewController class
// ...
#IBAction func sendTapped(sender: UIBarButtonItem) {
let popView = PopViewController(nibName: "PopView", bundle: nil)
self.presentViewControllerAsPopover(popView, barButtonItem: sender)
}
func presentViewControllerAsPopover(viewController: UIViewController, barButtonItem: UIBarButtonItem) {
if let presentedVC = self.presentedViewController {
if presentedVC.nibName == viewController.nibName {
// The view is already being presented
return
}
}
// Specify presentation style first (makes the popoverPresentationController property available)
viewController.modalPresentationStyle = .Popover
let viewPresentationController = viewController.popoverPresentationController?
if let presentationController = viewPresentationController {
presentationController.delegate = self
presentationController.barButtonItem = barButtonItem
presentationController.permittedArrowDirections = .Up
}
viewController.preferredContentSize = CGSize(width: 30, height: 30)
self.presentViewController(viewController, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
}
Real world implementation
I implemented this approach for input validation on a sign up form in an in-progress app that I host on Github. I implemented it as extensions to UIVIewController in UIViewController+Extensions.swift. You can see it in use in the validation functions in AuthViewController.swift. The presentAlertPopover method takes a string and uses it to set the value of a UILabel in a GenericAlertViewController that I have set up (makes it easy to have dynamic text popovers). But the actual popover magic all happens in the presentViewControllerAsPopover method, which takes two parameters: the UIViewController instance to be presented, and a UIView object to use as the anchor from which to present the popover. The arrow direction is hardcoded as UIPopoverArrowDirection.Up, but that wouldn’t be hard to change.

Resources