Custom Segue not working if NOT set as initial window - ios

Following is the code excerpts for the custom segue animation in Swift, which works fine if I make the source viewController as Initial Window in storyboard, only.
class CustomSegue: UIStoryboardSegue
{
override func perform()
{
var fromView = self.sourceViewController.view as UIView!
var toView = self.destinationViewController.view as UIView!
let offScreenHorizontalStart = CGAffineTransformMakeRotation(CGFloat(M_PI / 2))
let offScreenHorizontalEnd = CGAffineTransformMakeRotation(CGFloat(-M_PI / 2))
fromView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)
UIApplication.sharedApplication().keyWindow?.insertSubview(toView, belowSubview: fromView)
UIView.animateWithDuration(0.7, delay: 0.0, usingSpringWithDamping: 1.6, initialSpringVelocity: 0.0, options: nil, animations:
{
fromView.transform = offScreenHorizontalEnd
}, completion: {
finished in
self.sourceViewController.presentViewController(self.destinationViewController as! UIViewController, animated: false, completion: nil)
})
}
}
By this segue animation, the source view expects to disappear with an upside rotation from x/y = 0/0.
Thanks.

As far as you are using explicitly the keyWindow property of the UIApplication object here:
UIApplication.sharedApplication().keyWindow?.insertSubview(toView, belowSubview: fromView)
I think it is no surprise that it won't work for other windows.
You could use the windows property of UIApplication instead. It holds all UIWindow objects currently open in the app in an ordered NSArray (the last object in the array is the topmost window).
Or you pick the current window from the fromView.window property?

Related

Common iOS Memory Leak Issue Due to Custom Segues. How to fix?

I have seen variations of the following code all over StackOverflow:
import UIKit
class segueFromLeft: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
let src = self.source.view as UIView!
let dst = self.destination.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
// Specify the initial position of the destination view.
dst?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth,
height: screenHeight)
// Access the app's key window and insert the destination view
above the current (source) one.
let window = UIApplication.shared.keyWindow
window?.insertSubview(dst!, aboveSubview: src!)
// Animate the transition.
UIView.animate(withDuration: 0.5, animations: { () -> Void in
src?.frame = (src?.frame.offsetBy(dx: -screenWidth, dy: 0))!
dst?.frame = (dst?.frame.offsetBy(dx: -screenWidth, dy: 0))!
}) { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: nil) {
}
}
}
}
At first, the code operates as a nice way of transitioning from one view to another. But with continued use, most of the problems that have been listed on this website as a result from it relate to memory. Every time the segue is used, the destination view is initialized and the source view remains in memory. With continued use, the memory use continues to grow and grow.
A simple dismissal of the source view did not function for me, the screen just went black.
My question is, how can we fix this problem?

How to prevent from stacking views in a custom segue?

