3

I'm seeing some odd behavior when using UIImageView inside a UIStackView and I suspect there's something obvious that I'm overlooking - though I've tried quite a few things at this point.

What I'm trying to do is very straightforward: download an image from the web, set the UIImageView inside the UIStackView. The UIStackView also contains a UILabel (and my actual project contains quite a few UILabels, thus the reason for the UIStackView). The below image has colored backgrounds to demonstrate where things are going wrong, and I've also included the full Playground code so as to be easily copied/pasted.

The image being downloaded is bigger than the width/height of the screen, and I've set contentMode = .scaleAspectFit. The UITableView uses
tableView.rowHeight = UITableViewAutomaticDimension and .estimatedRowHeight. I've set constraints, and have also attempted to set the hugging priority on the UIImageView, to no avail. I don't know the exact cell height ahead of time, and the cell heights will all differ.

What's actually happening is that the image is being resized very small, despite the leading/trailing constraints on the UIImageView, constraints on the UIStackView, .rowHeight on the cell, and .scaleAspectFit on the UIImageView. You can even see a sliver of the (red) label, so I know the UIStackView is being pinned to the contentView of the UITableViewCell.

Color Key

  • Orange: UITableViewCell background
  • Red: UILabel background
  • Green: UIImageView background

enter image description here

import UIKit
import PlaygroundSupport



class CustomView: UIView, UITableViewDataSource {

    var tableView = UITableView()
    var tableData = [String]()

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

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.gray

        tableView.dataSource = self
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 300
        tableView.tableFooterView = UIView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(tableView)
        tableView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        tableData = [String](repeating: "https://img.webmd.com/dtmcms/live/webmd/consumer_assets/site_images/article_thumbnails/video/wibbitz/wbz-breakfast-most-important-meal.jpg", count: 2)
        tableView.register(CustomCell.self, forCellReuseIdentifier: "cell")
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableData.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
        let imageURL = tableData[indexPath.row]
        if let url = URL.init(string: imageURL) {
            cell.cellImageView?.downloadImage(with: url,
                                              completion: {
                                                // ...
            })
        }
        return cell
    }
}

class CustomCell: UITableViewCell {

    var cellImageView: UIImageView?

    required init?(coder aDecoder: NSCoder) {
        fatalError() // since we're not using
    }

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupCell()
    }

    func setupCell() {

        backgroundColor = UIColor.orange
        clipsToBounds = true

        let tempLabel = UILabel()
        tempLabel.translatesAutoresizingMaskIntoConstraints = false
        tempLabel.backgroundColor = UIColor.red
        tempLabel.text = "this is some text"
        tempLabel.numberOfLines = 0
        tempLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)

        cellImageView = UIImageView()
        cellImageView?.clipsToBounds = true
        cellImageView?.contentMode = .scaleAspectFit
        cellImageView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        cellImageView?.backgroundColor = UIColor.green
        cellImageView?.translatesAutoresizingMaskIntoConstraints = false

        let stackView = UIStackView(arrangedSubviews: [tempLabel, cellImageView!])
        contentView.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.distribution = .fillProportionally
        stackView.alignment = .leading
        stackView.spacing = 10
        stackView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
        stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        cellImageView?.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
        cellImageView?.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true

    }

}

extension UIImageView {

    func downloadImage(with url: URL, completion: @escaping () -> Void) {

        let task = URLSession.shared.dataTask(with: url) { [weak weakSelf = self] data, response, error in

            guard error == nil else {
                print("UIImageView: error downloading image: \(String(describing: error))")
                return
            }

            guard let data = data, let downloadedImage = UIImage.init(data: data) else {
                print("UIImageView: issue with data from downloaded image.")
                return
            }

            DispatchQueue.main.async {
                weakSelf?.image = downloadedImage
                completion()
            }
        }
        task.resume()
    }
}

let containerView = CustomView(frame: CGRect(x: 0, y: 0, width: 400, height: 600))
containerView.backgroundColor = UIColor.blue

PlaygroundPage.current.liveView = containerView
PlaygroundPage.current.needsIndefiniteExecution = true
JaredH
  • 2,338
  • 1
  • 30
  • 40

2 Answers2

2

You seem to think that the image view inside the stack view will somehow size the cell's height from the inside out. It won't. If you want to use automatic cell height, don't use a stack view (or add constraints to size the stack view explicitly, but I doubt that will give you the desired results).

What's the purpose of the stack view in your code anyway? All a stack view does is make constraints for you in difficult situations. But this is not a difficult situation. The label and the image view together can easily be configured by hand.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thanks for your response. The above is a simplified version of the actual project - the actual project has about a half dozen labels, so a stackview in theory would make things easier to manage. So from your response, it sounds like stackviews don't take on the intrinsic content size from their content, and thus don't resize from the inside out. – JaredH Apr 17 '18 at 15:32
  • 1
    But in fact the stack view is _the problem_. Try it with your simplified version and you'll see. – matt Apr 17 '18 at 15:33
  • You're right. Apple pushes StackViews _hard_ as evidenced [here](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutWithoutConstraints.html#//apple_ref/doc/uid/TP40010853-CH8-SW1). "_In general, use stack views to manage as much of your layout as possible. Resort to creating constraints only when you cannot achieve your goals with stack views alone._" I'm puzzled as to why a StackView seemingly has no way to pass along the intrinsic content size from it's contents, and thus has far more limited usage. – JaredH Apr 17 '18 at 16:11
  • 1
    There are _very_ few situations in which you attempt to size a view by "from the inside out" using the constraints of its subviews. In those situations, stack views will generally just get in your way (unless you can size the stack view itself from the _outside_, which you were unable to do here). Thus, Apple is not wrong to say that you can _usually_ use stack views — though in fact I don't agree with how they've oversold them. To me stack views are really good only for one use case, namely to solve the "equal distribution" problem (and even then you can do it without them). – matt Apr 17 '18 at 16:17
0

You simply have to give your estimated row height in this method

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
    return 300;   //Choose your custom row height
}
marc
  • 914
  • 6
  • 18
  • Thank you for your response.`tableView.rowHeight = UITableViewAutomaticDimension` and `tableView.estimatedRowHeight = 300` makes that method unnecessary I'm pretty sure. I also don't want the cell height firmly set + I don't know it ahead of time. – JaredH Apr 17 '18 at 15:01