I am building an app and recently discovered a huge memory leak caused by traditional segues.
Therefore I learned about unwind segue. Everything works just fine if I simply use:
#IBAction func prepareForUnwindToMainFromFriends(segue: UIStoryboardSegue) {
}
Memory leak is solved and 'everything is awesome'. But this solution looks ugly on a UI point of view. So I implemented this function from this website. And changed it a little.
override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController) {
let fromView = fromViewController.view
let toView = toViewController.view
if let containerView = fromView.superview {
let initialFrame = fromView.frame
var offscreenRect = initialFrame
var offscreenRectFinal = initialFrame
offscreenRect.origin.x += CGRectGetWidth(initialFrame)
offscreenRectFinal.origin.x -= CGRectGetWidth(initialFrame)
toView.frame = offscreenRect
containerView.addSubview(toView)
let duration: NSTimeInterval = 1.0
let delay: NSTimeInterval = 0.0
let options = UIViewAnimationOptions.CurveEaseInOut
let damping: CGFloat = 0.9
let velocity: CGFloat = 4.0
UIView.animateWithDuration(duration, delay: delay, usingSpringWithDamping: damping,
initialSpringVelocity: velocity, options: options, animations: {
toView.frame = initialFrame
fromView.frame = offscreenRectFinal
}, completion: { finished in
fromView.removeFromSuperview()
if let navController = toViewController.navigationController {
navController.popToViewController(toViewController, animated: false)
}
})
}
}
}
But now I get an error message:
2015-05-12 08:47:31.841 PING0.4[4343:1308313] Warning: Attempt to present <NotificationViewController: 0x177030b0> on <PING0_4.ViewController: 0x16271000> which is already presenting <NotificationViewController: 0x1a488170>
And I am blocked in my app. I can go from VC1 to VC2, then back to VC2 but then I cannot get back to VC1 again. It looks like I can only use this segue once.
Any one has any idea of what is going on?
Created Sample code for unwind segue with above transition animation code. Checkout SampleUnwind project that will help you to understand unwind segue(and how simple it is).
In project there is one navigation controller and inside it there are three view controller (Home->First->second).
In Home controller following unwind action is created, which will be called when 'Home' button of second controller is tapped(simple unwind stuff).
#IBAction func unwindToHomeViewController(segue:UIStoryboardSegue) {
}
I have created TempNavigationController subclassing UINavigationController and set that TempNavigationController to navigation controller in storyboard.
Above method you given is pesent inside it as this will be container of fromViewController as per following referance.
Reference: Apple documentation about Transitioning Between Two Child View Controllers.
You can compare this with your project and maybe you can find any duplicate(or multiple/wrong) segue in your project.
Related
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/
I have seen a lot of tutorial but did not see any interesting me. I want to make custom segue that moves only one view, not the whole page.
Something like this:
Top view is just poping without any animation. But bottom view normally slides on bottom viewcontroler
I have tried:
swift
override func perform() {
scale()
}
func scale(){
let toViewController = self.destination
let fromViewController = self.source
let containerView = fromViewController.view.superview
let originalCenter = fromViewController.view.center
toViewController.view.viewWithTag(1)?.viewWithTag(2)?.transform = CGAffineTransform(translationX:self.source.view.frame.width,y:0)
toViewController.view.center = originalCenter
containerView?.addSubview(toViewController.view)
UIView.animate(withDuration: 0.5, delay:0, options: .showHideTransitionViews ,animations:{
toViewController.view.viewWithTag(1)?.viewWithTag(2)?.transform = CGAffineTransform.identity
},completion:{success in fromViewController.present(toViewController,animated: false,completion: nil)})
}
If you do not understand, ask me and help me please.
I think you need this hierarchy
MainView
-topView
-containerView
-navigation
-firstvc
-secondvc
and set navigationBar to hidden in storyboard
I encountered an unusual behavior on which I am stuck a little, the problem is the following.
I'm using BWWalkthrough library in order to have a 4 slides as launch screen. So in my appdelegate i have the following code which initialize the viewcontrollers:
let storyboard = UIStoryboard(name: "SlidesFlow", bundle: nil)
let walkthrough = storyboard.instantiateViewController(withIdentifier: "SlidesView") as! BWWalkthroughViewController
let page_zero = storyboard.instantiateViewController(withIdentifier: "page_1")
let page_one = storyboard.instantiateViewController(withIdentifier: "page_2")
let page_two = storyboard.instantiateViewController(withIdentifier: "page_3")
let page_three = storyboard.instantiateViewController(withIdentifier: "page_4")
walkthrough.delegate = self
walkthrough.addViewController(page_zero)
walkthrough.addViewController(page_one)
walkthrough.addViewController(page_two)
walkthrough.addViewController(page_three)
Everything works as intended, so no problem here. On the viewController page_three i have a button which redirect me to an other view controller using a custom segue animation
class sentSegueFromRight: UIStoryboardSegue {
override func perform()
{
let src = self.source as UIViewController
let dst = self.destination as UIViewController
src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
dst.view.transform = CGAffineTransform(translationX: src.view.frame.size.width, y: 0)
UIView.animate(withDuration: 0.25,
delay: 0.0,
options: UIViewAnimationOptions.curveEaseInOut,
animations: {
dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
},
completion: { finished in
src.present(dst, animated: false, completion: nil)
}
)
}
}
Now the problem is, if i use the same code on a normal viewcontroller the button and the animation work without issues. The problem is when i use the segue defined above from the last slide of my BWWalkthrough. The first time i tapp the button the viewcontroller which should appear does appear but without the corresponding animation. After closing it and taping on the button again the animation is played but an error is returned:
Presenting view controllers on detached view controllers is
discouraged
If i use the button with the standard animation ( without using my custom animation code ) i get no error and the default animation is played.
I can't seem to find a solution to this problem. Does anybody stumbled upon something like this?
The problem here lies in the BWWalkthrough library which is using a scrollview to present all the views of the various ViewControllers that you add.
As such, you add the dst.view at the beginning of the scrollview (at offset screenwidth,0) which you then transform to offset (0,0).
All of this is offscreen as you are currently in the third screen of the walkthrough (at offset (screenwidth*3,0)). As such you don't get to see the animation and directly see the presented view controller when the segue ends.
To remedy this, add your dst.view in the segue to the superview of the scrollview. i.e. instead of src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
write src.view.superview?.superview?.insertSubview(dst.view, aboveSubview: src.view) in the segue. (Assuming that you are using the segue only from the walkthrough)
If you intend to use the segue in other places too, then you can maybe add a type check in the segue to check if the superview of src.view is a scrollview, if yes, add dst.view to the superview of the scrollview.
I have several swipe and pan gestures to navigate through some views. When I pan the view, a new ViewController will be instantiated and placed at the edge of the window. Along my pan, the view will come into view on top of the current view. If the pan has passed halfway, the new view will automatically finish and replaces the current view and on completion removing the old view.
var newViewController: UIViewController! {
didSet {
if let newView = NewViewController {
addChildViewController(newView)
newView.view.frame.origin.x = view.bounds.width }
view.addSubview(newView.view)
newView.didMoveToParentViewController(self)
}
}
}
var currentViewController: UIViewController! {
didSet(oldView) {
oldViewController = oldView
newViewController = nil
}
}
var oldViewController: UIViewController!
func removeViewController() {
if let oldView = oldViewController {
oldView.willMoveToParentViewController(nil)
oldView.view.removeFromSuperview()
oldView.removeFromParentViewController()
oldViewController = nil
}
}
newViewController = NewVIewController()
currentViewController = newViewController
UIView.animateWithDuration(2.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
self.currentViewController.view.frame.origin.x = 0 }) { _ in
self.removeViewController()
}
In the completion of the animation, the old view will be taken care of. But to make my app to be more responsive, I'd like to call removeViewController right away and still keeping a "snapshot" of the oldView so the transition will still look the same.
Should I use some other method for transitioning the views?
It's too late to follow the pan gesture once the swipe event has been fired. Instead consider using uiscreenedgegesturerecognizer
The problem is that this is not how you do an interactive view controller transition. You need to trigger an actual view controller transition (e.g. call presentViewController) and supply a UIViewControllerInteractiveTransitioning object.
I keep getting the error:
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Push segues can only be used when the source controller is managed by an instance of UINavigationController.'
When trying to switch to a new view controller. Below is the segue switching view controllers:
Even when I try put the name of my transition class in the Segue class, it still gives the error on my device, but works perfectly fine in the simulator.
The code for the transition class:
class TransitionManager: UIStoryboardSegue, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = true
// MARK: UIViewControllerAnimatedTransitioning protocol methods
// animate a change from one viewcontroller to another
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// set up from 2D transforms that we'll use in the animation
let π : CGFloat = 3.14159265359
let offScreenRight = CGAffineTransformMakeRotation(-π/2)
let offScreenLeft = CGAffineTransformMakeRotation(π/2)
// prepare the toView for the animation
toView.transform = self.presenting ? offScreenRight : offScreenLeft
// set the anchor point so that rotations happen from the top-left corner
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)
// updating the anchor point also moves the position to we have to move the center position to the top-left to compensate
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.8, options: nil, animations: {
// slide fromView off either the left or right edge of the screen
// depending if we're presenting or dismissing this view
fromView.transform = self.presenting ? offScreenLeft : offScreenRight
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
// return how many seconds the transiton animation will take
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.75
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
// remmeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// these methods are the perfect place to set our `presenting` flag to either true or false - voila!
self.presenting = true
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
override func perform() {
//
}
}
Is there anything I am missing? Or how can I get this to work?
Usually that error indicates that you're trying to perform a push segue, and the presenting view controller is not managed by a UINavigationController. The simple solution is usually to embed the presenting (i.e. from) view controller in a navigation controller as outlined in NSGenericException', reason: 'Push segues can only be used when the source controller is managed by an instance of UINavigationController.
But since you've rolled your own UIStoryboardSegue class, I suspect you aren't trying to simply use the default push segue animation. Without knowing how you are managing your transitioningDelegate, my guess is you need to actually override perform() to present your view controller instead of pushing it:
override func perform() {
let fromVC = sourceViewController as UIViewController
let toVC = destinationViewController as UIViewController
fromVC.presentViewController(toVC as UIViewController, animated: true, completion: nil)
}