Add expandable UITextView cell to UICollectionView - ios

I have collection view when one of it's cell is cell that contain UITextView. What i want is to add textView to this cell that will expand accordingly to entered text.
I created UIView that have text view inside:
private var textView: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isUserInteractionEnabled = true
textView.isEditable = true
return textView
}()
Inside i set up it's constraints to superview as following:
private func setConstraints() {
let width: CGFloat = 312
textView.topAnchor.constraint(equalTo: topAnchor).isActive = true
textView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
textView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
textView.widthAnchor.constraint(equalToConstant: width).isActive = true
}
Then inside cell i set constraints like that:
func setConstraints() {
textView.topAnchor.equalTo(topAnchor).isActive = true
textView.leftAnchor.equalTo(leftAnchor).isActive = true
textView.rightAnchor.equalTo(rightAnchor).isActive = true
textView.bottomAnchor.equalTo(bottomAnchor).isActive = true
}
I did achieve behaviour when cell is expanding after textView growth, but, i didn't set bottom constraint for UITextView->UIView, that why my collection view doesn't grow (scroll) when i type large bunch of text in my cell.
How to add cell with UITextView inside that will expand CollectionView it belongs to?

I think a better way to use manual estimation text view height and return one in
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
This approach is more difficult than using AutoLayout but gives more control and customization possibilities.

Related

Preventing UICollectionView from scrolling outside of contentOffset

I created a collectionView with 4 cells, the cell frames are equal to the entire window frame so that I can scroll through them as they were a PageViewController.
lazy var collectionView : PagesCollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
let collectionView = PagesCollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isPagingEnabled = true
collectionView.alwaysBounceHorizontal = false
return collectionView
}()
The goal I want to accomplish is that when I am in the first cell and scroll on the left (contentOffset negative) the collectionView scrolling stops, and when I am on the last cell the collectionView stops scrolling in the right direction, I want to see the cell still.
I tried different procedures:
This one blocked the scrollView only after having scrolled it in the first place, never re-enabling the scrolling in the other direction:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(scrollView.contentOffset.x == 0){
scrollView.isScrollEnabled = false
}
}
As suggested on other StackOverflow topics:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if(scrollView.contentOffset.x == 0){
collectionView.isScrollEnabled = false
}
collectionView.isScrollEnabled = true
}
But this just blocks the scrolling in both directions.
I actually don't know how to solve this.
I actually figured out how to solve this problem, it's really easy.
In my viewDidLoad method where I set the collectionView I set bounces equal to false:
collectionView.bounces = false

How to get UITableView to be positioned within the safe area

I am trying to get my UITableView to be positioned within the safe area but it doesn't seem to be working and I do not know why. I trying to do this programatically.
class MenuTableViewController: UITableViewController{
var margin: UILayoutGuide!
var tableDataSource: [userFolderObject]!
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup(){
margin = view.layoutMarginsGuide
tableDataSource = MockData.UITableDateSource
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true
/* I have also tried the below code
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
*/
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.backgroundColor = UIColor.lightGray
if section == 0{
label.text = "Search PubMed"
}else{
label.text = "My Folders"
}
return label
}
}
First, remove all of your setup code that attempts to mess with the margins of the table view. That is all done for you by default in a UITableViewController.
Since your issue is only with the layout of your custom section header views, you need to fix how you have implemented those views.
Like cells, you should use reusable header/footer views and your header/footer view should extend UITableViewHeaderFooterView. This will ensure proper margins by defaults and it already provides a standard textLabel you can set. No need to create your own UILabel.
As shown in the documentation for UITableViewHeaderFooterView you should register a class. Then in viewForHeader you should dequeque the header view and then set its textLabel as needed.
If you don't actually need anything but a plain old section label, then don't implement viewForHeader. Instead, implement titleForHeader. Much simpler.

What defines a size of CollectionViewCell inside a TableViewCell?

