I want to fit an image inside a rectangle that should have a specific aspect ratio. No matter what it is, it should find a form to fit inside the rectangle. I played around in storyboard and got this:
The ones with the dotted border have low priority (250).
This works in the storyboard. However, I need to create these constraints programmatically, so I tried it like this (I'm using SnapKit, which simply provides better AutoLayout
syntax. It should be self-explaining):
let topView = UIView()
topView.translatesAutoresizingMaskIntoConstraints = false
topView.backgroundColor = .gray
view.addSubview(topView)
topView.snp.makeConstraints { (make) in
make.top.equalToSuperview()
make.left.equalToSuperview()
make.trailing.equalToSuperview()
make.height.equalTo(250)
}
// This view should have a specific aspect ratio and fit inside topView
let holderView = UIView()
holderView.translatesAutoresizingMaskIntoConstraints = false
holderView.backgroundColor = .red
topView.addSubview(holderView)
holderView.snp.makeConstraints { (make) in
make.center.equalToSuperview() // If I remove this one, there's no auto-layout issue, but then it's offset
make.edges.equalToSuperview().priority(250) // sets leading, trailing, top and bottom
make.edges.greaterThanOrEqualToSuperview().priority(1000)
make.width.equalTo(holderView.snp.height).multipliedBy(3/2)
}
If you paste this into an empty ViewController and start it up, you get these issues:
2018-03-16 15:38:50.188867+0100 DemoProject[11298:850932] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
"<SnapKit.LayoutConstraint:[email protected]#24 UIView:0x7fcd82d12440.left == UIView:0x7fcd82d12640.left>",
"<SnapKit.LayoutConstraint:[email protected]#25 UIView:0x7fcd82d12440.trailing == UIView:0x7fcd82d12640.trailing>",
"<SnapKit.LayoutConstraint:[email protected]#26 UIView:0x7fcd82d12440.height == 250.0>",
"<SnapKit.LayoutConstraint:[email protected]#35 UIView:0x7fcd8580dad0.centerX == UIView:0x7fcd82d12440.centerX>",
"<SnapKit.LayoutConstraint:[email protected]#35 UIView:0x7fcd8580dad0.centerY == UIView:0x7fcd82d12440.centerY>",
"<SnapKit.LayoutConstraint:[email protected]#37 UIView:0x7fcd8580dad0.top >= UIView:0x7fcd82d12440.top>",
"<SnapKit.LayoutConstraint:[email protected]#37 UIView:0x7fcd8580dad0.right >= UIView:0x7fcd82d12440.right>",
"<SnapKit.LayoutConstraint:[email protected]#38 UIView:0x7fcd8580dad0.width == UIView:0x7fcd8580dad0.height>",
"<NSLayoutConstraint:0x600000092cf0 'UIView-Encapsulated-Layout-Width' UIView:0x7fcd82d12640.width == 414 (active)>"
Will attempt to recover by breaking constraint <SnapKit.LayoutConstraint:[email protected]#38 UIView:0x7fcd8580dad0.width == UIView:0x7fcd8580dad0.height>
This doesn't show up, when I remove the centering constraint make.center.equalToSuperview()
. But then, it's misplaced.
What is different between the storyboard and my code? I don't really understand this. I also tried this using the default swift syntax, the result was exactly the same. So I don't think it's a problem with SnapKit
Any ideas? Thank you guys for any help. Let me know if you need any more infos.
EDIT: I mixed something up. It's not about the image and its aspect ratio. It's just about a UIView
that should maintain a specific aspect ratio while fitting inside a rectangle. The actual image will just be put into that holderView
. Sorry
OK - here is one way to do it.
Take the "native" size of your subview, calculate the "aspect fit" ratio - that is, the ratio that will fit the width or height to the superview, and scale the other dimension appropriately.
Then, use centerXAnchor
and centerYAnchor
to position the subview, and widthAnchor
and heightAnchor
to size it.
Note: if you're trying to place an image, calculate the aspect fit size from the image size, put the image in an image view, set the image view scale mode to fill
, and finally apply the constraints to the image view.
You should be able to run this example as-is. Just play around with the "native" size values at the top to see how it fits the subview into the superview.
public class AspectFitViewController : UIViewController {
// "native" size for the holderView
let hViewWidth: CGFloat = 700.0
let hViewHeight: CGFloat = 200.0
let topView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.blue
return v
}()
let holderView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.cyan
return v
}()
public override func viewDidLoad() {
super.viewDidLoad()
view.bounds = CGRect(x: 0, y: 0, width: 400, height: 600)
view.backgroundColor = .yellow
// add topView
view.addSubview(topView)
// pin topView to leading / top / trailing
topView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
topView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
topView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
// explicit height for topView
topView.heightAnchor.constraint(equalToConstant: 250.0).isActive = true
// add holderView to topView
topView.addSubview(holderView)
// center X and Y
holderView.centerXAnchor.constraint(equalTo: topView.centerXAnchor, constant: 0.0).isActive = true
holderView.centerYAnchor.constraint(equalTo: topView.centerYAnchor, constant: 0.0).isActive = true
// holderView's width and height will be calculated in viewDidAppear
// after topView has been laid-out by the auto-layout engine
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let aspectWidth = topView.bounds.size.width / hViewWidth
let aspectHeight = topView.bounds.size.height / hViewHeight
let aspectFit = min(aspectWidth, aspectHeight)
let newWidth = hViewWidth * aspectFit
let newHeight = hViewHeight * aspectFit
holderView.widthAnchor.constraint(equalToConstant: newWidth).isActive = true
holderView.heightAnchor.constraint(equalToConstant: newHeight).isActive = true
}
}
Edit:
After clarification... this can be accomplished by constraints only. The key is that "Priority 1000" top
and leading
constraints must be .greaterThanOrEqual
to zero, and the bottom
and trailing
constraints must be .lessThanOrEqual
to zero.
public class AspectFitViewController : UIViewController {
// "native" size for the holderView
let hViewWidth: CGFloat = 700.0
let hViewHeight: CGFloat = 200.0
let topView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.blue
return v
}()
let holderView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.cyan
return v
}()
public override func viewDidLoad() {
super.viewDidLoad()
view.bounds = CGRect(x: 0, y: 0, width: 400, height: 600)
view.backgroundColor = .yellow
// add topView
view.addSubview(topView)
// pin topView to leading / top / trailing
topView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
topView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
topView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
// explicit height for topView
topView.heightAnchor.constraint(equalToConstant: 250.0).isActive = true
// add holderView to topView
topView.addSubview(holderView)
// center X and Y
holderView.centerXAnchor.constraint(equalTo: topView.centerXAnchor, constant: 0.0).isActive = true
holderView.centerYAnchor.constraint(equalTo: topView.centerYAnchor, constant: 0.0).isActive = true
// aspect ratio size
holderView.widthAnchor.constraint(equalTo: holderView.heightAnchor, multiplier: hViewWidth / hViewHeight).isActive = true
// two constraints for each side...
// the .equal constraints need .defaultLow priority
// top and leading constraints must be .greaterThanOrEqual to 0
// bottom and trailing constraints must be .lessThanOrEqual to 0
let topA = NSLayoutConstraint(item: holderView, attribute: .top, relatedBy: .greaterThanOrEqual, toItem: topView, attribute: .top, multiplier: 1.0, constant: 0.0)
let topB = NSLayoutConstraint(item: holderView, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .top, multiplier: 1.0, constant: 0.0)
let bottomA = NSLayoutConstraint(item: holderView, attribute: .bottom, relatedBy: .lessThanOrEqual, toItem: topView, attribute: .bottom, multiplier: 1.0, constant: 0.0)
let bottomB = NSLayoutConstraint(item: holderView, attribute: .bottom, relatedBy: .equal, toItem: topView, attribute: .bottom, multiplier: 1.0, constant: 0.0)
let leadingA = NSLayoutConstraint(item: holderView, attribute: .leading, relatedBy: .greaterThanOrEqual, toItem: topView, attribute: .leading, multiplier: 1.0, constant: 0.0)
let leadingB = NSLayoutConstraint(item: holderView, attribute: .leading, relatedBy: .equal, toItem: topView, attribute: .leading, multiplier: 1.0, constant: 0.0)
let trailingA = NSLayoutConstraint(item: holderView, attribute: .trailing, relatedBy: .lessThanOrEqual, toItem: topView, attribute: .trailing, multiplier: 1.0, constant: 0.0)
let trailingB = NSLayoutConstraint(item: holderView, attribute: .trailing, relatedBy: .equal, toItem: topView, attribute: .trailing, multiplier: 1.0, constant: 0.0)
topB.priority = .defaultLow
bottomB.priority = .defaultLow
leadingB.priority = .defaultLow
trailingB.priority = .defaultLow
NSLayoutConstraint.activate([
topA, topB,
bottomA, bottomB,
leadingA, leadingB,
trailingA, trailingB
])
}
}