I am migrating an UIKit project to SwiftUI but will keep one UIKit ViewController, which now will be wrapped using an UIViewControllerRepresentable View.

Everything seems to be working fine, except for the animation during the change of device orientation.

The problem seems to be the fact that - other than in a pure UIKit environment - the new view.bounds.size is still not available within the alongsideTransition: block in viewWillTransition(to:with:). The new size is available for the first time in the completion: block.

This can be seen in the following demo project:

ContentView

import SwiftUI

struct ContentView: View {
    var body: some View {
        UIKitView().edgesIgnoringSafeArea(.all)
    }
}

UIKitView

import SwiftUI

struct UIKitView: UIViewControllerRepresentable {
    class Coordinator: NSObject {
        var parent: UIKitView
        init(_ parent: UIKitView) { self.parent = parent }
    }
    
    func makeUIViewController(context: Context) -> ViewController { ViewController() }

    func updateUIViewController(_ viewController: ViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator { Coordinator(self) }
}

ViewController

class ViewController: UIViewController {
    
    var labelTopConstraint: NSLayoutConstraint!
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        updateConstraints()
    }
    
    override func loadView() {
        view = UIView()
        view.backgroundColor = .blue
        
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
        label.text = "This is a sample text"
        label.textColor = .yellow
        label.backgroundColor = .black
        label.textAlignment = .center
        
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        
        let topConstraint = NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 300.0)
        let centerXConstraint = NSLayoutConstraint(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0)
        let widthConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 200.0)
        let heightConstraint = NSLayoutConstraint(item: label, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 50.0)
        
        NSLayoutConstraint.activate([topConstraint, centerXConstraint, widthConstraint, heightConstraint])
        
        self.labelTopConstraint = topConstraint
    }
    
    func updateConstraints() {
        // simple illustration, calculation in actual project much more complex
        labelTopConstraint.constant = view.bounds.size.height / 2.0
        view.setNeedsDisplay()
    }
    
    override func viewWillTransition(
        to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator
    ) {
        print("view.bounds.size at Start: \(self.view.bounds.size)")
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(
            alongsideTransition: { [weak self] _ in
                print("view.bounds.size during Animation: \(self?.view.bounds.size ?? .zero)")
                self?.updateConstraints()
            },
            completion: { [weak self] _ in
                print("view.bounds.size during Completion: \(self?.view.bounds.size ?? .zero)")
                
                // the following is to demo where the label should have been placed during the transition
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                    self?.updateConstraints()
                }
            }
        )
    }
}

Running the app and rotating the Simulator generates the following console output:

view.bounds.size at Start: (428.0, 926.0)
view.bounds.size during Animation: (428.0, 926.0)  // still not updated!
view.bounds.size during Completion: (926.0, 428.0)

In comparison, when rotating the Simulator running exactly the same ViewController in a pure UIKit application, the console output is:

view.bounds.size at Start: (428.0, 926.0)
view.bounds.size during Animation: (926.0, 428.0)  // new size is already available here!
view.bounds.size during Completion: (926.0, 428.0)

As a workaround I tried calling updateConstraints() from viewDidLayoutSubviews(), but it is called too late (after the alongsideTransition: animation block), which gets noticeable in a more complex setup.

I am running the code in Xcode 13.4.1 and iOS 15.5. The behaviour is identical in Xcode 14 / iOS 16 beta 2.

Is there a way to get the new view size to be available already in the alongsideTransition: animation block?

0

There are 0 best solutions below