I coded a custom segue emulating a "Push Left". I takes screenshots of the 2 views, animates them from right to left, then present the destination view and remove the overlaying screenshots. I'm afraid that this ultimately results in stacking views on top of one another, which should be avoided in this case. I can't figure out how to properly dismiss the underlaying view once the animation is completed. I tried to use navigationController?.pushViewController instead of present but my attempts were not successful. How could I solve this issue ?
My custom segue :
class SeguePushLeft: UIStoryboardSegue
{
override func perform()
{
self.source.view.isUserInteractionEnabled = false
self.destination.view.isUserInteractionEnabled = false
let slideViewOut = UIImageView(image: source.view.capture()!)
let slideViewIn = UIImageView(image: destination.view.capture()!)
let screenWidth = source.view.frame.size.width
self.source.view.addSubview(slideViewIn)
self.source.view.addSubview(slideViewOut)
slideViewIn.transform = CGAffineTransform(translationX: screenWidth, y: 0)
UIView.animate(withDuration: 0.4,
delay: 0.0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: {
slideViewIn.transform = CGAffineTransform.identity
slideViewOut.transform = CGAffineTransform(translationX: -screenWidth, y: 0)
}, completion: { finished in
DispatchQueue.main.async{
(self.source as UIViewController).present((self.destination as UIViewController), animated: false, completion: {
self.source.view.isUserInteractionEnabled = true
self.destination.view.isUserInteractionEnabled = true
slideViewIn.removeFromSuperview()
slideViewOut.removeFromSuperview()
})
}
})
}
}
[Please forgive my previous too-hasty answer. I'm entering another rather than editing the first, because of all the comments that now blot the first.]
The problem is simple: you're doing this wrong. What you wish to do is a present transition, but with a custom animation. That's perfectly viable, and you can certainly do it with a custom segue, but the way you're doing it is not how you do that. You do it with a custom transition animation. There is a fixed way of implementing that, and it isn't how you're going about things. You need to read up on how to write a custom transition animation for a presentation transition and you'll be all set.

How to present UIViewController on top of current view controller with smaller height

I would like to present a UIViewController on top of the current view controller and set it's height to ~80% of the screen size. I've got the first part:
let additionalVC = ChartsViewController(currentSelection)
additionalVC = .overCurrentContext
present(additionalVC, animated: true)
I tried setting the self.view.frame inside my ChartsVC in viewDidLoad and couple of different things but it is always presented in the full screen mode.
That's what I want to achieve:
blueVC - currentVC
redVC - ChartsVC - VC on top of the current VC with ~80% of the original height
btw I'm doing everything programmatically, no xib and UIStoryboard.
There's a number of ways to achieve this.
You could use a 3rd party framework (http://transitiontreasury.com/) or the way I would do this.
Present the newVC where a transition = model over current context
ensure the newVC.views background color is clear
add another view where origin.y is the distance between the top and the desired gap. This is the view where all your objects will sit on.
If you need a coding example let me know, but its a pretty simple solution and looking at your code your 80% there.
Thomas
Implement a custom UIPresentationController. To use a custom view size, you only need to override a single property.
This code will simply inset the presented view controller by 50x100 pts:
class MyPresentationController: UIPresentationController {
// Inset by 50 x 100
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
}
To darken the presenting view controller, override presentationTransitionWillBegin() and dismissalTransitionWillBegin() to insert a shading view and animate it into view:
class MyPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
let shadeView = UIView()
override func presentationTransitionWillBegin() {
self.shadeView.backgroundColor = UIColor.black
self.shadeView.alpha = 0
// Insert the shade view above the presenting view controller
self.shadeView.frame = self.presentingViewController.view.frame
self.containerView?.insertSubview(shadeView,
aboveSubview: self.presentingViewController.view)
// Animate it into view
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.3
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.0
}, completion: nil)
}
}
To use your custom presentation controller, set the modalPresentationStyle and transitioningDelegate:
class MyViewController : UIViewController, UIViewControllerTransitioningDelegate {
//
// Your code
//
func presentCharts() {
let additionalVC = ChartsViewController(currentSelection)
additionalVC.modalPresentationStyle = .custom
additionalVC.transitioningDelegate = self
self.present(additionalVC, animated: true)
}
//
// UIViewControllerTransitioningDelegate protocol
//
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
return MyPresentationController(presentedViewController: presented,
presenting: presenting)
}
}
In IOS 13 and Xcode 11, you can present ViewController with modalPresentationStyle = .automatic
Take Two ViewController.First view controller have a button and the button action name is clicked.The target is to clicking the button we want to add secondVC as a child of first view controller and show secondVC 80% of the first view controller.again click button we remove secondVC from first view controller. below is the code for click button action.
#IBAction func clicked(_ sender: UIButton) {
if !isshown{
isshown = true
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
let height = view.frame.height
let width = view.frame.width
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)
}, completion: { (result) in
// do what you want to do
})
}else{
isshown = false
UIView.animate(withDuration: 0.3,
delay: 0,
options: UIViewAnimationOptions.curveEaseIn,
animations: { () -> Void in
var frame = self.vc.view.frame
frame.origin.y = UIScreen.main.bounds.maxY
self.vc.view.frame = frame
}, completion: { (finished) -> Void in
self.vc.view.removeFromSuperview()
self.vc.removeFromParentViewController()
})
}
}
here vc is a reference of secondVC.
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Second") as! secondVC
change below piece of code to get whatever percentage you want.
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)

How to replicate the drop/bounce animation effect as seen in iOS Notification Center window?

How would one replicate the bounce effect of the iOS Notification Center window that drops to the bottom of the screen and bounces without ever traveling below the height of the window? This appears to use damping and spring velocity but how do you prevent the object, such as in this case, from overshooting its mark and slinging back?
Take a look into UIKitDynamics. This will allow you to apply physics and collisions to your views.
You could have a view that drops from the top of the screen and collides with a view at the bottom of the screen. You'll need to use UIGravityBehavior and UICollisionBehavior, and should be able to adjust the constants to get the desired effect.
Updated answer: You could build your own custom transition.
Say you have two view controllers, StartController and EndController. You want to have this custom transition to happen from StartController to EndController.
1) Create your custom transition object like so:
class CustomSlideTransition: NSObject, UIViewControllerAnimatedTransitioning {
var duration: Double = 1.0
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Access EndController
let toViewController = transitionContext.viewController(forKey: .to) as! EndController
// Access the container view :
let containerView = transitionContext.containerView
// Move EndController's view frame:
toViewController.view.frame.origin.y -= UIScreen.main.bounds.size.height
containerView.addSubview(toViewController.view)
// Adjust the properties of the spring to what fits your needs the most:
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 7.0, options: [.curveEaseInOut], animations: {
// Get your EndController's view frame moves back to its final position
toViewController.view.frame.origin.y += UIScreen.main.bounds.size.height
}, completion: { (finished) in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
2) In the prepareForSegue method of your StartController, set the delegate to self :
class StartController: UIViewController {
// ...
// MARK: PrepareForSegue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destinationController = segue.destination as? EndController {
destinationController.transitioningDelegate = self
}
}
// ...
}
3) Make StartController conform to the UIViewControllerTransitioningDelegate :
extension StartController: UIViewControllerTransitioningDelegate {
// MARK: UIViewControllerTransitioningDelegate
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomSlideTransition()
}
}
4) Design your EndController however you want!
Note: You may want to add a custom transition for dismissing EndController as well.
Update: Now if you really don't want your view controller to bounce even a little beyond the window, using a spring here may not be the best way to go.
Since on iPhone the bounces effect "power" is calculated using the swipe (from the top) offset, you might want to custom even more your animation block using key frame animations, or UIKIT Dynamics if you're comfortable with.
UIView.animate(withDuration: duration, animations: {
toViewController.view.frame.origin.y += UIScreen.main.bounds.size.height
}, completion: { (finished) in
// Create a custom bounce effect that doesn't go beyond the window (ie. only negative values)
let animation = CAKeyframeAnimation(keyPath: "transform.translation.y")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 0.75
animation.autoreverses = false
// Depending on how you would like to present EndController, you may want to change the hardcoded values below.
// For example you could make it so that if the user swipes really fast, it would bounce even more..
animation.values = [0, -60, 0, -25, 0]
// Add the key frame animation to the view's layer :
toViewController.view.layer.add(animation, forKey: "bounce")
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})

