Joshua Maciak
Joshua Maciak

Reputation: 121

Preventing Auto Layout from stretching custom view

I've created a custom view in swift & am trying to get it to display appropriately. It is essentially a material card, with the ability to expand the view by pressing the more button. My issue comes in when specifying the bottom constraint. It is required, but setting it stretches my custom view.

I have implemented this already in android & I guess I'm trying to find the analog to android:height='wrap_content'. I've tried setting the aspect ratio constraint, which works to keep my view at the proper size, but prevents the custom view from expanding when its subviews change. Additionally I've tried using the lessThanOrEqualTo constraint, but that is too ambiguous to satisfy the bottom constraint.

This is what my UIExpandableCard view looks like:

import Foundation
import MaterialComponents

@IBDesignable
public class UIExpandableCard: UIView {

    // attributes
    @IBInspectable var overlineText: String? {
        didSet {
            overlineLabel.text = overlineText?.uppercased()
        }
    }

    @IBInspectable var headlineText: String? {
        didSet {
            headlineLabel.text = headlineText
        }
    }

    @IBInspectable var bodyText: String? {
        didSet {
            bodyLabel.text = bodyText
        }
    }

    @IBInspectable var logoImage: UIImage? {
        didSet {
            logoImageView.image = logoImage
        }
    }

    var cardView: MDCCard!
    var overlineLabel: UILabel!
    var headlineLabel: UILabel!
    var bodyLabel: UILabel!
    var moreButton: UIButton!
    var logoImageView: UIImageView!

    var isCardExpanded = false


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

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

    private func setup() {
        self.translatesAutoresizingMaskIntoConstraints = false
        setupViews()
        setupConstraints()
    }

    private func setupViews() {
        self.clipsToBounds = true
        cardView = MDCCard(frame: CGRect.zero)
        cardView.translatesAutoresizingMaskIntoConstraints = false
        cardView.isInteractable = false
        self.addSubview(cardView)


        overlineLabel = UILabel(frame: CGRect.zero)
        overlineLabel.translatesAutoresizingMaskIntoConstraints = false
        overlineLabel.font = UIFont.preferredFont(forTextStyle: .footnote)

        overlineLabel.text = overlineText?.uppercased()
        self.addSubview(overlineLabel)

        headlineLabel = UILabel(frame: CGRect.zero)
        headlineLabel.translatesAutoresizingMaskIntoConstraints = false
        headlineLabel.font = UIFont.preferredFont(forTextStyle: .title1)
        headlineLabel.text = headlineText
        self.addSubview(headlineLabel)

        bodyLabel = UILabel(frame: CGRect.zero)
        bodyLabel.translatesAutoresizingMaskIntoConstraints = false
        bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)
        bodyLabel.numberOfLines = 1
        bodyLabel.text = bodyText
        self.addSubview(bodyLabel)

        logoImageView = UIImageView(image: logoImage)
        logoImageView.translatesAutoresizingMaskIntoConstraints = false
        logoImageView.contentMode = .scaleAspectFit
        self.addSubview(logoImageView)

        moreButton = UIButton(type: .roundedRect)
        moreButton.isUserInteractionEnabled = true
        moreButton.translatesAutoresizingMaskIntoConstraints = false

        moreButton.setTitle("More", for: .normal)
        moreButton.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)

        self.addSubview(moreButton)

    }

    @objc func buttonClicked(_ sender: UIButton) {
        if !isCardExpanded {
            moreButton.setTitle("Less", for: .normal)
            bodyLabel.numberOfLines =  0
        } else {
            moreButton.setTitle("More", for: .normal)
            bodyLabel.numberOfLines = 1
        }

        isCardExpanded = !isCardExpanded
    }

    private func setupConstraints() {
        NSLayoutConstraint.activate([
            cardView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
            cardView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8),
            cardView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
            cardView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -8),

            overlineLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
            overlineLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 16),

            headlineLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
            headlineLabel.topAnchor.constraint(equalTo: overlineLabel.bottomAnchor, constant: 8),


            NSLayoutConstraint(item: logoImageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 48),
            NSLayoutConstraint(item: logoImageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 48),
            logoImageView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16),
            logoImageView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 16),


            bodyLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
            bodyLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 16),
            bodyLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),

            moreButton.topAnchor.constraint(greaterThanOrEqualTo: bodyLabel.bottomAnchor, constant: 8),
            moreButton.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
            moreButton.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -8)

        ])
    }
}

Basically I want something like the left. However, I'm getting the right, where one of the views (in blue) is being stretched to fill the constraint.

left right

I'm relatively new to iOS, but have experience with android, so any explanations relating to that would be extra helpful.

Thanks.

Upvotes: 1

Views: 1882

Answers (3)

Vicente Garcia
Vicente Garcia

Reputation: 6360

Following Joshua's answer, with this I prevented a custom view from stretching:

let width = widthAnchor.constraint(equalToConstant: 0)
width.priority = .defaultLow
width.isActive = true

Upvotes: 2

Joshua Maciak
Joshua Maciak

Reputation: 121

So I found a solution that appears to do what I need. I'm still trying to wrap my head around what I was actually trying to achieve. Basically what I wanted was my view height to be defined by its subviews & their constraints, rather than having to specify it in a constraint. At the same time I needed to satisfy the height constraint in my interface.

My solution was as follows: add a low priority height constraint of 0 to the the card view in my interface. This satisfies the requirement for a height in the scene, while also allowing my view to expand & contract without being stretched.

... a low priority constraint of height zero for the view as a whole. The low priority constraint will try to shrink the assembly, while the other constraints stop it from shrinking so far that it clips its subviews.

I found this solution on another stack overflow question.

Upvotes: 3

moraei
moraei

Reputation: 1693

You can't do that with lessThanOrEqualTo.Although i can't fully understand you're question but hope this approach help.

first of all define a flow constraint like:

var heightConstraint: NSLayoutConstraint?

and replace it with bottom constraint in setupConstraint:

private func setupConstraints() {

        heightConstraint = cardView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1, constant: 0)
        NSLayoutConstraint.activate([
            cardView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
            cardView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8),
            cardView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
            heightConstraint,

            overlineLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
            overlineLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 16),

.
.
.

and finally when press the button:

private func expandViewWithAnimation(_ isExpand: Bool) {
    UIView.animate(withDuration: 0.8) {
            self.heightConstraint?.constant = isExpand ? 80:0
            self.layoutIfNeeded()
        }
}

@objc func buttonClicked(_ sender: UIButton) {
        if !isCardExpanded {
            moreButton.setTitle("Less", for: .normal)
            bodyLabel.numberOfLines =  0
        } else {
            moreButton.setTitle("More", for: .normal)
            bodyLabel.numberOfLines = 1
        }

        expandViewWithAnimation(isCardExpanded)
        isCardExpanded = !isCardExpanded
}

i can't comment on your post because don't have enough reputation :)

Upvotes: 0

Related Questions