Troubleshooting an error: The behavior of the UICollectionViewFlowLayout is not defined because the item height must be less than the height - ios

I have a ViewController that at the top, holds a UIView with a UITextView. Constrained to the bottom of the UIView is a UICollectionView that scrolls horizontally.
The UITextView resizes based on user typing in additional lines of text but I am getting an error because of some sizing issue between the collectionView and the collectionViewFlowLayout but I can't for the life of me work it out.
The code is as follows:
import UIKit
class ViewController: UIViewController {
let containerView = UIView()
var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
setupKeyboardObservers()
setupTopTextView()
setupCollectionView()
}
func setupTopTextView() {
view.backgroundColor = .lightGray
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0),
])
let textView = UITextView()
containerView.addSubview(textView)
textView.backgroundColor = .systemBackground
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isScrollEnabled = false
textView.becomeFirstResponder()
textView.textContainer.maximumNumberOfLines = 0
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 50),
textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),
textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
containerView.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0)
])
textView.text = "This is a test"
}
func setupCollectionView() {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 0
flowLayout.minimumInteritemSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.backgroundColor = .systemYellow
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
])
collectionView.isPagingEnabled = true
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(BasicCollectionViewCell.self, forCellWithReuseIdentifier: BasicCollectionViewCell.reuseIdentifier)
}
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
4
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BasicCollectionViewCell.reuseIdentifier, for: indexPath) as! BasicCollectionViewCell
let backgroundColorOptions = [UIColor.systemGreen, .systemPurple, .brown, .systemRed]
cell.backgroundColor = backgroundColorOptions[indexPath.row]
return cell
}
}
The BasicCollectionViewCell literally has nothing yet as I am just trying to get these elements working and resizing correctly:
class BasicCollectionViewCell: UICollectionViewCell {
static let reuseIdentifier = "BASIC_COLLECTIONVIEW_CELL"
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The error I get happens when the UITextView increases or decreases in height:
2020-07-02 16:34:06.922059+1000 collectionViewResizingTest[97611:1371485] The behavior of the UICollectionViewFlowLayout is not defined because:
2020-07-02 16:34:06.922175+1000 collectionViewResizingTest[97611:1371485] the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.
2020-07-02 16:34:06.922554+1000 collectionViewResizingTest[97611:1371485] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7ffbfc016350>, and it is attached to <UICollectionView: 0x7ffbf9844600; frame = (0 94; 414 802); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x6000017c9200>; layer = <CALayer: 0x6000019d2ba0>; contentOffset: {0, 0}; contentSize: {1656, 816}; adjustedContentInset: {0, 0, 34, 0}; layout: <UICollectionViewFlowLayout: 0x7ffbfc016350>; dataSource: <collectionViewResizingTest.ViewController: 0x7ffbf9411ad0>>.
2020-07-02 16:34:06.922642+1000 collectionViewResizingTest[97611:1371485] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
2020-07-02 16:34:08.131628+1000 collectionViewResizingTest[97611:1371485] The behavior of the UICollectionViewFlowLayout is not defined because:
2020-07-02 16:34:08.131766+1000 collectionViewResizingTest[97611:1371485] the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.
2020-07-02 16:34:08.131940+1000 collectionViewResizingTest[97611:1371485] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7ffbfc016350>, and it is attached to <UICollectionView: 0x7ffbf9844600; frame = (0 107.5; 414 788.5); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x6000017c9200>; layer = <CALayer: 0x6000019d2ba0>; contentOffset: {0, 0}; contentSize: {1656, 768}; adjustedContentInset: {0, 0, 0, 0}; layout: <UICollectionViewFlowLayout: 0x7ffbfc016350>; dataSource: <collectionViewResizingTest.ViewController: 0x7ffbf9411ad0>>.
2020-07-02 16:34:08.132058+1000 collectionViewResizingTest[97611:1371485] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
Any help would be greatly appreciated. I think it has something to do with the safe area at the bottom (this happens on devices in the simulator that have FaceID).

Fixed by setting the collectionView.contentInsetAdjustmentBehavior to .never

Related

Nested CollectionView doesn't receive delegate events?

I have a horizontal collection view embedded in the cell of a vertical collection view (a carousel in a feed unit). For some strange reason, the inner scroll view is not receiving scroll view delegate events like scrollViewDidScroll. It does scroll, I just don't get any notifications about it.
The outer scrollview does, however, receive these events. I suspected the gesture recognizers might be interfering with eachother but I'm not sure. Do you have any idea what might be going wrong here or how I could get the inner collection view to behave normally?
EDIT: I determined that the source of the bug is actually a bug / undefined behavior with compositional layouts with orthogonal scrolling behavior:
https://developer.apple.com/forums/thread/127825
Basically they internally create additional horizontal collection views whose delegate events are not propagated upwards. That said, the approach in the accepted answer does work since it's not using this behavior.
Quick testing -- "horizontal collection view embedded in the cell of a vertical collection view" -- no trouble getting scrollViewDidScroll events for both.
Here's a quick example:
// simple collection view cell with centered label
class HorizItemCell: UICollectionViewCell {
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
contentView.addSubview(theLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -12.0),
theLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
func fillData(_ str: String, bk: UIColor) {
theLabel.text = str
contentView.backgroundColor = bk
}
}
// collection view cell with horizontal scrolling collection view
class VertItemCell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate {
var cv: UICollectionView!
var nItems: Int = 0
var bkColor: UIColor = .white
var vertRow: Int = 0
let theLabel: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 14.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .horizontal
fl.estimatedItemSize = CGSize(width: 120, height: 36)
cv = UICollectionView(frame: .zero, collectionViewLayout: fl)
cv.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(theLabel)
contentView.addSubview(cv)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cv.topAnchor.constraint(equalTo: theLabel.bottomAnchor, constant: 4.0),
cv.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 4.0),
cv.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -4.0),
cv.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0),
])
cv.register(HorizItemCell.self, forCellWithReuseIdentifier: "hc")
cv.dataSource = self
cv.delegate = self
contentView.layer.borderWidth = 2
contentView.layer.borderColor = UIColor.lightGray.cgColor
contentView.layer.cornerRadius = 12
}
func fillData(rowNum: Int, numItems: Int, bk: UIColor) {
nItems = numItems
bkColor = bk
vertRow = rowNum
theLabel.text = "Vertical Row: \(vertRow)"
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return nItems
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "hc", for: indexPath) as! HorizItemCell
c.fillData("\(indexPath.item)", bk: bkColor)
return c
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(type(of: self), "Horizontal Scrolling - Row:", vertRow, "contentOffset:", scrollView.contentOffset)
}
}
class TestingVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var cv: UICollectionView!
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow,
.red, .green, .blue,
.orange, .purple,
]
let counts: [Int] = [
10, 12, 19, 13, 8, 11, 17, 14, 16, 9, 15,
]
override func viewDidLoad() {
super.viewDidLoad()
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .vertical
cv = UICollectionView(frame: .zero, collectionViewLayout: fl)
cv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(cv)
let safeG = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
cv.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
cv.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 20.0),
cv.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -20.0),
cv.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -20.0),
])
cv.register(VertItemCell.self, forCellWithReuseIdentifier: "vc")
cv.dataSource = self
cv.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let fl = cv.collectionViewLayout as? UICollectionViewFlowLayout {
fl.itemSize = CGSize(width: cv.frame.width, height: 80.0)
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 12
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "vc", for: indexPath) as! VertItemCell
c.fillData(rowNum: indexPath.row, numItems: counts[indexPath.item % counts.count], bk: colors[indexPath.item % colors.count])
return c
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(type(of: self), "Vertical Scrolling", scrollView.contentOffset)
}
}
It looks like this when running:
and this is some sample Debug Console output:
TestingVC Vertical Scrolling (0.0, 100.5)
TestingVC Vertical Scrolling (0.0, 107.5)
TestingVC Vertical Scrolling (0.0, 115.5)
VertItemCell Horizontal Scrolling - Row: 3 contentOffset: (314.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 3 contentOffset: (315.0, 0.0)
VertItemCell Horizontal Scrolling - Row: 4 contentOffset: (1.0, 0.0)
VertItemCell Horizontal Scrolling - Row: 4 contentOffset: (22.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 5 contentOffset: (308.0, 0.0)
VertItemCell Horizontal Scrolling - Row: 5 contentOffset: (307.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 6 contentOffset: (12.5, 0.0)
VertItemCell Horizontal Scrolling - Row: 6 contentOffset: (32.5, 0.0)

Max width for content view of UICollectionView

A common pattern in UI is to maximize the size of the view up to some point and after that fill the rest of its superview with the spaces.
When using AutoLayout, it can be achieved easily with width <= X constraint. But when using this with UICollectionView, the scroll area matches the size of UICollectionView, so the sides are unscrollable which is unwanted for me.
So, the only way I found to achieve the behavior is to use the proper layout inside the cells themselves. I consider this as a not very good design decision (especially when you have multiple cells). But are there any alternatives available?
We can accomplish this by subclassing UICollectionView and implementing hitTest(_:with:).
What we'll do is extend the "touch area" wider than the collection view itself:
class ExtendedCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
// touch is inside self.bounds, so
// send it on to super (i.e. "normal" behavior)
// so we can select a cell on tap
return super.hitTest(point, with: event)
}
if self.bounds.insetBy(dx: -self.frame.origin.x, dy: 0).contains(point) {
// touch is outside self.bounds, but
// it IS inside bounds extended left and right, so
// capture the touch for self
return self
}
// touch was outside self.bounds (and outside our extended bounds), so
// send it on to super (i.e. "normal" behavior)
// so the rest of the view hierarchy (buttons, etc)
// can receive the gesture
return super.hitTest(point, with: event)
}
}
You can now use ExtendedCollectionView just as you would use UICollectionView, except you'll be able to scroll it starting from left or right, outside of its bounds.
Here's a complete example:
class ViewController: UIViewController {
var myData: [String] = []
// we'll use these colors for the cell backgrounds
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.systemPink, .systemYellow, .systemTeal,
]
// our "extended collection view"
var collectionView: ExtendedCollectionView!
let cellSize: CGSize = CGSize(width: 80, height: 100)
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Extended CollectionView"
// always respect the safe area
let g = view.safeAreaLayoutGuide
// let's add a "background" image view, sized to fit the view
// so we can easily see the reults
if let img = UIImage(named: "sampleBKG") {
let v = UIImageView()
v.image = img
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
view.sendSubviewToBack(v)
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
v.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
// fill myData array with 20 strings
// of different lengths to show this
// works with dynamic width cells
let strs: [String] = [
"Short",
"Bit Longer",
"Much Longer String",
]
for i in 0..<20 {
myData.append("C: \(i) \(strs[i % strs.count])")
}
// set the flow layout properties
let fl = UICollectionViewFlowLayout()
fl.estimatedItemSize = CGSize(width: 50, height: 100)
fl.scrollDirection = .horizontal
fl.minimumLineSpacing = 8
fl.minimumInteritemSpacing = 8
// create an instance of ExtendedCollectionView
collectionView = ExtendedCollectionView(frame: .zero, collectionViewLayout: fl)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .clear
view.addSubview(collectionView)
// let's make the collection view
// 80% of the width of the view's safe area
let cvWidthPercent = 0.8
// let's add a label below our custom view
// the same percentage width, so we can
// easily see the layout
let v = UILabel()
v.backgroundColor = .green
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "\(cvWidthPercent * 100)%"
view.addSubview(v)
NSLayoutConstraint.activate([
// let's put our collection view
// 80-pts from the top
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
// centered Horizontally
collectionView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// height equal to cell Height
collectionView.heightAnchor.constraint(equalToConstant: cellSize.height),
// 80% of the width of the safe area
collectionView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: cvWidthPercent),
// constrain label 8-pts below the collection view
v.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 8.0),
// centered Horizontally
v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// same percentage width
v.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: cvWidthPercent),
])
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(MyDynamicCVCell.self, forCellWithReuseIdentifier: "cvCell")
}
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cvCell", for: indexPath) as! MyDynamicCVCell
cell.contentView.backgroundColor = colors[indexPath.item % colors.count]
cell.label.text = myData[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Did Select Cell At:", indexPath)
}
}
a simple dynamic-width cell
class MyDynamicCVCell: UICollectionViewCell {
let label: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
])
}
}
The result looks like this:
When you run it, you'll see that you can scroll horizontally, even if you start dragging from the left or right of the cells.

