UIScrollView Layout issue

I run into a layout problem where I cannot center an image inside ScrollView which is also inside Navigation Controller. The problem is surely the fact that there is a navigation bar because using this view without NavigationContoller works fine and the image is centered but I don’t know how to account for the space that navigation bar takes up.

Here is the code:

import UIKit

class PhotoViewController: UIViewController {
    var photoName: String
    
    private lazy var photoView  = {
        let image = UIImageView()
        image.translatesAutoresizingMaskIntoConstraints = false
        image.contentMode = .scaleAspectFit
        image.clipsToBounds = true
        
        return image
    }()
    
    var photoViewBottomConstraint: NSLayoutConstraint?
    var photoViewLeadingConstraint: NSLayoutConstraint?
    var photoViewTopConstraint: NSLayoutConstraint?
    var photoViewTrailingConstraint: NSLayoutConstraint?
    
    private lazy var scrollView  = {
        let sv = UIScrollView()
        sv.translatesAutoresizingMaskIntoConstraints = false
        
        return sv
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        updateMinZoomScaleForSize(view.bounds.size)
    }
    
    func updateMinZoomScaleForSize(_ size: CGSize) {
        let widthScale = size.width / photoView.bounds.width
        let heightScale = size.height / photoView.bounds.height
        let minScale = min(widthScale, heightScale)
        
        scrollView.minimumZoomScale = minScale
        scrollView.zoomScale = minScale
    }
    
    func setupUI() {        
        photoView.image = UIImage(named: photoName)
        scrollView.delegate = self
        view.addSubview(scrollView)
        scrollView.addSubview(photoView)
        setupConstraints()
    }
    
    func setupConstraints() {
        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
        
        photoViewLeadingConstraint = NSLayoutConstraint(
            item: photoView,
            attribute: .leading,
            relatedBy: .equal,
            toItem: scrollView,
            attribute: .leading,
            multiplier: 1,
            constant: 0
        )
        
        photoViewTopConstraint = NSLayoutConstraint(
            item: photoView,
            attribute: .top,
            relatedBy: .equal,
            toItem: scrollView,
            attribute: .top,
            multiplier: 1,
            constant: 0
        )
        
        photoViewTrailingConstraint = NSLayoutConstraint(
            item: photoView,
            attribute: .trailing,
            relatedBy: .equal,
            toItem: scrollView,
            attribute: .trailing,
            multiplier: 1,
            constant: 0
        )
        
        photoViewBottomConstraint = NSLayoutConstraint(
            item: photoView,
            attribute: .bottom,
            relatedBy: .equal,
            toItem: scrollView,
            attribute: .bottom,
            multiplier: 1,
            constant: 0
        )
        
        photoViewLeadingConstraint?.isActive = true
        photoViewTopConstraint?.isActive = true
        photoViewTrailingConstraint?.isActive = true
        photoViewBottomConstraint?.isActive = true
    }
    
    init(photoName: String) {
        self.photoName = photoName
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension PhotoViewController: UIScrollViewDelegate {
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        photoView
    }
    
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateConstraintsForSize(view.bounds.size)
    }
    
    func updateConstraintsForSize(_ size: CGSize) {
        let yOffset = max(0, (size.height - photoView.frame.height) / 2)
        photoViewTopConstraint?.constant = yOffset
        photoViewBottomConstraint?.constant = yOffset
        
        let xOffset = max(0, (size.width - photoView.frame.width) / 2)
        photoViewLeadingConstraint?.constant = xOffset
        photoViewTrailingConstraint?.constant = xOffset
        
        view.layoutIfNeeded()
    }
}

Hello ArsD,

Thank you for providing sample code. For completeness, can you please place it into a test project that also has the navigation controller which contains the PhotoViewController and the sample image you are using? If so, please share a link to it. That'll help us better understand what's going on. If you're not familiar with preparing a test project, take a look at Creating a test project.

Thanks for your question,

Richard Yeh  Developer Technical Support

Hello ArsD,

Two functions (updateMinZoomScaleForSize called from viewWillLayoutSubviews() and updateConstraintsForSize called from scrollViewDidZoom()) use the full view's bounds when they should be using the scroll view's bounds. If you use scrollView.bounds.size instead in those instances, it would account for the height occupied by the navigation bar.

Thank you for your patience,

Richard Yeh  Developer Technical Support

Hey @DTS Engineer, thanks for the suggestion. I tried it and whilst it seems better than it was I can still clearly see that the image isn't centered properly. I just cannot comprehend the way UINavigationController affects the layout.

Edit: Also, there is this strange behavior with scrollViewDidZoom(_:) method that I found. Basically, when I present this PhotoViewController inside the UINavigationController context either as a root view controller or as a pushed view controller scrollViewDidZoom(_:) always gets called instantly even before the user actually zooms. However, when I present PhotoViewController outside the UINavigationController hierarchy then scrollViewDidZoom(_:) method doesn't get called until the user starts zooming. I have no idea why that happens

UIScrollView Layout issue
 
 
Q