UIViewControllerAnimatedTransitioning doesn't complete on iOS 9/10, but works iOS 11 and later? - swift

I have a custom presentation controller which shows a UIViewController as a "bottom sheet" above the current ViewController, and allows an interactive gesture to dismiss the presented modal. The modal's modalPresentationStyle is set to .current, and the transitioningDelegate is set to my custom class during init(). The current code works perfectly on iOS 11 and 12, but appears to "freeze" the device on iOS 9 and 10.
Here is where the animation takes place. I have confirmed the presentedFrame and dismissedFrame are correct.
extension BottomSheetPresentationManager: UIViewControllerAnimatedTransitioning {
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.25
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let key = presenting ? UITransitionContextViewControllerKey.to
: UITransitionContextViewControllerKey.from
let keyV = presenting ? UITransitionContextViewKey.to
: UITransitionContextViewKey.from
let controller = transitionContext.viewController(forKey: key)!
let view = transitionContext.view(forKey: keyV)!
if presenting {
transitionContext.containerView.addSubview(view)
}
let presentedFrame = transitionContext.finalFrame(for: controller)
var dismissedFrame = presentedFrame
switch direction {
case .left:
dismissedFrame.origin.x = -presentedFrame.width
case .right:
dismissedFrame.origin.x = transitionContext.containerView.frame.size.width
case .top:
dismissedFrame.origin.y = -presentedFrame.height
case .bottom:
dismissedFrame.origin.y = transitionContext.containerView.frame.size.height
}
let initialFrame = presenting ? dismissedFrame : presentedFrame
let finalFrame = presenting ? presentedFrame : dismissedFrame
// Runs when the animation finishes
let completionBlock: (Bool) -> Void = { (finished) in
// tell our transitionContext object that we've finished animating
if transitionContext.transitionWasCancelled {
if self.interactive {
transitionContext.cancelInteractiveTransition()
}
transitionContext.completeTransition(false)
} else {
if self.interactive {
finished ? transitionContext.finishInteractiveTransition() : transitionContext.cancelInteractiveTransition()
}
transitionContext.completeTransition(finished)
}
}
// Put what you want to animate here.
let animationBlock: () -> Void = {
view.frame = finalFrame
}
// Set up for the animation
let animationDuration = transitionDuration(using: transitionContext)
view.frame = initialFrame
// Perform a different animation based on whether we're interactive (performing a gesture) or not
if interactive {
// Do a linear animation so we match our dragging with our transition
UIView.animate(withDuration: animationDuration,
delay: 0,
options: .curveLinear,
animations: animationBlock,
completion: completionBlock)
} else {
// Do a spring animation with easing
UIView.animate(withDuration: animationDuration,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.45,
options: [.curveEaseInOut],
animations: animationBlock,
completion: completionBlock)
}
}
}
After much testing, I have figured out that the completion block on the UIView.animate block never gets called (completionBlock in the code), and in the Capture View Hierarchy view, it actually shows my presented view on top, with what looks like a screenshot of the view (UIVisualEffectView) directly under the presented menu.
What you are seeing there is my bottom menu (the UITableView with cells that you see in front), with a UIVisualEffectView behind it (appears to be a screenshot of the UICollection view behind that), and a UICollectionView that called the present() method for the menu.
What would cause the UIView.animate block not to complete? Is there any reason the behavior would differ between iOS 10 and 11? Any help would be greatly appreciated.

Related

View transitions when TabBatController.selectedIndex changed programmatically

