I'm trying to make a slide animation for my navigation controller transitions. For instance, when I push a VC, the presenting and the presented VCs will transition just like a UIPageViewController transition.
Here is what I've coded so far:
guard let fromView = transitionContext.view(forKey: .from),
let toView = transitionContext.view(forKey: .to)
else { return }
let containerView = transitionContext.containerView
toView.frame = CGRect(x: -toViewFrameHorizationtalPosition,
y: 64,
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
containerView.addSubview(fromView)
containerView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: .curveEaseInOut,
animations: {
containerView.frame.origin.x = self.toViewFrameHorizationtalPosition
}) { completed in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
The transition animation is happening as I expected it to be but even though against various tries, once the animation executes the view becomes unresponsive; it does not recognise any touch events.
The navigation bar works fine but when I switch back to the initial view, that view is unresponsive as well.
I've read a lot on this and been struggling with this for a while now although I can't seem to spot the issue. Any ideas?
Alright then, I finally found the solution. Many many thanks to #DonMag for pointing me in the right direction with his great comment!
First off, as #DonMag has also mentioned, it was needless of me to add the fromView into the containerView as it's already present inside there.
Anyways, the problem with the presented view, toView, being unresponsive was that for the purposes of making the slide animation:
I was changing the x value of the toViews frame origin to position it right inside the container view,
and moving the container view again by changing it's x value of it's frame origin. I'm moving the container view because I want both the fromView and the toView to act one next to the other, i.e. just like how a UIPageViewController animates.
The problem was that I was not setting them back to their original values. Once I set toViews and containerViews frame origins back to their original values in the animation completion block, everything worked as expected.
Here is the revised animation code:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to)
else { return }
toView.removeFromSuperview()
toView.frame = CGRect(x: -toViewFrameHorizationtalPosition,
y: 64,
width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
let containerView = transitionContext.containerView
containerView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: .curveEaseInOut,
animations: {
containerView.frame.origin.x = self.toViewFrameHorizationtalPosition
}) { completed in
containerView.frame.origin.x = 0
toView.frame.origin.x = 0
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
P.S. Again, many many thanks to #DonMag. Your insight has been really helpful.
Related
I have a problem with a custom animation viewController transition.
I have a collectionView (fromView) and when a cell is selected, it grows to fullSize (toView).
It's working great except for the bottom cells, they are cropped when animating to center.
I tried to change the frame of the containerView to get extra space to aboid the crop but it's messing up the positions of the viewcontrollers in it.
This is the relevant code for the transition and a sketch to spot the issue.
Many thanks.
Actual behaviour:
Expected:
// originFrame is define by the selected cell in fromView
var originFrame = CGRect.zero
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from)
else { return }
let finalFrame = toView.frame
let growFactor = finalFrame.width / originFrame.width
let centerDiffX = fromView.center.x-originFrame.midX
let centerDiffY = fromView.center.y-originFrame.midY
// Animate the transition.
UIView.animate(
withDuration: duration,
delay:0.0,
animations: {
let scaleTransform = CGAffineTransform(scaleX: growFactor, y: growFactor)
fromView.transform = CGAffineTransform(translationX: deltaX, y: deltaY).concatenating(scaleTransform)
...
Make a snapshot of the collectionview do the trick ;)
I have a custom animator for view controller interactive transition. There is also a blur effect that is set to nil depending on the transition progress. The effect's animation code is the following:
#objc func blurEffectDissmisal() {
UIView.animate(withDuration: dismissAnimator.durationOfAnimation + 1, animations: {
self.blurEffectView?.effect = nil
}) { (done) in
if (done) {
self.blurEffectView?.removeFromSuperview()
}
}
}
I call it by a notification, which is called on the second view controller when the transition from it to the first one starts.
However, I have a problem here. The completion block is called before the animation ends. When I run the transition for the first time (without canceling it) it works fine, but during the subsequents runs it doesn't.
I had also tried to add the animation to my animator but it didn't work out, either.
Moreover, the completion block gets called before the actual animation ends when I cancel the transition (in this case, I understand why but can't figure out how to make it move backwards. Maybe I should create a reverse animation in a completion block?)
I have tried the suggestion from this answer as you can see, but doesn't help.
If you know how this problem could be solved, I would really appreciate your help.
Use a delay before calling animate function .
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
UIView.animate(withDuration: 2.0,delay: 0, animations: {
self.frame.origin.x = -2000
}) { (done) in
if(done){
self.removeFromSuperview()
}
}
}
I've created a playground where you can take a look at this change, just create new playground, click on the assistant editor (top left corner, two joined circles) and take a look at the animation. This should do.
import PlaygroundSupport
import UIKit
let rect = CGRect(x: 0, y: 0, width: 40, height: 40)
let view = UIView(frame: rect )
view.backgroundColor = UIColor.yellow
let label = UILabel(frame: rect)
label.text = "A"
label.textAlignment = .center
view.addSubview(label)
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(blurEffectView)
PlaygroundPage.current.liveView = view
UIView.animate(withDuration: 4, animations: {
blurEffectView.alpha = 0
}) { (done) in
if (done) {
blurEffectView.removeFromSuperview()
}
}
The problem you are facing is not that the UIView.animate doesn't do it's job, it's just because you are setting the view to nil is not animable. Like imagine deleting someething could be animated...
UIView.animate(withDuration: 1, animations: {
self.blurEffectView?.frame = CGRect(x: self.blurEffectView?.frame.origin.x, y: self.view.frame.size.height, width: self.blurEffectView?.frame.size.width, height: self.blurEffectView?.frame.size.height)
}, completion: {
(value: Bool) in
self.blurEffectView?.removeFromSuperview()
})
Using the same animation (on UIView) I am able to achieve this :
I have a view which animates from the bottom of my ViewController, and I'm trying to get the animation to be the same as the default
present(viewController, animated:true, completion:nil)
I want to animate a view with the same easing and timing as the above code. Right now I'm using this:
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseInOut, animations: {
self.myView.frame = newFrame
}, completion: nil)
The animations aren't similar though, and I don't know what values to use in my UIView animation to make it look like a viewController present. Anyone know what values could get me close?
here is sample code with somewhat animation like presenting viewcontroller
var subViewNew = UIView()
make initial dream setup of you subview which you want to make to add on main view as below
override func viewDidLoad() {
super.viewDidLoad()
subViewNew.frame = CGRect(x: 100,y: 100,width:self.view.frame.width - 200,height:200)
self.subViewNew.frame.origin.y = self.view.frame.origin.y + self.view.frame.size.height
subViewNew.backgroundColor = UIColor.gray
}
for appearance of the view subview you can go like this
UIView.animate(withDuration: 0.7, animations: {
self.subViewNew.frame.origin.y = 300 + 20
self.view.addSubview(self.subViewNew)
})
for dismiss or hiding it you can go like this
UIView.animate(withDuration: 0.7, animations: {
self.subViewNew.frame.origin.y = self.view.frame.origin.y + self.view.frame.size.height
self.subViewNew.removeFromSuperview() //If you want then only
})
Hope this help you
I am using below code to move from left to right.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.rightNavController?.view.frame = CGRect(x: 0, y: 0, width: (appDelegate.window?.frame.size.width)!, height: (appDelegate.window?.frame.size.height)!)
self.removeNavigationController()
appDelegate.rightNavController = UINavigationController(rootViewController: leftmenuViewController)
appDelegate.rightNavController?.navigationBar.isHidden = true
baseController.addChildViewController(appDelegate.rightNavController!)
baseController.view.addSubview((appDelegate.rightNavController?.view)!)
UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions(), animations: {
}, completion: { finished in
appDelegate.rightNavController?.view.frame = CGRect(x: 0, y: 0, width: 320, height: 568)
})
my view hierarchy in side menu is UIView->UiTableView.I have added tap gesture on UIView to dismiss side menu.
Below is the code used to dismiss:(tap gesture method)
self.willMove(toParentViewController: nil)
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions(), animations: {
self.view.frame = CGRect(x:-self.view.frame.size.width, y: 0 ,width : self.view.frame.size.width, height :self.view.frame.size.height)
}, completion: { finished in
self.removeFromParentViewController()
self.view.removeFromSuperview()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.rightNavController = nil
appDelegate.rightNavController?.view = nil
})
Now the issue is when child viewController is removed, parent view controller user interaction is disabled.
Any help to solve this issue.
It's not entirely clear what you're trying to achieve from the wording of the question but I would look for the isUserInteractionEnabled property on the view that's causing you trouble.
Also, maybe have a look to yourView.layer.zPosition. The view getting the interaction is the one with the highest zPosition value. Everyone defaults to zPosition = 0. All else being equal, the order in which the views where added will dictate who gets the events.
In my application implemented TabBarController Transitions using reference code by Apple Objective-C link And Swift link. But when switch fast between two tabs some times I am getting blank screen, I tried many answers in Stack Overflow but no luck.
Please check below code for reference while doing TabBarController Transitions using Swift
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let containerView = transitionContext.containerView
let fromView: UIView
let toView: UIView
// In iOS 8, the viewForKey: method was introduced to get views that the
// animator manipulates. This method should be preferred over accessing
// the view of the fromViewController/toViewController directly.
if #available(iOS 8.0, *) {
fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
} else {
fromView = fromViewController.view
toView = toViewController.view
}
let fromFrame = transitionContext.initialFrame(for: fromViewController)
let toFrame = transitionContext.finalFrame(for: toViewController)
// Based on the configured targetEdge, derive a normalized vector that will
// be used to offset the frame of the view controllers.
var offset: CGVector
if self.targetEdge == UIRectEdge.left {
offset = CGVector(dx: -1.0, dy: 0.0)
} else if self.targetEdge == .right {
offset = CGVector(dx: 1.0, dy: 0.0)
} else {
fatalError("targetEdge must be one of UIRectEdgeLeft, or UIRectEdgeRight.")
}
// The toView starts off-screen and slides in as the fromView slides out.
fromView.frame = fromFrame
toView.frame = toFrame.offsetBy(dx: toFrame.size.width * offset.dx * -1,
dy: toFrame.size.height * offset.dy * -1)
// We are responsible for adding the incoming view to the containerView.
containerView.addSubview(toView)
let transitionDuration = self.transitionDuration(using: transitionContext)
UIView.animate(withDuration: transitionDuration, animations: {
fromView.frame = fromFrame.offsetBy(dx: fromFrame.size.width * offset.dx,
dy: fromFrame.size.height * offset.dy)
toView.frame = toFrame
}, completion: {finshed in
let wasCancelled = transitionContext.transitionWasCancelled
// When we complete, tell the transition context
// passing along the BOOL that indicates whether the transition
// finished or not.
transitionContext.containerView.addSubview(toView)
transitionContext.completeTransition(!wasCancelled)
})
}
Below is the screen shot
It seems Like you have taken UINavigationController for your second tab. But somehow your connection from UINavigationController to your secondViewController is lost. Please check the image of a storyboard which may be in your scenario.
I also get the same as a blank screen and I fixed it by making the current view it the initial view controller.
check is Initial View Controller