I want to present my view controller with a custom transition so the fromViewController will be darkened as if presenting a AlertViewController.
I have created my customTransition manager:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
let fadeRect = CGRectMake(0, 0, fromView.frame.size.width, fromView.frame.size.height)
let fadeView = UIView(frame: fadeRect)
if (self.presenting == true){
fadeView.backgroundColor = UIColor.blackColor()
fadeView.alpha = 0
fromView.addSubview(fadeView)
container.insertSubview(fadeView, aboveSubview: fromView)
} else {
// adding subviews to container
}
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: { () -> Void in
if (self.presenting == true){
fadeView.alpha = 0.5
} else {
}
}) { (Bool) -> Void in
if self.presenting{
container.addSubview(toView)
}
transitionContext.completeTransition(true)
}
}
}
I assign my transitioning manager to the toViewController as it can be seen below:
var purchaseSpecialItemsViewController = self.storyboard?.instantiateViewControllerWithIdentifier("specialItemPurchaseVC") as! BRSpecialItemPurchaseViewController
purchaseSpecialItemsViewController.transitioningDelegate = self.fadedTransitionManager
self.presentViewController(purchaseSpecialItemsViewController, animated: true, completion: nil)
The View Controller I am about to display is in storyboard with a fixed size. It has a clear background and I added a tableview there that I want to be in the center of the screen.
The TableView is displayed without the problem, but I can't see the fromViewController at all. It is just black even though I set the alpha of the black background to 0 which is later animated to 0.5.
I am presenting the view controller modally, is that causing the problem? Should I use instead a push and pop operation?
As described here: http://mathewsanders.com/custom-menu-transitions-in-swift/
I needed to add the modal presentation style like this:
purchaseSpecialItemsViewController.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
and instead getting ViewControllersViews, use
let screens: (from: UIViewController, to: UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)
to access the ViewControllers.
Related
I have a tableViewController in a containerView of another viewController.
When an image is tapped, a custom transition occurs to another viewController to view the image. The transition works well.
When the custom transition takes place though viewWillDisappear is called on the containerView's tableViewController and then viewWillAppear again when the presented viewController is dismissed (again with a transition).
This is causing some unusual scrolling occasionally in the tableView.
What I would like is to present the imageViewer with a transition but overCurrentContext.
I have tried what people suggest which is the following:
self.definesPresentationContext = true
vc.modalPresentationStyle = .overCurrentContext
vc.transitioningDelegate = self
self.present(vc, animated: true, completion: nil)
Unfortunately this is not working and when the imageViewController is dismissed, initially the transition back is correct and the tableViewController can be seen. But once the transition is complete the view goes black.
I think this may have to do with the fact that I'm using a containerView. But not sure and not sure how to resolve this.
This is the animator method I'm using:
private var image: UIImage?
private var fromDelegate: ImageTransitionProtocol?
private var toDelegate: ImageTransitionProtocol?
// MARK: Setup Methods
func setupImageTransition(image: UIImage, fromDelegate: ImageTransitionProtocol, toDelegate: ImageTransitionProtocol) {
self.image = image
self.fromDelegate = fromDelegate
self.toDelegate = toDelegate
}
// MARK: UIViewControllerAnimatedTransitioning
// 1: Set animation speed
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
// 2: Get view controllers involved
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
// 3: Set the destination view controllers frame
toVC.view.frame = fromVC.view.frame
// 4: Create transition image view
let imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFill
imageView.frame = (fromDelegate == nil) ? CGRect() : fromDelegate!.imageWindowFrame()
imageView.clipsToBounds = true
containerView.addSubview(imageView)
// 5: Create from screen snapshot
fromDelegate!.transitionSetup()
toDelegate!.transitionSetup()
let fromSnapshot = fromVC.view.snapshotView(afterScreenUpdates: true)!
fromSnapshot.frame = fromVC.view.frame
containerView.addSubview(fromSnapshot)
// 6: Create to screen snapshot
let toSnapshot = toVC.view.snapshotView(afterScreenUpdates: true)!
toSnapshot.frame = fromVC.view.frame
containerView.addSubview(toSnapshot)
toSnapshot.alpha = 0
// 7: Bring the image view to the front and get the final frame
containerView.bringSubview(toFront: imageView)
let toFrame = (self.toDelegate == nil) ? CGRect() : self.toDelegate!.imageWindowFrame()
// 8: Animate change
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: .curveEaseOut, animations: {
toSnapshot.alpha = 1
imageView.frame = toFrame
}, completion:{ [weak self] (finished) in
self?.toDelegate!.transitionCleanup()
self?.fromDelegate!.transitionCleanup()
// 9: Remove transition views
imageView.removeFromSuperview()
fromSnapshot.removeFromSuperview()
toSnapshot.removeFromSuperview()
// 10: Complete transition
if !transitionContext.transitionWasCancelled {
containerView.addSubview(toVC.view)
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
And this is in the presenting viewController:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let imagePresentationViewController = (presented as! UINavigationController).topViewController as! ImagePresentationViewController
self.transition.setupImageTransition( image: pickedImageForTransition!, imageType: nil,
fromDelegate: self,
toDelegate: imagePresentationViewController)
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let imagePresentationViewController = (dismissed as! UINavigationController).topViewController as! ImagePresentationViewController
transition.setupImageTransition( image: pickedImageForTransition!, imageType: .listsImage,
fromDelegate: imagePresentationViewController,
toDelegate: self)
return transition
}
Had the same problem, you just need to remove container view from func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
First off, this is my view controller/segue setup:
The three rightmost view controllers' background views are UIVisualEffectViews through which the source view controllers should be visible. They were added in the various viewDidLoad()s like this:
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = self.view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.tableView.backgroundView = blurEffectView
Now, the main view controller (the "Garbage Day" one) is visible through the settings view controller, but the settings VC disappears whenever one of the two rightmost VCs is fully on screen. Here's a screen recording:
Screen recording of the source view controller dis- and reappearing
(Please ignore the glitches, the app I used to upload this apparently corrupted the video)
I get that technically, the Show segue doesn't have the "Over Current Context" thingy and therefore, I shouldn't expect the source VC to not disappear, but there has to be a way to make this work without custom segues.
I suggest you create a custom transition between view controllers.
I just wrote and tested this class:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning
{
var pushing = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let duration = transitionDuration(using: transitionContext)
let toVc = transitionContext.viewController(forKey: .to)!
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
let container = transitionContext.containerView
if pushing {
container.addSubview(fromView)
container.addSubview(toView)
}
var finalFrame = transitionContext.finalFrame(for: toVc)
if pushing {
finalFrame.origin.x = finalFrame.width
toView.frame = finalFrame
finalFrame.origin.x = 0
} else {
finalFrame.origin.x = finalFrame.width
}
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
if self.pushing {
toView.frame = finalFrame
} else {
fromView.frame = finalFrame
}
}) { (_) in
transitionContext.completeTransition(true)
if self.pushing {
container.insertSubview(fromView, belowSubview: toView)
} else {
fromView.removeFromSuperview()
}
}
}
}
In your UINavigationController class do the following:
class NavigationController: UINavigationController {
let animationController = AnimationController()
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
And this extension:
extension NavigationController: UINavigationControllerDelegate
{
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animationController.pushing = operation == .push
return animationController
}
}
However, this makes you lose the interactive dismiss gesture (Swiping from the left of the screen to dismiss) So you would need to fix that yourself.
I'm trying to animate up a UICollectionViewController over an original UIViewController with the original UIViewController blurred as background, however, every time the animation finishes, I can see clearly through the blurred view that the original view controller is dismissed, what do I do to avoid the first UIViewController being dismissed?
Code in the first view controller to present the second:
let VC = storyboard?.instantiateViewController(withIdentifier: "PopoverCollectionVC") as! PopoverCollectionVC
VC.setDataSource(with: .calcDPSItems)
VC.collectionView?.backgroundColor = UIColor.clear
VC.transitioningDelegate = self
self.present(VC, animated: true, completion: nil)
Code in the animator object for custom animation:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
if presenting {
// configure blur
effectView.frame = fromView.bounds
containerView.addSubview(effectView)
// configure collection view
toView.frame = CGRect(x: 0, y: fromView.frame.height, width: fromView.frame.width, height: fromView.frame.height / 2)
containerView.addSubview(toView)
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.6) {
toView.center.y = fromView.center.y
}
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 1) {
self.effectView.effect = UIBlurEffect(style: .dark)
}
}) { _ in
transitionContext.completeTransition(true)
}
} else {
...
}
You need to specify that the modal presentation style will be over the current context :
VC.modalPresentationStyle = .overCurrentContext
Then to get the the presented view you need
// Get the from view from The ViewController because there is a bug in iOS when
// using some modalPresentationStyle values
// that causes the viewForKey to returm nil for UITransitionContextFromViewKey
// www.splinter.com.au/2015/04/17/ios8-view-controller-transitioning-bug/
let fromVC = transitionContext.viewController(forKey: .from)
let fromView = fromVC?.view
You should instead take a snapshot of the previous view, then blur than and use it as a background on the new VC.
let oldView = view_to_copy.snapshotView(afterScreenUpdates: false)
Then blur this, and add as a subview to your collectionView controller.
EDIT:
Just to add if you are using the VC inside something like UITabbar or UINavigationController you may need to snapshot that view to ensure all the UI is inside the snapshot.
I followed many tutorials about how to create custom transitions but most of them apply to the presentation and dismissing of the view controllers. I just want to apply custom transitions to the UINavigationController for the push/pop actions.
After reading apple documentation I created simple project to test if it works.
Here is all my code for custom Navigation Controller:
class CustomNC: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension CustomNC: UINavigationControllerDelegate {
func navigationController(
navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("Creating UIViewControllerAnimatedTransitioning for operation: \(operation.rawValue)")
return self
}
}
extension CustomNC: UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
print("Returning duration for transition")
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
print("Animating transition")
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 offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
// start the toView to the right of the screen
toView.transform = offScreenRight
// 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: [],
animations: {
fromView.transform = offScreenLeft
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
}
Then I simply create two default view controllers First and Second. First has UIButton with show action to the Second. First is embedded in NavigationController and this NavigationController has Custom Class set to CustomNC.
After running I see V1 controller but when I tap UIButton application crash without any error message. In Logs I see only those lines:
Creating UIViewControllerAnimatedTransitioning for operation: 1
Returning duration for transition
(lldb)
So as I understand the delegate is visible and navigation controller is using this delegate properly. When transition starts it gets the animation duration time interval but animateTransition method is never called because I don't see "Animating transition" message in log.
Can somebody point me where I have problem?
I'm developing an ios app. I have a a main view and in this view
im trying to present a modal view controller with dimmed background(black with opacity).
The problem is that the status bar is not affected by this color and remains the same.
This is how i present the view controller:
let shareViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ShareViewController") as! ShareViewController
shareViewController.battle = battle
shareViewController.delegate = self
let animation = CATransition()
animation.duration = 1
animation.type = kCATransitionFade
self.view.window?.layer.addAnimation(animation, forKey: kCATransition)
presentViewController(shareViewController, animated: false) {
() in
// nothing here
}
Here are some screenshots to demonstrate the problem:
This is the problem(status bar color):
Problem illustration
This is the modal view in storyboard:
storyboard
I cannot reproduce your problem, the following code works without problems in my single view app:
let viewController = UIViewController()
viewController.modalPresentationStyle = .overFullScreen
viewController.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
let animation = CATransition()
animation.duration = 1
animation.type = kCATransitionFade
self.view.window?.layer.add(animation, forKey: kCATransition)
self.present(viewController, animated: false, completion: nil)
However note that you should be presenting over the root controller of the view. Sometimes you can get strange effects when presenting from your internal controllers:
self.view.window?.rootViewController?.present(viewController, animated: false, completion: nil)
Also make sure you are using the correct modalPresentationStyle.
Set your view controller as the root view controller of a UIWindow, then present the window at the UIWindowLevelAlert level.
Below is a Swift 3 class used to animate a modal popup over all other UI elements, including the status bar. A scrim view is used to shade background UI and intercept touches to dismiss the view.
import UIKit
class ModalViewController: UIViewController {
private let scrimView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.black
view.alpha = 0.0
return view
}()
private var myWindow: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.clear
// Setup scrim View
view.addSubview(scrimView)
view.topAnchor.constraint(equalTo: scrimView.topAnchor).isActive = true
view.leftAnchor.constraint(equalTo: scrimView.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: scrimView.rightAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: scrimView.bottomAnchor).isActive = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismiss as (Void) -> Void))
scrimView.addGestureRecognizer(tapGestureRecognizer)
// Layout custom popups or action sheets
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.25) {
self.scrimView.alpha = 0.5
// Animate in custom popups or action sheets
}
}
func present() {
myWindow = UIWindow(frame: UIScreen.main.bounds)
myWindow?.windowLevel = UIWindowLevelAlert
myWindow?.backgroundColor = UIColor.clear
myWindow?.rootViewController = self
myWindow?.isHidden = false
}
func dismiss() {
UIView.animate(
withDuration: 0.25,
animations: {
self.scrimView.alpha = 0.0
// Animate out custom popups or action sheets
},
completion: { success in
self.myWindow = nil
}
)
}
}
To present the view:
let modalView = ModalViewController()
modalView.present()
To dismiss the view, tap anywhere on the scrim.
this code works for me, when I am presenting UIViewController with alpha != 1. present UIViewController like:
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let destinationVC = storyBoard.instantiateViewController(withIdentifier: "AddComment") as! AddCommentViewController
destinationVC.modalPresentationStyle = .overCurrentContext //this line is important
destinationVC.delegate = self
destinationVC.restId = self.restaurant.id
self.present(destinationVC, animated: true, completion: nil)
then in destinationVC view controller
override func viewWillDisappear(_: Bool) {
UIView.animate(withDuration: 1, animations: { () in
self.view.backgroundColor = .clear
})
super.viewWillDisappear(true)
}
override func viewWillAppear(_: Bool) {
UIView.animate(withDuration: 1, animations: { () in
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
})
super.viewWillAppear(true)
}
and set its backgroundColor to .clear in viewDidLoad or storyboard. So UIViewController covers whole screen including status bar.
Here is the solution you might be looking for:
if let window = UIApplication.shared.keyWindow {
window.windowLevel = UIWindowLevelStatusBar + 1
}
The main idea behind this code is, window of your application has a window level which is lower than status bar window level. And what this code does is, just put your window's window level higher than status bar window level, and your window can now cover the status bar. Don't forget, this code has to be called on main thread, just before presenting your view controller. Good luck!
Custom animation transitions should be performed using UIViewControllerAnimatedTransitioning. Here is a tutorial for this purpose:
https://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions
If all you want is a fade animation you can have it by changing the modalTransitionStyle property of the viewController you are going to display.
Try by fixing your code this way:
guard let shareViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ShareViewController") as! ShareViewController else {
//Fallback in case of nil?
return
}
shareViewController.modalTransitionStyle = .crossDissolve
presentViewController(shareViewController, animated: true, completion: nil)
Also please note that presentViewController(shareViewController, animated: true, completion: nil) is for swift 2. The equivalent swift 3 would be present(shareViewController, animated: true, completion: nil)
you can add this code to view controller for Swift 3:
let statusView: UIView = UIView(frame: CGRect(x: 0.0, y: -20.0, width: UIScreen.main.bounds.size.width, height: 20.0))
statusView.backgroundColor = UIColor.black
statusView.alpha = 0.8
self.addSubview(self.statusView)
You could be extremely practical and simply hide the status bar when your modal view controller is up:
override func prefersStatusBarHidden() -> Bool {
return true
}