I have a nested tableview setup, where each parent tableViewCell contains a tableview of it's own. Each parent tableViewCell, expands or collapses accordingly when it's selected. However once the cell is expanded, the tableView of the cell is not scrollable. only parent tableView's scroll is performed.
Any help would be much appreciated!
/* sub 0, sub 1... table view is not scrolling */
The approach you are taking to implement the design is unnecessarily complicated and will create problems like this. The design you have can be very easily solved using one UITableView instance (without nesting another into all of it's UITableViewCell instances.
First of all, Delete your nested UITableView instance that's inside a UITableViewCell.
See following implementation -
import UIKit
class ViewController: UIViewController {
var expandedSections: Set<Int> = Set()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if expandedSections.contains(section) {
return 10 // 1 (parentRow) + number of subrows
} else {
return 1 // (parentRow)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
// return parentCell
} else {
// return subrowCell (with indexPath.row - 1 as index)
}
}
func expandSection(_ section: Int) {
tableView.beginUpdates()
if expandedSections.insert(section).inserted {
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
}
tableView.endUpdates()
}
func collapseSection(_ section: Int) {
tableView.beginUpdates()
if expandedSections.remove(section) != nil {
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
}
tableView.endUpdates()
}
}
Related
I have a TableViewController which displays to-do list items. In the controller I have made a button which when pressed creates a new TableViewCell at the bottom which has a UITextView along with other elements.
Till now this is what I have managed to do -
Create a new cell upon button tap
Make the newly created cell's text view first responder
However, from what I have observed everything is working fine except when the last cell in the table is not visible, i.e., it is below the frame. In that case the cell gets created but is not made the first responder or some other cell's text view gets the cursor.
See the output here -
https://drive.google.com/file/d/1mRN8MEO5HBJ3ICUiRE0Yc4ib8tp62MYc/view?usp=sharing
Here is the code -
import UIKit
class TableViewController: UITableViewController, InboxCellDelegate {
var cell = InboxCell()
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.leftBarButtonItem = self.editButtonItem
tableView.register(UINib(nibName: "InboxCell", bundle: nil), forCellReuseIdentifier: "InboxCell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 50
}
#IBAction func inboxAddPressed(_ sender: UIBarButtonItem) {
addRowToEnd()
}
func addRowToEnd() {
Task.tasks.append("")
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: Task.tasks.count - 1, section: 0)], with: .automatic)
tableView.endUpdates()
cell.inboxTaskTextView.becomeFirstResponder()
}
func didChangeText(text: String?, cell: InboxCell) {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return Task.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell = tableView.dequeueReusableCell(withIdentifier: "InboxCell", for: indexPath) as! InboxCell
// Configure the cell...
cell.delegate = self
cell.inboxTaskTextView.text = Task.tasks[indexPath.row]
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tableView.beginUpdates()
Task.tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
I have tried to scroll to the bottom of the table first and then making the newly created cell first responder but that didn't work. In that case only the very first cell created becomes the first responder while the subsequent cells are created but the cursor remains in the very first cell.
Here is the block of code I used for scrolling before cell.inboxTaskTextView.becomeFirstResponder() -
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
Edit -
After having tried for a while this is the closest I have got to a solution.
After Task.tasks.append("") I have added the following code which scrolls down the view to the bottom -
if tableView.contentSize.height >= tableView.frame.size.height {
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
In this case the newly created cell becomes first responder but only momentarily. The keyboard doesn't even appear fully before it gets dismissed automatically in a flash. This happens only for cells that are created below the fold - i.e. when the table view has to scroll down and then create a new cell.
Try to keep it simple first. Put a breakpoint or print("indexPath.row = \(indexPath.row)") at the beginning of cellForRow UITableViewDataSource method that you implemented already.
Add the new task and see if your cellForRow is being called for the indexPath corresponding to your new cell.
If not - you may have to scroll the tableView up at least 44points or whatever is needed to reach the area where the new cell should already be displayed. If you don't do that - the cell might not be created yet, and cell most probably refers to the last cell in the table view (or it could also be referring to a cell in the pool if some logic is not implemented correctly). So the new cell must be in the visible area before making its UITextField or UITextView become first responder.
If you know that cell is already visible - better to get an reference to it via
let index = IndexPath(row: rowForNewCell, section: 0)
let cell = self.table.cellForRow(at: index)
Finally call:
cell.inboxTaskTextView.becomeFirstResponder()
I have a TableView that manages two rows of data. Each row is managed by a CollectionView which allows the user to scroll horizontally and filter the data. The filters are part of the table view and above the CollectionView.
Here's a visual:
When I click the filters along row one (off screen in the image above) the ui smoothly updates the data. However when I click the filters along row two the collection view shifts up briefly before back down to the proper position.
I'm fairly confident that the issue has to do with my interface builder configuration but for the record, this is the code that reloads the collection views.
func onFetchCompleted() {
if shouldRefreshRow() {
//tableView.reloadData() // reload all rows
tableView.reloadRows(at: [IndexPath(row: viewModel!.rowToFetch!, section: 0)], with: .none)
}
}
func shouldRefreshRow() -> Bool {
return self.viewModel?.businessesStore[viewModel!.rowToFetch!].previousPage == 1
}
And here's the TableView config:
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? BusinessesTableViewCell {
cell.configureCell(dataSourceDelegate: self, filterDelegate: self, forPath: indexPath, indexPathsToReload:
return cell
}
return UITableViewCell()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(viewModel?.businessesStore[collectionView.tag].businesses.count ?? 0 > 0) {
return (viewModel?.businessesStore[collectionView.tag].total)!
}
return 0
}
In interface builder I have my table view leading, trailing and bottom constraints set to 0 with respect to the superview and top to 0 with respect to the view above the table view (the solid black one in the image above).
I'm fairly certain this is an interface builder issue because If I remove my bottom constraint and set my TableView height to something like 400, the view behaves properly when a filter is clicked. The catch is that the user has to be scrolled down to the bottom of the screen otherwise it behaves in the janky manner explained above.
Here's the layout:
Any ideas?
I was able to find a solution to this issue after quite a bit of persistence. I had two things wrong in my configuration.
1: When I was reloading data with this line of code tableView.reloadRows(at: [IndexPath(row: viewModel!.rowToFetch!, section: 0)], with: .none) it seems to be including some sort of bounce animation on the collection view. I replaced that with tableView.reloadData() which fixed most of the bounce that was occurring.
2: I increased the size of my table view rows which removed the rest of the bounce.
I have implemented a tableview (4 sectional) in IOS. Problem is that
I have just added a section header in first section.Other sections don't have a header.First section does not have row (number of rows is 0).other sections have multiple rows.When I scroll , first section's header is not sticky.it is scrolling and out of screen.My table view style is plain.How can I want to make first section's header is always sticky.Code is below.Unfortunatelly tableview is so complicated and I don't want to make it only one section so that I have implemented it multi sectional.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
return tabHeaderView.contentView
}
else {
return UIView()
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
return UITableView.automaticDimension
}
else {
return 0 //just first section have a header (tabview)
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
func numberOfSections(in tableView: UITableView) -> Int {
if dataReady {
totalSectionCount = getSectionCount()
return totalSectionCount
}
else {
return 1 //permanent because of tabview
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
if !dataReady || ApplicationContext.instance.userAuthenticationStatus.value == .semiSecure{
return 1//for shimmer cell and semisecure view
}
else {
return getNumberOfRows(sectionNumber: section)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if ApplicationContext.instance.userAuthenticationStatus.value == .semiSecure {
semisecureViewCell = EarningsSemisecureViewCell()
setSemisecureViewTextInfo(cell: semisecureViewCell)
semisecureViewCell.delegate = self
semisecureViewCell.layoutIfNeeded()
return semisecureViewCell
}
else if !dataReady {
return getShimmerCell()
}
else {
if indexPath.row == 0 && !(selectedTabIndex == .BRANDPOINT && indexPath.section == 1){//marka puan listesinde header cell olmayacak
return getSectionHeaderViewCell(tableView: tableView,sectionNumber: indexPath.section)
}
else {
let cell = getTableViewCell(tableView: tableView, indexPath: indexPath)
cell.layoutIfNeeded()
return cell
}
}
}
Each section header will stick to the top until that section has some rows to display. Once you scroll all the rows for the section up. section header will be replaced by the next section header.
Here you can use one of the two solutions.
Instead of table view section header. Put your view on top of UITableView.
You can use only one section and combine all the rows in it.
From how you are describing your setup, I tend to believe that what you are looking for is the tableHeaderView of the tableView, not the section header.
See either this question, or official documentation for more info.
If that does not meet your requirements, you might wanna consider a custom view on top of the tableView, as is described here.
If the first section must always be sticky when any section is displaying, I will consider to put a custom view on top of tableView. Use autoLayout or UIStackView to let it work as a table header view.
So every time I scroll my tableView it reloads data which I find ridiculous since it makes no sense to reload data as it hasn't been changed.
So I setup my tableView as follows:
func numberOfSections(in tableView: UITableView) -> Int {
return self.numberOfElements
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 6
}
My cells are really custom and they require spacing between them. I couldn't add an extra View to my cell to fake that spacing because I have corner radius and it just ruins it. So I had to make each row = a section and set the spacing as a section height.
My cell has a dynamic height and can change it's height when I click "more" button, so the cell extends a little.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if self.segmentedControl.selectedSegmentIndex == 0 {
if self.isCellSelectedAt[indexPath.section] {
return self.fullCellHeight
} else {
return self.shortCellHeight
}
} else {
return 148
}
}
And here's how I setup my cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if self.segmentedControl.selectedSegmentIndex == 0 {
cell = tableView.dequeueReusableCell(withIdentifier: String.className(CurrentDocCell.self)) as! CurrentDocCell
(cell as! CurrentDocCell).delegate = self
(cell as! CurrentDocCell).ID = indexPath.section
} else {
cell = tableView.dequeueReusableCell(withIdentifier: String.className(PromissoryDocCell.self)) as! PromissoryDocCell
}
return cell
}
So I have a segmentedControl by switching which I can present either one cell of a certain height or the other one which is expandable.
In my viewDidLoad I have only these settings for tableView:
self.tableView.registerCellNib(CurrentDocCell.self)
self.tableView.registerCellNib(PromissoryDocCell.self)
And to expand the cell I have this delegate method:
func showDetails(at ID: Int) {
self.tableView.beginUpdates()
self.isCellSelectedAt[ID] = !self.isCellSelectedAt[ID]
self.tableView.endUpdates()
}
I set a breakpoint at cellForRowAt tableView method and it indeed gets called every time I scroll my tableView.
Any ideas? I feel like doing another approach to make cell spacing might fix this issue.
A UITableView only loads that part of its datasource which gets currently displayed. This dramatically increases the performance of the tableview, especially if the datasource contains thousands of records.
So it is the normal behaviour to reload the needed parts of the datasource when you scroll.
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.