iOS - Custom transition with Material design container transform effect - ios

I am trying to achieve 3. Material design - container transform effect with custom transitioning in iOS. Below is the code for the presentation part.
class CustomTransition: NSObject {
var duration: TimeInterval = 5
}
extension CustomTransition:UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) as? ViewController else {
return
}
let finalFrame = transitionContext.finalFrame(for: toViewController)
toViewController.view.frame = fromViewController.menuButton.frame
toViewController.view.layer.cornerRadius = fromViewController.menuButton.frame.size.width / 2
toViewController.view.clipsToBounds = true
toViewController.view.alpha = 0
let archive = NSKeyedArchiver.archivedData(withRootObject: fromViewController.menuButton!)
let menuButtonCopy = NSKeyedUnarchiver.unarchiveObject(with: archive) as! UIButton
menuButtonCopy.layer.cornerRadius = menuButtonCopy.frame.size.width / 2
transitionContext.containerView.addSubview(menuButtonCopy)
transitionContext.containerView.addSubview(toViewController.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toViewController.view.frame = finalFrame
toViewController.view.alpha = 1
menuButtonCopy.alpha = 0
menuButtonCopy.frame = finalFrame
}, completion: { _ in
transitionContext.completeTransition(true)
menuButtonCopy.removeFromSuperview()
})
}
}
Here is the result
Actually, I want to align the '+' button in the centre of the container as long as it animates to the full screen as seen in the design. What is the I am missing here? Why is the '+' seen arising from bottom centre?

Related

How to make transition of a ViewController from Bottom to Top?

So here is a class for Slide in transition which adds a ViewController with animation from left to right and it works flawlessly I want a transition from bottom to top.
import UIKit
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width * 0.8
let finalHeight = toViewController.view.bounds.height
if isPresenting {
// Add dimming view
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.0
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
// Add menu view controller to container
containerView.addSubview(toViewController.view)
// Init frame off the screen
toViewController.view.frame = CGRect(x: -finalWidth, y: 0, width: finalWidth, height: finalHeight)
}
// Move on screen
let transform = {
self.dimmingView.alpha = 0.5
toViewController.view.transform = CGAffineTransform(translationX: finalWidth, y: 0)
}
// Move back off screen
let identity = {
self.dimmingView.alpha = 0.0
fromViewController.view.transform = .identity
}
// Animation of the transition
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
}
}
}
To be honest I copied this code from somewhere a while ago and I don't have a source of it.
I'm fairly new to iOS so any help would be appreciated.
Try this,
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width
let finalHeight = toViewController.view.bounds.height * 0.8
if isPresenting {
// Add dimming view
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.0
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
// Add menu view controller to container
containerView.addSubview(toViewController.view)
// Init frame off the screen
toViewController.view.frame = CGRect(x: 0, y: finalHeight, width: finalWidth, height: finalHeight)
}
// Move on screen
let transform = {
self.dimmingView.alpha = 0.5
toViewController.view.transform = CGAffineTransform(translationX: 0, y: -finalHeight)
}
// Move back off screen
let identity = {
self.dimmingView.alpha = 0.0
fromViewController.view.transform = .identity
}
// Animation of the transition
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
}
}
}

CGAffineTransform.identity doesn't reset transform correctly after device rotation

