Interactive Transitioning

prerequisite: animationController(forDismissed:)

//MARK: - Interactive Transition

private var dismissInteractiveTransition = InteracitveTransition()

override func viewDidLoad() {
        super.viewDidLoad()

        addPanGestureRecognizer()
    }

extension ChromecastExpandedViewController {
    override func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return dismissInteractiveTransition.hasStarted ? dismissInteractiveTransition : nil
    }
    
    private func addPanGestureRecognizer() {
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan(sender:)))
        panGestureRecognizer.maximumNumberOfTouches = 1
        view.addGestureRecognizer(panGestureRecognizer)
    }
    
    @objc private func didPan(sender: UIPanGestureRecognizer) {
        let progressThreshold:CGFloat = 0.3
        let progress = dismissProgress(of: sender)
        
        switch sender.state {
        case .began:
            dismissInteractiveTransition.hasStarted = true
            dismiss(animated: true, completion: nil)
        case .changed:
            dismissInteractiveTransition.shouldFinish = progress > progressThreshold
            dismissInteractiveTransition.update(progress)
        case .cancelled:
            dismissInteractiveTransition.hasStarted = false
            dismissInteractiveTransition.cancel()
        case .ended:
            dismissInteractiveTransition.hasStarted = false
            dismissInteractiveTransition.shouldFinish
                ? dismissInteractiveTransition.finish()
                : dismissInteractiveTransition.cancel()
        default:
            break
        }
    }
    
    private func dismissProgress(of gestureRecognizer:UIPanGestureRecognizer) -> CGFloat {
        let touchPoint = gestureRecognizer.translation(in: gestureRecognizer.view)
        var downwardMovement = Float(touchPoint.y / view.bounds.height)
        downwardMovement = fmaxf(downwardMovement, 0.0)
        downwardMovement = fminf(downwardMovement, 1.0)
        let progress = CGFloat(downwardMovement)
        return progress
    }
}

class InteracitveTransition: UIPercentDrivenInteractiveTransition {
    var hasStarted = false
    var shouldFinish = false
}

interface orientation among windows

UserInterfaceOrientation among windows is shared.

When I present a view controller on the other window like


@objc static func viewControllerToChangeOrientation() -> UIViewController {
        let newWindow = UIWindow(frame: UIScreen.main.bounds)
        let viewController = PlayerPresentingViewController()
        viewController.view.backgroundColor = .clear
        newWindow.windowLevel = UIWindow.Level.alert + 1
        newWindow.rootViewController = viewController
        newWindow.makeKeyAndVisible()
        
        if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
            appDelegate.playerWindow = newWindow
        }
        
        return viewController
    }

func presentPlayerViewController() {
UIViewController *playerPresentingViewController = [UIHelper viewControllerToChangeOrientation];
            [playerPresentingViewController presentViewController:vc animated:YES completion:^{
            }];
}

When you check orientation of the delegate window ( window that doesn’t present player view controller)

class PlayerPresentAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return PlayerTransitionDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewController(forKey: .from), <<<If you check at this line
}

(You can check by “po UIApplication.shared.delegate?.window??.windowScene?.interfaceOrientation.isPortrait)

It prints “false” even though the view controller on that delegate window does not support landscape.

If you present Landscape only view controller from Portrait only view controller, right after “present”, the orientation of the “all” windows becomes Landscape.

And likewise, right after “dismiss”~even during dismissal animation, before completion block~,, the orientation of the window becomes Portrait.

If the presenting view controller supports all orientations(default: follows app’s setting), then it does not change to Portrait on dismissal if the device orientation is landscape. Because it supports all orientations. If you need to change back to Portrait on dismissal, then you need to constrain supported orientations of the presenting view controller to Portrait.

** And even if the other window is key and visible, if the view controller on the window below presents some other view controller<B>, viewDidLoad of the <B> view controller is called immediately even if it’s not shown on screen. And even when the window on the top gets deallocated and the <B> view controller gets shown, viewWillAppear is not called at that time.

So changing some constraints or contentInset after viewDidLoad and before the other window gets deallocated might not be applied. For my case I changed the contentsInset at that time, it didn’t applied sometimes. So I called “setContentOffset(contentInset)” programmatically.

Device orientation – how the device is held / User Interface orientation – how view controllers support

User Interface orientation is not affected by transform of the root view of the view controller

window.safeAreaInsets on iPhone X

landscape

– top : 0.0 – left : 44.0 – bottom : 21.0 – right : 44.0

portrait

– top : 44.0 – left : 0.0 – bottom : 34.0 – right : 0.0

(The safe area of a view reflects the area not covered by navigation bars, tab bars, toolbars, and other ancestors that obscure a view controller’s view. (In tvOS, the safe area reflects the area not covered by the screen’s bezel.) You obtain the safe area for a view by applying the insets in this property to the view’s bounds rectangle. If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the edge insets in this property are 0. For the view controller’s root view, the insets account for the status bar, other visible bars, and any additional insets that you specified using the additionalSafeAreaInsets property of your view controller. For other views in the view hierarchy, the insets reflect only the portion of the view that is covered. For example, if a view is entirely within the safe area of its superview, the edge insets in this property are 0.)

**How to know interface orientation

windowScene.interfaceOrientation

windowScene: subclass of UIScene, scene manages windows

  1. UIApplication.shared.statusBarOrientation.isPortrait *deprecated*
  2. UIApplication.shared.delegate?.window??.windowScene?.interfaceOrientation.isPortrait
  3. UIInterfaceOrientationIsPortrait([(AppDelegate *)[[UIApplication sharedApplication] delegate] window].windowScene.interfaceOrientation) *objc*