0

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.

Cameron Delong
  • 454
  • 1
  • 6
  • 12
  • But since you know when the device is rotating can't you just ignore the ones you don't want? – matt Aug 06 '23 at 01:18
  • No, I don't fully understand how, but whatever is causing `viewDidLayoutSubviews` to be called is causing problems in the rotation animation outside of it just being called too often. I did try using `viewWillTransition` to set a flag variable when the device is rotating and ignoring the calls when it is set, but there are still problems. Specifically, the image inside the scroll view stops being centered when `viewDidLayoutSubviews` is called, and `viewDidLayoutSubviews` is usually responsible for re-centering the image. Ignoring the calls prevents this and the image stops being centered. – Cameron Delong Aug 06 '23 at 02:55

0 Answers0