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.