Self sizing UICollectionView with autolayout

I'm trying to figure out is it possible for UICollectionView to calculate it's own height using autolayout? My custom cells are built on autolayout and the UICollectionViewFlowLayout.automaticSize property for itemSize seems to be working, but the size of UICollectionView itself should be set. I believe that this is normal behavior, since the collection can have bigger size than it's cells, but maybe it is possible to make height of the content view of UICollectionView to be equal to cell with some insets?
Here is the code for test UIViewController with UICollectionView
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 100)
layout.itemSize = UICollectionViewFlowLayout.automaticSize
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
if let collectionView = collectionView {
collectionView.showsHorizontalScrollIndicator = false
collectionView.isPagingEnabled = true
collectionView.register(CollectionViewCell.self,
forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.backgroundColor = .clear
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor),
// I want to get rid of this constraint
collectionView.heightAnchor.constraint(equalToConstant: 200)
])
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell",
for: indexPath) as? CollectionViewCell else {
return UICollectionViewCell()
}
return cell
}
}
And for custom cell
final class CollectionViewCell: UICollectionViewCell {
var mainView = UIView()
var bigView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
var label: UILabel = {
let label = UILabel()
label.text = "Here is text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var anotherLabel: UILabel = {
let label = UILabel()
label.text = "Here may be no text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let inset: CGFloat = 16.0
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
mainView.backgroundColor = .white
addSubview(bigView)
addSubview(label)
addSubview(anotherLabel)
NSLayoutConstraint.activate([
mainView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width),
mainView.topAnchor.constraint(equalTo: topAnchor),
mainView.leftAnchor.constraint(equalTo: leftAnchor),
mainView.rightAnchor.constraint(equalTo: rightAnchor),
mainView.bottomAnchor.constraint(equalTo: bottomAnchor),
bigView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: inset),
bigView.leftAnchor.constraint(equalTo: mainView.leftAnchor, constant: inset),
bigView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: -inset),
bigView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -inset),
label.topAnchor.constraint(equalTo: bigView.topAnchor, constant: inset),
label.leftAnchor.constraint(equalTo: bigView.leftAnchor, constant: inset),
label.rightAnchor.constraint(equalTo: bigView.rightAnchor, constant: -inset),
anotherLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: inset),
anotherLabel.leftAnchor.constraint(equalTo: bigView.leftAnchor, constant: inset),
anotherLabel.rightAnchor.constraint(equalTo: bigView.rightAnchor, constant: -inset),
anotherLabel.bottomAnchor.constraint(equalTo: bigView.bottomAnchor, constant: -inset)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And maybe someone, who knows the answer also could tell whether it's possible to later put this paging collection into UITalbleViewCell and make size of the cell change with selected item in the collection? I provide screenshot of the collection I'm trying to make:
Item's without second text label will have smaller height, than items on the picture
Screenshot
Solution 1:
Get collection view content size from collectionViewLayout :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Set your collection view height here.
let collectionHeight = self.yourCollectionView.collectionViewLayout.collectionViewContentSize.height
}
Solution 2:
Use KVO.
// Register observer
self.yourCollectionView.addObserver(self, forKeyPath: "contentSize", options: [.new, .old, .prior], context: nil)
#objc override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
// content size changed. Set your collection view height here.
let contentSize = change?[NSKeyValueChangeKey.newKey] as? CGSize {
print("contentSize:", contentSize)
}
}
// Remove register observer
deinit {
self.yourCollectionView.removeObserver(self, forKeyPath: "contentSize")
}
Solution 3:
Assign this class to UICollectionView. If you set height constraint from storyboard then set and enable remove at runtime.
class DynamicCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size, self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
var size = contentSize
size.height += (contentInset.top + contentInset.bottom)
size.width += (contentInset.left + contentInset.right)
return size
}
}