I am doing a custom transition and if after present animation, device will be rotated and then destinationVC will be dismissed, originVC transform is not correct (not fulfil screen). If there is no device rotation, everything works perfectly fine. Does any one can help me?
Here is my code for present and dismiss animation:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: 0)
originViewController.view.transform = originViewController.view.transform.scaledBy(x: 0.95, y: 0.95)
originViewController.view.layer.cornerRadius = 8.0
}, completion: { completed in
transitionContext.completeTransition(completed)
})
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
originViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
destinationViewController.view.transform = CGAffineTransform.identity
destinationViewController.view.layer.cornerRadius = 0.0
}, completion: { completed in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
Screens:
Before present animation
After present animation
After device rotation
After dismiss animation
EDIT:
when I add destinationViewController.view.frame = transitionContext.finalFrame(for: destinationViewController) to dismiss animation everything seems works fine but I don't know if this is right way
Add a subView in ViewC1 with leading, top, bottom, trailing constraints.
Working full code
class ViewC1: UIViewController {
#IBAction func presentNow(_ sender: Any) {
let viewc = storyboard!.instantiateViewController(withIdentifier: "ViewC2") as! ViewC2
viewc.transitioningDelegate = viewc
viewc.modalPresentationStyle = .overCurrentContext
present(viewc, animated: true, completion: nil)
}
}
class ViewC2: UIViewController {
let pres = PresentAnimator()
let diss = DismissAnimator()
#IBAction func dissmisNow(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
extension ViewC2: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return pres
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return diss
}
}
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
transitionContext.containerView.addSubview(destinationViewController.view)
destinationViewController.view.frame = transitionContext.containerView.bounds
let originView = originViewController.view.subviews.first
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: 0)
originView?.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
originView?.layer.cornerRadius = 8.0
}, completion: { completed in
transitionContext.completeTransition(completed)
})
}
}
class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
let originView = destinationViewController.view.subviews.first
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
originViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
originView?.transform = CGAffineTransform.identity
originView?.layer.cornerRadius = 0.0
}, completion: { completed in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Update:
or you can override willRotate and didRotate if you dont want to use view.subviews.first
var prevTrans: CGAffineTransform?
override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
prevTrans = view.transform
view.transform = CGAffineTransform.identity
}
override func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
if let prevTrans = prevTrans {
view.transform = prevTrans
}
}

Snapshot view is hidden while performing UIViewControllerAnimatedTransitioning

I am trying to make a simple animated transitioning when pushing a UIViewController, but it seems I am missing something.
I animate a snapshot of a subview from the fromViewController to the toViewController. I am animating snapshot’s frame, but the snapshot is invisible for the whole duration of the animation.
Here is a simple code example. I am trying to animate a single UILabel from the first controller to the second. I specifically want to animate a snapshot taken from the toViewConroller and not from the fromViewController.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from) as! ViewController
let toVC = transitionContext.viewController(forKey: .to) as! SecondViewController
let container = transitionContext.containerView
toVC.view.frame = fromVC.view.frame
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
let animatedFromView = fromVC.label!
let animatedToView = toVC.label!
let initialFrame = container.convert(animatedFromView.frame,
from: animatedFromView.superview)
let finalFame = container.convert(animatedToView.frame,
to: animatedToView.superview)
let snapshot = animatedToView.snapshotView(afterScreenUpdates: true)!
snapshot.frame = initialFrame
container.addSubview(snapshot)
animatedFromView.alpha = 0
animatedToView.alpha = 0
UIView.animate(withDuration: 2,
animations: {
snapshot.frame = finalFame
}) { (_) in
snapshot.removeFromSuperview()
fromVC.label.alpha = 1
toVC.label.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
I guess that the snapshot is hidden, because setting the animatedToView’s alpha to 0, however I am not sure how to achieve that animation without setting that.
I tried your code its working fine.I changed a few things like initial frame hardcoded it so i can see the effect and also from viewController alpha.
::::::for presenting view controller
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from) as! ViewController
let toVC = transitionContext.viewController(forKey: .to) as! SecondViewController
let container = transitionContext.containerView
toVC.view.frame = fromVC.view.frame
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
let animatedFromView = fromVC.view!
let animatedToView = toVC.view!
let initialFrame = container.convert(CGRect(x: 0, y: 200, width: 100, height: 100),
from: animatedFromView.superview)
let finalFame = container.convert(animatedToView.frame,
to: animatedToView.superview)
let snapshot = animatedToView.snapshotView(afterScreenUpdates: true)!
snapshot.frame = initialFrame
container.addSubview(snapshot)
animatedFromView.alpha = 1
animatedToView.alpha = 0
UIView.animate(withDuration: 2,
animations: {
snapshot.frame = finalFame
}) { (_) in
snapshot.removeFromSuperview()
fromVC.view.alpha = 1
toVC.view.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
:::::::::::::::::
Use while you are pushing view controller
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else { return }
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
let container = transitionContext.containerView
container.insertSubview(toView, belowSubview: fromView)
let animatedFromView = fromView
let animatedToView = toView
let initialFrame = container.convert(CGRect(x: 0, y: 200, width: 100, height: 100),
from: animatedFromView.superview)
let finalFame = container.convert(animatedToView.frame,
to: animatedToView.superview)
let snapshot = animatedToView.snapshotView(afterScreenUpdates: true)!
snapshot.frame = initialFrame
container.addSubview(snapshot)
animatedFromView.alpha = 1
animatedToView.alpha = 1
UIView.animate(withDuration: 2,
animations: {
snapshot.frame = finalFame
}) { (_) in
snapshot.removeFromSuperview()
//fromVC.view.alpha = 1
//toVC.view.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}

UIViewControllerAnimatedTransitioning only works every other time?

I have been at this for hours and cant understand what i am doing wrong. I have rigged up custom tab bar controller transitions by conforming to UITabBarControllerDelegate as described in my previous Swift: Problems with custom UIView.transition?
I don't use the normal storyboard tab bar buttons, I switch selectedIndex programmatically. My problem is that only with this implemented:
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = ModalTransition()
animator.fromView = fromVC.view
animator.toView = toVC.view
return animator
}
the animations AND switching of index occurs only every other time. I have custom buttons to switch the index and every other time, nothing happens when I click the switch button. Here is my animation:
//
// ModalTransition.swift
// Adventures In Design
//
// Created by Skylar Thomas on 8/28/17.
//
import UIKit
class ModalTransition: NSObject, UIViewControllerAnimatedTransitioning {
weak var transitionContext: UIViewControllerContextTransitioning?
var fromView = UIView()
var toView = UIView()
var duration = 1.1
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
containerView.addSubview(toView)
containerView.sendSubview(toBack: toView)
print("ANIMATING")
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear, animations: {
self.fromView.center.y += 900
}, completion: {
finished in
//only works every OTHER click
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.fromView.center.y -= 900
})
}
}
What is causing this? Is it something
I think you are not assigning your toView and FromView. Try something like this
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let fromView = fromViewController.view
let toView = toViewController.view
let container = transitionContext.containerView
container.addSubview(toView!)
// Replace your animations here
toView?.frame = transitionContext.finalFrame(for: toViewController)
toView?.alpha = 0
let duration = self.transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: {
toView?.alpha = 1
fromView?.alpha = 0
}, completion: { finished in
toView?.alpha = 1.0
fromView?.alpha = 1
fromView?.removeFromSuperview()
transitionContext.completeTransition(true)
})
}