iOS custom transitions and rotation

I'm using custom transitions to display a fullscreen modal game view. When the user starts a game, the root view controller scales down "into" the screen, while the fullscreen game view controller zooms from a larger scale down onto the display while transitioning from 0% opacity to 100% opacity. Simple!
The transition looks great and works fine, also correctly reversing the animation when dismissing the game view controller.
The issue I'm having is that if the device is rotated while the fullscreen game view controller is displayed, on returning to the root view controller, the layout is all screwy. And further rotation doesn't fix the problem, the layout is screwy and stays screwy.
If I disable the use of a custom transition, this problem is gone. Also, if I keep the custom transition, but disable calls setting a CATransform3D on my source and destination views in the transition animation, the problem goes away again.
Here's my transitioning delegate:
class FullscreenModalTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting:Bool = true
// MARK: UIViewControllerAnimatedTransitioning protocol methods
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)!
let scale:CGFloat = 1.075
//let bigScale = CGAffineTransformMakeScale(scale, scale)
//let smallScale = CGAffineTransformMakeScale(1/scale,1/scale)
let bigScale = CATransform3DMakeScale(scale, scale, 1)
let smallScale = CATransform3DMakeScale(1/scale, 1/scale, 1)
let smallOpacity:CGFloat = 0.5
let presenting = self.presenting
if presenting {
// when presenting, incoming view must be on top
container.addSubview(fromView)
container.addSubview(toView)
toView.layer.transform = bigScale
toView.opaque = false
toView.alpha = 0
fromView.layer.transform = CATransform3DIdentity
fromView.opaque = false
fromView.alpha = 1
} else {
// when !presenting, outgoing view must be on top
container.addSubview(toView)
container.addSubview(fromView)
toView.layer.transform = smallScale
toView.opaque = false
toView.alpha = smallOpacity
fromView.layer.transform = CATransform3DIdentity
fromView.opaque = false
fromView.alpha = 1
}
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration,
delay: 0.0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0,
options: nil,
animations: {
if presenting {
fromView.layer.transform = smallScale
fromView.alpha = smallOpacity
} else {
fromView.layer.transform = bigScale
fromView.alpha = 0
}
},
completion: nil )
UIView.animateWithDuration(duration,
delay: duration/6,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: nil,
animations: {
toView.layer.transform = CATransform3DIdentity
toView.alpha = 1
},
completion: { finished in
transitionContext.completeTransition(true)
})
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.5
}
// 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
}
}
And, for visual reference, here's what my root view controller normally looks like:
And, for visual reference, here's what my root view controller looks like if I rotate my device (at least once) while in the game view, and return to the root view controller.
And a final note - just in case it rings any bells - I'm using autolayout and size classes to lay out my root view controller.
Thanks,
I had a similar problem, I had written a custom transition for push and pop on a navigation controller. If you rotated the device after you did a push, when you popped back to the root view controller it's frame would be unchanged.
Problem
Solution
Objective C
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// ViewController Reference
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// Fix layout bug in iOS 9+
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
// The rest of your code ...
}
Swift 3.0
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// ViewController reference
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
// Fix layout bug in iOS 9+
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
// The rest of your code ...
}
I accidentally figured this out, while doing some manual layout code on views with non-identity transforms. Turns out if your view has a non-identity transform, normal layout code fails.
The fix, in my transitioning delegate was to take the transited-out view, and in the animation complete callback set its transform to identity (since that view's invisible at the end of the animation, and behind the new view, this has no effect on the appearance)
UIView.animateWithDuration(duration,
delay: 0.0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0,
options: nil,
animations: {
if presenting {
fromView.transform = smallScale
fromView.alpha = smallOpacity
} else {
fromView.transform = bigScale
fromView.alpha = 0
}
},
completion: { completed in
// set transform of now hidden view to identity to prevent breakage during rotation
fromView.transform = CGAffineTransformIdentity
})
Finally I found solution for this issue. You need to improve #agilityvision code. You need to add BOOL value, something like closeVCNow that indicates you want to close VC, and in animateTransition:, in animation block do that:
if (self.closeVCNow) {
toVC.view.transform = CGAffineTransformIdentity;
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
self.closeVCNow = NO;
}

Resources