How to set left and right margin for UITableView in Swift

I am using UITableView inside my controller that displays cells with some informations (title, subtitle and image). I would like to add some padding to the tableView, but I can't find the right solution. This is my code for table view:
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.allowsSelection = false
tableView.showsVerticalScrollIndicator = false
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableView.automaticDimension
return tableView
}()
Inside viewDidLoad() I setup constraints for that tableView
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
Right now my view looks like this:
Now when I try to configure contentInset property inside my tableView closure by adding this:
tableView.contentInset = .init(top: 0, left: 23.5, bottom: 0, right: -23.5)
As you can see it only added space on the left side, but looking at the debug view hierarchy
I can say that it moved that contentView to the right side (It's still have the same width as its superview)
Info from view debugger.
TableView:
<UITableView: 0x7fba82016000; frame = (0 0; 390 844); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x60000023de90>; layer = <CALayer: 0x600000c4b9e0>; contentOffset: {-23.333333333333332, -47}; contentSize: {390, 635.66666666666663}; adjustedContentInset: {47, 23.5, 34, -23.5}; dataSource: <Myapp.WelcomeViewController: 0x7fba81c08f60>>
and First cell:
<UITableViewCellContentView: 0x7fba84021250; frame = (0 0; 390 87); gestureRecognizers = <NSArray: 0x6000002cf900>; layer = <CALayer: 0x600000c2b900>>
TableView has the same width as cell (390 vs 390). How can I add my paddings to the left and right to the tableView, without making constraints on that tableView (I want that area to also be scrollable)
You can do this either by setting the .layoutMargins on the cell's contentView (either in the cell class itself or in cellForRowAt:
cell.contentView.layoutMargins = .init(top: 0.0, left: 23.5, bottom: 0.0, right: 23.5)
or on the table view itself:
override func viewDidLoad() {
super.viewDidLoad()
tableView.layoutMargins = .init(top: 0.0, left: 23.5, bottom: 0.0, right: 23.5)
// if you want the separator lines to follow the content width
tableView.separatorInset = tableView.layoutMargins
}
You may try this:
Add the constant while adding the constraints
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 20),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
Try this
override var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
var frame = newFrame
frame.origin.x += 10
frame.size.width -= 2 * 10
super.frame = frame
}
}

