CollectionView Disappears within StackView (Swift) - ios

I'm trying to achieve the stackView arrangement shown in 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()
}
}
}

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:

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

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

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.

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.

Related

collectionView Animation on SwipUP?

I want to animate a collectionView on swipe on the basis of content offset. it's like a page controller but shows only one swipe. I have attached a video because I know the explanation is not good.
Link
you can download the video from there.
There can be several ways to do what you want, here is one way:
I am setting up a UIImageView along with an overlay UIView which is pinned to the main view's leading, trailing, top and bottom anchors.
My aim is to swipe up a collection view like your example and then change the color of the overlay view based on the cell tapped in the collection view.
Here are some vars and the initial set up:
class SwipeCollectionView: UIViewController
{
private var collectionView: UICollectionView!
private let imageView = UIImageView()
private let overlayView = UIView()
// Collection view data source
private let colors: [UIColor] = [.red,
.systemBlue,
.orange,
.systemTeal,
.purple,
.systemYellow]
// Store the collection view's bottom anchor which is used
// to animate the collection view
private var cvBottomAnchor: NSLayoutConstraint?
// Use this flag to disable swipe actions when carousel is showing
private var isCarouselShowing = false
// Use this to check if we are swiping up
private var previousSwipeLocation: CGPoint?
// Random width and height, change as you wish
private let cellWidth: CGFloat = 100
private let collectionViewHeight: CGFloat = 185
private let reuseIdentifier = "cell"
override func viewDidLoad()
{
super.viewDidLoad()
configureNavigationBar()
configureOverlayView()
configureCollectionView()
}
private func configureNavigationBar()
{
title = "Swipe CV"
let appearance = UINavigationBarAppearance()
// Color of the nav bar background
appearance.backgroundColor = .white // primary black for you
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
}
Now once some basic set up is done, here is how the image and the overlay view was set up. Nothing fancy happens here but pay attention to the gesture recognizer added to the overlay view
private func configureOverlayView()
{
imageView.image = UIImage(named: "dog")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
overlayView.backgroundColor = colors.first!.withAlphaComponent(0.5)
overlayView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
view.addSubview(overlayView)
// Auto layout pinning the image view and overlay view
// to the main container view
view.addConstraints([
imageView.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
imageView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
imageView.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
imageView.bottomAnchor
.constraint(equalTo: view.bottomAnchor),
overlayView.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
overlayView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
overlayView.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
overlayView.bottomAnchor
.constraint(equalTo: view.bottomAnchor)
])
// We will observe a swipe gesture to check if it is a swipe
// upwards and then react accordingly
let swipeGesture = UIPanGestureRecognizer(target: self,
action: #selector(didSwipe(_:)))
overlayView.addGestureRecognizer(swipeGesture)
}
Now once that is done, we need to create a horizontal scrolling uicollectionview that is positioned off screen which can be animated in when we swipe up, here is how this is done
private func configureCollectionView()
{
collectionView = UICollectionView(frame: .zero,
collectionViewLayout: createLayout())
collectionView.backgroundColor = .clear
collectionView.register(UICollectionViewCell.self,
forCellWithReuseIdentifier: reuseIdentifier)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.showsHorizontalScrollIndicator = false
// Add some padding for the content on the left
collectionView.contentInset = UIEdgeInsets(top: 0,
left: 15,
bottom: 0,
right: 0)
collectionView.dataSource = self
collectionView.delegate = self
overlayView.addSubview(collectionView)
// Collection View should start below the screen
// We need to persist with this constraint so we can change it later
let bottomAnchor = overlayView.safeAreaLayoutGuide.bottomAnchor
cvBottomAnchor
= collectionView.bottomAnchor.constraint(equalTo: bottomAnchor,
constant: collectionViewHeight)
// Collection View starts as hidden and will be animated in swipe up
collectionView.alpha = 0.0
// Add collection view constraints
overlayView.addConstraints([
collectionView.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor),
cvBottomAnchor!,
collectionView.trailingAnchor.constraint(equalTo: overlayView.trailingAnchor),
collectionView.heightAnchor.constraint(equalToConstant: collectionViewHeight)
])
}
private func createLayout() -> UICollectionViewFlowLayout
{
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: cellWidth, height: collectionViewHeight)
layout.minimumInteritemSpacing = 20
return layout
}
The code up to this point should give you something like this:
You still do not see the collection view and for that, implement the didSwipe action of the pan gesture
#objc
private func didSwipe(_ gesture: UIGestureRecognizer)
{
if !isCarouselShowing
{
let currentSwipeLocation = gesture.location(in: view)
if gesture.state == .began
{
// record the swipe location when we start the pan gesture
previousSwipeLocation = currentSwipeLocation
}
// On swipe continuation, verify the swipe is in the upward direction
if gesture.state == .changed,
let previousSwipeLocation = previousSwipeLocation,
currentSwipeLocation.y < previousSwipeLocation.y
{
isCarouselShowing = true
revealCollectionView()
}
}
}
// Animate the y position of the collection view and the alpha
private func revealCollectionView()
{
// We need to set the top constraint (y position)
// to be somewhere above the screen plus some padding
cvBottomAnchor?.constant = 0 - 75
UIView.animate(withDuration: 0.25) { [weak self] in
// animate change in constraints
self?.overlayView.layoutIfNeeded()
// reveal the collection view
self?.collectionView.alpha = 1.0
} completion: { (finished) in
// do something
}
}
Finally, just for completeness, here is the collection view data source and delegate:
extension SwipeCollectionView: UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int
{
return colors.count
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell
= collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
for: indexPath)
cell.layer.cornerRadius = 20
cell.clipsToBounds = true
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
}
extension SwipeCollectionView: UICollectionViewDelegate
{
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath)
{
overlayView.backgroundColor
= colors[indexPath.item].withAlphaComponent(0.5)
}
}
Now running this should give you this experience when you swipe up:
I believe this should be enough to get you started
Update
The full source code of the example is available on a gist here

