viewDidLayoutSubviews being called many times on orientation change in UIViewControllerRepresentable

96 views Asked by At

I'm trying to implement a zoomable image in SwiftUI to display in a page view like the iOS photos app. I'm using UIViewControllerRepresentable to wrap a controller containing a UIScrollView to achieve this, and I'm running into a strange problem with the viewDidLayoutSubviews method being called way to many times when the device rotates. As far as I can tell, this is the cause of some animation problems I'm having with the scroll view.

After some experimentation I found a minimal reproducible example that happens to be extremely minimal:

class ViewController: UIViewController {
    override func viewDidLayoutSubviews() {
        print("viewDidLayoutSubviews: \(self.view.bounds)")
    }
}

struct Representable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> ViewController {
        return ViewController()
    }

    func updateUIViewController(_ uiViewController: ViewController, context: Context) {
        
    }
}

struct ContentView: View {
    var body: some View {
        Representable()
    }
}

Launching the app and rotating the device once results in the following output:

viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 395.038046836853, 756.6870260238647)
viewDidLayoutSubviews: (0.0, 0.0, 395.038046836853, 756.6870260238647)
viewDidLayoutSubviews: (0.0, 0.0, 401.33917903900146, 749.5358877182007)
viewDidLayoutSubviews: (0.0, 0.0, 401.33917903900146, 749.5358877182007)
viewDidLayoutSubviews: (0.0, 0.0, 412.1404695510864, 737.2775316238403)
viewDidLayoutSubviews: (0.0, 0.0, 412.1404695510864, 737.2775316238403)
viewDidLayoutSubviews: (0.0, 0.0, 427.58273124694824, 719.7521495819092)
viewDidLayoutSubviews: (0.0, 0.0, 427.58273124694824, 719.7521495819092)
viewDidLayoutSubviews: (0.0, 0.0, 447.6386470794678, 696.990743637085)
viewDidLayoutSubviews: (0.0, 0.0, 447.6386470794678, 696.990743637085)
viewDidLayoutSubviews: (0.0, 0.0, 472.0353717803955, 669.3029651641846)
viewDidLayoutSubviews: (0.0, 0.0, 472.0353717803955, 669.3029651641846)
viewDidLayoutSubviews: (0.0, 0.0, 500.1738815307617, 637.3686447143555)
viewDidLayoutSubviews: (0.0, 0.0, 500.1738815307617, 637.3686447143555)
viewDidLayoutSubviews: (0.0, 0.0, 531.0925512313843, 602.279128074646)
viewDidLayoutSubviews: (0.0, 0.0, 531.0925512313843, 602.279128074646)
viewDidLayoutSubviews: (0.0, 0.0, 563.5, 565.5)
viewDidLayoutSubviews: (0.0, 0.0, 563.5, 565.5)
viewDidLayoutSubviews: (0.0, 0.0, 595.9074487686157, 528.720871925354)
viewDidLayoutSubviews: (0.0, 0.0, 595.9074487686157, 528.720871925354)
viewDidLayoutSubviews: (0.0, 0.0, 626.8261184692383, 493.63135528564453)
viewDidLayoutSubviews: (0.0, 0.0, 626.8261184692383, 493.63135528564453)
viewDidLayoutSubviews: (0.0, 0.0, 654.9646282196045, 461.69703483581543)
viewDidLayoutSubviews: (0.0, 0.0, 654.9646282196045, 461.69703483581543)
viewDidLayoutSubviews: (0.0, 0.0, 679.3613529205322, 434.00925636291504)
viewDidLayoutSubviews: (0.0, 0.0, 679.3613529205322, 434.00925636291504)
viewDidLayoutSubviews: (0.0, 0.0, 699.4172687530518, 411.2478504180908)
viewDidLayoutSubviews: (0.0, 0.0, 699.4172687530518, 411.2478504180908)
viewDidLayoutSubviews: (0.0, 0.0, 714.8595304489136, 393.72246837615967)
viewDidLayoutSubviews: (0.0, 0.0, 714.8595304489136, 393.72246837615967)
viewDidLayoutSubviews: (0.0, 0.0, 725.6608209609985, 381.4641122817993)
viewDidLayoutSubviews: (0.0, 0.0, 725.6608209609985, 381.4641122817993)
viewDidLayoutSubviews: (0.0, 0.0, 731.961953163147, 374.31297397613525)
viewDidLayoutSubviews: (0.0, 0.0, 731.961953163147, 374.31297397613525)
viewDidLayoutSubviews: (0.0, 0.0, 734.0, 372.0)

Interestingly, the bounds of the view are changing as the rotation animation happens.

Testing the same code in a fully UIKit application results in expected output:

viewDidLayoutSubviews: (0.0, 0.0, 393.0, 852.0)
viewDidLayoutSubviews: (0.0, 0.0, 852.0, 393.0)

viewDidLayoutSubviews is called once before and once after the orientation change.

Also worth noting, the problem is much less severe on iOS 17 (previous tests have been on iOS 16):

viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 393.0, 759.0)
viewDidLayoutSubviews: (0.0, 0.0, 734.0, 372.0)
viewDidLayoutSubviews: (0.0, 0.0, 734.0, 372.0)

It is called more than expected, but not enough to break anything, and my zoomable image view works fine on iOS 17.

So is this just a bug with SwiftUI on iOS 16? Are there any workarounds? Unfortunately I can't just develop on iOS 17 for now, since it comes with its own collection of SwiftUI bugs that affect my code.

0

There are 0 answers