Search code examples
swiftuikit

Image view does not resize according to a new image


Let's say one has a placeholder image, which is loaded into an UIImageView. Then, an async operation eventually loads an image with a different size.

The issue I am observing is that I don't know how to make imageView to "wrap" the new image, according to its size, preserving the image original aspect ratio.

Note that when using contentMode=.scaleAspect*, the view scales the image to fit its view. In my case, I want the view to fit the image.

import Foundation
import UIKit

class ViewController: UIViewController {
    let imageView = UIImageView()

    override func loadView() {
        let view = UIView()
        view.addSubview(imageView)
        imageView.contentMode = .scaleAspectFill
        imageView.backgroundColor = .red
        imageView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            imageView.widthAnchor.constraint(equalToConstant: 100),
            imageView.heightAnchor.constraint(equalToConstant: 100),
        ])

        self.view = view
    }
    override func viewDidLoad() {
//        imageView.image = // some placeholder image is shown; size is 100 x 100
//        imageView.image = // network call is made which eventually loads a different image with dynamic size
    }
}

// Present the view controller in the Live View window
import PlaygroundSupport
PlaygroundPage.current.liveView = ViewController()

Solution

  • One approach is to create a NSLayoutConstraint var/property to use as the image view's Height constraint. Then, when the new image comes in, de-activate the constraint, re-create it with the new aspect-ratio (multiplier), and re-activate it.

    Here's a quick example...

    It starts with an empty image view (red background), at 1:1 ratio.

    Each time you tap (well, in playground, click), we'll use a new SF Symbol to simulate getting a new image from a server.

    import UIKit
    import PlaygroundSupport
    
    class ViewController: UIViewController {
        
        let imageView = UIImageView()
        
        // we'll update the image view height constraint when a new image comes in
        var ivHeight: NSLayoutConstraint!
        
        let sysNames: [String] = [
            "ruler",                    // wide and short
            "flashlight.off.fill",      // tall and narrow
            "folder",                   // square-ish
        ]
        var idx: Int = -1
        
        override func loadView() {
            let view = UIView()
            view.addSubview(imageView)
            //imageView.contentMode = .scaleAspectFill
            imageView.contentMode = .scaleToFill
            imageView.backgroundColor = .red
            imageView.translatesAutoresizingMaskIntoConstraints = false
    
            ivHeight = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0)
    
            NSLayoutConstraint.activate([
                imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                imageView.widthAnchor.constraint(equalToConstant: 100),
                ivHeight,
            ])
            
            self.view = view
        }
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            
            // to simulate getting an image from a server...
            
            idx += 1
            let sysName = sysNames[idx % sysNames.count]
            
            let cfg = UIImage.SymbolConfiguration(pointSize: 120.0, weight: .bold, scale: .large)
            
            guard let sysImg = UIImage(systemName: sysName, withConfiguration: cfg)?.withTintColor(.systemBlue, renderingMode: .alwaysOriginal) else {
                print("failed to create image \(sysName)")
                return
            }
    
            // set the image
            imageView.image = sysImg
    
            // get the size of the image
            let sz = sysImg.size
    
            // de-activate the image view height constraint
            ivHeight.isActive = false
    
            // set the new image view height constraint, using
            //  image size height / width
            //  as the multiplier
            ivHeight = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: sz.height / sz.width)
        
            // re-activate the image view height constraint
            ivHeight.isActive = true
        }
        
    }
    
    // Present the view controller in the Live View window
    PlaygroundPage.current.liveView = ViewController()
    

    It will give you these results... the image view will always have a width of 100, but the height will change to match the aspect ratio of the image:

    enter image description here