I have some troubles to get the views transitions animated when I change the UITabBarController.selectedIndex changed programmatically.
When I tap on a TabBar icon the animation works fine, but when I change the selectedIndex from a gestureRecognizer action.
The transition code at the TabBar controller class is the following:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if CanChangeTab {
guard let fromView = tabBarController.selectedViewController!.view, let toView = viewController.view else {
return false // Make sure you want this as false
}
if fromView != toView {
if (tabBarController.prevIndex > tabBarController.selectedIndex) {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionFlipFromLeft], completion: nil)
} else {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionFlipFromRight], completion: nil)
}
}
return true
} else {
return false
}
}
The gesture recogniser is calling the following function, from which the above code is not called:
#objc func swiped(_ gesture: UISwipeGestureRecognizer) {
if (CanChangeTab) {
self.tabBarController?.prevIndex = (self.tabBarController?.selectedIndex)!
if gesture.direction == .left {
if (self.tabBarController?.selectedIndex)! < 4 { // set your total tabs here
self.tabBarController?.selectedIndex += 1
}
} else if gesture.direction == .right {
if (self.tabBarController?.selectedIndex)! > 0 {
self.tabBarController?.selectedIndex -= 1
}
}
}
}
I cannot see what should be called or overridden to get the animations for the gesture base change too.
The problem is that what you are doing is not how to do a tab bar controller animation. You have to write a formally structured custom animation transition.
This means that:
Your tab bar controller has a delegate implementing animationControllerForTransitionFrom to return a UIViewControllerAnimatedTransitioning object, and interactionControllerFor to return a UIViewControllerInteractiveTransitioning object.
These objects implement startInteractiveTransition, interruptibleAnimator(using:), transitionDuration(using:), animateTransition(using:), and animationEnded to perform the animation through a UIViewPropertyAnimator.
The gesture recognizer will then be able to trigger the animation by setting the selectedIndex and will be able to track and update the animation through the supplied UIViewControllerContextTransitioning object and the UIViewPropertyAnimator.
Ok. I have found the solution, using ViewController slide animation
as Matt proposed.
So using the extension + the animateToTab function in the extension and changing my swipe method it works just as expected.
#objc func swiped(_ gesture: UISwipeGestureRecognizer) {
if (CanChangeTab) {
let thisTabController = self.tabBarController as! iBayTabController
thisTabController.prevIndex = (thisTabController.selectedIndex)
if gesture.direction == .left {
if thisTabController.selectedIndex < 4 { // set your total tabs here
thisTabController.animateToTab(toIndex: thisTabController.selectedIndex+1)
}
} else if gesture.direction == .right {
if (self.tabBarController?.selectedIndex)! > 0 {
thisTabController.animateToTab(toIndex: thisTabController.selectedIndex-1)
}
}
}
}

How to Mirror(Flip) Video playing using AVPlayer?

I want to Mirror (Flip) video playing using AVPlayer.
Like : MirrorTube :- https://chrome.google.com/webstore/detail/mirrortube/olomckflnlligkboahmaihmeaffjdbfm/related?hl=en
i want to achieve same functionality.
I have tried to change CGAffineTransform but it does not work same.
Thanks in advance!
Here's an example how to flip the player vertically and horizontally by using CGAffineTransform:
PlayerView:
import AVKit
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
// Override UIView property
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
ViewController using the playerView defined in xib/storyboard:
#IBOutlet var playerView: PlayerView!
#IBAction func flipVerticallyBarButtonItemTouched(_ sender: UIBarButtonItem) {
UIView.animate(withDuration: 0.2) { [unowned self] in
self.playerView.transform = self.playerView.transform.concatenating(CGAffineTransform(scaleX: 1, y: -1))
}
}
#IBAction func flipHorizontallyBarButtonItemTouched(_ sender: UIBarButtonItem) {
UIView.animate(withDuration: 0.2) { [unowned self] in
self.playerView.transform = self.playerView.transform.concatenating(CGAffineTransform(scaleX: -1, y: 1))
}
}
Note: For an iOS app in Mac Catalyst (with a AVPlayerController subclass) the root view is AVPlayerView.
But oddly AVPlayerView isn't public on iOS > Mac Catalyst so self.playerView wont work so you cant cast it. You get class not found for 'AVPlayerView'
But when you run the app in Mac Catalyst and inspect it the self.view is an AVPlayerView
workaround just flip the root view itself without casting - self.view
`class MyAVPlayerController: AVPlayerController`
to flip horizontal etc
self.view.transform = self.view.transform.concatenating(CGAffineTransform(scaleX: -1, y: 1))
to flip vertical etc
self.view.transform = self.view.transform.concatenating(CGAffineTransform(scaleX: 1, y: -1))
KNOWN ISSUE this flips the whole AVPlayerView including controls. in iOS 16 the actual player is a view where Swift.type(of:view) is "__AVPlayerLayerView" so you can walk the hierarchy and find the UIView return it then apply the transform to only that subview
func flipHorizontal(){
print("TradAVPlayerViewController.swift flipHorizontal")
if self.player != nil{
//------------------------------------------------------------------
//v1 - flips VC.view which is AVPlayerView
//but also flips TOAST and CONTROLS
//https://developer.apple.com/documentation/uikit/uiview/1622459-transform
//------------------------------------------------------------------
//OK ROTATES VIEW
//self.view.transform = CGAffineTransform(rotationAngle: CGFloat((90 * Double.pi)/180))
//OK FLIP VERTICAL
//self.view.transform = self.view.transform.concatenating(CGAffineTransform(scaleX: 1, y: -1))
//OK FLIPS HORIZONTAL
// self.view.transform = self.view.transform.concatenating(CGAffineTransform(scaleX: -1, y: 1))
//note the flip remains in place as long as self.player isnt recreated so PREV/NEXT will stay flipped
//------------------------------------------------------------------
//ISSUE flipping the whole view also flips the TOAST and SCRUBBER
//------------------------------------------------------------------
//in iOS 15 the player is a class with name "__AVPlayerLayerView"
//find it in the hierarchy and only apply transform to that on
if let rootView = self.view{
let className = "__AVPlayerLayerView"
if let subViewFound
= findSubViewByName(name: className,
view: rootView)
{
print("FOUND className:'\(className)' flipping it")
subViewFound.transform = subViewFound.transform.concatenating(CGAffineTransform(scaleX: -1, y: 1))
}else{
print("NOT FOUND className:'\(className)' CANT FLIP IT - debug hierarchy in 3d and get class name for the AVPlayerLayerView")
}
}else{
logger.error("self.view is nil")
}
print()
}else{
logger.error("self.player is nil")
}
}
func findSubViewByName(name: String, view: UIView) -> UIView?{
var viewFound: UIView? = nil
for subView in view.subviews{
//MUST WRAP IN STRING else types wont match
let typeName = "\(Swift.type(of: subView))"
//print("typeName:\(typeName)")
if typeName == name {
print("SUB VIEW - typeName:\(typeName) - MATCH!!")
viewFound = subView
break
}else{
print("SUB VIEW - typeName:\(typeName) - NO MATCH")
}
//recurse depth first
if let subViewFound = findSubViewByName(name: name, view: subView){
viewFound = subViewFound
break
}else{
//no match in subviewsf
}
}
return viewFound
}