I'm implementing UITableView that contains UICollectionView inside. Of course, my UITableViewCell has a UICollectionView, but the problem is I don't know how to configure the height of the UICollectionViewCell. I tried to set height constraint to view inside my UICollectionViewCell, but it does not affect. There are many ways to play with size:
Handle size of UITableViewCell
Set height constraint to UICollectionView inside UITableViewCell
Use method of UICollectionViewDelegateFlowLayout
Set height constraint to view inside UICollectionViewCell (my method)
The last way is the most appropriate for me, but I want to know why it does not work. Here is the configuration of UICollectionView:
lazy var booksCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
var booksCollectionView = UICollectionView(frame: contentView.bounds, collectionViewLayout: layout)
booksCollectionView.backgroundColor = .clear
booksCollectionView.translatesAutoresizingMaskIntoConstraints = false
return booksCollectionView
}()
Constraints:
booksCollectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
booksCollectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
booksCollectionView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
booksCollectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
Cell inside UICollectionView contains only UIView:
bookView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
bookView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
bookView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
bookView.heightAnchor.constraint(equalToConstant: 440).isActive = true
Changing the height of the bookView does not affect at all. Why that happens? How should I handle the height for UICollectionViewCell?
Conform to protocol:UICollectionViewDelegateFlowLayout and implement the following method:
collectionView(_:layout:sizeForItemAt:)

Collection view with autosizing cells stops working after reloading

In my app I have a collection view with cells autosizing horizontally.
Here's some code:
// called in viewDidLoad()
private func setupCollectionView() {
let cellNib = UINib(nibName: SomeCell.nibName, bundle: nil)
collectionView.register(cellNib, forCellWithReuseIdentifier: SomeCell.reuseIdentifier)
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
flowLayout.itemSize = UICollectionViewFlowLayout.automaticSize
}
The cell has 1 view, which has constraint for heigth. This view subviews a label, which is limited with 2 rows and is not limited by width. The idea here is to allow label to calculate its own width fitting text in 2 rows.
In order to make this work I've added the following code to the cell class:
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
Now, it works perfectly. Cells are autosizng, scrollView is scrolling horizontally etc. Until I call reloadData() at least once. Then cells have size 50x50 and never autosize any more until I leave the screen and come back again.
Do you have any ideas on why is it happening?

UICollectionView cells resizing when deleting items with estimatedItemSize

