I have been working on an app that has a UICollectionView that works like the main screen with the UICollectionViewCells acting as different pages (scrolling horizontally). I have added a text field on each cell to edit, but when I click on the textfield, the cell height is extended when the keyboard appears. When the keyboard is hidden the cell height remains extended. I have been searching for an answer to this problem, but I have not come across a solution that works.
I have tried to invalidate the layout, set the translatesAutoresizingMaskIntoConstraints to false, and directly setting the content offset to zero. None of these options have fixed the issue.
Below is my code:
private let reuseIdentifier = ["Cell1", "Cell2", "Cell3", "Cell4", "Cell5"]
let navi_btn_array: [UIButton] = [navi_home_btn, navi_gavel_btn, navi_orders_btn, navi_profile_btn, navi_lightning_btn]
var collectionView: UICollectionView = {
var layout = UICollectionViewFlowLayout();
layout.sectionInset = UIEdgeInsets.zero
var cv = UICollectionView(frame: .zero, collectionViewLayout: layout);
cv.autoresizesSubviews = false
cv.contentInset = UIEdgeInsets.zero
cv.translatesAutoresizingMaskIntoConstraints = false;
return cv;
}();
class MainCVC: UICollectionViewController,UICollectionViewDelegateFlowLayout, CLLocationManagerDelegate, UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(SimpleDispensaryPage_Cell.self, forCellWithReuseIdentifier: reuseIdentifier[0])
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.isPagingEnabled = true
let nc:NotificationCenter = NotificationCenter.default
nc.addObserver(self, selector: #selector(keyboardDidShow(notification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
nc.addObserver(self, selector: #selector(keyboardDidHide(notification:)), name: NSNotification.Name.UIKeyboardDidHide, object: nil)
}
#objc func keyboardDidShow(notification: Notification){
collectionViewLayout.invalidateLayout()
collectionView?.contentOffset.y = 0
}
#objc func keyboardDidHide(notification: Notification){
collectionViewLayout.invalidateLayout()
collectionView?.contentOffset.y = 0
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier[0], for: indexPath)
cell.backgroundColor = Custom_Colors.color_pine.withAlphaComponent(0.5)
var txt_fld = UITextField()
txt_fld.frame = CGRect(x: 0, y: 50, width: 100, height: 50)
cell_label.text = "Placeholder #"+String(indexPath.item)
cell_label.textAlignment = .center
cell.addSubview(cell_label)
return cell
}
}
}
I also get this error when it runs:
2019-03-14 11:41:20.770799-0700 app[57379:5390785] 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.
2019-03-14 11:41:20.771043-0700 app[57379:5390785] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fda15c13f40>, and it is attached to <UICollectionView: 0x7fda1706f000; frame = (0 0; 375 730.8); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x600000447bc0>; layer = <CALayer: 0x600000437720>; contentOffset: {8, -38.333333333333336}; contentSize: {1891, 579}; adjustedContentInset: {0, 0, 151.79999999999995, 0}> collection view layout: <UICollectionViewFlowLayout: 0x7fda15c13f40>.
2019-03-14 11:41:20.771245-0700 app[57379:5390785] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
You have a little bit of strange setup here. I suggest making a couple of changes:
1 - Never add subviews in collectionView(_, cellForItemAt ...). This would result in multiple additions every time the cell is being reused. The subviews should be added by the cell (and preferably at creation time).
2 - Remove cv.autoresizesSubviews = false and cv.translatesAutoresizingMaskIntoConstraints = false.
3 - If you want a fixed size for your cells you can set the layout.delegate = self and implement the following method:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: screenWidth, height: screenHeight)
}
4 - If you really just want a couple of pages why don't you use UIPageViewController?
Related
I have a horizontally scrolling UICollectionView using the flow layout that has its delegate and datasource set. It has a height of 50 and is pinned to my view using autolayout. Unfortunately, it never displays and always gives the following error:
2020-10-09 21:30:08.687110+0100 MyApp[8679:1601772] The behavior of the UICollectionViewFlowLayout is not defined because:
2020-10-09 21:30:08.687428+0100 MyApp[8679:1601772] 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-10-09 21:30:08.687743+0100 MyApp[8679:1601772] Please check the values returned by the delegate.
2020-10-09 21:30:08.688641+0100 MyApp[8679:1601772] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x105dc6350>, and it is attached to <UICollectionView: 0x10611d000; frame = (0 0; 375 90); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x28372ccf0>; layer = <CALayer: 0x2839465c0>; contentOffset: {0, 0}; contentSize: {1081.6666666666667, 90}; adjustedContentInset: {0, 0, 0, 0}> collection view layout: <UICollectionViewFlowLayout: 0x105dc6350>.
2020-10-09 21:30:08.688943+0100 MyApp[8679:1601772] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
I've noticed that it is saying the contentSize is {1081.6666666666667, 90} but it actually only calls the numberOfSections and numberOfItemsInSection delegate methods. The others aren't called, and neither is the UICollectionViewDelegateFlowLayout sizing method. Why is this and how is it getting a contentSize without actually calling the datasource/delegate methods? It makes no difference if I set the autolayout height from 50 to 90.
My collectionView is set up in the storyboard. I have registered my cell and set the estimated flow layout size like so:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(
UINib(nibName: "VideoClipCell", bundle: nil),
forCellWithReuseIdentifier: "VideoClipCell"
)
if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 30, height: 50)
}
}
These are the datasource/flow delegate methods:
extension EditingViewController : UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "VideoClipCell", for: indexPath) as? VideoClipCell else {
return UICollectionViewCell()
}
return cell
}
}
extension EditingViewController: UICollectionViewDelegateFlowLayout {
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 50)
}
}
Do you have an UIImageView in your cell? That's probably why you're getting 1081.6666666666667 for the content size.
You probably don't want self-sizing cells, so turn off "Estimate Size" in the storyboard. Otherwise, your collection view won't read what's inside sizeForItemAt.
Can you show your UICollectionView`s init func,i guess you did not set the UICollectionView delegate to self,and the layout itemsize height is larger than 90
I am trying to create a collection view that has a header cell (dynamically sized) and some "normal" content cells (also dynamically sized). Therefore, the collection view is initialized as follows:
private lazy var timelineCollectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
flowLayout.sectionInset = UIEdgeInsets(top: self.coloredTitleBarHeight, left: 0, bottom: 0, right: 0) // assume that coloredTitlebarHeight is a constant of 120
flowLayout.estimatedItemSize = CGSize(width: self.view.frame.width, height: 10)
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
cv.alwaysBounceVertical = true
cv.contentInsetAdjustmentBehavior = .never
cv.backgroundColor = .clear
cv.scrollIndicatorInsets = UIEdgeInsets(top: self.coloredTitleBarHeight - self.getStatusBarHeight(), left: 0, bottom: 0, right: 0)
cv.delegate = self
cv.dataSource = self
cv.register(TLContentCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "content-header-cell")
cv.register(TLContentCell.self, forCellWithReuseIdentifier: "comment-cell")
return cv
}()
The collectionView is part of a ViewController that conforms to the following protocols: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
Then, I use the following code to create a header cell:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let cell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "content-header-cell", for: indexPath) as! TLContentCell
cell.timelineContent = tlContentItem
cell.delegate = self
return cell
}
Last but not least, the dequeuing of the regular cells:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "comment-cell", for: indexPath)
return cell
}
Output:
The collectionView displays the header cell as well as the item cells, however, the section Insets are not applied.
I would be very happy if you could provide me with any suggestion regarding this strange behavior.
Section insets are applied to contents only in UICollectionViewFlowLayout.
Using Section Insets to Tweak the Margins of Your Content
Section insets are a way to adjust the space available for laying out cells.
You can use insets to insert space after a section’s header view and
before its footer view. You can also use insets to insert space around
the sides of the content. Figure 3-5 demonstrates how insets affect
some content in a vertically scrolling flow layout. source
I have a collection view with 6 elements that should be laid out with 3 rows and 2 columns. I use the following code to achieve this:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalWidth = collectionView.bounds.size.width - 12
let totalHeight = collectionView.bounds.size.height - 18
let heightOfView = (totalHeight / 3)
let dimensions = CGFloat(Int(totalWidth))
return CGSize(width: dimensions / 2, height: heightOfView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 5, 0, 5)
}
In viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 5
flowLayout.minimumInteritemSpacing = 5
This produces a collection view that looks like this which is perfect:
This is the desired output
However when I situationally hide a view below the collection view before the view is loaded and adjust the size of it(making it 50 pts taller in height), then the output changes to the following (see image) with cells 4 & 5 off the screen unless scrolled to, only 2 rows instead of 3 and a large gap between rows:
Collection view appearance when adjusted to account for hidden view
To situationally hide the view in viewWillAppear I added the following:
override func viewWillAppear(_ animated: Bool) {
if testVariable == true {
bottomView.isHidden = true
// make bottomView height = 0
bottomViewHeight.constant = 0
// make collectionView space to the bottom of the view controller 0
collectionToBase.constant = 0
view.layoutIfNeeded()
}
}
This code tests if testVariable is true, and if it is hides the bottomView and makes the collectionView 50 pts larger to fill the space. I added this code in viewWillAppear in the hope that collectionViewLayout would account for the extra 50 pts of height and adjust according, but it doesnt.
You need to invalidate the layout. It does not do so on its own. Call invalidateLayout() on the flow layout after your call to layoutIfNeeded().
flowLayout.invalidateLayout()
Problem looks like this: http://i.imgur.com/5iaAiGQ.mp4
(red is a color of cell.contentView)
Here is the code: https://github.com/nezhyborets/UICollectionViewContentsAnimationProblem
Current status:
The content of UICollectionViewCell's contentView does not animate alongside contentView frame change. It gets the size immediately without animation.
Other issues faced when doing the task:
The contentView was not animating alongside cell's frame change either, until i did this in UICollectionViewCell subclass:
override func awakeFromNib() {
super.awakeFromNib()
//Because contentView won't animate along with cell
contentView.frame = bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
Other notes:
Here is the code involved in cell size animation
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.performBatchUpdates({
collectionView.reloadData()
}, completion: nil)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let isSelected = self.selectedIndex == indexPath.row
let someSize : CGFloat = 90 //doesn't matter
let sizeK : CGFloat = isSelected ? 0.9 : 0.65
let size = CGSize(width: someSize * sizeK, height: someSize * sizeK)
return size
}
I get the same results when using collectionView.setCollectionViewLayout(newLayout, animated: true), and there is no animation at all when using collectionView.collectionViewLayout.invalidateLayout() instead of reloadData() inside batchUpdates.
UPDATE
When I print imageView.constraints inside UICollectionView's willDisplayCell method, it prints empty array.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
for view in cell.contentView.subviews {
print(view.constraints)
}
//Outputs
//View: <UIImageView: 0x7fe26460e810; frame = (0 0; 50 50); autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x608000037280>>
//View constraints: []
}
This is a finicky problem, and you're very close to the solution. The issue is that the approach to animating layout changes varies depending on whether you're using auto layout or resizing masks or another approach, and you're currently using a mix in your ProblematicCollectionViewCell class. (The other available approaches would be better addressed in answer to a separate question, but note that Apple generally seems to avoid using auto layout for cells in their own apps.)
Here's what you need to do to animate your particular cells:
When cells are selected or deselected, tell the collection view layout object that cell sizes have changed, and to animate those changes to the extent it can do so. The simplest way to do that is using performBatchUpdates, which will cause new sizes to be fetched from sizeForItemAt, and will then apply the new layout attributes to the relevant cells within its own animation block:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.performBatchUpdates(nil)
}
Tell your cells to layout their subviews every time the collection view layout object changes their layout attributes (which will occur within the performBatchUpdates animation block):
// ProblematicCollectionViewCell.swift
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
If you want greater control over your animations, you can nest the call to performBatchUpdates inside a call to one of the UIView.animate block-based animation methods. The default animation duration for collection view cells in iOS 10 is 0.25.
The solution is very easy. First, in ViewController.collectionView(_,didSelectItemAt:), write only this:
collectionView.performBatchUpdates({
self.selectedIndex = indexPath.row
}, completion: nil)
And then, in the class ProblematicCollectionViewCell add this func:
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
self.layoutIfNeeded()
}
Enjoy!
You can apply a transform to a cell, although it has some drawbacks, such as handling orientation changes.
For extra impact, I have added a color change and a spring effect in the mix, neither of which could be achieved using the reloading route:
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
UIView.animate(
withDuration: 0.4,
delay: 0,
usingSpringWithDamping: 0.4,
initialSpringVelocity: 0,
options: UIViewAnimationOptions.beginFromCurrentState,
animations: {
if( self.selectedIndexPath.row != NSNotFound) {
if let c0 =
collectionView.cellForItem(at: self.selectedIndexPath)
{
c0.contentView.layer.transform = CATransform3DIdentity
c0.contentView.backgroundColor = UIColor.lightGray
}
}
self.selectedIndexPath = indexPath
if let c1 = collectionView.cellForItem(at: indexPath)
{
c1.contentView.layer.transform =
CATransform3DMakeScale(1.25, 1.25, 1)
c1.contentView.backgroundColor = UIColor.red
}
},
completion: nil)
}
I have an UITableView and I want to add a UICollectionView with a horizontal flow layout as subview in the backgroundView of the tableView, to do the same effect of the AppStore. Here I have the implementation code:
in the viewDidLoad:
UIView *tableViewBackgroundView = [[UIView alloc] initWithFrame:self.view.bounds];
self.tableView.backgroundView = tableViewBackgroundView;
// HighlightView heightFactor returns the reason which is 9.0/16.0
CGFloat headerViewHeight = CGRectGetWidth(self.view.frame) * [HighlightView heightFactor];
self.tableView.contentInset = UIEdgeInsetsMake(headerViewHeight, 0, 0, 0);
self.headerView = [[HighlightView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), headerViewHeight)];
[tableViewBackgroundView addSubview:self.headerView];
The HighlightView is a view with a collectionView inside.
But I'm having an issue, when the user interacts with the collectionView I start to receive this log:
Please check the values return by the delegate. the behavior of the
UICollectionViewFlowLayout is not defined because: the item height
must be less than the height of the UICollectionView minus the section
insets top and bottom values.
And this becomes a loop that doesn't stop even when the user stops to interact.
HighlightView (CollectionView) code:
override init(frame: CGRect) {
super.init(frame: frame)
self.viewModel.delegate = self
self.configureHighlightsCollectionView()
}
func configureHighlightsCollectionView() {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = UICollectionViewScrollDirection.Horizontal
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
if systemVersion > 8.0 {
flowLayout.estimatedItemSize = self.frame.size
}
flowLayout.itemSize = self.frame.size
self.highlightsCollectionView = UICollectionView(frame: self.bounds, collectionViewLayout: flowLayout)
self.highlightsCollectionView.frame = self.bounds
self.highlightsCollectionView.scrollsToTop = false
self.highlightsCollectionView.pagingEnabled = true
self.highlightsCollectionView.registerClass(CachedImageCollectionViewCell.self, forCellWithReuseIdentifier: "ImageCell")
self.addSubview(self.highlightsCollectionView)
self.highlightsCollectionView.invalidateIntrinsicContentSize()
self.highlightsCollectionView.dataSource = self
self.highlightsCollectionView.delegate = self
self.highlightsCollectionView.backgroundColor = UIColor.greenColor()
self.highlightsCollectionView.snp_makeConstraints { (make) -> Void in
make.top.equalTo(self)
make.bottom.equalTo(self)
make.left.equalTo(self)
make.right.equalTo(self)
}
}
//Mark: UICollectionViewDataSource
public func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.viewModel.highlightsArray.count
}
public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageCell", forIndexPath: indexPath) as! CachedImageCollectionViewCell
cell.highlightData = self.viewModel.highlightsArray[indexPath.item]
return cell
}
public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
println(self.frame.size)
return self.frame.size
}
The viewModel is a class who controls the data flow of the collectionView.
First I don't think it's a good idea to put your view as datasource and delegate of your UICollection inside your UIView subclass. You're not respecting the MVC pattern. Learn more informations about it at Introducing iOS Design Pattern . You should set your controller as it.
The problem is you're setting the itemSize of your UICollectionViewFlowLayout in initFrame: based on the frame of your view. In that method, the frame of your UIView is not correct since you're using AutoLayout. You have to wait until AutoLayout calculates the layouts of your views so when layoutSubviews: is called. Learn about UIView and AutoLayout in the Matt's book. It's for iOS 6 and in Objective - C but still great.