ViewController slide animation

I want to create an animation like the iOS app facebook at tabswitch[1]. I have already tried to develop some kind of animation, the problem that occurs is that the old view controller becomes invisible directly on the switch, instead of fading out slowly while the new controller is sliding in fast.
I've found this SO question How to animate Tab bar tab switch with a CrossDissolve slide transition? but the as correct marked solution does not really work for me (it is not a slide it is a fade transition). What I'd also like to get is the function to make slide left or right to switch the tabs. Like it was on a older version of facebook.
What I've got so far is this:
extension TabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view,
let toView = viewController.view else { return false }
if fromView != toView {
toView.transform = CGAffineTransform(translationX: -90, y: 0)
UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
toView.transform = CGAffineTransform(translationX: 0, y: 0)
})
}; return true
}
}
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
How to fix this?
[1]
I would very much like to add a gif from the Facebook app. The problem is that I don't want to censor the video and just reveal too much of my data. (Even if fb already has them). Also on youtube I didn't find a suitable recording. Please try it yourself in the fb app in iOS.
You can use the following idea: https://samwize.com/2016/04/27/making-tab-bar-slide-when-selected/
Also, here's the code updated to Swift 4.1 and I also removed the force unwrappings:
import UIKit
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let tabViewControllers = tabBarController.viewControllers, let toIndex = tabViewControllers.index(of: viewController) else {
return false
}
animateToTab(toIndex: toIndex)
return true
}
func animateToTab(toIndex: Int) {
guard let tabViewControllers = viewControllers,
let selectedVC = selectedViewController else { return }
guard let fromView = selectedVC.view,
let toView = tabViewControllers[toIndex].view,
let fromIndex = tabViewControllers.index(of: selectedVC),
fromIndex != toIndex else { return }
// Add the toView to the tab bar view
fromView.superview?.addSubview(toView)
// Position toView off screen (to the left/right of fromView)
let screenWidth = UIScreen.main.bounds.size.width
let scrollRight = toIndex > fromIndex
let offset = (scrollRight ? screenWidth : -screenWidth)
toView.center = CGPoint(x: fromView.center.x + offset, y: toView.center.y)
// Disable interaction during animation
view.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.3,
delay: 0.0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: .curveEaseOut,
animations: {
// Slide the views by -offset
fromView.center = CGPoint(x: fromView.center.x - offset, y: fromView.center.y)
toView.center = CGPoint(x: toView.center.x - offset, y: toView.center.y)
}, completion: { finished in
// Remove the old view from the tabbar view.
fromView.removeFromSuperview()
self.selectedIndex = toIndex
self.view.isUserInteractionEnabled = true
})
}
}
So, you need to subclass UITabBarController and you also have to write the animation part, you can tweak the animation options (delay, duration, etc).
I hope it helps, cheers!
I've never seen Facebook so I don't know what the animation is. But you can have any animation you like when a tab bar controller changes its tab (child view controller), coherently and without any hacks, using the built-in mechanism that Apple provides for adding custom animation to a transition between view controllers. It's called custom transition animation.
Apple first introduced this mechanism in 2013. Here's a link to their video about it: https://developer.apple.com/videos/play/wwdc2013/218/
I immediately adopted this in my apps, and I think it makes them look a lot spiffier. Here's a demo of a tab bar controller custom transition that I like:
The really cool thing is that once you've decided what animation you want, making the transition interactive (i.e. drive it with a gesture instead of a button click) is easy:
Now, you might be saying: Okay, but that's not quite the animation I had in mind. No problem! Once you've got the hang of the custom transition architecture, changing the animation to anything you like is easy. In this variant, I just commented out one line so that the "old" view controller doesn't slide away:
So let your imagination run wild! Adopt custom transition animations, the way that iOS intends.
If you want something for pushViewController navigation, you can try this.
However, when switching between tabs on a TabBarController, this will not work. For that, I'd go with #mihai-erős 's solution
Change the Animation duration as per your liking, and assign this class to your navigation segues, for a Slide Animation.
class CustomPushSegue: UIStoryboardSegue {
override func perform() {
// first get the source and destination view controllers as UIviews so that they can placed in navigation stack
let sourceVCView = self.source.view as UIView!
let destinationVCView = self.destination.view as UIView!
let screenWidth = UIScreen.main.bounds.size.width
//create the destination view's rectangular frame i.e starting at 0,0 and equal to screenwidth by screenheight
destinationVCView?.transform = CGAffineTransform(translationX: screenWidth, y: 0)
//the destinationview needs to be placed on top(aboveSubView) of source view in the app window stack before being accessed by nav stack
// get the window and insert destination View
let window = UIApplication.shared.keyWindow
window?.insertSubview(destinationVCView!, aboveSubview: sourceVCView!)
// the animation: first remove the source out of screen by moving it at the left side of it and at the same time place the destination to source's position
// Animate the transition.
UIView.animate(withDuration: 0.3, animations: { () -> Void in
sourceVCView?.transform = CGAffineTransform(translationX: -screenWidth,y: 0)
destinationVCView?.transform = CGAffineTransform.identity
}, completion: { (Finished) -> Void in
self.source.present(self.destination, animated: false, completion: nil)
})
}
}

