Swift ImageView's top Constraints looks wrong and covered its superView?

172 Views Asked by At

I want to achieve a "show more" effect in my view. The desired effect is as follows(sorry for my poor painting), the view's height is set to a specific value at first(such as 304), and when I click the "check more" button, the view's height will return to its original height. enter image description here

In my demo, the image always covered superview, and the contentLabel disappear at first, just like the gif below. I tried to set setContentCompressionResistancePriority and setContentHuggingPriority, but it's not working. I have two questions. Firstly how to make the image not to cover it's superview, and just show a part of it when its superview's height is not enough for imageView to show completely. Second, the contentLabel should not disappear at first, I noticed that it is because the label does not know its height at first, so the height constraint is 0, but how to calculate the right height for the content label so that it can be shown on screen even the superview's height is not enough.

enter image description here

Here is my code, I have tried half a day but still not a clue. Thanks for helping:

import UIKit
import SnapKit
import SDWebImage

class ViewController: UIViewController {

    var isExpanding = false
    var defaultHeightConstraint: Constraint?
    
    private lazy var panelView: UIView = {
        let view = UIView()
        view.layer.masksToBounds = true
        return view
    }()
    
    private lazy var stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.distribution = .fill
        return stackView
    }()
    
    private lazy var textPanelView: UIView = {
        let view = UIView()
        view.backgroundColor = .gray
        return UIView()
    }()
    
    private lazy var textTitleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        label.textColor = .black
        return label
    }()
    
    private lazy var textContentLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 12)
        label.textColor = .black
        label.numberOfLines = 0
        return label
    }()
    
    private lazy var imagePanelView: UIView = {
        let view = UIView()
        view.backgroundColor = .cyan
        view.layer.masksToBounds = true
        return UIView()
    }()
    
    private lazy var imageTitleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        label.textColor = .black
        return label
    }()
    
    private lazy var imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        view.layer.masksToBounds = true
        return imageView
    }()
    
    private lazy var expandPanel: UIView = {
        UIView()
    }()
    
    private lazy var expandBtnBgView: UIView = {
        let view = UIView()
        return view
    }()
    
    private lazy var expandBtn: UIButton = {
        let button = UIButton()
        button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
        button.setTitleColor(.systemGreen, for: .normal)
        button.setTitle("Check More", for: .normal)
        button.addTarget(self, action: #selector(onClickExpandBtn), for: .touchUpInside)
        button.backgroundColor = .white
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupModel()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
    
    override func viewWillLayoutSubviews() {
        
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        
        view.addSubview(panelView)
        panelView.addSubview(stackView)
        stackView.addArrangedSubview(textPanelView)
        stackView.addArrangedSubview(imagePanelView)
        textPanelView.addSubview(textTitleLabel)
        textPanelView.addSubview(textContentLabel)
        imagePanelView.addSubview(imageTitleLabel)
        imagePanelView.addSubview(imageView)
        
        panelView.addSubview(expandPanel)
        expandPanel.addSubview(expandBtnBgView)
        expandBtnBgView.addSubview(expandBtn)
        
        panelView.snp.makeConstraints { make in
            make.top.equalToSuperview().inset(40)
            make.left.right.equalToSuperview().inset(16)
            defaultHeightConstraint = make.height.lessThanOrEqualTo(304).constraint
        }
        defaultHeightConstraint?.isActive = true
        
        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        textPanelView.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
        }
        
        textTitleLabel.snp.makeConstraints { make in
            make.left.top.equalToSuperview()
            make.height.equalTo(22)
        }
        
        textContentLabel.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.top.equalTo(textTitleLabel.snp.bottom).offset(12)
            make.bottom.equalToSuperview().inset(24)
        }
                
        imagePanelView.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
        }
        
        imageTitleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview()
            make.left.equalToSuperview()
            make.height.equalTo(22)
        }
        
        imageView.snp.makeConstraints { make in
            make.top.equalTo(imageTitleLabel.snp.bottom).offset(12)
            make.left.right.equalToSuperview().inset(16)
            make.width.equalTo(UIScreen.main.bounds.width - 64)
            make.height.equalTo(0)
            make.bottom.equalToSuperview().inset(24)
        }
        
        expandPanel.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.height.equalTo(86)
            make.bottom.equalToSuperview()
        }

        expandBtnBgView.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.height.equalTo(46)
            make.bottom.equalToSuperview()
        }

        expandBtn.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.bottom.equalToSuperview().inset(16)
        }
        
        // this code did not work
        textPanelView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        textPanelView.setContentCompressionResistancePriority(.required, for: .vertical)
        textContentLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        textContentLabel.setContentHuggingPriority(.required, for: .vertical)
        imagePanelView.setContentHuggingPriority(.defaultLow, for: .vertical)
        imagePanelView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
        imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
        imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
    }
    
    private func setupModel() {
        // setupText
        textTitleLabel.text = "Package Title"
        textContentLabel.text = "Beef\nCheese Beef\nDouble Cheese Beef\nDouble Cheese Beef\nDouble Cheese Beef\nDouble Cheese Beef\nDouble Cheese Beef "
        // setupImage
        imageTitleLabel.text = "Image Title"
        let imgString = "https://i.postimg.cc/Wzxfw1JY/chic.jpg"
        imageView.sd_setImage(with: URL(string: imgString)) { [weak self] image, _, _, _ in
            guard let self = self, let image = image else { return }
            let imgSize = image.size
            let actualWidth = UIScreen.main.bounds.width - 64
            let actualHeight = (actualWidth / imgSize.width) * imgSize.height
            self.imageView.snp.updateConstraints { make in
                make.width.equalTo(actualWidth)
                make.height.equalTo(actualHeight)
            }
        }
    }
    
    @objc func onClickExpandBtn() {
        isExpanding = !isExpanding
        
        if isExpanding {
            defaultHeightConstraint?.isActive = false
            expandBtn.setTitle("Collapse", for: .normal)
        } else {
            defaultHeightConstraint?.isActive = true
            expandBtn.setTitle("Check More", for: .normal)
        }
    }
}