UIImageView not resizing as circle and UILabel not resizing within StackView and Custom Collection Cell

I am trying to resize my UIImageView as a circle, however; every time I try to resize the UIImageView, which is inside a StackView along with the UILabel, I keep on ending up with a more rectangular shape. Can someone show me where I am going wrong I have been stuck on this for days? Below is my code, and what it's trying to do is add the image with the label at the bottom, and the image is supposed to be round, and this is supposed to be for my collection view controller.
custom collection view cell
import UIKit
class CarerCollectionViewCell: UICollectionViewCell {
static let identifier = "CarerCollectionViewCell"
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.frame = CGRect(x: 0, y: 0, width: 20, height: 20);
//imageView.center = imageView.superview!.center;
imageView.contentMode = .scaleAspectFill
imageView.layer.borderWidth = 4
imageView.layer.masksToBounds = false
imageView.layer.borderColor = UIColor.orange.cgColor
imageView.layer.cornerRadius = imageView.frame.height / 2
return imageView
}()
private let carerNamelabel: UILabel = {
let carerNamelabel = UILabel()
carerNamelabel.layer.masksToBounds = false
carerNamelabel.font = .systemFont(ofSize: 12)
carerNamelabel.textAlignment = .center
carerNamelabel.layer.frame = CGRect(x: 0, y: 0, width: 50, height: 50);
return carerNamelabel
}()
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.layer.masksToBounds = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.backgroundColor = .systemOrange
stackView.distribution = .fillProportionally
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureContentView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func configureContentView() {
imageView.clipsToBounds = true
stackView.clipsToBounds = true
carerNamelabel.clipsToBounds = true
contentView.addSubview(stackView)
configureStackView()
}
private func configureStackView() {
allContraints()
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(carerNamelabel)
}
private func allContraints() {
setStackViewConstraint()
}
private func setStackViewConstraint() {
stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true
stackView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true
stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
public func configureImage(with imageName: String, andImageName labelName: String) {
imageView.image = UIImage(named: imageName)
carerNamelabel.text = labelName
}
override func layoutSubviews() {
super.layoutSubviews()
stackView.frame = contentView.bounds
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
carerNamelabel.text = nil
}
}
Below here is my code for the CollectionViewControler
custom collection view
import UIKit
class CarerViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: 120, height: 120)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(CarerCollectionViewCell.self, forCellWithReuseIdentifier: CarerCollectionViewCell.identifier)
collectionView.showsVerticalScrollIndicator = false
collectionView.backgroundColor = .clear
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for `collectionView`
NSLayoutConstraint.activate([
collectionView.widthAnchor.constraint(equalTo: view.widthAnchor),
collectionView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor, constant: 600),
collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
])
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CarerCollectionViewCell.identifier, for: indexPath) as! CarerCollectionViewCell
cell.configureImage(with: "m7opt04g_ms-dhoni-afp_625x300_06_July_20", andImageName: "IMAGE NO. 1")
return cell
}
}
Can somebody show or point to me what I am doing wrong, thank you
This is what I am trying to achieve
UIStackView could be tricky sometimes, you can embed your UIImageView in a UIView, and move your layout code to viewWillLayoutSubviews(), this also implys for the UILabel embed that also inside a UIView, so the containers UIViews will have a static frame for the UIStackViewto be layout correctly and whats inside them will only affect itself.

