2

I'm trying to display a dynamically sized UITextView inside a stack view, but the text view is not adjusting to the size of the content.

First I have the arranged subview:

class InfoView: UIView {
    private var title: String!
    private var detail: String!
    private var titleLabel: UILabel!
    private var detailTextView: UITextView!
    
    init(infoModel: InfoModel) {
        self.title = infoModel.title
        self.detail = infoModel.detail
        super.init(frame: .zero)
        
        configure()
        setConstraint()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func configure() {
        titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.font = .rounded(ofSize: titleLabel.font.pointSize, weight: .bold)
        titleLabel.textColor = .lightGray
        titleLabel.sizeToFit()
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(titleLabel)
        
        detailTextView = UITextView()
        detailTextView.sizeToFit()
        detailTextView.text = detail
        detailTextView.font = UIFont.systemFont(ofSize: 19)
        detailTextView.isEditable = false
        detailTextView.textColor = .lightGray
        detailTextView.isUserInteractionEnabled = false
        detailTextView.isScrollEnabled = false
        detailTextView.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(detailTextView)
    }
    
    private func setConstraint() {
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: self.topAnchor),
            titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5),
            titleLabel.heightAnchor.constraint(equalToConstant: 40),
            
            detailTextView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
            detailTextView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            detailTextView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            detailTextView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        ])
    }
}

Then I implement the stack view in a view controller:

class MyViewController: UIViewController {
    var infoModelArr: [InfoModel]!
    var stackView: UIStackView!
    var scrollView: UIScrollView!
    
    init(infoModelArr: [InfoModel]) {
        self.infoModelArr = infoModelArr
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        var infoViewArr = [InfoView]()
        for infoModel in infoModelArr {
            let infoView = InfoView(infoModel: infoModel)
            infoViewArr.append(infoView)
        }
        
        stackView = UIStackView(arrangedSubviews: infoViewArr)
        stackView.axis = .vertical
        stackView.spacing = 10
        stackView.distribution = .fillProportionally
        stackView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
        ])
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.contentSize = stackView.bounds.size
    }
}

Finally, I call the view controller as following:

let myVC = MyViewController(infoModelArr: [InfoModel(title: "title", detail: "detail"), InfoModel(title: "title", detail: "detail")])
self.present(myVC, animated: true, completion: nil)

Notably, if I were to instantiate the stack view with a single arranged subview, the height of the stack view seems to be dynamically adjusted, but as soon as 2 or more subviews are introduced, the height doesn't reflect the content.

When I attempted to set the intrinsic size of the InfoView,

override func layoutSubviews() {
    super.layoutSubviews()
    height = titleLabel.bounds.height + detailTextView.bounds.height
}

var height: CGFloat! = 200 {
    didSet {
        self.invalidateIntrinsicContentSize()
    }
}

override var intrinsicContentSize: CGSize {
    let originalSize = super.intrinsicContentSize
    return CGSize(width: originalSize.width, height: height)
}

detailTextView.bounds.height returns 0.

Kevvv
  • 3,655
  • 10
  • 44
  • 90
  • Are you trying to reinvent `UITableView`? You don't seem to be using `UIScrollView` properly (you never even initialised it?). `fillProportionally` doesn't seem appropriate here either. There should be some constraints breaking, right? You should include the conflicting constraints in your question. I think `fill` is more appropriate, but why are you not using a `UITableView` in the first place? – Sweeper Jul 06 '21 at 02:17
  • @Sweeper oops the lack of `UIScrollView` was a mistake on my part in copying the question. `fill` seems to be doing the trick so if you want to post an answer I'll mark it. I'm using `UIStackView` because I'll be using 1 - 3 items only, but I'll definitely keep that option into consideration. – Kevvv Jul 06 '21 at 02:40

1 Answers1

1

The fillProportionally distribution tries to scale the heights of the arranged subviews according to their intrinsic content size, as a proportion of of the stack view's height. e.g. if the stack view has a height of 120, and arranged subview A has an intrinsic height of 10, and arranged subview B has an intrinsic height of 20, then A and B will have a height of 40 and 80 respectively in the stack view.

Your stack view doesn't have a defined height, so fillProportionally doesn't make much sense here.

Instead, a distribution of fill should do the job:

stackView.distribution = .fill

(as an experiment, you can try adding a height constraint to the stack view, and you'll see how fillProportionally works)

Sweeper
  • 213,210
  • 22
  • 193
  • 313