3

I need to disable orientation change animations on devices updated recently to iOS 16.

For older iOS versions there was a solution to prevent animations. The view hierarchy was changing instantly after disabling animations using the UIView method:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {

   NSLog(@"breakpoint");

   [UIView setAnimationsEnabled:NO];

   [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
                    ///
            }
                completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
                    [UIView setAnimationsEnabled:YES];
            }];
}

After updating to iOS 16, the animation seems to begin before -viewWillTransitionToSize:withTransitionCoordinator: is even called. For example, when I add a breakpoint in the code above, then on iOS 15 (and older) the window will remain unchanged before reaching the breakpoint. With iOS 16, the screen rotates to the new orientation before the breakpoint is hit.

Setting [UIView setAnimationsEnabled:NO]; permanently, at app launch cancels other animations, but the orientation change animations still take place. However, the orientation change animations look worse with permanently disabled animation, like only the views angle (rotation) was animated but not the scale / aspect.

Greg
  • 442
  • 4
  • 16

1 Answers1

4

The key is to abandon viewWillTransitionToSize for the desired outcome in iOS 16 and instead use setNeedsUpdateOfSupportedInterfaceOrientations() within a performWithoutAnimation block and override supportedInterfaceOrientations. an elaborate/messy workaround.

class ViewController: UIViewController {
    private var deviceOrientationObservation: NSObjectProtocol?

    //
    ... constraints
    
    var portraitConstraints: [NSLayoutConstraint] = []
    var landscapeConstraints: [NSLayoutConstraint] = []
    
    private var lastSupportedInterfaceOrientation : UIInterfaceOrientation! = .unknown
    
    
    var currentSupportedOrientation : UIInterfaceOrientation {
        let viewOrientation = self.preferredInterfaceOrientationForPresentation
        return viewOrientation
    }
    
    

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        //
        YourView.translatesAutoresizingMaskIntoConstraints = false

        self.lastSupportedInterfaceOrientation = self.currentSupportedOrientation
        
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)


        //For a solution we now completely move away from setAnimationEnabled
        //UIView.setAnimationsEnabled(false)  
        
        //Solution
        /// When the view is appearing, start listening to device orientation changes...
        deviceOrientationObservation = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification,
                  object: UIDevice.current,
                  queue: .main, using: { [weak self] _ in

            /// When the device orientation changes to a valid interface orientation, save the current orientation as our current supported orientations.
            if let self, let mask = UIInterfaceOrientationMask(deviceOrientation: UIDevice.current.orientation), self.currentSupportedOrientations != mask {
                self.currentSupportedOrientations = mask
                
                // after the rotation
                print("mask is updated:: \(mask)")
                if (mask == .landscapeLeft || mask == .landscapeRight) {
                    //print("Landscape after")
                    self.applyLandscapeConstraints()
                } else if (mask == .portrait) {
                    //print("Portrait  after")
                    self.applyPortraitConstraints()
                } else {
                    //print("postOrientation::\(postOrientation)")
                }

                
            }
        })
        

    }
    

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        /// Stop observing when the view disappears
        deviceOrientationObservation = nil


    }

    
    private var currentSupportedOrientations: UIInterfaceOrientationMask = .portrait {
        didSet {
            /// When the current supported orientations changes, call `setNeedsUpdate...` within a `performWithoutAnimation` block.
            /// This will trigger the system to read the `supportedInterfaceOrientations` of this view controller again and apply any changes
            /// without animation, but still async.
            UIView.performWithoutAnimation {
                if #available(iOS 16.0, *) {
                    setNeedsUpdateOfSupportedInterfaceOrientations()
                } else {
                    print("get ready to fork")
                    // Fallback on earlier versions
                }
            }
        }
    }

    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return currentSupportedOrientations
    }
    
    func applyLandscapeConstraints() {
        ...
    }
        
    func applyPortraitConstraints() {
        ...
    }
    

}



//UIDeviceOrientation.landscapeRight is assigned to UIInterfaceOrientation.landscapeLeft and UIDeviceOrientation.landscapeLeft is assigned to UIInterfaceOrientation.landscapeRight. The reason for this is that rotating the device requires rotating the content in the opposite direction.

extension UIInterfaceOrientationMask {
    init?(deviceOrientation: UIDeviceOrientation) {
        switch deviceOrientation {
        case .portrait:
            self = .portrait
        /// Landscape device orientation is the inverse of the interface orientation (see docs: https://developer.apple.com/documentation/uikit/uiinterfaceorientation)
            ///
        case .landscapeLeft:
            self = .landscapeRight
        case .landscapeRight:
            self = .landscapeLeft
        default:
            return nil
        }
    }
}
Graham
  • 188
  • 1
  • 6