Calculating Size of Cell for CollectionView Mosaic Layout

I'm trying to make a mosaic collection view layout similar to Google's Keep app. I've subclassed UICollectionViewLayout similar to the many tutorials found online. In order to properly layout the collection view cells, the calling class must implement a delegate, HeightForCellAtIndexPath method to get the cell's height. In my case, I also get the cell's width to create 1, 2 or 3 column layouts.
In all of the tutorials, the height of the cell's content is known and does not need to be computed. In my case, the size of content is not known and needs to be computed. I've tried many different ways of calculating this but none work perfectly. My latest attempt entails creating a CardContent class and adding that to a cell's contentView in cellForItemAt and also instantiate a CardContent instance in HeightForCellAtIndexPath to calculate the size of the content that is passed to the layout class.
I'm sure there are many problems with my methodology, but from what I can gather, the issue appears to be with the multi-line labels not laid out correctly in HeightForCellAtIndexPath in that the labels are not wrapping to multi line and remain as a single line thus giving me an incorrect height of the contentView.
CardContentCell.swift
import UIKit
class CardContentCell: UICollectionViewCell {
var todoList: TodoList! {
didSet {
self.backgroundColor = UIColor(todoList.color)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 5.0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CardContent.swift
Edit: Added createLineItem method. See answer below.
class CardContent: UIStackView {
var todoList: TodoList!
var verticalItemSpacing: CGFloat = 10.0
var cellWidth: CGFloat!
init(todoList: TodoList, cellWidth: CGFloat = 0.0) {
self.todoList = todoList
self.cellWidth = cellWidth
super.init(frame: CGRect(x: 0, y: 0, width: cellWidth, height: 0))
self.axis = .vertical
self.alignment = .fill
self.distribution = .fill
self.contentMode = .scaleToFill
self.spacing = 10.0
layoutContent()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createTitleLabel(title: String) -> UILabel {
let label = UILabel()
label.text = title
label.font = label.font.withSize(20.0)
label.numberOfLines = 2
label.lineBreakMode = .byTruncatingTail
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
func createItemLabel(text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = label.font.withSize(17.0)
label.numberOfLines = 3
label.lineBreakMode = .byTruncatingTail
label.translatesAutoresizingMaskIntoConstraints = false
label.sizeToFit()
return label
}
func createLineItem(text: String) -> UIStackView {
let hstack = UIStackView()
hstack.axis = .horizontal
hstack.alignment = .fill
hstack.distribution = .fillProportionally
let imgView = createImgView(withFont: lineItemFont)
let textLabel = createItemLabel(text: text)
hstack.addArrangedSubview(imgView)
hstack.addArrangedSubview(textLabel)
return hstack
}
func layoutContent() {
self.addArrangedSubview(createTitleLabel(title: todoList.title))
for todo in todoList.todos.prefix(6) {
let lineItem = createLineItem(text: todo.text)
self.addArrangedSubview(lineItem)
}
}
}
MyCollectionView.swift
extension MyCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! CardContentCell
cell.todoList = todoLists[indexPath.row]
let content = CardContent(todoList: cell.todoList)
cell.contentView.addSubview(content)
content.pinTopAndSides(to: cell.contentView) // See extension below
return cell
}
}
extension MyCollectionView: CardLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, HeightForCellAtIndexPath indexPath: IndexPath, cellWidth: CGFloat) -> CGFloat {
let todoList = todoLists[indexPath.row]
let stackView = CardContent(todoList: todoList, cellWidth: cellWidth)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.setNeedsLayout()
stackView.layoutIfNeeded()
let size = stackView.frame.size
return size.height
}
}
extension UIView {
func pinTopAndSides(to other: UIView) {
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraint(equalTo: other.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: other.topAnchor).isActive = true
}
}
The result is, if there are always 6 line items, then the computed height is always 230 (in a 2 column layout). In the screen shot below, the cell is colored while the rest of the content overflows.
Barring a better solution, the answer for me involved not using a nested horizontal UIStackview. That was fraught with unknowns and hard to diagnose auto layout issues. Instead, I used a UIView and added my own constraints.
Here's the method that creates said view. It's interesting that no one took a close enough look at my question that in my hurry to copy and past, I omitted this most crucial method in the original post. I will update the question with the original implementation of this method for reference.
func createLineItem(text: String) -> UIView {
let view = UIView()
let imgView = createImgView(withFont: lineItemFont)
imgView.translatesAutoresizingMaskIntoConstraints = false
let textLabel = createItemLabel(text: text)
textLabel.translatesAutoresizingMaskIntoConstraints = false
imgView.tintColor = self.textColor
view.addSubview(imgView)
view.addSubview(textLabel)
imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
imgView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
textLabel.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 5.0).isActive = true
textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
textLabel.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
textLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
return view
}
And,as for the HeightForCellAtIndexPath delegate function, setting the widthAnchor to the cell width provided the correct height of the cell:
func collectionView(_ collectionView: UICollectionView, HeightForCellAtIndexPath indexPath: IndexPath, cellWidth: CGFloat) -> CGFloat {
let stackView = CardContent(todoList: todoList)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.widthAnchor.constraint(equalToConstant: cellWidth).isActive = true
stackView.setNeedsLayout()
stackView.layoutIfNeeded()
let size = stackView.frame.size
return size.height
}

