简体   繁体   中英

AutoLayout constraints to fit view inside rectangle, preserving a certain aspect ratio (programmatically)

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:0x6000000a7c80@ViewController.swift#24 UIView:0x7fcd82d12440.left == UIView:0x7fcd82d12640.left>",
"<SnapKit.LayoutConstraint:0x6000000a7ce0@ViewController.swift#25 UIView:0x7fcd82d12440.trailing == UIView:0x7fcd82d12640.trailing>",
"<SnapKit.LayoutConstraint:0x6000000a7d40@ViewController.swift#26 UIView:0x7fcd82d12440.height == 250.0>",
"<SnapKit.LayoutConstraint:0x6000000a7da0@ViewController.swift#35 UIView:0x7fcd8580dad0.centerX == UIView:0x7fcd82d12440.centerX>",
"<SnapKit.LayoutConstraint:0x6000000a7e00@ViewController.swift#35 UIView:0x7fcd8580dad0.centerY == UIView:0x7fcd82d12440.centerY>",
"<SnapKit.LayoutConstraint:0x6000000a8580@ViewController.swift#37 UIView:0x7fcd8580dad0.top >= UIView:0x7fcd82d12440.top>",
"<SnapKit.LayoutConstraint:0x6000000a8a60@ViewController.swift#37 UIView:0x7fcd8580dad0.right >= UIView:0x7fcd82d12440.right>",
"<SnapKit.LayoutConstraint:0x6000000a9360@ViewController.swift#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:0x6000000a9360@ViewController.swift#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
            ])

    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM