'CollectionView Disappears within StackView (Swift)

I'm trying to achieve the stackView arrangement shown in the middle of this figure:the middle of this figure, but for some reason the top stack, containing a collectionView, disappears when using: a .fill distribution

stackView.distribution = .fill        (stack containing collectionView disappears)
stackView.distribution = .fillEqually (collectionView appears fine in stackView)

I've been struggling with this for days, and you'll see residues in my commented out sections: setting compressionResistance/hugging priorities, attempting to change the intrinsic height, changing .layout.itemSize of UICollectionViewFlowLayout(), etc... Nothing works in my hands. The code here will run if you simply paste it in and associate it with an empty UIViewController. The top, collectionView stack contains a pickerView, and the stacks below that are a pageControllView, subStack of buttons, and a UIView. It all works fine in the .fillEqually distribution, so this is purely a layout issue. Much Thanks!

// CodeStackVC2
// Test of programmatically generated stack views
// Output: nested stack views
// To make it run:
// 1) Assign the blank storyboard VC as CodeStackVC2
// 2) Move the "Is Inital VC" arrow to the blank storyboard

import UIKit

class CodeStackVC2: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate,UICollectionViewDelegateFlowLayout, UIGestureRecognizerDelegate {

  let fruit = ["Apple", "Orange", "Plum", "Qiwi", "Banana"]
  let veg = ["Lettuce", "Carrot", "Celery", "Onion", "Brocolli"]
  let meat = ["Beef", "Chicken", "Ham", "Lamb"]
  let bread = ["Wheat", "Muffin", "Rye", "Pita"]
  var foods = [[String]]()
  let button = ["bread","fruit","meat","veg"]

  var sView = UIStackView()
  let cellId = "cellId"

  override func viewDidLoad() {
    super.viewDidLoad()
    foods = [fruit, veg, meat, bread]
    setupViews()
  }

  //MARK: Views
  lazy var cView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    layout.minimumLineSpacing = 0
    layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
    layout.itemSize = CGSize(width: self.view.frame.width, height: 120)
    let cv = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
     cv.backgroundColor = UIColor.lightGray
    cv.isPagingEnabled = true
    cv.dataSource = self
    cv.delegate = self
    cv.isUserInteractionEnabled = true
    //    var intrinsicContentSize: CGSize {
    //      return CGSize(width: UIViewNoIntrinsicMetric, height: 120)
    //    }
    return cv
  }()

  lazy var pageControl: UIPageControl = {
    let pageC = UIPageControl()
    pageC.numberOfPages = self.foods.count
    pageC.pageIndicatorTintColor = UIColor.darkGray
    pageC.currentPageIndicatorTintColor = UIColor.white
    pageC.backgroundColor = .lightGray
    pageC.addTarget(self, action: #selector(changePage(sender:)), for: UIControlEvents.valueChanged)
//    pageC.setContentHuggingPriority(900, for: .vertical)
//    pageC.setContentCompressionResistancePriority(100, for: .vertical)
    return pageC
  }()

  var readerView: UIView = {
    let rView = UIView()
    rView.backgroundColor = UIColor.brown
//    rView.setContentHuggingPriority(100, for: .vertical)
//    rView.setContentCompressionResistancePriority(900, for: .vertical)
    return rView
  }()


  func makeButton(_ name:String) -> UIButton{
    let newButton = UIButton(type: .system)
    let img = UIImage(named: name)?.withRenderingMode(.alwaysTemplate)
    newButton.setImage(img, for: .normal)
    newButton.contentMode = .scaleAspectFit
    newButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleButton)))
    newButton.isUserInteractionEnabled = true
    newButton.backgroundColor = .orange
    return newButton
  }
  //Make a 4-item vertical stackView containing
  //cView,pageView,subStackof 4-item horiz buttons, readerView
  func setupViews(){
    cView.register(FoodCell.self, forCellWithReuseIdentifier: cellId)
    //generate an array of buttons
    var buttons = [UIButton]()
    for i in 0...foods.count-1 {
      buttons += [makeButton(button[i])]
    }
    let subStackView = UIStackView(arrangedSubviews: buttons)
    subStackView.axis = .horizontal
    subStackView.distribution = .fillEqually
    subStackView.alignment = .center
    subStackView.spacing = 20
    //set up the stackView
    let stackView = UIStackView(arrangedSubviews: [cView,pageControl,subStackView,readerView])
    stackView.axis = .vertical
    //.fillEqually works, .fill deletes cView, .fillProportionally & .equalSpacing delete cView & readerView
    stackView.distribution = .fillEqually
    stackView.alignment = .fill
    stackView.spacing = 5
    //Add the stackView using AutoLayout
    view.addSubview(stackView)
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
    stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  }

  func handleButton() {
    print("button pressed")
  }
  //pageControl page changer
  func changePage(sender: AnyObject) -> () {
    let x = CGFloat(pageControl.currentPage) * cView.frame.size.width
    cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
  }

  //MARK: horizontally scrolling Chapter collectionView
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    //    let scrollBarLeft = CGFloat(scrollView.contentOffset.x) / CGFloat(book.chap.count + 1)
    //    let scrollBarWidth = CGFloat( menuBar.frame.width) / CGFloat(book.chap.count + 1)
  }

  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let index = targetContentOffset.pointee.x / view.frame.width
    pageControl.currentPage = Int(index) //change PageControl indicator
  }

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return foods.count
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FoodCell
    cell.foodType = foods[indexPath.item]
    return cell
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: view.frame.width, height: 120)
  }
}

class FoodCell:UICollectionViewCell, UIPickerViewDelegate, UIPickerViewDataSource {

  var foodType: [String]? {
    didSet {
      pickerView.reloadComponent(0)
      pickerView.selectRow(0, inComponent: 0, animated: true)
    }
  }

  lazy var pickerView: UIPickerView = {
    let pView = UIPickerView()
    pView.frame = CGRect(x:0,y:0,width:Int(pView.bounds.width), height:Int(pView.bounds.height))
    pView.delegate = self
    pView.dataSource = self
    pView.backgroundColor = .darkGray
    return pView
  }()

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

  func setupViews() {
    backgroundColor = .clear
    addSubview(pickerView)
    addConstraintsWithFormat("H:|[v0]|", views: pickerView)
    addConstraintsWithFormat("V:|[v0]|", views: pickerView)
  }
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
  }

  func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    if let count = foodType?.count {
      return count
    } else {
      return 0
    }
  }

  func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
    let pickerLabel = UILabel()
    pickerLabel.font = UIFont.systemFont(ofSize: 15)
    pickerLabel.textAlignment = .center
    pickerLabel.adjustsFontSizeToFitWidth = true
    if let foodItem = foodType?[row] {
      pickerLabel.text = foodItem
      pickerLabel.textColor = .white
      return pickerLabel
    } else {
      print("chap = nil in viewForRow")
      return UIView()
    }
  }


}


Solution 1:[1]

The problem is that you have a stack view with a fixed height that contains two views (cView and readerView) that have no intrinsic content size. You need to tell the layout engine how it should size those views to fill the remaining space in the stack view.

It works when you use a .fillEqually distribution because you are telling the layout engine to make all four views in the stack view have an equal height. That defines a height for both the cView and readerView.

When you use a .fill distribution there is no way to determine how high the cView and readerView should be. The layout is ambiguous until you add more constraints. The content priorities do nothing as those views have no intrinsic size that can be stretched or squeezed. You need to set the height of one of the views with no intrinsic size and the other will take the remaining space.

The question is how high should the collection view be? Do you want it to be the same size as the reader view or maybe some proportion of the container view?

For example, suppose your design calls for the collection view to be 25% of the height of the container view with the readerView using the remaining space (the two other views are at their natural intrinsic content size). You could add the following constraint:

NSLayoutConstraint.activate([
  cView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25)
])

A Simpler Example

To reduce the layout to its most basic elements. You have a stack view pinned to its superview with four arranged subviews two of which have no intrinsic content size. For example, here is a view controller with two plain UIView, a label and a button:

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    setupViews()
  }

  private func setupViews() {

    let blueView = UIView()
    blueView.backgroundColor = .blue

    let titleLabel = UILabel()
    titleLabel.text = "Hello"

    let button = UIButton(type: .system)
    button.setTitle("Action", for: .normal)

    let redView = UIView()
    redView.backgroundColor = .red

    let stackView = UIStackView(arrangedSubviews: [blueView, titleLabel, button, redView])
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.axis = .vertical
    stackView.spacing = 8
    view.addSubview(stackView)

    NSLayoutConstraint.activate([
      stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      stackView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor),
      stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
      blueView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25)
      ])
  }
}

Here is how it looks on an iPhone in portrait with the blue view using 25% of the vertical space:

iPhone Screenshot

Solution 2:[2]

UIStackView works well with arranged subviews that are UIView but not directly with UICollectionView.

I suggest you put all your subviews items inside a UIView before stack them in a UIStackView, also you can use .fill distribution without use intrinsic content size, use instead constraints to make your subviews proportional as you need.

This solution also work seamless with autolayout without force translatesAutoresizingMaskIntoConstraints = false which make you less compliant with trait changes if you know what I mean.

/GM

Solution 3:[3]

  1. Set the top, bottom, leading and trailing constraint of desired controls inside xib or storyboard.
  2. Provide distribution of stack .fill.
  3. Then provide height constraint of all stacks in Xib or storyboard.
  4. Then set appropriate heights for every stacks inside code.

Hopefully it works for you.

Solution 4:[4]

I had the same issue, and for me it worked when I gave height and width constraints to the collection view which was placed inside the stack view.

Solution 5:[5]

I experienced this behavior with Xamarin CollectionView and tracked it down to an interaction being made with the CollectionView after the page was removed from the MainPage as the result of a web api call. Even blocking that, though it still had issues reloading the page. I finally resolved to clearing the collection list when the page is about to be hidden and saving a backup copy of the items, then on display of the page, running an async task that waited 10ms and then reinstalled the items. Failing to clear the list or installing items into the list immediately upon redisplay both leads to the error. The following shows in the console list and the CollectionView seems to flag itself to longer try to work after this message:

2022-04-16 19:56:33.760310-0500 .iOS[30135:2117558] The behavior of the UICollectionViewFlowLayout is not defined because: 2022-04-16 19:56:33.760454-0500 .iOS[30135:2117558] the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values. 2022-04-16 19:56:33.760581-0500 .iOS[30135:2117558] Please check the values returned by the delegate. 2022-04-16 19:56:33.760754-0500 .iOS[30135:2117558] The relevant UICollectionViewFlowLayout instance is <Xamarin_Forms_Platform_iOS_ListViewLayout: 0x7f99e4c4e890>, and it is attached to <UICollectionView: 0x7f99e562a000; frame = (0 0; 420 695); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x6000015ad9b0>; layer = <CALayer: 0x600005be5860>; contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0}; layout: <Xamarin_Forms_Platform_iOS_ListViewLayout: 0x7f99e4c4e890>; dataSource: <Xamarin_Forms_Platform_iOS_GroupableItemsViewController_1: 0x7f99e4c7ace0>>. 2022-04-16 19:56:33.760829-0500 .iOS[30135:2117558] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Giuseppe Mazzilli
Solution 3 Suraj Rao
Solution 4 Naval Hasan
Solution 5