Search code examples
iosswiftuiviewautolayoutmeasure

How to estimate preferred UIView size where one of the child views uses ratio constraint?


I've realised that I don't understand AutoLayout.

I want to measure view's required height given the constant width.

This is my TestViewTwo.xib

TestViewTwo.xib

TestViewTwo.swift

import UIKit

class TestViewTwo: UIView {
    @IBOutlet weak var imageView: UIImageView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() {
        let nib = Bundle.main.loadNibNamed("TestViewTwo", owner: self, options: nil)
        let view = nib!.first as! UIView
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
        view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0).isActive = true
        view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
        view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true
    }
}

Test Controller

import Foundation
import UIKit

class TestControllerTwo : UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let testView = TestViewTwo()
        
        let estimatedSize = testView.systemLayoutSizeFitting(CGSize(width: 200, height: 500))
        
        print("Estimated size: \(estimatedSize), imageView.frame: \(testView.imageView.frame)")
    }
}

The output is

Estimated size: (100.0, 500.0), imageView.frame: (0.0, 0.0, 414.0, 621.0)

I don't understand why estimated width is 100 ? Where is this coming from ? Why estimated height is 500 and not 300 (200x1.5) ? I also don't understand why imageView frame is set and why to such values

Please help me understand what I'm doing wrong.

I want to get estimatedSize = 200x300

Update:

I guess there is something fundamentally wrong I'm doing here. It is not about ratio that I use.

When I set constant width and height of image view

enter image description here

I get

Estimated size: (200.0, 500.0), imageView.frame: (0.0, 0.0, 200.0, 300.0)

When I set constant height only

enter image description here

I get

Estimated size: (100.0, 500.0), imageView.frame: (0.0, 0.0, 414.0, 300.0)

What is wrong in my layout / code so that I don't get estimatedSize = 200x300 ?

Let's deal with constant dimensions before moving to ratio problem.


Solution

  • You definitely need to constrain the bottom of the image view to the bottom of its superview...

    enter image description here

    Give the bottom constraint Priority: High (750)

    Then, when you want to know the estimated Height based on a given rectangle:

        let estimatedSize = testView.systemLayoutSizeFitting(CGSize(width: 200, height: 500),
                                         withHorizontalFittingPriority: .defaultHigh,
                                         verticalFittingPriority: .defaultLow)
    
        print("Estimated size: \(estimatedSize), imageView.frame: \(testView.imageView.frame)")
        
        // output: Estimated size: (200.0, 300.0), imageView.frame: (0.0, 0.0, 197.0, 295.5)
    

    If you want to know the estimated Width based a given rectangle:

        let estimatedSize = testView.systemLayoutSizeFitting(CGSize(width: 200, height: 500),
                                         withHorizontalFittingPriority: .defaultLow,
                                         verticalFittingPriority: .defaultHigh)
        
        print("Estimated size: \(estimatedSize), imageView.frame: \(testView.imageView.frame)")
    
        // output: Estimated size: (333.5, 500.0), imageView.frame: (0.0, 0.0, 197.0, 295.5)
    

    Note that the imageView.frame will NOT be set yet, so it will evaluate to whatever size you have it in IB.

    Also note that we give the image view a Bottom constraint with a less-than-required Priority. This avoids the IB warnings when we don't have the view frame sized to exactly 1:1.5 ratio, and avoids auto-layout warning/errors messages at run-time.


    Here is the source to the XIB:

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
        <device id="retina4_0" orientation="portrait" appearance="light"/>
        <dependencies>
            <deployment identifier="iOS"/>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <objects>
            <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="TestViewTwo" customModule="DrawingTutorial" customModuleProvider="target">
                <connections>
                    <outlet property="imageView" destination="QgA-Qr-3jM" id="MGu-3W-9i4"/>
                </connections>
            </placeholder>
            <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
            <view contentMode="scaleToFill" id="iN0-l3-epB">
                <rect key="frame" x="0.0" y="0.0" width="197" height="391"/>
                <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                <subviews>
                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QgA-Qr-3jM">
                        <rect key="frame" x="0.0" y="0.0" width="197" height="295.5"/>
                        <color key="backgroundColor" red="0.99998801950000005" green="0.62141335009999998" blue="0.00022043679199999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstAttribute="width" secondItem="QgA-Qr-3jM" secondAttribute="height" multiplier="1:1.5" id="CpW-r1-rJA"/>
                        </constraints>
                    </imageView>
                </subviews>
                <color key="backgroundColor" red="0.45009386540000001" green="0.98132258650000004" blue="0.4743030667" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                <constraints>
                    <constraint firstItem="QgA-Qr-3jM" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="3As-tz-AZL"/>
                    <constraint firstAttribute="trailing" secondItem="QgA-Qr-3jM" secondAttribute="trailing" id="4Q2-dC-O75"/>
                    <constraint firstItem="QgA-Qr-3jM" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="xJ2-05-m7l"/>
                    <constraint firstAttribute="bottom" secondItem="QgA-Qr-3jM" secondAttribute="bottom" priority="750" id="xy9-yL-2gg"/>
                </constraints>
                <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
                <point key="canvasLocation" x="109.6875" y="126.23239436619718"/>
            </view>
        </objects>
    </document>
    

    And example classes to demonstrate:

    class TestControllerTwo : UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let testView = TestViewTwo()
            
            // withHorizontalFittingPriority: .defaultHigh
            // verticalFittingPriority: .defaultLow
            //  gives priority to the WIDTH
            //  returns a size based on fitting the Target WIDTH
            let estimatedSizeW = testView.systemLayoutSizeFitting(
                CGSize(width: 200, height: 500),
                withHorizontalFittingPriority: .defaultHigh,
                verticalFittingPriority: .defaultLow)
            
            print("Width Priority Estimated size: \(estimatedSizeW)",
                "imageView.frame: \(testView.imageView.frame)")
    
            // withHorizontalFittingPriority: .defaultLow
            // verticalFittingPriority: .defaultHigh
            //  gives priority to the HEIGHT
            //  returns a size based on fitting the Target HEIGHT
            let estimatedSizeH = testView.systemLayoutSizeFitting(
                CGSize(width: 200, height: 500),
                withHorizontalFittingPriority: .defaultLow,
                verticalFittingPriority: .defaultHigh)
            
            print("Height Priority Estimated size: \(estimatedSizeH)",
                "imageView.frame: \(testView.imageView.frame)")
            
        }
    
    }
    
    class TestViewTwo: UIView {
        @IBOutlet weak var imageView: UIImageView!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() {
            let nib = Bundle.main.loadNibNamed("TestViewTwo", owner: self, options: nil)
            let view = nib!.first as! UIView
            addSubview(view)
            view.translatesAutoresizingMaskIntoConstraints = false
            view.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
            view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0).isActive = true
            view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
            view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true
        }
    }