Currently in my code, I'm programmatically adding a UITextView within a UITableViewCell and added the ability to auto-resize the cell based on how much content is typed by the user.
It currently looks like so:
override func viewDidLoad()
{
tableView.estimatedRowHeight = 30.0
tableView.rowHeight = UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
// Dequeue the cell to load data
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if indexPath.section == 0
{
// Code
}
if indexPath.section == 1
{
let textView: UITextView = UITextView()
textView.delegate = self
textView.textColor = UIColor.black
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(textView)
let leadingConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.leading, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.leading, multiplier: 1.0, constant: 8.0)
let trailingConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: -8.0)
cell.contentView.addConstraint(leadingConstraint)
cell.contentView.addConstraint(trailingConstraint)
let topConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.top, multiplier: 1.0, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.bottom, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 0)
cell.contentView.addConstraint(topConstraint)
cell.contentView.addConstraint(bottomConstraint)
}
else if indexPath.section == 2
{
// Code
}
return cell
}
override func numberOfSections(in tableView: UITableView) -> Int
{
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 1
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let headerCell = tableView.dequeueReusableCell(withIdentifier: "HeaderCell")
let titleLabel = headerCell?.viewWithTag(1) as! UILabel
if section == 0
{
titleLabel.text = "Title"
}
else if section == 1
{
titleLabel.text = "Title"
}
else if section == 2
{
titleLabel.text = "Title"
}
return headerCell?.contentView
}
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
{
let footerCell = tableView.dequeueReusableCell(withIdentifier: "FooterCell")
return footerCell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
if indexPath.section == 1
{
return UITableViewAutomaticDimension
}
return super.tableView(tableView, heightForRowAt: indexPath)
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
return 35.0
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
return 15.0
}
func textViewDidChange(_ textView: UITextView)
{
tableView.beginUpdates()
tableView.endUpdates()
}
I only care about section 1 because that's the only cell that I want to auto-resize the cell's height. All other sections should maintain a static cell height.
The problem I have is that as soon as I press the enter key for the FIRST time in the UITextView to skip to the next line, I see very quickly some "ghost" cells and get a warning stating:
no index path for table cell being reused
The cell's height DOES dynamically re-size itself accordingly and after the initial return key is pressed, any subsequent new lines does not re-produce the "ghost" cells, but I still get the warnings.
What am I seem to be doing wrong?
Thanks
TextView is inherited from scrollView and hence the content size of textView can be greater than frame (In which case textView will scroll). In order for tableView to calculate the automatic size for cell it needs components with implicit size like label or button. So in order for tableView to calculate implicit size for textView you should disable scroll.
textView.isScrollEnabled = false
Should allow your cell to expand :)
EDIT:
Looks like this happens when a reusable cell is used as section header (as in your case) Have a look at this : What is the meaning of the "no index path for table cell being reused" message in iOS 6/7?
Suggestion :
Create a normal xib add a view and return it as a section header
else follow the answer posted in above link
Try this.
Add this method too.
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat{
if(indexPath.row == 0){
return heightForFirstRow
}
return heightForStaticCell
}
Related
I have a simple UICollectionViewController that is one section with a header and footer pinned to visible bounds. When I click a textField in the footer, the footer automatically animates above the keyboard and the collectionView scrolls to show the cells that otherwise would have been hidden by the keyboard as you would expect. However, when I click one outside of the keyboard, and dismiss it with a call to self.view.endEditing(true) the footer and collectionView do not react unless the collectionView is scrolled near the bottom. If the collectionView is scrolled near the bottom, the footer and collectionView animate as expected.
How can I force the footer and collectionView to animate properly every time?
In the images below, the header is orange, footer is green, and there are 10 cells that alternate red and blue.
Actual Behavior (occurs when you are not scrolled near the bottom of the collectionView when the keyboard dismisses)
Desired Behavior (occurs when you are scrolled at or near the bottom of the collectionView when the keyboard dismisses)
I know there are some methods to achieve this with Notifications when the keyboard appears and disappears but I would prefer to just use the default UICollectionViewController behavior if possible. Is there some configuration or setting I am missing?
Code:
New Single View App. Delete StoryBoard and ViewController.swift. Delete the main storyboard entry from the info plist file.
AppDelegate.swift
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = CollectionViewController.init()
return true
}
}
CollectionViewController.swift
import UIKit
private let reuseIdentifier = "Cell"
private let collectionViewHeaderFooterReuseIdentifier = "MyHeaderFooterClass"
class CollectionViewController: UICollectionViewController {
init() {
let collectionViewFlowLayout = UICollectionViewFlowLayout.init()
collectionViewFlowLayout.sectionHeadersPinToVisibleBounds = true
collectionViewFlowLayout.sectionFootersPinToVisibleBounds = true
super.init(collectionViewLayout: collectionViewFlowLayout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
self.collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
self.collectionView?.register(MyHeaderFooterClass.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: collectionViewHeaderFooterReuseIdentifier)
self.collectionView?.register(MyHeaderFooterClass.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: collectionViewHeaderFooterReuseIdentifier)
self.collectionView?.backgroundColor = UIColor.white
self.collectionView?.dataSource = self
self.collectionView?.delegate = self
}
}
// MARK: UICollectionViewDelegate
extension CollectionViewController {
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
DispatchQueue.main.async {
self.view.endEditing(true)
self.collectionViewLayout.invalidateLayout()
self.collectionView?.layoutIfNeeded()
}
}
}
// MARK: - UICollectionViewDataSource
extension CollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return 10
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
switch indexPath.row % 2 {
case 0:
cell.backgroundColor = UIColor.red
case 1:
cell.backgroundColor = UIColor.blue
default:
cell.backgroundColor = UIColor.gray
}
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: collectionViewHeaderFooterReuseIdentifier, for: indexPath)
if kind == UICollectionView.elementKindSectionHeader {
view.backgroundColor = UIColor.orange
}
else { //footer
view.backgroundColor = UIColor.green
}
return view
}
}
// MARK: - Collection View Flow Layout Delegate
extension CollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 100)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 100)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 100)
}
}
MyHeaderFooterClass
import UIKit
class MyHeaderFooterClass: UICollectionReusableView {
let textField: UITextField = {
let view = UITextField.init()
view.placeholder = "enter text here"
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(textField)
self.setNeedsUpdateConstraints()
}
override func updateConstraints() {
textFieldConstraints()
super.updateConstraints()
}
private func textFieldConstraints() {
NSLayoutConstraint(item: textField, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: textField, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: textField, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint(item: textField, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0.0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
From documentation:
func layoutIfNeeded()
Use this method to force the view to update its layout immediately.
func invalidateLayout()
This method invalidates the layout of the collection view itself and
returns right away.
Solution:
Looking at your code, you are already updating collectionViewLayout. So, you don't need to force update collectionView again. Therefore, simply removing self.collectionView?.layoutIfNeeded() alone will solve your issue.
Change your code as below:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
DispatchQueue.main.async {
self.view.endEditing(true)
self.collectionViewLayout.invalidateLayout()
//self.collectionView?.layoutIfNeeded()
}
}
So this is my first time working with CollectionView and I have collection of images and I want my collection hight to adjust to show all the images so I do not have to scroll to see the rest of them
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return list.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MycellCollectionViewCell
cell.myimage.image = UIImage(named: list[indexPath.row]+".jpg")
return cell
}
You can get the height of UICollectionView by using the performBatchUpdates(_:completion:) method after calling reloadData()
var height = CGFloat(0)
self.collectionView.reloadData()
self.collectionView.performBatchUpdates(nil) { (result) in
if result {
height = self.collectionView.contentSize.height
}
}
After that you can either set the frame height of the collection view:
collectionView.frame.size.height = height
or add a constraint:
let height = NSLayoutConstraint(item: collectionView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: height)
collectionView.addConstraint(height)
or if you have set the the collectionview's height from the storyboard or XIB, then you can connect the constraint to your code and simply change the constant:
collectionHeight.constant = height
I need to put UITableView inside UITableViewCell with auto-layout because second table has different number of rows and one row can have different height.
This is my ViewController
class ViewController: UIViewController {
let tableView = UITableView()
let cellId = "firstTableCellId"
override func viewDidLoad() {
super.viewDidLoad()
setupView()
tableView.reloadData()
view.backgroundColor = UIColor.gray
}
func setupView() {
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.register(NextTable.self, forCellReuseIdentifier: cellId)
tableView.backgroundColor = UIColor.green
tableView.separatorStyle = .singleLine
view.addSubview(tableView)
view.addConstraintsWithFormat("V:|-60-[v0]-5-|", views: tableView)
view.addConstraintsWithFormat("H:|-8-[v0]-8-|", views: tableView)
}
}
extension ViewController: UITableViewDelegate {
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! NextTable
cell.layoutIfNeeded()
return cell
}
}
And NextTable which is the cell in first table
class NextTable: UITableViewCell {
var myTableView: UITableView!
let cellId = "nextTableCellId"
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.brown
setupView()
myTableView.reloadData()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.text = "Next table:"
label.textColor = UIColor.black
label.sizeToFit()
label.backgroundColor = UIColor.cyan
return label
}()
func setupView() {
myTableView = UITableView()
myTableView.delegate = self
myTableView.dataSource = self
myTableView.separatorStyle = .singleLineEtched
myTableView.backgroundColor = UIColor.blue
myTableView.register(TableCell.self, forCellReuseIdentifier: cellId)
myTableView.isScrollEnabled = false
addSubview(myTableView)
addSubview(label)
addConstraintsWithFormat("H:|-30-[v0]-30-|", views: myTableView)
addConstraintsWithFormat("H:|-30-[v0]-30-|", views: label)
addConstraint(NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 15))
addConstraint(NSLayoutConstraint(item: myTableView, attribute: .top, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1.0, constant: 0))
addConstraint(NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: myTableView, attribute: .top, multiplier: 1.0, constant: 0))
addConstraint(NSLayoutConstraint(item: myTableView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -15))
}
}
extension NextTable: UITableViewDelegate {
}
extension NextTable: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! TableCell
cell.layoutIfNeeded()
return cell
}
}
And cell in second table
class TableCell: UITableViewCell {
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.text = "Some text"
label.textColor = UIColor.black
label.sizeToFit()
label.backgroundColor = UIColor.red
return label
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.yellow
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView(){
addSubview(label)
addConstraintsWithFormat("V:|-8-[v0]-8-|", views: label)
addConstraintsWithFormat("H:|-5-[v0]-5-|", views: label)
}
}
This is the effect of my code
There is no second table so I create new class and use it in cell as new table view
class InnerTableView: UITableView {
override var intrinsicContentSize: CGSize {
return self.contentSize
}
}
And now table is show but size is too large
What can I do to show full second table without empty space at bottom of cell.
I added override for property in my table view:
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
Full code for my table in table can you find on my GitHub
In your first table view, add this to your cellForRowAt after you dequeue your cell (might need to be slightly tweaked to fit your implementation):
cell.tableView.reloadData()
DispatchQueue.main.async {
cell.tableView.scrollToRow(at: IndexPath(row: cell.numberOfRowsInInnerTableView.count.count - 1, section: 0), at: .bottom, animated: false)
}
DispatchQueue.main.async {
cell.tableView.invalidateIntrinsicContentSize()
cell.tableView.layoutIfNeeded()
self.updateHeight()
}
Then define a function updateHeight in the same class (outer table view):
func updateHeight() {
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
Obviously, this is kind of hacky, but essentially it allows the cell's InnerTableView to know the actual height of all of the inner cells and resize the outer tableview appropriately. This method worked for me.
I tried the above solution but it haven't worked for me. I have done the small change and then it worked fine for me.
A) In my ViewController:
//NOTE: TestQuestionAnsOptionCell is the outer cell
if let cell = tableView.dequeueReusableCell(withIdentifier: String(describing:TestQuestionAnsOptionCell.self), for: indexPath) as? TestQuestionAnsOptionCell {
cell.reloadData()
setUpInnerCellListner(cell: cell)
cell.selectionStyle = .none
return cell
}
private func setUpInnerCellListner(cell:TestQuestionAnsOptionCell) {
cell.reloadTable = { // Closure called from TestQuestionAnsOptionCell
DispatchQueue.main.async {
UIView.setAnimationsEnabled(false)
self.tbQuestion.beginUpdates()
self.tbQuestion.endUpdates()
UIView.setAnimationsEnabled(true)
}
}
}
Extention used here:
extension NSObject {
class var className: String {
return String(describing: self)
}
}
B) In TestQuestionAnsOptionCell:
class TestQuestionAnsOptionCell: UITableViewCell {
var tblOptions: OwnTableView = OwnTableView()
var reloadTable:(()->(Void))?
override func awakeFromNib() {
super.awakeFromNib()
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupView() {
tblOptions.estimatedRowHeight = 60.0
tblOptions.rowHeight = UITableView.automaticDimension
tblOptions.delegate = self
tblOptions.dataSource = self
tblOptions.separatorStyle = .none
tblOptions.register(UINib(nibName: TestQuestionOptionCell.className, bundle: nil), forCellReuseIdentifier: TestQuestionOptionCell.className)
tblOptions.isScrollEnabled = false
addSubview(tblOptions)
addConstraintsWithFormat("H:|-30-[v0]-30-|", views: tblOptions)
addConstraint(NSLayoutConstraint(item: tblOptions, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 15))
addConstraint(NSLayoutConstraint(item: tblOptions, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -15))
}
func reloadData() {
tblOptions.reloadData()
}
}
With TableView Delegate and datasource, I used the following:
extension TestQuestionAnsOptionCell:UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//NOTE: TestQuestionOptionCell is the inner cell
if let cell = tableView.dequeueReusableCell(withIdentifier: TestQuestionOptionCell.className, for: indexPath) as? TestQuestionOptionCell {
cell.updateCell()
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
tblOptions.invalidateIntrinsicContentSize()
tblOptions.layoutIfNeeded()
reloadTable?()//Closure to update outer tableview
}
}
C) Inner tableview is used as:
class OwnTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet{
self.invalidateIntrinsicContentSize()
}
}
}
D) UIView Extention is as:
extension UIView {
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String:UIView]()
for(index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: viewsDictionary ))
}
}
All you need is invalidate content size in side ParentTableView's cellForRowAt indexPath
parentCell.InsideTableview.invalidateIntrinsicContentSize()
I am posting another answer and I wish to make your life easier this time.
I have searched and tried everything I found on google. What worked for me was combination of what I found here and there + something weird I tried and worked:
I cannot believe that this worked but it did guys.
1- Override the UITableView as mentioned in the answers to this question + override reloadData():
class InnerAutoTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet{
self.invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
}
2- Set rowHeight to be automaticDimension and estimatedRowHeight to be 0:
private lazy var tableView: UITableView = {
let tv = InnerAutoTableView()
tv.register(ItemTableCell.self, forCellReuseIdentifier: "itemId")
tv.delegate = self
tv.dataSource = self
tv.isScrollEnabled = false
tv.rowHeight = UITableView.automaticDimension
tv.estimatedRowHeight = 0
return tv
}()
I tried this on iOS 15.4 simulator and real devices (iPhone 12 Pro Max + iPhone 12).
In Xcode 8.2.1 and Swift 3 I have a UICollectionView inside a UITableViewCell but I can't get the layout right. basically I want to be able to see all of the star. I can adjust the size of the star but I want to fix the underlying problem, expanding the blue band.
Here is a shot of the view The red band is contentView and the blue band is collectionView. At first glance it looks like collectionView height is the problem but it is actually being hidden by something else. I can't find or change whatever it is hiding the collectionView cell.
I am not using the storyboard beyond a view controller embedded in a navigator.
Here's all the relevant code.
class WishListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView = UITableView()
let tableCellId = "WishListsCell"
let collectionCellId = "WishListsCollectionViewCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .white
tableView.register(WishListsCell.self, forCellReuseIdentifier: tableCellId)
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
func numberOfSections(in tableView: UITableView) -> Int {
return wishLists.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCell(withIdentifier: tableCellId, for: indexPath) as! WishListsCell
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView,willDisplay cell: UITableViewCell,forRowAt indexPath: IndexPath) {
guard let tableViewCell = cell as? WishListsCell else { return }
//here setting the uitableview cell contains collectionview delgate conform to viewcontroller
tableViewCell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forSection: indexPath.section)
tableViewCell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
}
func tableView(_ tableView: UITableView,didEndDisplaying cell: UITableViewCell,forRowAt indexPath: IndexPath) {
guard let tableViewCell = cell as? WishListsCell else { return }
storedOffsets[indexPath.row] = tableViewCell.collectionViewOffset
}
}
extension WishListsViewController: UICollectionViewDelegate , UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionCellId, for: indexPath) as! WishListsCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 100)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
}
class WishListsCell: UITableViewCell {
private var collectionView: UICollectionView!
let cellId = "WishListsCollectionViewCell"
var collectionViewOffset: CGFloat {
get { return collectionView.contentOffset.x }
set { collectionView.contentOffset.x = newValue }
}
func setCollectionViewDataSourceDelegate<D: protocol<UICollectionViewDataSource, UICollectionViewDelegate>>
(dataSourceDelegate: D, forSection section : Int) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = section
collectionView.reloadData()
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
collectionView = UICollectionView(frame: contentView.frame, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .blue
collectionView.register(WishListsCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
contentView.backgroundColor = .red
self.contentView.addSubview(collectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class WishListsCollectionViewCell: UICollectionViewCell {
var imageView: UIImageView
override init(frame: CGRect) {
imageView = UIImageView()
super.init(frame: frame)
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
contentView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
NSLayoutConstraint.activate([
NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal,
toItem: contentView, attribute: .width,
multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal,
toItem: contentView, attribute: .height,
multiplier: 1.0, constant: 0.0),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You can Use View Debugging
Run the project, go back to Xcode and click on the Debug View Hierarchy button in the Debug bar. Alternatively, go to Debug\View Debugging\Capture View Hierarchy.
Now you can select your red view and Xcode will indicate which view is selected.
For more info. Check out: View Debugging in Xcode
Specifically, the problem was the collection view frame had not been properly set. This was only revealed by the great tip that I didn't know about in the accepted answer.
This fixed it:
self.collectionView.frame = CGRect(0, 0, screenWidth, (screenWidth / 4) * Constants.IMAGE_ASPECT_RATIO)
Is it good practice to have a Swift class function that returns a completed UIView or UITableVIew?
I haven't seen any examples of that being done, but I assume that it is possible.
I'm looking for this to behave like a Javascript/Angular controller.
Desired:
#IBOutlet var tableView: UITableView!
#IBAction func ReloadContent(sender: UIButton, type: Type) {
if(type == "test"){
self.tableView = BuildView().ReloadContent("test")
} else {
self.tableView = BuildView().ReloadContent("default")
}
}
Current (Not very reusable):
var sections = [[String:String]]()
var sectionRows = [[[String:String]]]()
#IBOutlet var tableView: UITableView!
#IBAction func ReloadContent(sender: UIButton) {
let path = "testcontent"
let parameters = [
"todo":"Token should come from somewhere.",
"todo2":"There may be other parameters"
]
// This loads the data into the table.
populateTableData()
}
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.frame = CGRectMake(0, 75, 320, 450);
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sectionRows[section].count
}
// This is what builds the cell for each row, and returns it.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
// Collect the data for each
let object = sectionRows[indexPath.section][indexPath.row] // as NSDictionary
cell.textLabel?.text = object["thingName"]
return cell
}
// Select management
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
println("Selected at \(indexPath.row)")
}
// Sets the height of the section header. Required to make the first section header appear.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 50
}
// Sets the height of the section header. Required to make the first section header appear.
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 25
}
// This creates a view for every row in the header.
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
var view = UIView()
var listName = UILabel()
var userName = UILabel()
// Populate the data for the cell.
listName.text = self.sections[section]["listname"]
userName.text = self.sections[section]["userName"]
listName.setTranslatesAutoresizingMaskIntoConstraints(false)
userName.setTranslatesAutoresizingMaskIntoConstraints(false)
let views = ["listName": listName, "view": view, "userName": userName ]
view.addSubview(listName)
view.addSubview(userName)
var horizontallayoutContraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-10-[listName]-10-|", options: .AlignAllCenterY, metrics: nil, views: views)
view.addConstraints(horizontallayoutContraints)
var verticalLayoutContraint = NSLayoutConstraint(item: listName, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1, constant: 0)
view.addConstraint(verticalLayoutContraint)
// Returns the completed view.
return view
}