UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

huangapple go评论59阅读模式
英文:

UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

问题

I am not supplying code because this is purely a conceptual "how to achieve this" question. But I have listed my attempts and the constraints below.

我没有提供代码,因为这纯粹是一个概念性的“如何实现这一点”的问题。但我已经列出了我的尝试和约束如下。

I've tried a few different strategies to achieve this. Basically, I have variable width images and would like the image to line up perfectly on the left and maintain the 8px spacing with the label on the right. See the images below!

我尝试了一些不同的策略来实现这一点。基本上,我有可变宽度的图像,希望图像能够完美地与左侧对齐,并保持与右侧标签的8像素间距。请参见下面的图片!

The views themselves are inside a vertical UIStackView. This row a horizontal UIStackView but I've also tried it with a simple UIView container. The only constraint set is on the UIImageView, fixed height of 20px. But I'm looking for any viable strategy to achieve variable width, consistent margins behavior.

这些视图本身位于垂直UIStackView内。这一行是一个水平UIStackView,但我还尝试了将其放入一个简单的UIView容器中。唯一设置的约束是在UIImageView上,固定高度为20像素。但我正在寻找任何可行的策略来实现可变宽度和一致的边距行为。

I've tried setting the compressionResistence/contentHuggingPriority low on the UIImageView and every combination in between. Is this just not possible?

我尝试在UIImageView上设置compressionResistance和contentHuggingPriority的值较低,以及各种组合。这是否根本不可能?

I've highlighted the background blue to demonstrate the issue.

我已经突出显示了蓝色背景以说明问题。

Undesired Behavior: not left aligned, extra space between text
不希望的行为:不左对齐,文本之间有额外的空间
UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

Desired Behavior: no wasted space, everything lines up
期望的行为:没有浪费的空间,一切都对齐
UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

As shown above, the Blue image view is 20 x 40 points, with Content Mode set to Aspect Fit.

如上所示,蓝色图像视图为20 x 40点,内容模式设置为Aspect Fit。

How can I get the image view width to grow or shrink to match the aspect ratio of the image?

如何使图像视图的宽度能够根据图像的宽高比而增长或缩小?

英文:

I am not supplying code because this is purely a conceptual "how to achieve this" question. But I have listed my attempts and the constraints below.

I've tried a few different strategies to achieve this. Basically, I have variable width images and would like the image to line up perfectly on the left and maintain the 8px spacing with the label on the right. See the images below!

The views themselves are inside a vertical UIStackView. This row a horizontal UIStackView but I've also tried it with a simple UIView container. The only constraint set is on the UIImageView, fixed height of 20px. But I'm looking for any viable strategy to achieve variable width, consistent margins behavior.

I've tried setting the compressionResistence/contentHuggingPriority low on the UIImageView and every combination in between. Is this just not possible?

I've highlighted the background blue to demonstrate the issue

Undesired Behavior: not left aligned, extra space between text
UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

Desired Behavior: no wasted space, everything lines up
UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

As shown above, the Blue image view is 20 x 40 points, with Content Mode set to Aspect Fit.

How can I get the image view width to grow or shrink to match the aspect ratio of the image?

答案1

得分: 4

为了使UIImageView的大小与图像的纵横比相匹配,您可以将宽度约束设置为高度约束的倍数。

如果我们想要一个图像视图的高度为20个点,并且具有"aspect-fit"的宽度:

imageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: img.size.width / img.size.height).isActive = true

所以,假设我们有一个大小为100 x 100的图像... 我们这样说:

  • img.size.width / img.size.height == 1.0
  • widthAnchor等于20.0 * 1.0
  • 结果imageView的框架大小是20, 20

如果我们的图像大小是250 x 100

  • img.size.width / img.size.height == 2.5
  • widthAnchor等于20.0 * 2.5
  • 结果imageView的框架大小是50, 20

假设您有一个UIView子类,在初始化期间设置了约束,然后设置了图像,您可以使用一个可修改的约束作为视图的变量/属性:

class SomeCustomView: UIView {
    
    // 当设置小图像时,我们将更新这个约束
    private var smallImageWidthConstraint: NSLayoutConstraint!
    
    // 初始约束设置
    smallImageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
    
    // 小图像的宽度约束 - 在设置图像时将修改
    // 小图像的默认大小是20x40
    smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
    smallImageWidthConstraint.isActive = true
}

// 当设置.image属性时,我们更新该约束
// 取消激活
smallImageWidthConstraint.isActive = false

// 根据高度设置宽度以匹配图像的纵横比
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)

// 重新激活
smallImageWidthConstraint.isActive = true

这是一个快速示例,使用了这些3个图像。

