-1

I am implementing a UIScrollView in a UIViewController. I center the image which is slightly smaller than the ScrollView in the contentView and add the contentView to the ScrollView. Zooming in works fine but when zooming out (zoomScale < 1) the image is shifted to the upper left corner.

func makeBgView() {
        if let bgImage = UIImage(named: "testImage") {
            bgView = UIImageView(image: bgImage)
            
            // calculate contentViewSize from imagesize and height
            let imgWidth = bgView.image!.size.width
            let imgHeight = bgView.image!.size.height
            let bgWidth = view.frame.width * 0.96
            let ratio = bgWidth/imgWidth
            let bgHeight = imgHeight * ratio
            let bgSize = CGSize(width: bgWidth, height: bgHeight)
            
            contentView.addSubview(bgView)
            contentView.sendSubviewToBack(bgView)
            
            contentView.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
                contentView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
                contentView.widthAnchor.constraint(equalToConstant: bgSize.width),
                contentView.heightAnchor.constraint(equalToConstant: bgSize.height),
            ])

            bgView.pinToEdges(inView: contentView)

            scrollView.addSubview(contentView)
}
extension CanvasVC: UIScrollViewDelegate {
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return contentView
    }
}

extension UIView {
    func pinToEdges(constant: CGFloat = 0, inView superview: UIView) {
        superview.addSubview(self)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.topAnchor.constraint(equalTo: superview.topAnchor, constant: constant).isActive = true
        self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: constant).isActive = true
        self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: constant).isActive = true
        self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: constant).isActive = true
    }
}

The green frame is the ScrollView the RedFrame is the contentView (a UIVIew).

not zoomed

zoomed

jakob witsch
  • 157
  • 1
  • 1
  • 12

1 Answers1

0

First, it's a good idea to use the scroll view's Content Layout Guide and Frame Layout Guide -- helps to avoid ambiguities, e.g.:

contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor)

contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, multiplier: 0.96)

Second, let's take advantage of constraint relative multiplier to keep the content view at the same aspect ratio of the image:

// content view should have same aspect ratio as image
contentView.heightAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: bgImage.size.height / bgImage.size.width),

Next, when the view is zoomed, we can adjust the scroll view's .contentInset to center the content view:

func centerContentView() {
    // this will keep the "content view" centered when it is smaller
    //  than the scroll view frame
    let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: 0, right: 0)
}

Here's a complete example:

class CanvasVC: UIViewController {
    
    let scrollView = UIScrollView()
    let contentView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        scrollView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        guard let bgImage = UIImage(named: "testImage") else {
            fatalError("Could not load testImage!")
        }
        
        let bgView = UIImageView(image: bgImage)
        
        [bgView, contentView, scrollView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        contentView.addSubview(bgView)
        scrollView.addSubview(contentView)
        view.addSubview(scrollView)
        
        let g = view.safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // let's inset the scroll view by 20-points on all 4 sides
            //  so we can clearly see the scroll zoom
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            
            // constrain content view to scroll view's Content Layout Guide
            contentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            contentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            contentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
            contentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
            
            // constrain image view to content view
            bgView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
            bgView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0),
            bgView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0),
            bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
            
            // content view should have same aspect ratio as image
            contentView.heightAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: bgImage.size.height / bgImage.size.width),
            
            // content view width (at 1.0 zoom) should be 96% of scroll view width
            contentView.widthAnchor.constraint(equalTo: fg.widthAnchor, multiplier: 0.96),
            
        ])
        
        scrollView.delegate = self
        
        // set your min/max zoom scales as desired
        scrollView.minimumZoomScale = 0.25
        scrollView.maximumZoomScale = 4.0
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        centerContentView()
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { _ in
            self.centerContentView()
        }, completion: { _ in
        })
        
    }
    
    func centerContentView() {
        // this will keep the "content view" centered when it is smaller
        //  than the scroll view frame
        let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
        let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
        scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: 0, right: 0)
    }
    
}

extension CanvasVC: UIScrollViewDelegate {
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return contentView
    }

    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        centerContentView()
    }
    
}
DonMag
  • 69,424
  • 5
  • 50
  • 86