Close partially shown modal view controller by tapping outside of the view controller

I made a custom modal transition by using UIViewControllerAnimatedTransitioning. After the transition has completed, a modally shown view controller fills part of parent view that some part of parent view controller is still visible.
I need to know how to detect tap on outside of shown view controller(or partially shown parent view controller) so I can use it to close the modal. I understand how to add gesture to a view but I cannot find which view to add a gesture to.
I looked through stackoverflow but there aren't example specifically for custom modal transition.
Here is a code for custom transition
class PartialSlideInAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
var direction: SlideAnimationDirectionStyle
var duration: TimeInterval = 0.125
var presentedControllerWidth: CGFloat = 250
let fadeAlpha: CGFloat = 0.5
init(direction: SlideAnimationDirectionStyle) {
self.direction = direction
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(duration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Setup Views
guard let presentedFromVC = transitionContext.viewController(forKey: .from) else {transitionContext.completeTransition(false); return }
guard let presentingToVC = transitionContext.viewController(forKey: .to) else {transitionContext.completeTransition(false); return }
let contrainerView = transitionContext.containerView // アニメーションはこのViewで行われる、アニメーションを実装する presenter, presenteeをこの上に載せないとアニメーションが動かない
// Setup Frames
let positionHidden = CGRect(x: contrainerView.frame.width + 1, y: 0, width: presentedControllerWidth, height: contrainerView.frame.height)
let positionShown = CGRect(x: contrainerView.frame.width - presentedControllerWidth, y: 0, width: presentedControllerWidth, height: contrainerView.frame.height)
switch direction {
case .slideIn:
contrainerView.addSubview(presentingToVC.view)
presentingToVC.view.frame = positionHidden
UIView.animate(withDuration: duration, animations: {
presentingToVC.view.frame = positionShown
presentedFromVC.view.alpha = self.fadeAlpha
}, completion: { success in
transitionContext.completeTransition( success )
})
case .slideOut:
contrainerView.addSubview(presentedFromVC.view)
presentedFromVC.view.frame = positionShown
UIView.animate(withDuration: duration, animations: {
presentedFromVC.view.frame = positionHidden
presentingToVC.view.alpha = 1.0
}, completion: { success in
transitionContext.completeTransition( success )
})
}
}
}
Have you tried using UIGestureRecognizer yet? The UIGestureRecognizer can be utilized to recognize tap events, both inside a child view controller and a parent view controller. For swift 3, there should be a handleTapBehind function of the UIRecognizerDelegate that should allow you to do what you're looking for.
See if the what is documented here
See the "Swift 3.1 solution that works in both portrait and landscape" in the linked post.