英文:

To get a UIImageView size to match the image aspect-ratio, you can set the width constraint as a multiple of the height constraint.

If we want an image view to have a Height of 20-points, and an "aspect-fit" Width:

imageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: img.size.width / img.size.height).isActive = true

So, let's say we have an image that is 100 x 100 ... we're saying:

  • img.size.width / img.size.height == 1.0
  • widthAnchor equals 20.0 * 1.0
  • resulting imageView frame size is 20, 20

If our image size is 250 x 100

  • img.size.width / img.size.height == 2.5
  • widthAnchor equals 20.0 * 2.5
  • resulting imageView frame size is 50, 20

Assuming you have a UIView subclass where the constraints are setup during init, and you are then setting the image, you want to use a "modifiable" constraint as a var / property of your view:

class SomeCustomView: UIView {
	
	// we'll update this constraint when the small image is set
	private var smallImageWidthConstraint: NSLayoutConstraint!

During initial constraint setup:

smallImageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true

// small image width constraint - will be modified when we set the image
//	small image default is 20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
smallImageWidthConstraint.isActive = true

When the .image is set, we update that constraint:

// de-activate
smallImageWidthConstraint.isActive = false

// set width proportional to height to match image aspect ratio
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)

// re-activate
smallImageWidthConstraint.isActive = true

Here's a quick example, using these 3 images:

UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

In assets, I named them p272x120, p91x200 and p232x209 since those are the actual image dimensions.

Using this custom view:

class SomeCustomView: UIView {
	
	public var bigImage: UIImage? {
		didSet { bigImageView.image = bigImage }
	}
	public var topString: String = "" {
		didSet { topLabel.text = topString }
	}
	public var sideString: String = "" {
		didSet { sideLabel.text = sideString }
	}
	
	// when we set the small image, we also update the width constraint
	public var smallImage: UIImage? {
		didSet {
			smallImageView.image = smallImage
			// unwrap optional
			if let img = smallImage {
				// de-activate
				smallImageWidthConstraint.isActive = false
				// set width proportional to height to match image aspect ratio
				smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
				// re-activate
				smallImageWidthConstraint.isActive = true
			}
		}
	}

	private let bigImageView = UIImageView()
	private let smallImageView = UIImageView()
	private let topLabel: UILabel = {
		let v = UILabel()
		v.font = .systemFont(ofSize: 15.0, weight: .bold)
		v.numberOfLines = 0
		return v
	}()
	private let subLabel: UILabel = {
		let v = UILabel()
		v.font = .italicSystemFont(ofSize: 14.0)
		v.numberOfLines = 0
		return v
	}()
	private let sideLabel: UILabel = {
		let v = UILabel()
		v.font = .systemFont(ofSize: 12.0, weight: .bold)
		v.numberOfLines = 0
		return v
	}()
	
	// we'll update this constraint when the small image is set
	private var smallImageWidthConstraint: NSLayoutConstraint!
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		commonInit()
	}
	required init?(coder: NSCoder) {
		super.init(coder: coder)
		commonInit()
	}
	func commonInit() {
		
		// horizontal stack view to hold
		//	small image and side label
		let hStack = UIStackView(arrangedSubviews: [smallImageView, sideLabel])
		hStack.spacing = 8
		hStack.alignment = .top

		// vertical stack view to hold labels and ssmall image
		let vStack = UIStackView(arrangedSubviews: [topLabel, subLabel, hStack])
		vStack.axis = .vertical
		vStack.spacing = 8

		[bigImageView, vStack].forEach { v in
			v.translatesAutoresizingMaskIntoConstraints = false
			addSubview(v)
		}

		// small image width constraint - will be modified when we set the image
		//	small image default is 20x40
		smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
		
		NSLayoutConstraint.activate([
			
			// big image 80x80
			bigImageView.widthAnchor.constraint(equalToConstant: 80.0),
			bigImageView.heightAnchor.constraint(equalToConstant: 80.0),
			// leading
			bigImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
			// center vertically
			bigImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
			// at least 12-points top/bottom
			bigImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
			bigImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
			
			// vStack leading 8-points from big image
			vStack.leadingAnchor.constraint(equalTo: bigImageView.trailingAnchor, constant: 8.0),
			// center vStack vertically
			vStack.centerYAnchor.constraint(equalTo: centerYAnchor),
			// at least 12-points top/bottom
			vStack.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
			vStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
			// trailing
			vStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
			
			// small image height 20-points
			smallImageView.heightAnchor.constraint(equalToConstant: 20.0),
			// activate the "changeable" width constraint
			smallImageWidthConstraint,
			
		])
		
		// let's set some properties
		
		backgroundColor = UIColor(white: 0.9, alpha: 1.0)
		
		// edit this after development...

		// big image will be set by controller
		bigImageView.backgroundColor = .systemRed
		if let img = UIImage(systemName: "swift") {
			bigImageView.image = img
		}

		// these will be .clear
		smallImageView.backgroundColor = .systemBlue
		topLabel.backgroundColor = .yellow
		subLabel.backgroundColor = .cyan
		sideLabel.backgroundColor = .green
		
	}
	
	// this is here during development to show
	//	the resulting small imageView size
	override func layoutSubviews() {
		super.layoutSubviews()

		// let's use Int values, so we don't output .33333333...
		let w = Int(smallImageView.frame.width)
		let h = Int(smallImageView.frame.height)
		subLabel.text = "Actual size (rounded): (w: \(w), h: \(h))"
	}
	
}

