Search code examples
swiftuiviewautolayoutuilabeldynamic-type-feature

Resize superview according to label's font size and image's aspect ratio


This is an auto-layout related question. I've containerView which has two subviews: imageView and label. I want to let the fontsize of the label determine the size of the containerView according to the aspect ratio of imageView.

When the font size increases, the containerView and the imageView should get bigger maintaining the aspect ratio and keeping the label centered with some padding as shown in the image below.

And I want to achieve it programmatically.

Any help will be much appreciated

enter image description here


Solution

  • You can accomplish this by:

    • constrain image view to all 4 sides of container
    • constrain label centered in container
    • constrain image view to 16:9 ratio
    • constrain image view's height to label's height + desired "padding"

    Here's an example, including buttons to increase / decrease the font size:

    class WalterViewController: UIViewController {
    
        let theContainerView: UIView = {
            let v = UIView()
            v.backgroundColor = .blue
            return v
        }()
    
        let theImageView: UIImageView = {
            let v = UIImageView()
            v.backgroundColor = .red
            v.contentMode = .scaleToFill
            return v
        }()
    
        let theLabel: UILabel = {
            let v = UILabel()
            v.backgroundColor = .yellow
            v.textAlignment = .center
            v.text = "TEST"
            // content vertical hugging REQUIRED !!!
            v.setContentHuggingPriority(.required, for: .vertical)
            return v
        }()
    
        let btnUp: UIButton = {
            let b = UIButton(type: .system)
            b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            b.setTitle("Increase", for: .normal)
            return b
        }()
    
        let btnDn: UIButton = {
            let b = UIButton(type: .system)
            b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            b.setTitle("Decrease", for: .normal)
            return b
        }()
    
        let btnStack: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.spacing = 12
            v.distribution = .fillEqually
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // we'll be using constraints
            [theContainerView, theImageView, theLabel, btnUp, btnDn, btnStack].forEach {
                $0.translatesAutoresizingMaskIntoConstraints = false
            }
    
            // add buttons to the stack
            btnStack.addArrangedSubview(btnUp)
            btnStack.addArrangedSubview(btnDn)
    
            // add imageView and label to container
            theContainerView.addSubview(theImageView)
            theContainerView.addSubview(theLabel)
    
            // add button stack and container view to view
            view.addSubview(btnStack)
            view.addSubview(theContainerView)
    
            // respect safe area
            let g = view.safeAreaLayoutGuide
    
            NSLayoutConstraint.activate([
    
                // horizontal button stack 20-points from top, 40-points on each side
                btnStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
    
                // container view centered in view safeArea
                theContainerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                theContainerView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
    
                // constrain image view to its superView (container view)
                // 8-pts on all 4 sides
                theImageView.topAnchor.constraint(equalTo: theContainerView.topAnchor, constant: 8.0),
                theImageView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor, constant: 8.0),
                theImageView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor, constant: -8.0),
                theImageView.bottomAnchor.constraint(equalTo: theContainerView.bottomAnchor, constant: -8.0),
    
                // label is centered in its superView (container view)
                theLabel.centerXAnchor.constraint(equalTo: theContainerView.centerXAnchor),
                theLabel.centerYAnchor.constraint(equalTo: theContainerView.centerYAnchor),
    
                // constrain imageView to 16:9 ratio
                theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 16.0 / 9.0),
    
                // constrain imageView's height to label's height +40
                // will result in 20-pts on top and bottom
                theImageView.heightAnchor.constraint(equalTo: theLabel.heightAnchor, constant: 40.0),
    
            ])
    
            // load an image
            if let img = UIImage(named: "bkg640x360") {
                theImageView.image = img
            }
    
            // add targets to buttons to increase / decrease the label's font size
            btnUp.addTarget(self, action: #selector(increaseTapped(_:)), for: .touchUpInside)
            btnDn.addTarget(self, action: #selector(decreaseTapped(_:)), for: .touchUpInside)
    
        }
    
        @objc func increaseTapped(_ sender: Any?) -> Void {
            theLabel.font = theLabel.font.withSize(theLabel.font.pointSize + 1.0)
        }
    
        @objc func decreaseTapped(_ sender: Any?) -> Void {
            theLabel.font = theLabel.font.withSize(theLabel.font.pointSize - 1.0)
        }
    
    }
    

    How it looks on launch (container view is centered in root view):

    enter image description here

    and, after tapping Increase a bunch of times:

    enter image description here