UICollectionView full width and height problem

UICollectionView width is not full width, One cell per row
UICollectionView
private func setCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.scrollDirection = .horizontal
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.showsHorizontalScrollIndicator = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.isPagingEnabled = true
collectionView.register(ImageCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
}
UICollectionView -> Constraint
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
view.topAnchor.constraint(equalTo: collectionView.topAnchor),
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
])
UIColletionViewCellSize
By adding UICollectionViewFlowLayoutDelegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
ImageCollectionViewCell
class ImageCollectionViewCell: UICollectionViewCell {
....
private let imageView: UIImageView = {
let img = UIImageView()
img.contentMode = .scaleAspectFill
img.translatesAutoresizingMaskIntoConstraints = false
return img
}()
....
}
ImageCollectionViewCell -> imageView Constraint
addSubview(imageView)
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
topAnchor.constraint(equalTo: imageView.topAnchor),
bottomAnchor.constraint(equalTo: imageView.bottomAnchor)
])
UICollectionView FlowLayout Warning
The item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.
The relevant UICollectionViewFlowLayout instance is , and it is attached to ; layer = ; contentOffset: {0, -44}; contentSize: {0, 0}; adjustedContentInset: {44, 0, 34, 0}> collection view layout: .
This is Keep Happening
https://gph.is/g/EqNL8jr
UICollectionView Warning Fix
if #available(iOS 11.0, *) { collectionView.contentInsetAdjustmentBehavior = .never }
else { automaticallyAdjustsScrollViewInsets = false }
Scale to fill was giving bigger image than UIImageView size
img.clipsToBounds = true

Resources