如何估计其中一个子视图使用比率约束的首选 UIView 大小?

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

我发现我不懂 AutoLayout。

我想在给定恒定宽度的情况下测量视图所需的高度。

这是我的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
    }
}

测试控制器

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)")
    }
}

输出为

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

我不明白为什么估计宽度是 100?这是从哪里来的? 为什么估计高度是 500 而不是 300 (200x1.5)? 我也不明白为什么要设置 imageView 框架以及为什么要设置这样的值

请帮助我理解我做错了什么。

我想得到 estimatedSize = 200x300

更新:

我想我在这里做的事情根本上是错误的。 这与我使用的比例无关。

当我设置图像视图的宽度和高度恒定时

我明白了

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

当我只设置恒定高度时

我明白了

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

我的布局/代码有什么问题导致我无法得到 estimatedSize = 200x300?

在讨论比率问题之前,让我们先处理常量维度。

systemLayoutSizeFitting 的一个常见问题是,当您使用纵横比时,它无法正常工作。它使用 intrinsicContentSize 来设置它的高度,因此你得到不同的值。为了解决这个问题,您需要删除纵横比并明确设置高度。在代码中它看起来像:

imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5).isActive = true

也[参见][1] 类似的答案。

更新:

在你提供了更多信息后,我发现了一些问题:

  1. 你的身高是 500,因为你试图估计 TestViewTwo 的身高,而不是 UIImageView 的实际身高。 TestViewTwo 将具有 500 高度,因为它没有底部锚点,并且会拉伸以填充可用的 space。
  2. 对于估算,更合适的方法是使用 systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)

基于文档:

Use this method when you want to prioritize the view's constraints when determining the best possible size of the view. This method does not actually change the size of the view.

应用这些更改我为您准备了一个演示,因此您可以验证它并试用它。我将持有 UIImageView 的视图命名为 ImageView:

class ImageView: UIView {
    let imageView = UIImageView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        imageView.image = UIImage(named: "image1")
        addSubview(imageView)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
        imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0).isActive = true
        imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        imageView.heightAnchor.constraint(equalToConstant: 300).isActive = true
        backgroundColor = .blue
    }
}

TestViewTwo我命名为ImageViewHolder:

class ImageViewHolder: UIView {
    let view = ImageView(frame: .zero)

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        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
        // Bottom anchor should not be set here, as the view has explicitly defined it's height
        backgroundColor = .red
    }
}

这里是 ViewController 你可以测试它的地方:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // I gave a frame to this so you can see the actual layout
        // But the estimation logic also works if you pass here .zero a
        let imageViewHollder = ImageViewHolder(frame: CGRect(x: 0, y: 100, width: 200, height: 500)) 

        let estimatedSize = imageViewHollder.view.systemLayoutSizeFitting(CGSize(width: 200, height: 500), withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultHigh)
        print("Estimated size: \(estimatedSize)")
        
        // Just to see the actual layout
        view.addSubview(imageViewHollder)
    }
}

请注意,当您打印 imageView 的估计尺寸时:

let estimatedSize = imageViewHollder.view.systemLayoutSizeFitting(CGSize(width: 200, height: 500), withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultHigh)
print("Estimated size: \(estimatedSize)")

你得到:

// Estimated size: (200.0, 300.0)

但是如果你打印持有它的视图的估计大小

let estimatedSize = imageViewHollder.systemLayoutSizeFitting(CGSize(width: 200, height: 500), withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultHigh)
print("Estimated size: \(estimatedSize)")

你得到

Estimated size: (200.0, 500.0)

这是因为如上所述,holder 视图将尝试填充可用的 space,因为它没有底部锚点。

您肯定需要将图像视图的底部限制在其父视图的底部...

给底约束Priority: High (750)

然后,当您想知道基于给定矩形的估计高度时:

    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)

如果您想知道基于给定矩形的估计宽度

    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)

请注意,imageView.frameNOT 设置,因此它将计算为您拥有的任何大小IB.

另请注意,我们为图像视图提供了一个 低于要求 优先级的底部约束。这避免了当我们没有将视图框架大小精确调整为 1:1.5 比例时的 IB 警告,并避免在 运行 时自动布局 warning/errors 消息。


这是 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>

并举例类进行演示:

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
    }
}