How access frame of a toView object in custom animation

I want to make an animation like iOS home screen folders, and I have to know the frame of the final position of the "folder":
I'm using a custom transition and here is the code of the animation file:
class FolderAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 5.0
var presenting = true
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let toViewC = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey)! as! ProjectViewController
let fromViewC = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey)!as! ViewController
let cell : FolderCollectionViewCell = fromViewC.folderCollectionView.cellForItem(at: (fromViewC.folderCollectionView.indexPathsForSelectedItems()?.first!)!) as! FolderCollectionViewCell
let cellSnapshot: UIView = cell.snapshotView(afterScreenUpdates: false)!
let cellFrame = containerView.convert(cell.frame, from: cell.superview)
cellSnapshot.frame = cellFrame
cell.isHidden = true
toViewC.view.frame = transitionContext.finalFrame(for: toViewC)
toViewC.view.alpha = 0
containerView.addSubview(toViewC.view)
containerView.addSubview(cellSnapshot)
UIView.animate(withDuration: duration, animations: {
toViewC.view.alpha = 1.0
let finalFrame = containerView.convert(toViewC.containerView.frame, from: toViewC.view)
cellSnapshot.frame = finalFrame
}) { (_) in
cellSnapshot.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
}
All works correctly, except let finalFrame = containerView.convert(toViewC.containerView.frame, from: toViewC.view) that set finalFrame values (is a CGRect variable) to (origin = (x = 0, y = 0), size = (width = 0, height = 0)).
I've followed this tutorial, writing my code in Swift
So, how can I access the property correctly?
The frame is the zero rect because at the point you are accessing it, the view's layout pass has not occurred. After you set the toVC.view frame, add this line:
toVC.view.layoutIfNeeded()
I've written about the use of snapshots in view controller transitions here if you're interested.

Resources