I'm building an app which presents departures of busses. I use a tableview to represent it, the name of the busstop is set as the section header, and each row in a section represents a departure from that busstop.
The API always provide me with the next 20 departures for each stop, but initially I just show the next 6 departures in each section, I keep all the 20 in my datasource though. At the end of each section I have a cells which is supposed to double the shown departures in each section. It's done it this way:
tableView.beginUpdates()
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
tableView.endUpdates()
My initial thought was that I could just double the returnvalue of the numberOfRowsInSection-function for the specific section, but that doesn't seem to work.
var scale = [Int : Int]()
func numberOfSections(in tableView: UITableView) -> Int {
if stops.count != nil {
return stops.count
} else {
return 1
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stops[indexPath.row].departures.count - scale[stops[section].id]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if stops != nil {
let currentDeparture = stops[(indexPath as NSIndexPath).section].departures![(indexPath as NSIndexPath).row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DepartureTableViewCell
// Configuration of the cell
return cell
}
}
return UITableViewCell()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == (stops[indexPath.row].departures.count)! {
var currentScale = scale[stops[indexPath.section].id]
scale[stops[indexPath.section].id] = currentScale - 6
tableView.beginUpdates()
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
tableView.endUpdates()
}
}
The dictionary scale is just mapping the ID of the stop to the number of departures that should be shown, starting at 14 (20-6) and each time the cell that is supposed to reload the section is tapped, it get reduced by 6. So we get 6 more departures in the given section. currentDepartureresInSection is the number of departures at the specific stop.
It is possible to do it this way, or do I have to update the datasource of the tableview?
Related
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.
Is there a way to prevent cells in a tableView from being moved to a different section?
The sections have data for different types of cells, so the app crashes when the user tries to drag a cell into a different section.
I would like to only allow the user to move a cell inside the section, and not in between sections.
Relevant code is below:
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let reorderedRow = self.sections[sourceIndexPath.section].rows.remove(at: sourceIndexPath.row)
self.sections[destinationIndexPath.section].rows.insert(reorderedRow, at: destinationIndexPath.row)
self.sortedSections.insert(sourceIndexPath.section)
self.sortedSections.insert(destinationIndexPath.section)
}
You will need to implement the UITableViewDelegate method targetIndexPathForMoveFromRowAt.
Your strategy will be to allow the move if the source and destination section are the same. If they aren't then you can return either row 0, if the proposed destination section is less than the source section or the last row of the section if the proposed destination section is greater than the source section.
This will constrain the move to the source section.
override func tableview(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
let sourceSection = sourceIndexPath.section
let destSection = proposedDestinationIndexPath.section
if destSection < sourceSection {
return IndexPath(row: 0, section: sourceSection)
} else if destSection > sourceSection {
return IndexPath(row: self.tableView(tableView, numberOfRowsInSection:sourceSection)-1, section: sourceSection)
}
return proposedDestinationIndexPath
}
You can retarget the proposed destination for restriction by implementing the tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: method
func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
// Finds number of items in source group
let numberOfItems = self.tableView(tableView, numberOfRowsInSection: sourceIndexPath.section)
// Restricts rows to relocation in their own group by checking source and destination sections
if (sourceIndexPath.section != proposedDestinationIndexPath.section) {
/*
if we move the row to the not allowed upper area, it is moved to the top of the allowed group and vice versa
if we move the row to the not allowed lower area, it is moved to the bottom of the allowed group
also prevents moves to the last row of a group (which is reserved for the add-item placeholder).
*/
let rowInSourceSection = (sourceIndexPath.section > proposedDestinationIndexPath.section) ? 0 : numberOfItems - 1;
return IndexPath(row: rowInSourceSection, section: sourceIndexPath.section)
}
// Prevents moves to the last row of a group (which is reserved for the add-item placeholder).
else if (proposedDestinationIndexPath.row >= numberOfItems) {
return IndexPath(row: numberOfItems - 1, section: sourceIndexPath.section)
}
// Passing all restrictions
return proposedDestinationIndexPath
}
I have a table view that keeps track of hotel receipts (each time the user enters the date, cost, and name of the hotel and hits the add button it adds a new row to the table with the information.
I have 2 text fields below the table that I want to show the total entries (number of rows) and the sum of the cost field. Problem is I can't figure out how to do this or if it is even possible.
I have found a couple posts about it but they all seem to have a set number of rows.
extension HotelViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
let numberOfSections = frc.sections?.count
return numberOfSections!
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberOfRows = frc.sections?[section].numberOfObjects
return numberOfRows!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HotelCell", for: indexPath) as! HotelTableViewCell
let item = frc.object(at: indexPath) as! DriveAwayHotel
cell.date.text = item.date
cell.name.text = item.name
cell.cost.text = "$\(item.cost ?? 0.00)"
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let managedObject : NSManagedObject = frc.object(at: indexPath) as! NSManagedObject
pc.delete(managedObject)
do {
try pc.save()
} catch {
print(error)
return
}
}
You should not try to do math on your cells. You should do math on your model.
Are you trying to sum all the entries in your table, or just the cells that are visible?
If you want to sum all the entries in your table view, then loop through all the sections in your frc.sections array, loop through all the entries in each section, and add them all up. (That code would be easy to write.)
If you only want to sum the entries for the currently visible cells, call the table view's indexPathsForVisibleRows method to get an array of the indexPaths of the cells that are visible, loop through those indexPaths, fetch the entry at each section and row, and add those together. (That code would also be easy to write.)
EDIT:
The code to sum all your entries might look something like the below (I'm having to guess a bit since I don't know your data model)
var total = 0.0
guard let sections = frc.sections?.count else { return }
for section in 0..<sections {
guard let rows = frc.sections?[section].numberOfObjects else { continue }
for row in 0..<rows {
let indexPath = IndexPath(row: row, section: section)
let item = frc.object(at: indexPath) as! DriveAwayHotel
total += item.cost
}
}
I have an table view with three cell which contains the label and one image (check box).Now when ever i select any cell.That particular cell image (check box) alone needs to get tick.png. And remaining two cell image should be untick.png.
But now if i select first cell then the first cel image get as tick.png.Then if i select second and third cell.That cell image also getting tick.png
But i need only one image alone needs to tick.png.Which ever table view cell i am selecting that particular cell image alone needs to be tick.png.And remaining two cell image should be untick.png.
My code :
var Data: [String] = ["First","Second","Third"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.Data.count > 0{
return self.Data.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewCell
cell.Lbl.text = self.aData[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! suggestionCell
cell.suggestionImg.image = UIImage(named: "tick")
}
If I understand you correctly you only want a single check mark at any given time. If this is true then you would simply setup a property in your view controller like this:
var checkedRow: Int
and set the row index in tableView(_:didSelectRowAt:). By setting it to -1 you would disable all check marks. Then in tableView(_:, cellForRowAt:) you would conditionally enable the check mark for the cell if indexPath.row is equal to checkedRow:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
checkedRow = indexPath.row
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewCell
if indexPath.row == checkedRow {
cell.suggestionImg.image = UIImage(named: "tick.png")
cell. suggestionLbl.text = "<ticked text>"
} else {
cell.suggestionImg.image = UIImage(named "untick.png")
cell. suggestionLbl.text = "<unticked text>"
}
return cell
}
As an add-on to Tom's answer, I suggest storing IndexPath instead of Int adding also a
var lastCheckedRow:IndexPath = IndexPath(row: 0, section: 0)
This allows you to only reload the newly checked row and the previously checked row instead of the whole table view plus it will support multiple sections too. It does not matter much at your current stage where there is only 3 rows but for larger table views this will be more efficient. Also it removes the blinking effect of UITableView.reloadData().
The code is something like:
//0 based on assumption that first cell is checked by default
var checkedRow:IndexPath = IndexPath(row: 0, section: 0)
var lastCheckedRow:IndexPath = IndexPath(row: 0, section: 0)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Update checkedRow for reload but keep track of current tick
lastCheckedRow = checkedRow
checkedRow = indexPath
//Remove previous tick
tableView.reloadRows(at: [lastCheckedRow], with: .automatic)
//Update new tick
tableView.reloadRows(at: [checkedRow], with: .automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewCell
if indexPath.row == checkedRow {
cell.suggestionImg.image = UIImage(named: "tick.png")
} else {
cell.suggestionImg.image = UIImage(named "untick.png")
}
return cell
}
You can also play around to create an ideal visual effect when ticking different cell by changing the with:UITableViewRowAnimation parameter which I use .automatic for the example.
allowsMultipleSelection: Is only easiest thing that will help you.
Add following line in your viewDidLoad after setting up the tableView
override func viewDidLoad() {
// ... setup you need
tableView.allowsMultipleSelection = false
}
Hope this helps!
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.