I have a simple project with a storyboard containing only a single a UICollectionViewController, built with Xcode 7.1.1 for iOS 9.1
class ViewController: UICollectionViewController {
var values = ["tortile", "jetty", "tisane", "glaucia", "formic", "agile", "eider", "rooter", "nowhence", "hydrus", "outdo", "godsend", "tinkler", "lipscomb", "hamlet", "unbreeched", "fischer", "beastings", "bravely", "bosky", "ridgefield", "sunfast", "karol", "loudmouth", "liam", "zunyite", "kneepad", "ashburn", "lowness", "wencher", "bedwards", "guaira", "afeared", "hermon", "dormered", "uhde", "rusher", "allyou", "potluck", "campshed", "reeda", "bayonne", "preclose", "luncheon", "untombed", "northern", "gjukung", "bratticed", "zeugma", "raker"]
#IBOutlet weak var flowLayout: UICollectionViewFlowLayout!
override func viewDidLoad() {
super.viewDidLoad()
flowLayout.estimatedItemSize = CGSize(width: 10, height: 10)
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return values.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as! MyCell
cell.name = values[indexPath.row]
return cell
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
values.removeAtIndex(indexPath.row)
collectionView.deleteItemsAtIndexPaths([indexPath])
}
}
class MyCell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
var name: String? {
didSet {
label.text = name
}
}
}
When deleting the cells from the collection view, all remaining cells animate to their estimatedItemSize, and then swap back to the correct size.
Interestingly, this produces auto layout constraint warnings for each cell when the animation occurs:
2015-12-02 14:30:45.236 CollectionTest[1631:427853] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x14556f780 h=--& v=--& H:[UIView:0x1456ac6c0(10)]>",
"<NSLayoutConstraint:0x1456acfd0 UIView:0x1456ac6c0.trailingMargin == UILabel:0x1456ac830'raker'.trailing>",
"<NSLayoutConstraint:0x1456ad020 UILabel:0x1456ac830'raker'.leading == UIView:0x1456ac6c0.leadingMargin>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x1456acfd0 UIView:0x1456ac6c0.trailingMargin == UILabel:0x1456ac830'raker'.trailing>
My initial thought was that breaking these constraints was what was causing the resizing problem.
Updating the cell's awakeFromNib method:
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
}
fixes the warnings, but the problem still occurs.
I tried re-adding my own constraints between the cell and its contentView, but this didn't resolve the issue:
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
for constraint in [
contentView.leadingAnchor.constraintEqualToAnchor(leadingAnchor),
contentView.trailingAnchor.constraintEqualToAnchor(trailingAnchor),
contentView.topAnchor.constraintEqualToAnchor(topAnchor),
contentView.bottomAnchor.constraintEqualToAnchor(bottomAnchor)]
{
constraint.priority = 999
constraint.active = true
}
}
Thoughts?
flow layout calculates actual sizes of cells after doing layout by estimated sizes to define which ones are visible. After that it adjusts the layout based on real sizes.
However, when it animates, when it calculates initial position for animation, it doesn't reach the stage of dequeueing cells and running auto layout there, so it uses only estimated sizes.
The easiest way is to try to give the closest estimated sizes, or if you could provide the size in the delegate in sizeForItemAt call.
In my case, I was trying to animate layoutAttributes without inserting or deleting cells and for that specific case I subclassed UICollectionViewFlowLayout and then overridden this method:
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
if !context.invalidateEverything && context.invalidatedItemIndexPaths == nil && context.contentOffsetAdjustment == .zero && context.contentSizeAdjustment == .zero {
return
}
super.invalidateLayout(with: context)
}
This prevents recalculating layout attributes using estimated sizes when nothing has been changed.
TL;DR: I could only get a collection view to properly behave with the delegate sizeForItem method. Working sample here: https://github.com/chrisco314/CollectionView-AutoLayout
In the controller:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
var cell = Cell.prototype
let contents = data[indexPath.section][indexPath.item]
cell.text = contents
cell.expand = selected.contains(indexPath)
let width = collectionView.bounds
.inset(collectionView.contentInset)
.inset(layout.sectionInset)
.width
let finalSize = cell.systemLayoutSizeFitting(
.init(width: width, height: 0),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
.withWidth(width)
print("sizeForItemAt: \(finalSize)")
return finalSize
}
In the cell:
override func systemLayoutSizeFitting(
_ targetSize: CGSize,
withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority,
verticalFittingPriority: UILayoutPriority) -> CGSize {
let contentSize = contentView.systemLayoutSizeFitting(
targetSize,
withHorizontalFittingPriority: horizontalFittingPriority,
verticalFittingPriority: verticalFittingPriority)
return contentSize
}
Constraints for an expanding panel:
lazy var panel: UIView = {
let view = Panel()
view.pin(body, to: .left, .top, .right)
view.clipsToBounds = true
panelHeight = view.heightAnchor.constraint(equalTo: body.heightAnchor)
return view
}()
var panelHeight: NSLayoutConstraint!
lazy var height:CGFloat = 60
lazy var body: UIView = {
let view = Body()
view.backgroundColor = .blue
view.pin(contents, inset: 9)
let bodyHeight = view.heightAnchor.constraint(equalToConstant: height)
bodyHeight.isActive = true
return view
}()
lazy var contents: UILabel = {
let label = UILabel()
label.backgroundColor = .white
label.numberOfLines = 0
label.text = "Body with height constraint of \(height)"
return label
}()
I had a host of problems like this and many others, spent a stupid amount of time trying to find a path through that worked for all cases - rendering with autolayout, rational animations for insertion and deletion, handling rotations, etc. In my experience, the only way that worked was to use the sizeForItem delegate method. You can use estimatedSize and auto layout, but for me, the animations would always collapse to the top, and everything then spring down again - perhaps what you are seeing.
I have a sample that is basically my playground for testing. I tried different approaches across the different tabs of the tab view controller here, using estimated sizes, constraints on the cells themselves, custom systemSizeFitting that returns the desired size, and the delegate based sizeThatFits
The sample is a bit hacked up, but the third tab demonstrates a delegate based method that works for expanding cells, and insertion and deletion animations. Note that tab2? demonstrates inconsistent animations that the collection view uses, based on the ratio of expanding cells. If the ratio is greater than 2:1, it fades and snaps, if it is less then 2:1, it animates up and down smoothly.
All the non delegate approaches that tried failed when it came to animations, per above. Maybe there is an approach that works without the delegate method (and I would love to see if it it did), but I could not find it.

Resources