Prevent UIStackView from compressing UITableView

I am adding a UITableView into vertical UIStackView. That vertical UIStackView is within a UIScrollView.
However the table is not displaying unless I force an explicit Height constraint on it which I obviously don't want to do.
According to this SO question and answer UITableView not shown inside UIStackView this is because "stackview tries to compress content as much as possible"
If I add a UILabel to the StackView it is displayed fine. There is something specific about the UITableView that means it is not. I am using Xamarin and creating the UITableView in code
this.recentlyOpenedPatientsTable = new UITableView()
{
RowHeight = UITableView.AutomaticDimension,
EstimatedRowHeight = 44.0f,
AllowsMultipleSelectionDuringEditing = false,
TranslatesAutoresizingMaskIntoConstraints = false,
Editing = false,
BackgroundColor = UIColor.Clear,
TableFooterView = new UIView(),
ScrollEnabled = false,
};
The UIScrollView is pinned to the Top, Bottom, Left and Right of the View and works fine. It takes the Height I expect.
I have tried both the suggestions in this SO question and neither have worked. I find it odd that I cannot find others having this issue.
Any other suggestions?
Here is a very basic example, using a UITableView subclass to make it auto-size its height based on its content.
The red buttons (in a horizontal stack view) are the first arranged subView in the vertical stack view.
The table is next (green background for the cells' contentView, yellow background for a multi-line label).
And the last arranged subView is a cyan background UILabel:
Note that the vertical stack view is constrained 40-pts from Top, Leading and Trailing, and at least 40-pts from the Bottom. If you add enough rows to the table to exceed the available height, you'll have to scroll to see the additional rows.
//
// TableInStackViewController.swift
//
// Created by Don Mag on 6/24/19.
//
import UIKit
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
class TableInStackCell: UITableViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.textAlignment = .left
v.numberOfLines = 0
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .green
contentView.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: 0.0),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TableInStackViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let addButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Add a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let deleteButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete a Row", for: .normal)
v.backgroundColor = .red
return v
}()
let buttonsStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 20
return v
}()
let theTable: ContentSizedTableView = {
let v = ContentSizedTableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bottomLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.textAlignment = .center
v.numberOfLines = 0
v.text = "This label is the last element in the stack view."
// prevent label from being compressed when the table gets too tall
v.setContentCompressionResistancePriority(.required, for: .vertical)
return v
}()
var theTableData: [String] = [
"Content Sized Table View",
"This row shows that the cell heights will auto-size, based on the cell content (multi-line label in this case).",
"Here is the 3rd default row",
]
var minRows = 1
let reuseID = "TableInStackCell"
override func viewDidLoad() {
super.viewDidLoad()
minRows = theTableData.count
view.addSubview(theStackView)
NSLayoutConstraint.activate([
// constrain stack view 40-pts from top, leading and trailing
theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
theStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
// constrain stack view *at least* 40-pts from bottom
theStackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
])
buttonsStack.addArrangedSubview(addButton)
buttonsStack.addArrangedSubview(deleteButton)
theStackView.addArrangedSubview(buttonsStack)
theStackView.addArrangedSubview(theTable)
theStackView.addArrangedSubview(bottomLabel)
theTable.delegate = self
theTable.dataSource = self
theTable.register(TableInStackCell.self, forCellReuseIdentifier: reuseID)
addButton.addTarget(self, action: #selector(addRow), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteRow), for: .touchUpInside)
}
#objc func addRow() -> Void {
// add a row to our data source
let n = theTableData.count - minRows
theTableData.append("Added Row: \(n + 1)")
theTable.reloadData()
}
#objc func deleteRow() -> Void {
// delete a row from our data source (keeping the original rows intact)
let n = theTableData.count
if n > minRows {
theTableData.remove(at: n - 1)
theTable.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theTableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! TableInStackCell
cell.theLabel.text = theTableData[indexPath.row]
return cell
}
}