and this example controller (putting the custom views in a vertical stack view):

class AspectImageVC: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()

		let stackView = UIStackView()
		stackView.axis = .vertical
		stackView.spacing = 20.0

		let myData: [[String]] = [
			["p272x120", "Wide Image", "Nintendo Switch"],
			["p91x200", "Tall, Narrow Image", "Left Controller"],
			["p232x209", "Square-ish Image", "Nintendo Cube"],
			["", "No Image", "So we can see default 20x40 small image view size"],
		]

		myData.forEach { d in

			let imgName = d[0]
			let top = d[1]
			let side = d[2]
			
			let someView = SomeCustomView()
			
			if imgName.isEmpty {
				someView.topString = top
			} else {
				someView.topString = imgName + " " + top
				// safely load the image
				if let img = UIImage(named: imgName) {
					someView.smallImage = img
				}
			}
			someView.sideString = side

			stackView.addArrangedSubview(someView)

		}

		stackView.translatesAutoresizingMaskIntoConstraints = false
		view.addSubview(stackView)
		
		let g = view.safeAreaLayoutGuide
		NSLayoutConstraint.activate([
			stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
			stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
			stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
		])
		
	}
	
}

We get this output:

UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?


Edit - in response to comment...

Views can have more than one of "the same" constraint.

If you want your "small image view" to have a MAX width of 40-points...

Add a lessThanOrEqualToConstant width constraint:

smallImageView.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
smallImageView.widthAnchor.constraint(lessThanOrEqualToConstant: 40.0).isActive = true

// small image width constraint - will be modified when we set the image
//  small image default is 20x40
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
smallImageWidthConstraint.isActive = true

By default, constraints have .priority = .required. So, when we set the image and modify the width constraint:

// de-activate
smallImageWidthConstraint.isActive = false
// set width proportional to height to match image aspect ratio
smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
// use less-than-required so we can limit its width
smallImageWidthConstraint.priority = .required - 1
// re-activate
smallImageWidthConstraint.isActive = true

So, the SomeCustomView with those modifications:

class SomeCustomView: UIView {
	
	public var bigImage: UIImage? {
		didSet { bigImageView.image = bigImage }
	}
	public var topString: String = "" {
		didSet { topLabel.text = topString }
	}
	public var sideString: String = "" {
		didSet { sideLabel.text = sideString }
	}
	