1

There are 1 best solutions below

1
Rob C On

I'd use a different approach. I would cover the botton half of the imageView with an overlay. The overlay hides the bottom half of the image until you tap on "Check More". When you tap "Check More", you hide the overlay view (set its height to zero), revealing the remaining portion of the imageView. With this approach you don't have to mess with content hugging or compression resistance priorities.

You had a couple other bugs in your code, but here's a working solution.

class ViewController: UIViewController {

    var isExpanding = false

    private lazy var panelView: UIView = {
        let view = UIView()
        view.layer.masksToBounds = true
        return view
    }()

    private lazy var stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.distribution = .fill
        return stackView
    }()

    private lazy var textPanelView: UIView = {
        let view = UIView()
        return view
    }()

    private lazy var textTitleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        label.textColor = .black
        return label
    }()

    private lazy var textContentLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 12)
        label.textColor = .black
        label.numberOfLines = 0
        return label
    }()

    private lazy var imagePanelView: UIView = {
        let view = UIView()
        view.layer.masksToBounds = true
        return view
    }()

    private lazy var imageTitleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        label.textColor = .black
        return label
    }()

    private lazy var imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.layer.masksToBounds = true
        return imageView
    }()

    private lazy var imageOverlayView: UIView = {
        let view = UIView()
        view.backgroundColor = self.view.backgroundColor
        return view
    }()

    private lazy var expandBtn: UIButton = {
        let button = UIButton()
        button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
        button.setTitleColor(.systemGreen, for: .normal)
        button.setTitle("Check More", for: .normal)
        button.addTarget(self, action: #selector(onClickExpandBtn), for: .touchUpInside)
        button.backgroundColor = .white
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupModel()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }

    override func viewWillLayoutSubviews() {

    }

    private func setupUI() {

        view.addSubview(panelView)
        panelView.addSubview(stackView)
        stackView.addArrangedSubview(textPanelView)
        stackView.addArrangedSubview(imagePanelView)
        textPanelView.addSubview(textTitleLabel)
        textPanelView.addSubview(textContentLabel)
        imagePanelView.addSubview(imageTitleLabel)
        imagePanelView.addSubview(imageView)
        imagePanelView.addSubview(imageOverlayView)

        panelView.addSubview(expandBtn)

        panelView.snp.makeConstraints { make in
            make.top.equalToSuperview().inset(40)
            make.left.right.equalToSuperview().inset(16)
        }

        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }

        textPanelView.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
        }

        textTitleLabel.snp.makeConstraints { make in
            make.left.top.equalToSuperview()
            make.height.equalTo(22)
        }

        textContentLabel.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.top.equalTo(textTitleLabel.snp.bottom).offset(12)
            make.bottom.equalToSuperview().inset(24)
        }

        imagePanelView.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
        }

        imageTitleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview()
            make.left.right.equalToSuperview()
            make.height.equalTo(22)
        }

        imageView.snp.makeConstraints { make in
            make.top.equalTo(imageTitleLabel.snp.bottom).offset(12)
            make.left.right.equalToSuperview().inset(16)
            make.width.equalTo(UIScreen.main.bounds.width - 64)
            make.height.equalTo(0)
            make.bottom.equalToSuperview().inset(24)
        }

        imageOverlayView.snp.makeConstraints { make in
            make.left.right.equalTo(imageView)
            make.bottom.equalTo(imageView)
            make.height.equalTo(0)
        }

        expandBtn.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.bottom.equalTo(imageOverlayView.snp.top)
        }
    }

    private func setupModel() {
        // setupText
        textTitleLabel.text = "Package Title"
        textContentLabel.text = "Beef\nCheese Beef\nDouble Cheese Beef\nDouble Cheese Beef\nDouble Cheese Beef\nDouble Cheese Beef\nDouble Cheese Beef "
        // setupImage
        imageTitleLabel.text = "Image Title"
        let imgString = "https://i.postimg.cc/Wzxfw1JY/chic.jpg"
        imageView.sd_setImage(with: URL(string: imgString)) { [weak self] image, _, _, _ in
            guard let self = self, let image = image else { return }
            let imgSize = image.size
            let actualWidth = UIScreen.main.bounds.width - 64
            let actualHeight = (actualWidth / imgSize.width) * imgSize.height
            self.imageView.snp.updateConstraints { make in
                make.width.equalTo(actualWidth)
                make.height.equalTo(actualHeight)
            }

            self.imageOverlayView.snp.updateConstraints { make in
                make.height.equalTo(actualHeight / 2.0)
            }
        }
    }

    @objc func onClickExpandBtn() {
        defer { isExpanding = !isExpanding }

        if isExpanding {
            expandBtn.setTitle("Check More", for: .normal)
            imageOverlayView.snp.updateConstraints { make in
                make.height.equalTo(imageView.frame.height / 2.0)
            }

        } else {
            expandBtn.setTitle("Collapse", for: .normal)
            imageOverlayView.snp.updateConstraints { make in
                make.height.equalTo(0)
            }
        }

        UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut) {
            self.view.layoutIfNeeded()
        }
    }
}