UIScrollView not showing up in the view

I am implementing a UIScrollView in a CollectionViewCell. I have a custom view which the scroll view should display, hence I am performing the following program in the CollectionViewCell. I have created everything programmatically and below is my code :
struct ShotsCollections {
let title: String?
}
class ShotsMainView: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
containerScrollView.contentSize.width = frame.width * CGFloat(shotsData.count)
shotsData = [ShotsCollections.init(title: "squad"), ShotsCollections.init(title: "genral")]
var i = 0
for data in shotsData {
let customview = ShotsMediaView(frame: CGRect(x: containerScrollView.frame.width * CGFloat(i), y: 0, width: containerScrollView.frame.width, height: containerScrollView.frame.height))
containerScrollView.addSubview(customview)
i += 1
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var shotsData = [ShotsCollections]()
var containerScrollView: UIScrollView = {
let instance = UIScrollView()
instance.isScrollEnabled = true
instance.bounces = true
instance.backgroundColor = blueColor
return instance
}()
private func setupViews() { //These are constraints by using TinyConstraints
addSubview(containerScrollView)
containerScrollView.topToSuperview()
containerScrollView.bottomToSuperview()
containerScrollView.rightToSuperview()
containerScrollView.leftToSuperview()
}
}
Now the issue is, while the scrollview is displayed, the content in it is not. I on printing the contentSize and frame of the scrollview, it displays 0. But if I check the Debug View Hierarchy, scrollview containes 2 views with specific frames.
I am not sure whats going wrongs. Any help is appreciated.
When you are adding customView in your containerScrollView, you are not setting up the constraints between customView and containerScrollView.
Add those constraints and you will be able to see your customViews given that your customView has some height. Also, when you add more view, you would need to remove the bottom constraint of the last added view and create a bottom constraint to the containerScrollView with the latest added view.
I created a sample app for your use case. I am pasting the code and the resultant screen shot below. Hope this is the functionality you are looking for. I suggest you paste this in a new project and tweak the code until you are satisfied. I have added comments to make it clear.
ViewController
import UIKit
class ViewController: UIViewController {
// Initialize dummy data array with numbers 0 to 9
var data: [Int] = Array(0..<10)
override func loadView() {
super.loadView()
// Add collection view programmatically
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ShotsMainView.self, forCellWithReuseIdentifier: ShotsMainView.identifier)
self.view.addSubview(collectionView)
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: collectionView.topAnchor),
self.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
self.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
self.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = UIColor.white
self.view.addSubview(collectionView)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor.white
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShotsMainView.identifier, for: indexPath) as! ShotsMainView
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// The cell dimensions are set from here
return CGSize(width: collectionView.frame.size.width, height: 100.0)
}
}
ShotsMainView
This is the collection view cell
import UIKit
class ShotsMainView: UICollectionViewCell {
static var identifier = "Cell"
weak var textLabel: UILabel!
override init(frame: CGRect) {
// Initialize with zero frame
super.init(frame: frame)
// Add the scrollview and the corresponding constraints
let containerScrollView = UIScrollView(frame: .zero)
containerScrollView.isScrollEnabled = true
containerScrollView.bounces = true
containerScrollView.backgroundColor = UIColor.blue
containerScrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(containerScrollView)
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
self.bottomAnchor.constraint(equalTo: containerScrollView.bottomAnchor),
self.leadingAnchor.constraint(equalTo: containerScrollView.leadingAnchor),
self.trailingAnchor.constraint(equalTo: containerScrollView.trailingAnchor)
])
// Add the stack view that will hold the individual items that
// in each row that need to be scrolled horrizontally
let stackView = UIStackView(frame: .zero)
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
containerScrollView.addSubview(stackView)
stackView.backgroundColor = UIColor.magenta
NSLayoutConstraint.activate([
containerScrollView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
containerScrollView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
containerScrollView.topAnchor.constraint(equalTo: stackView.topAnchor),
containerScrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
])
// Add individual items (Labels in this case).
for i in 0..<10 {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(label)
label.text = "\(i)"
label.font = UIFont(name: "System", size: 20.0)
label.textColor = UIColor.white
label.backgroundColor = UIColor.purple
label.layer.masksToBounds = false
label.layer.borderColor = UIColor.white.cgColor
label.layer.borderWidth = 1.0
label.textAlignment = .center
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1.0, constant: 0.0),
label.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.2, constant: 0.0)
])
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Screenshot

Resources