0
votes

UIKit/Programmatic UI

I have an UIScrollView with an UIImageView inside. The image is set by user selection and can have all kinds of sizes. What I want is that the image initially fills the screen (view) and then can be zoomed and scrolled all the way to the edges of the image.

If I add the ImageView directly to the view (no scrollView), I get it to fill the screen with the following code:

        mapImageView.image = ProjectImages.projectDefaultImage
        mapImageView.translatesAutoresizingMaskIntoConstraints = false
        mapImageView.contentMode = .scaleAspectFill
        view.addSubview(mapImageView)

Now the same with the scrollView and the embedded imageView:

        view.insertSubview(mapImageScrollView, at: 0)
        mapImageScrollView.delegate = self
        mapImageScrollView.translatesAutoresizingMaskIntoConstraints = false
        mapImageScrollView.contentMode = .scaleAspectFill
        mapImageScrollView.maximumZoomScale = 4.0
        mapImageScrollView.pinToEdges(of: view, safeArea: true)
        
        mapImageView.image = ProjectImages.projectDefaultImage
        mapImageView.translatesAutoresizingMaskIntoConstraints = false
        mapImageView.contentMode = .scaleAspectFill
        mapImageScrollView.addSubview(mapImageView)

And now, if the image's height is smaller than the view's height, the image does not fill the screen and I'm left with a blank view area below the image. I can zoom and scroll ok, and then the image does fill the view.

Adding contsraints will fill the view as I want, but interferes with the zooming and scrolling and prevents me getting to the edges of the image when zoomed in.

How to set this up correctly ?

Scrollview with embedded image

2
Do you want your image to maintain its aspect ratio? So when you see the full width there will be space on top and bottom? Or do you want it scaled (stretched vertically) to fit the scroll view ratio? - DonMag
If you provide the image you're using, I should be able to give you a solution. - DonMag
Thank you! yes, I want to maintain aspect ratio. I've found a solution that works for me. It leaves me with only just a little quirk ... Solution posted below - DeveloperSammy

2 Answers

2
votes

You might find this useful...

It allows you to zoom an image in a scrollView, starting with it centered and maintaining aspect ratio.

Here's a complete implementation. It has two important variables at the top:

// can be .scaleAspectFill or .scaleAspectFit
var fitMode: UIView.ContentMode = .scaleAspectFill

// if fitMode is .scaleAspectFit, allowFullImage is ignored
// if fitMode is .scaleAspectFill, image will start zoomed to .scaleAspectFill
//  if allowFullImage is false, image will zoom back to .scaleAspectFill if "pinched in"
//  if allowFullImage is true, image can be "pinched in" to see the full image
var allowFullImage: Bool = true

Everything is done via code - no @IBOutlet or other connections - so just create add a new view controller and assign its custom class to ZoomAspectViewController (and edit the name of the image you want to use):

class ZoomAspectViewController: UIViewController, UIScrollViewDelegate {
    
    var scrollView: UIScrollView!
    var imageView: UIImageView!
    var imageViewBottomConstraint: NSLayoutConstraint!
    var imageViewLeadingConstraint: NSLayoutConstraint!
    var imageViewTopConstraint: NSLayoutConstraint!
    var imageViewTrailingConstraint: NSLayoutConstraint!
    
    // can be .scaleAspectFill or .scaleAspectFit
    var fitMode: UIView.ContentMode = .scaleAspectFit
    
    // if fitMode is .scaleAspectFit, allowFullImage is ignored
    // if fitMode is .scaleAspectFill, image will start zoomed to .scaleAspectFill
    //  if allowFullImage is false, image will zoom back to .scaleAspectFill if "pinched in"
    //  if allowFullImage is true, image can be "pinched in" to see the full image
    var allowFullImage: Bool = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let img = UIImage(named: "myImage") else {
            fatalError("Could not load the image!!!")
        }
        
        scrollView = UIScrollView()
        imageView = UIImageView()
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        imageView.translatesAutoresizingMaskIntoConstraints = false
        
        imageView.contentMode = .scaleToFill
        
        scrollView.addSubview(imageView)
        view.addSubview(scrollView)
        
        // respect safe area
        let g = view.safeAreaLayoutGuide
        
        imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: scrollView.topAnchor)
        imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
        imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
        imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)

        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: g.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),

            imageViewTopConstraint,
            imageViewBottomConstraint,
            imageViewLeadingConstraint,
            imageViewTrailingConstraint,
            
        ])
        
        scrollView.delegate = self
        scrollView.minimumZoomScale = 0.1
        scrollView.maximumZoomScale = 5.0
        
        imageView.image = img
        imageView.frame.size = img.size
        
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { _ in
            self.updateMinZoomScaleForSize(size, shouldSize: (self.scrollView.zoomScale == self.scrollView.minimumZoomScale))
            self.updateConstraintsForSize(size)
        }, completion: {
            _ in
        })
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateMinZoomScaleForSize(scrollView.bounds.size)
        updateConstraintsForSize(scrollView.bounds.size)
        
        if fitMode == .scaleAspectFill {
            centerImageView()
        }
        
    }
    
    func updateMinZoomScaleForSize(_ size: CGSize, shouldSize: Bool = true) {
        guard let img = imageView.image else {
            return
        }
        
        var bShouldSize = shouldSize
        
        let widthScale = size.width / img.size.width
        let heightScale = size.height / img.size.height
        
        var minScale = min(widthScale, heightScale)
        let startScale = max(widthScale, heightScale)
        
        if fitMode == .scaleAspectFill && !allowFullImage {
            minScale = startScale
        }
        if scrollView.zoomScale < minScale {
            bShouldSize = true
        }
        scrollView.minimumZoomScale = minScale
        if bShouldSize {
            scrollView.zoomScale = fitMode == .scaleAspectFill ? startScale : minScale
        }
    }
    
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateConstraintsForSize(scrollView.bounds.size)
    }
    
    func centerImageView() -> Void {
        let yOffset = (scrollView.frame.size.height - imageView.frame.size.height) / 2
        let xOffset = (scrollView.frame.size.width - imageView.frame.size.width) / 2
        scrollView.contentOffset = CGPoint(x: -xOffset, y: -yOffset)
    }
    
    func updateConstraintsForSize(_ size: CGSize) {
        let yOffset = max(0, (size.height - imageView.frame.height) / 2)
        imageViewTopConstraint.constant = yOffset
        imageViewBottomConstraint.constant = yOffset
        
        let xOffset = max(0, (size.width - imageView.frame.width) / 2)
        imageViewLeadingConstraint.constant = xOffset
        imageViewTrailingConstraint.constant = xOffset
        
        view.layoutIfNeeded()
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }
    
}

Edit

As an example, I used this image (2560 x 1440):

enter image description here

and I get this result on launch:

enter image description here

and maximum zoom in (5.0) scrolled to top-center:

enter image description here


Edit 2

Same image, at launch, with:

var fitMode: UIView.ContentMode = .scaleAspectFill

instead of .scaleAspectFit:

enter image description here

0
votes

I've found this solution that works for me, when setting and changing the image, I calculate the minimum needed zoom scale and set it on the scrollView:

    var selectedMapImage: MapImage? {
        didSet {
            mapImageView.image = mapImagesController.getImageForMapImage(selectedMapImage!)
            mapImageScrollView.minimumZoomScale = view.bounds.height / mapImageView.image!.size.height
            mapImageScrollView.setZoomScale(mapImageScrollView.minimumZoomScale, animated: true)
            mapImageScrollView.scrollRectToVisible(view.bounds, animated: true)
        }
    }