I have a table view in Swift with headers that expand or contract to show or hide cells, but for some reason there are extra row dividers in the middle of the page for no reason, and the label in the header is not showing up.
class ExpandableHeader: UITableViewHeaderFooterView {
var section: Int = 0
let expandLabel = UILabel()
}
func numberOfSections(in tableView: UITableView) -> Int {
// return number of section in table from data
return list.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return number of rows in each section from data
return list[section].items.count + 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create cell with the identifier which was set in the storyboard prototype cell
// set cell data/name from our data object
if indexPath.row < list[indexPath.section].items.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = list[indexPath.section].items[indexPath.row].name
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "custom", for: indexPath)
return cell
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// create a header of type of our subclassed header with section number
let headerView = ExpandableHeader()
headerView.expandLabel.text = "+"
headerView.expandLabel.frame.size.height = 30
headerView.expandLabel.frame.size.width = 30
headerView.expandLabel.textAlignment = NSTextAlignment.center
headerView.addSubview(headerView.expandLabel)
headerView.expandLabel.frame.origin.x = view.frame.maxX - headerView.expandLabel.frame.width
// assign selected/current section number to the header object
headerView.section = section
// create Gesture Recognizer to add ability to select header to this cutsom header with an action
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(headerClicked(sender:)))
// add Gesture Recognizer to our header
headerView.addGestureRecognizer(tapGesture)
return headerView
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// check if row's section expanded parameter is set to true or false. set height of rows accordingly to hide or show them
if list[indexPath.section].expanded == true {
return 44
} else {
return 0
}
}
I added the relevant lines of code above. When you change the row height to 0, should the cell separator lines also be changed to height 0, or hidden?
Why is the header in each section not going all the way to the right side of the screen when the storyboard has it all the way to the right edge?
Is this also why the text label for the header is not being displayed? Because the right edge is getting cut off?
Sorry for the basic questions, I'm still getting the hang of this, so any suggestions would be much appreciated.
To hide extra row divider use below lines of code
let backgroundView = UIView(frame: CGRect.zero)
self.tableView.tableFooterView = backgroundView //instead of tableView give your tableView name
Related
I have a UITableView which represent a questionnaire with Dynamic UI Cells. I've created 2 cells.
The first UI Cell need to be all the time on the top (like a Title/Header) and the second UI Cell need to be Dynamic (this means that the second UI Cell can display 10 or 20 questions). Is there any way to do this ?
I've already created a UITableViewCell for both UI Cells and I linked all the components.
Now I don't know where to specify that Cell 0 to be returned 1 time and Cell 1 to be returned X times.
Also how I keep Cell 0 on the top all the time ?
Here is a printscreen which reflect my issue:
Here is my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
// Static cell - Checks Left
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "checksLeftCell", for: indexPath) as! ChecksLeftTableViewCell
return cell
// Dynamic cell - Questions
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: Constants.defectAndDamageCell, for: indexPath) as! DefectAndDamageCheckCell
cell.configCell()
cell.delegate = self
return cell
default:
return UITableViewCell()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch indexPath.row {
case 0: return 100
case 1: return 400
default:
return 170
}
}
I expect to have "frozen" cell on the top and the second one to be a dynamic cell.
Thanks if you read this and I hope that I explained well what is in my mind.
This isn't sign of good thinking about design. It looks like you don't need UITableView at all. You can achieve better result using basic UIViews for each component such as Title, question details, tags, etc.
You can help yourself using UIStackView.
Instead of a static cell you can use a Section Header with a custom View inside of it:
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let nameLabel = UILabel(frame: CGRect(x: 10, y: 0, width: tabelView.frame.width, height: 40))
nameLabel.text = //
nameLabel.font = taskNameLabel.font.withSize(14)
//Or add whatever you need here
let sub = UIView(frame: CGRect(x: 0, y: 0, width: tabelView.frame.size.width, height: 40))
sub.backgroundColor = .white
sub.addSubview(NameLabel)
return sub
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Constants.defectAndDamageCell, for: indexPath) as! DefectAndDamageCheckCell
cell.configCell()
cell.delegate = self
return cell
}
I would try using UICollectionView instead of the tableview.
From UICollectionView, use the UICollectionViewDelegateFlowLayout delegate.
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { #your code for header view }
https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618037-collectionview
or
Create a section header in your TableView, and create a custom view in it.
So your TableView would have 1 section header and 1 row.
Good luck.
There are many ways to solve this.
If you want a static view that sticks to the top try this:
Subclass a UIViewController and add a container UIView at the top/leading/trailing. Embed your header labels and content in the content view. Next, add a UITableView and pin it to the leading/trailing/bottom of the superview. Make your view controller the data source and delegate.
In this way, you will still have scrolling within the tableview and can reload the cells as needed while managing your static header content as IBOutlets.
If you use the selected answer with stack views, you will have to implement a scrolling solution by embedding your stack view in a UIScrollView when your content is longer than the screen size. You should probably just stick to using a tableview because that behavior is inherited.
I have a table view cell. I make an app for a tenant in the apartment to report the defect of the room facility. if the defect has been repaired (status: Complete), data from server will give defect.status == 2 (if defect.status == 1, still on process to be repaired), and it will show YES and NO Button like the picture above.
I want if it still on the repairment process, the view that contains "Are You satisfied" label and Yes No Button will not appear. The expected result should be like the picture below
here is the code I use to remove that satisfied or not view
extension RequestDefectVC : UITableViewDataSource {
//MARK: Table View Delegate & Datasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listDefects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.removeFromSuperview()
}
}
return cell
}
}
but unfortunately, if that wantToRemoveView.removeFromSuperview() is triggered, it will remove all the view in all cell, even though the status is completed like picture below
I want that satisfied or not view appears if the status is complete, otherwise, it will be removed. how to do that ?
For your costumed cells are reused, removing views will cause uncertain effects. You don't actually need the specific view to be removed, only if it stays invisible.
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = true
}
} else {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = false
}
}
Create a height constraint for that view and hook it as IBOutlet and control it's constant according to that in cellForRowAt
self.askViewH.constant = show ? 50 : 0
cell.layoutIfNeeded()
return cell
I expect you using automatic tableView cells
#Alexa289 One suggestion is that you can take heightConstraints of UIView. then create IBOutlet of your height constraints and make its constant 0 when you want to hide otherwise assign value to your static height.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisficationHeightConstraints.constant = 50
} else {
cell.viewSatisficationHeightConstraints.constant = 0
}
return cell
}
Second suggestion is that you can take label and button in view and embed stackview to your view(view contain label and button)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisfication.isHidden = false
} else {
cell.viewSatisfication.isHidden = true
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
you can read about UIStackView which makes hiding things easier. If you are not using stackview and hiding things the UI will not good as the space used by the hidden view will be still there. So better to use stackView when need to hide or show some view.
UIStackView : https://developer.apple.com/documentation/uikit/uistackview
I am new to iOS Development and I just implemented a simple expandable sections UITableView. I am not able to understand why some rows disappear and sometimes change position when the row heights are recalculated on tapping the section header. I went through all the already answered questions on this topic and have not been able to find the right solution.
Following is a scenario:
Launch the app:
Tap on the section header:
Section expands
All other headers disappear
Tap again
Section collapses
The headers continue to be blank
Scrolled to the bottom and back to the top
The positions of headers changed
Scrolled to the bottom and back to the top again
The positions of headers changed again with some cells still blank
Things I have already tried:
Wrapping reloadRowsAtIndexPaths in updates block (beginUpdates() and endUpdates())
Using reloadRowsAtIndexPaths with animation set to .none
Removing reloadRowsAtIndexPaths at all while keeping the updates block
Using reloadData() instead which actually works but I lose animation
Code:
Here is the link to the project repository.
You're using cells for the header. You shouldn't do that, you need a regular UIView there, or at least a cell that's not being dequeued like that. There's a few warnings when you run it that give that away. Usually just make a standalone xib with the view and then have a static method like this in your header class. Make sure you tie your outlets to the view itself, and NOT the owner:
static func view() -> HeaderView {
return Bundle.main.loadNibNamed("HeaderView", owner: nil, options: nil)![0] as! HeaderView
}
You're reloading the cells in the section that grows, but when you change the section that's grown you'd need to at least reload the former section for it to take the changes to it's cell's height. You can reload the section by index instead of individual rows in both cases
Ok as you ask, I am changing my answer according to you.
import UIKit
class MyTableViewController: UITableViewController {
let rows = 2
var categories = [Int](repeating: 0, count: 10)
struct Constants {
static let noSelectedSection = -1
}
var selectedSection: Int = Constants.noSelectedSection
func selectedChanged(to selected: Int?) {
let oldIndex = selectedSection;
if let s = selected {
if selectedSection != s {
selectedSection = s
} else {
selectedSection = Constants.noSelectedSection
}
tableView.beginUpdates()
if(oldIndex != -1){
tableView.reloadSections([oldIndex,s], with: .automatic)
}else{
tableView.reloadSections([s], with: .automatic)
}
tableView.endUpdates()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("reloading section \(section)")
return (selectedSection == section) ? rows : 0;//rows
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.rowHeight
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "Header")
if let categoryCell = cell as? MyTableViewCell {
categoryCell.category = section + 1
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
recognizer.numberOfTapsRequired = 1
recognizer.numberOfTouchesRequired = 1
categoryCell.contentView.tag = section;
categoryCell.contentView.addGestureRecognizer(recognizer)
}
return cell?.contentView
}
func handleTapGesture(recognizer: UITapGestureRecognizer) {
if let sindex = recognizer.view?.tag {
selectedChanged(to: sindex)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Body", for: indexPath)
if let label = cell.viewWithTag(1) as? UILabel {
label.text = "Body \(indexPath.section + 1) - \(indexPath.row + 1)"
}
return cell
}
}
As you can see now I am just reloading a particular section instead of reloading the whole table.
also, I have removed gesture recognizer from the cell & put this into the main controller.
Requirement :
I have a list of UITableviewCell loaded from a nib that I'm presenting on UITableview. The first time I open the UIViewController all cells are shown correctly and work as expected.
Issue :
If I navigate back to the parent and then open the UIViewController again the UITableviewCell are 'invisible'. I say invisible because with a breakpoint in cellForRowAt I can see that the table view does load all cells and the cells are valid.
Code :
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 13
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = (project?.sliderData.sliders[indexPath.row].view)! as UITableViewCell
print(cell.contentView.subviews.count)
if let left = cell.viewWithTag(2) as? UILabel {
left.text = "left"
}
if let middle = cell.viewWithTag(3) as? UILabel {
middle.text = "middle"
}
if let right = cell.viewWithTag(4) as? UILabel {
right.text = "right"
}
return cell
}
Screen Shot Image
Expected observation :
I was thinking that maybe the subviews of the cells get released because I don't have any bindings to them in IB. To test this I'm printing the count of subviews and writing some text to the subview labels. And everything seems to go fine, the cells are loaded and the labels are there but the cells just don't show up.
But then, if I scroll the TableView up and down a little to get some cells updated those cells do appear at the top and bottom of the view as shown in the pic.
You need to call dequeueReusableCell(withIdentifier: "cell") inside your code then will show your table cell. It will reuse cell for your all numbers of row data content.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! UITableViewCell
return cell
}
More Details : How to create uitableview with multiple sections in iOS Swift.
Did not find reason why the tableView behaves the way it does so I solved the issue by dequeueing default cells. The views provided by the slider objects are added as subviews to the dequeued cells. Now the subviews can of course be any UIViews.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "sliderCell")
if cell == nil {
cell = UITableViewCell.init(style: .default, reuseIdentifier: "sliderCell")
}
cell?.addSubview((project?.sliderData.sliders[indexPath.row].view)!)
return cell!
}
Im experiencing a crash when adding new rows to a tableview. In short, the crash log says "Missing cell for newly visible row 3".
Reproduction
1. Add N amount of objects to datasource
2. Manually add the same amount
of cells to tableview
3. Reload and animate using beginUpdates - endUpdates
Known Problem
This crash has already been discussed at question and reported at Apple. Their solution to this problem (not using estimated cell heights) does not work for me as i need 2 different heights for the cells.
My tableview is composed out of 2 different cell classes. Both have their own standard height, configured as such:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row % (straightsetLogs.count + 1) == 0 {
return 44
} else {
return tableView.rowHeight
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// (exerciseSets.count / straightsetLogs.count) = amount of (super)sets
// (straightsetLogs.count + 1) = amount of cells needed for 1 (super)set
return (exerciseSets.count / straightsetLogs.count) * (straightsetLogs.count + 1)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Headercell: 1 headercell for every (super)set
if indexPath.row % (straightsetLogs.count + 1) == 0 {
// Create header cells once for every set
let headerCell = tableView.dequeueReusableCell(withIdentifier: CellID.setHeaderCell, for: indexPath) as! SetHeaderTableViewCell
return configureHeaderCell(headerCell, forIndexPath: indexPath, totalCellsPerSet: straightsetLogs.count + 1)
} else {
// Create picker cells for rest of rows
let pickerCell = tableView.dequeueReusableCell(withIdentifier: CellID.setPickerCell, for: indexPath) as! SetPickerTableViewCell
// Configure according to set and movement
return configurePickerCell(pickerCell, forIndexPath: indexPath, totalCellsPerSet: straightsetLogs.count + 1)
}
}
In storyboard i've configured the tableview itself to have a row height of 260.0 and for the header cell i've checked the custom row height box and set it to 44.0.
Current Status
The code for determining the correct cells for the index paths works and the crash can be solved by removing the height for row at indexPath code. But then i end up with all cells being 260.0 height.
Goal
I need 44.0 height for header cells and 260.0 height for picker cells without experiencing this crash.