	// when we set the small image, we also update the width constraint
	public var smallImage: UIImage? {
		didSet {
			smallImageView.image = smallImage
			// unwrap optional
			if let img = smallImage {
				// de-activate
				smallImageWidthConstraint.isActive = false
				// set width proportional to height to match image aspect ratio
				smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalTo: smallImageView.heightAnchor, multiplier: img.size.width / img.size.height)
				// use less-than-required so we can limit its width
				smallImageWidthConstraint.priority = .required - 1
				// re-activate
				smallImageWidthConstraint.isActive = true
			}
		}
	}

	private let bigImageView = UIImageView()
	private let smallImageView: UIImageView = {
		let v = UIImageView()
		v.contentMode = .scaleAspectFit
		return v
	}()
	private let topLabel: UILabel = {
		let v = UILabel()
		v.font = .systemFont(ofSize: 15.0, weight: .bold)
		v.numberOfLines = 0
		return v
	}()
	private let subLabel: UILabel = {
		let v = UILabel()
		v.font = .italicSystemFont(ofSize: 14.0)
		v.numberOfLines = 0
		return v
	}()
	private let sideLabel: UILabel = {
		let v = UILabel()
		v.font = .systemFont(ofSize: 12.0, weight: .bold)
		v.numberOfLines = 0
		return v
	}()
	
	// we'll update this constraint when the small image is set
	private var smallImageWidthConstraint: NSLayoutConstraint!
	
	override init(frame: CGRect) {
		super.init(frame: frame)
		commonInit()
	}
	required init?(coder: NSCoder) {
		super.init(coder: coder)
		commonInit()
	}
	func commonInit() {
		
		// horizontal stack view to hold
		//	small image and side label
		let hStack = UIStackView(arrangedSubviews: [smallImageView, sideLabel])
		hStack.spacing = 8
		hStack.alignment = .center

		// vertical stack view to hold labels and ssmall image
		let vStack = UIStackView(arrangedSubviews: [topLabel, subLabel, hStack])
		vStack.axis = .vertical
		vStack.spacing = 8

		[bigImageView, vStack].forEach { v in
			v.translatesAutoresizingMaskIntoConstraints = false
			addSubview(v)
		}

		// small image width constraint - will be modified when we set the image
		//	small image default is 20x40
		smallImageWidthConstraint = smallImageView.widthAnchor.constraint(equalToConstant: 40.0)
		
		NSLayoutConstraint.activate([
			
			// big image 80x80
			bigImageView.widthAnchor.constraint(equalToConstant: 80.0),
			bigImageView.heightAnchor.constraint(equalToConstant: 80.0),
			// leading
			bigImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
			// center vertically
			bigImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
			// at least 12-points top/bottom
			bigImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
			bigImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
			
			// vStack leading 8-points from big image
			vStack.leadingAnchor.constraint(equalTo: bigImageView.trailingAnchor, constant: 8.0),
			// center vStack vertically
			vStack.centerYAnchor.constraint(equalTo: centerYAnchor),
			// at least 12-points top/bottom
			vStack.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 12.0),
			vStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12.0),
			// trailing
			vStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
			
			// small image height 20-points
			smallImageView.heightAnchor.constraint(equalToConstant: 20.0),
			// MAX width of 40-points
			smallImageView.widthAnchor.constraint(lessThanOrEqualToConstant: 40.0),
			// activate the "changeable" width constraint
			smallImageWidthConstraint,
			
		])
		
		// let's set some properties
		
		backgroundColor = UIColor(white: 0.9, alpha: 1.0)
		
		// edit this after development...

		// big image will be set by controller
		bigImageView.backgroundColor = .systemRed
		bigImageView.tintColor = .systemYellow
		if let img = UIImage(systemName: "swift") {
			bigImageView.image = img
		}

		// these will be .clear
		smallImageView.backgroundColor = .systemBlue
		topLabel.backgroundColor = .yellow
		subLabel.backgroundColor = .cyan
		sideLabel.backgroundColor = .green
		
	}
	
	// this is here during development to show
	//	the resulting small imageView size
	override func layoutSubviews() {
		super.layoutSubviews()

		// let's use Int values, so we don't output .33333333...
		let w = Int(smallImageView.frame.width)
		let h = Int(smallImageView.frame.height)
		subLabel.text = "Actual size (rounded): (w: \(w), h: \(h))"
	}
	
}

and the output is now:

UIImageView with fixed height: How can I get the image view width to grow or shrink to match the aspect ratio of the image?

Note that the top image is only 40-points wide, instead of 45 as before.

答案2

得分: -1

Using DonMag的答案,我想出了自己的解决方案。我还没有测试边缘情况或其他任何东西,但它适用于我的用途。我在界面生成器中设置了固定的高度和内容模式为纵向适合

@IBDesignable
@objc class UIVariableWidthImageView : UIImageView {
    
    override var image: UIImage! {
        get {
            return super.image
        }
        set {
            super.image = newValue
            
            widthConstraint?.isActive = false
            if newValue != nil {
                widthConstraint = self.widthAnchor.constraint(equalTo: self.heightAnchor, multiplier: newValue.size.width / newValue.size.height)
                widthConstraint?.isActive = true
            }
        }
    }
        
    private var widthConstraint: NSLayoutConstraint?
}
英文:

Using DonMag's answer I came up with my own drop in solution. I haven't test edge cases or anything, but it works for my use. I set a fixed height and the content mode to aspect fit in interface builder

@IBDesignable
@objc class UIVariableWidthImageView : UIImageView {
    
    override var image: UIImage! {
        get {
            return super.image
        }
        set {
            super.image = newValue
            
            widthConstraint?.isActive = false
            if newValue != nil {
                widthConstraint = self.widthAnchor.constraint(equalTo: self.heightAnchor, multiplier: newValue.size.width / newValue.size.height)
                widthConstraint?.isActive = true
            }
        }
    }
        
    private var widthConstraint: NSLayoutConstraint?
}

huangapple
  • 本文由 发表于 2023年3月9日 12:42:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/75680500.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定