Weird UIVisualEffectView behavior in multitasking

I have an AccountsViewController which has UIVisualEffectView with blur effect in background (subview at index 0), so that it covers previous controller with blur during transition. But when I then switch to another app and open multitasking menu again to switch back to my app, its blurry UIVisualEffectView seem to be damaged (as shown on the screenshot 1). When I make my app active again, the view gets "repaired" and looks ok again (as shown on the second screenshot)
Some code from my custom transitioning class where I add the blur view
// in the animateTransition.. method
guard let container = transitionContext.containerView() else {
return
}
let blurEffectView: UIVisualEffectView
if container.viewWithTag(101) != nil {
blurEffectView = container.viewWithTag(101)! as! UIVisualEffectView
}
else {
blurEffectView = UIVisualEffectView(frame: container.frame)
blurEffectView.translatesAutoresizingMaskIntoConstraints = false
blurEffectView.tag = 101
container.addSubview(blurEffectView)
}
if self.isPresenting {
// We're presenting a view controller over another
toViewController.view.transform = CGAffineTransformMakeScale(0.7, 0.7)
container.addSubview(toViewController.view)
container.sendSubviewToBack(blurEffectView)
toViewController.view.alpha = 0
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: { () -> Void in
fromViewController.view.transform = CGAffineTransformMakeScale(0.8, 0.8)
toViewController.view.transform = CGAffineTransformMakeScale(1, 1)
blurEffectView.effect = UIBlurEffect(style: .Dark)
toViewController.view.alpha = 1
}, completion: { (completed) -> Void in
self.context?.completeTransition(completed)
})
}
else {
// We're dismissing a view controller
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: { () -> Void in
fromViewController.view.transform = CGAffineTransformMakeScale(0.7, 0.7)
toViewController.view.transform = CGAffineTransformMakeScale(1, 1)
blurEffectView.effect = nil
fromViewController.view.alpha = 0
}, completion: { (completed) -> Void in
fromViewController.view.removeFromSuperview()
self.context?.completeTransition(completed)
})
}
Seems like I've figured it out myself: this issue appears only when there is an UIWindow back (which is black) showing through.
In this question I had a transition animation container with these views:
fromViewController's view (with a scale transform 0.8;0.8)
UIVisualEffectView with .Dark style blur applied to it
toViewController's view
Because of fromViewController's view size (which was smaller than the screen because of scale transform), the black UIWindow view was showing through. And according to my research this causes UIVisualEffectView to produce a glitch described in this question.
The solution is to add a black UIView with the frame of an UIWindow and add it to the bottom of the container's view hierarchy:
let blackView = UIView(frame: container.frame)
blackView.backgroundColor = UIColor.blackColor()
container.insertSubview(blackView, atIndex: 0)
And then this glitch seem to appear no longer.