I have a tableview need to be updated very second. The code are as following. I design the headerview to have a dropdown function, when the header tap the rest are displayed. The code will crashes when I am trying to tap the header, the thread stops, xcode is not giving any hint on how and why.
func didListOfBLEDevicesUpdate(newDevice: BLEDevice)
{
println("receivedDevice from scanner every second: \(newDevice.deviceName)")
self.deviceTableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return BLEDevice.listOfDevices.items.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1;
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return BLEDevice.listOfDevices.items[section].deviceName
}
func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 1
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if(IsExpandedMode[indexPath.section] == true){
return 400
}
return 70;
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: CGRectMake(0, 0, tableView.frame.size.width, 40))
headerView.backgroundColor = UIColor.grayColor()
headerView.tag = section
let headerString = UILabel(frame: CGRect(x: 10, y: 10, width: tableView.frame.size.width-10, height: 30)) as UILabel
headerString.text = BLEDevice.listOfDevices.items[section].deviceName
headerView .addSubview(headerString)
let headerTapped = UITapGestureRecognizer (target: self, action:"sectionHeaderTapped:")
headerView .addGestureRecognizer(headerTapped)
return headerView
}
func sectionHeaderTapped(recognizer: UITapGestureRecognizer) {
println("Tapping working")
println(recognizer.view?.tag)
var indexPath : NSIndexPath = NSIndexPath(forRow: 0, inSection:(recognizer.view?.tag as Int!)!)
if (indexPath.row == 0) {
var collapsed = self.IsExpandedMode [indexPath.section]
collapsed = !collapsed;
self.IsExpandedMode[indexPath.section] = collapsed
//reload specific section animated
var range = NSMakeRange(indexPath.section, 1)
var sectionToReload = NSIndexSet(indexesInRange: range)
self.deviceTableView.reloadSections(sectionToReload, withRowAnimation:UITableViewRowAnimation.Fade)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : DeviceTableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! DeviceTableViewCell
let row = indexPath.row
cell.deviceName!.text = BLEDevice.listOfDevices.items[row].deviceName
cell.connectionStatus.text = BLEDevice.listOfDevices.items[row].connectionStatus
cell.deviceSignalStrengthen.text = BLEDevice.listOfDevices.items[row].RSSI
cell.manufacturerData.text = BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataManufacturerData
cell.serviceUUID.text = BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataServiceUUIDs
cell.serviceData.text = DataConvertHelper.getNSDictionary(BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataServiceData)
cell.TxPowerLevel.text = BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataTxPowerLevel
cell.IsConnectable.text = DataConvertHelper.getBool(BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataIsConnectable)
cell.solicitedServiceUUID.text = BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataSolicitedServiceUUIDs
cell.shortenedLocalName.text = BLEDevice.listOfDevices.items[row].advertisementPackage.cBAdvertisementDataLocalName
return cell
}
Use reload sections and reload rows rather than reloading data
The method you have used to handle the table seems to be rather complex. An alternative would be as follows:
1) Assumption from you code is that each device is associated with a section. As noted in the comments, your cellForRorAtIndexPath method seems to be using [row] to index your data model, but the model is based on [section] as you always return the number of rows as 1 for every section and the number of sections is the number of devices.
2) Rather than using a header view for each section and having to add gesture recognizers, simply create a custom cell to represent the device and make this row 0 of the section.
3) So each device is associated with a section, and row 0 of each section is the header information cell, NOT a header view. Make the header view nil. You can use a header height to leave a gap between sections.
4) Add code to detect selection of cells. When the cell row is 0, its the header cell. If the device is collapsed, set it to be expanded and vice versa and reload the section.
5) Make a new custom cell for you dropdown information. this will be row 1 of any section which is showing information.
6) Update your number of rows in section to return 2 if expanded, or 1 if collapsed.
7) Update cellForRowAtIndexPath to return the header cell for row 0 and the detail cell for row 1. Make sure to fix the [row] indexing to be [section] indexing.
This gives you a table of device header cells, which when clicked insert a detail cell below and when clicked again remove it and no gesture recognizers needed.
You need to make sure that your data model updates are working correctly. Seems from your errors that you are not updating the data model properly: in particular removal of devices.
Related
I have two tableviews inside my stack view. I am resizing them depending on the amount of data that is retrieved from Firestore. The issue I am facing is whilst the tableview is resize the top table view "ingredientsTV" shows all the data where as the "instructionsTV" only shows some of the data. My array.count displays the correct number of items in the array but them items are not getting displayed.
//Code for resize tableviews
override func viewWillLayoutSubviews() {
super.updateViewConstraints()
self.ingredientsTVHeight?.constant = self.ingredientsTV.contentSize.height
self.instructionsTVHeight.constant = self.instructionsTV.contentSize.height
self.ingredientsTV.contentInset = UIEdgeInsets(top: 0, left: -20, bottom: 0, right: 0)
self.instructionsTV.contentInset = UIEdgeInsets(top: 0, left: -20, bottom: 0, right: 0)
}
//setupview, called in viewdidload
//MARK: Functions
private func setupView() {
ingredientsTV.delegate = self
ingredientsTV.dataSource = self
instructionsTV.delegate = self
instructionsTV.dataSource = self
recipeImage.layer.cornerRadius = 5
recipeNameLbl.text = recipe.name
prepTimeLbl.text = recipe.prepTime
cookTimeLbl.text = recipe.cookTime
servesLabel.text = recipe.serves
if let url = URL(string: recipe.imageUrl) {
recipeImage.kf.setImage(with: url)
recipeImage.layer.cornerRadius = 5
}
}
//MARK: Tableview functions
extension PocketChefRecipeDetailsVC {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (tableView == self.ingredientsTV) {
return recipe.ingredients.count
}else {
return recipe.method.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (tableView == self.ingredientsTV) {
let cell = ingredientsTV.dequeueReusableCell(withIdentifier: "ingredientsCell", for: indexPath) as? ingredientsCell
cell?.ingredientsLbl.text = recipe.ingredients[indexPath.row]
return cell!
}else {
let cellB = instructionsTV.dequeueReusableCell(withIdentifier: "instructionsCell", for: indexPath) as? InstructionsCell
cellB?.instructionsLbl.text = recipe.method[indexPath.row]
return cellB!
}
}
}
*Recipe data is getting passed from previous view controller
I'm going to take a shot in the dark and say that maybe your stack view needs to be re laid out after you reload data.
Make sure to call
// after ingredientsTV.reloadData() and instructionsTV.reloadData() gets called
stackview.setNeedsLayout()
You can try adding a fixed height to each row and see if it has any populated data or not. Add this to your extension
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
Additionally, you can also set the background color of the cell in cellForRowAt() just to ensure the rows are visible.
Note: Do check if your stack view's constraints are set for all 4 sides.
I have a requirement. Where I have to get the list of students and then I have to show their subjects in which they are enrolled in.
Example
Now you can see below I have list of students i.e Student1, student2, and so on. and each student have different number of subjects
What I have done So far:
I have created a Custom cell that Contains a Label and Empty vertical stackview.
Then in method tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) I am running the for loop that makes some UiLabel dynamically and adds them into the vertical stackview
Problem: By doing this I am getting what I want. But when I scroll up and down the for loop repeats data in the cell again and again on each scroll up/down
Please help if there is anyother way of doing that.
You can use tableview with section.
Set student name in section
Set your subjects in cell
This is sample of tableview with section.
https://blog.apoorvmote.com/uitableview-with-multiple-sections-ios-swift/
Here is the sample code it is just for your reference.
class TableViewController: UITableViewController {
let section = ["pizza", "deep dish pizza", "calzone"]
let items = [["Margarita", "BBQ Chicken", "Pepperoni"], ["sausage", "meat lovers", "veggie lovers"], ["sausage", "chicken pesto", "prawns", "mushrooms"]]
// MARK: - Table view data source
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.section\[section\]
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return self.section.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.items\[section\].count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath)
// Configure the cell...
cell.textLabel?.text = self.items[indexPath.section][indexPath.row]
return cell
}
Update
Customised section view
Create your custom view and show your view as section
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = UIView(frame: CGRect(x:0, y:0, width:tableView.frame.size.width, height:18))
let label = UILabel(frame: CGRect(x:10, y:5, width:tableView.frame.size.width, height:18))
label.font = UIFont.systemFont(ofSize: 14)
label.text = "This is a test";
view.addSubview(label);
view.backgroundColor = UIColor.gray;
return view
}
Sample code for Customised section
Update 2
Custom header with reference of cell
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! CustomHeaderCell
headerCell.backgroundColor = UIColor.cyanColor()
switch (section) {
case 0:
headerCell.headerLabel.text = "Student Name 1";
//return sectionHeaderView
case 1:
headerCell.headerLabel.text = "Student Name 2";
//return sectionHeaderView
case 2:
headerCell.headerLabel.text = "Student Name 3";
//return sectionHeaderView
default:
headerCell.headerLabel.text = "Other";
}
return headerCell
}
You can try a different approach. Make a list with number of sections = number of students. Each section should have number of rows equal to the subjects for that student. This can be achieved easily by making a student model with subjects array as it's property.
class Student: NSObject {
var subjectsArray : [String] = []
}
First add this extension to your source code.
extension UIStackView{
func removeAllArrangedSubviews() {
let removedSubviews = arrangedSubviews.reduce([]) { (allSubviews, subview) -> [UIView] in
removeArrangedSubview(subview)
return allSubviews + [subview]
}
removedSubviews.forEach({ $0.removeFromSuperview() })
}
}
Now override this method in your Custom cell class. it will remove all child views from stackview before reuse.
override func prepareForReuse() {
super.prepareForReuse()
self.stackView.removeAllArrangedSubviews()
}
I am using CollapsibleTableView from here and modified it as per my requirement to achieve collapsible sections. Here is how it looks now.
Since there is a border for my section as per the UI design, I had chosen the section header to be my UI element that holds data in both collapsed and expanded modes.
Reason: I tried but couldn't get it working in this model explained below -
** Have my header elements in section header and details of each item in its cell. By default, the section is in collapsed state. When user taps on the header, the cell is toggled to display. As I said, since there is a border that needs to be shown to the whole section (tapped header and its cell), I chose section header to be my UI element of operation. Here is my code for tableView -
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections.count
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return sections[indexPath.section].collapsed! ? 0 : (100.0 + heightOfLabel2!)
case 1:
return 0
case 2:
return 0
default:
return 0
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier("header") as! CollapsibleTableViewHeader
if sections.count == 0 {
self.tableView.userInteractionEnabled = false
header.cornerRadiusView.layer.borderWidth = 0.0
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
header.amountLabel.hidden = true
header.titleLabel.text = "No_Vouchers".localized()
}
else {
header.amountLabel.hidden = false
header.cornerRadiusView.layer.borderWidth = 1.0
self.tableView.userInteractionEnabled = true
header.titleLabel.text = sections[section].name
header.arrowImage.image = UIImage(named: "voucherDownArrow")
header.setCollapsed(sections[section].collapsed)
let stringRepresentation = sections[section].items.joinWithSeparator(", ")
header.benefitDetailText1.text = stringRepresentation
header.benefitDetailText2.text = sections[section].shortDesc
header.benefitDetailText3.text = sections[section].untilDate
header.section = section
header.delegate = self
if sections[section].collapsed == true {
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
else {
if sections[section].isNearExpiration == true {
header.benefitAlertImage.hidden = false
header.benefitAlertText.hidden = false
}
else {
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
}
if appLanguageDefault == "nl" {
self.totalAmountLabel.text = "€ \(sections[section].totalAvailableBudget)"
}
else {
self.totalAmountLabel.text = "\(sections[section].totalAvailableBudget) €"
}
}
return header
}
Function to toggle collapse/expand -
I am using height values of the "dynamically changing" UILabels inside the section and then using those values to extend the border (using its layoutconstraint).
func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
let collapsed = !sections[section].collapsed
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
// Toggle collapse
sections[section].collapsed = collapsed
header.setCollapsed(collapsed)
// Toggle Alert Labels show and hide
if sections[section].collapsed == true {
header.cornerRadiusViewBtmConstraint.constant = 0.0
header.cornerRadiusViewTopConstraint.constant = 20.0
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
else {
heightOfLabel2 = header.benefitDetailText2.bounds.size.height
if sections[section].isNearExpiration == true {
header.benefitAlertImage.hidden = false
header.benefitAlertText.hidden = false
header.cornerRadiusViewBtmConstraint.constant = -100.0 - heightOfLabel2!
header.cornerRadiusViewTopConstraint.constant = 10.0
if let noOfDays = sections[section].daysUntilExpiration {
if appLanguageDefault == "nl" {
header.benefitAlertText.text = "(nog \(noOfDays) dagen geldig)"
}
else {
header.benefitAlertText.text = "(valable encore \(noOfDays) jour(s))"
}
}
}
else {
header.cornerRadiusViewBtmConstraint.constant = -80.0 - heightOfLabel2!
header.cornerRadiusViewTopConstraint.constant = 20.0
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
}
// Adjust the height of the rows inside the section
tableView.beginUpdates()
for i in 0 ..< sections.count {
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic)
}
tableView.endUpdates()
}
The problem:
I need to have, few section headers in this table view to be expanded by default on the first launch of the view, based on some conditions. As I am calculating the height of the labels and using the heights to set for the border's top and bottom constraint, it has become difficult to show the expanded section header as per design.
The content comes out of the border since the height of my UILabel is being taken as 21 by default.
UPDATE: The row height changes only after I scroll through the view or when I toggle between collapse/expand
The Question:
How do I calculate the heights of the UILabels present in my Section header by the first time launch of the view? (That means, after my REST call is done, data is fetched and then I need to get the UIlabel height).
Currently, I am using heightOfLabel2 = header.benefitDetailText2.bounds.size.height
(Or)
Is there a better way to achieve this?
Thanks in advance!
Here's what I got working based on my understanding of the overall goals of OP. If I'm misunderstanding, the following is still a working example. Full working project is also linked below.
Goals:
Dynamically sized TableViewCells that are also
Collapsable to show/hide additional details
I tried a number of different ways, this is the only one that I could get working.
Overview
Design makes use of the following:
custom TableViewCells
Autolayout
TableView Automatic Dimension
So if you're not familiar with those (especially Autolayout, might want to review that first.
Dynamic TableViewCells
Interface Builder
Lay out your a prototype cell. It's easiest to increase the row height size. Start simply with just a few elements to make sure you can get it working. (even though adding into Autolayout can be a pain). For example, simply stack two labels vertically, full width of the layout. Make the top label 1 line for the "title" and the second 0 lines for the "details"
Important: To configure Labels and Text Areas to grow to the size of their content, you must set Labels to have 0 lines and Text Areas to not be scrollable. Those are the triggers for fit to contents.
The most important thing is making sure there is a constraint for all four sides of every element. This is essential to get the Automatic Dimensioning working.
CollapsableCell
Next we make a very basic custom class for that table cell prototype. Connect the labels to outlets in the custom cell class. Ex:
class CollapsableCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var detailLabel: UILabel!
}
Starting simply with two labels is easiest.
Also make sure that in Interface Builder you set the prototype cell class to CollapsableCell and you give it a reuse ID.
CollapsableCellViewController
On to the ViewController. First the standard things for custom TableViewCells:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "collapsableCell", for: indexPath) as! CollapsableCell
let item = data[indexPath.row]
cell.titleLabel?.text = item.title
cell.detailLabel?.text = item.detail
return cell
}
We've added functions to return the number of rows and to return a cell for a given Row using our custom Cell. Hopefully all straightforward.
Now normally there would be one more function, TableView(heightForRowAt:), that would be required, but don't add that (or take it out if you have it). This is where Auto Dimension comes in. Add the following to viewDidLoad:
override func viewDidLoad() {
...
// settings for dynamic resizing table cells
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 50
...
}
At this point if you set up the detail label to be 0 lines as described above and run the project, you should get cells of different sizes based on the amount of text you're putting in that label. That Dynamic TableViewCells done.
Collapsable Cells
To add collapse/expand functionality, we can just build off the dynamic sizing we have working at this point. Add the following function to the ViewController:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CollapsableCell else { return }
let item = data[indexPath.row]
// update fields
cell.detailLabel.text = self.isExpanded[indexPath.row] ? item.detail1 : ""
// update table
tableView.beginUpdates()
tableView.endUpdates()
// toggle hidden status
isExpanded[indexPath.row] = !isExpanded[indexPath.row]
}
Also add 'var isExpanded = Bool' to your ViewController to store the current expanded status for your rows (This could also be class variable in your custom TableViewCell).
Build and click on one of the rows, it should shrink down to only show the title label. And that's the basics.
Sample Project:
A working sample project with a few more fields and a disclosure chevron image is available at github. This also includes a separate view with a demo of a Stackview dynamically resizing based on content.
A Few Notes:
This is all done in normal TableViewCells. I know the OP was using header cells, and while I can't think of a reason why that wouldn't work the same way, there's no need to do it that way.
Adding and removing a subView is the method I originally thought would work best and be most efficient since a view could be loaded from a nib, and even stored ready to be re-added. For some reason I couldn't get this to resize after the subViews were added. I can't think of a reason it wouldn't work, but here is a solution that does.
If I understood your question correctly, what you want to do is to resize your tableHeaderView when you call toggleSection.
Therefore what you need to do for your tableHeaderView to resize is this
// get the headerView
let headerView = self.tableView(self.tableView, viewForHeaderInSection: someSection)
// tell the view that it needs to refresh its layout
headerView?.setNeedsDisplay()
// reload the tableView
tableView.reloadData()
/* or */
// beginUpdates, endUpdates
Basically what you would do is to place the above code snippet inside your function toggleSection(header: CollapsibleTableViewHeader, section: Int)
func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
...
// I'm not sure if this `header` variable is the headerView so I'll just add my code snippet at the bottom
header.setNeedsDisplay()
/* code snippet start */
// get the headerView
let headerView = self.tableView(self.tableView, viewForHeaderInSection: someSection)
// tell the view that it needs to refresh its layout
headerView?.setNeedsDisplay()
/* code snippet end */
// reload the tableView
// Adjust the height of the rows inside the section
tableView.beginUpdates()
// You do not need this
for i in 0 ..< sections.count {
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic)
}
// You do not need this
tableView.endUpdates()
}
Explanation: A tableView's headerView/footerView does not update its layout even if you call reloadData() and beginUpdates,endUpdates. You need to tell the view that it needs to update first and then you refresh the tableView
Finally you also need to apply these two codes
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
return estimatedHeight
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
In this method,
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return sections[indexPath.section].collapsed! ? 0 : (100.0 + heightOfLabel2!)
case 1:
return 0
case 2:
return 0
default:
return 0
}
}
instead of using heightOfLabel2, try implementing the following method to calculate heights specific to each cell(since we know the text to be filled, its font and label width, we can calculate the height of label),
func getHeightForBenefitDetailText2ForIndexPath(indexPath: NSIndexPath)->CGFloat
So your method should look like this,
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return sections[indexPath.section].collapsed! ? 0 : (100.0 + getHeightForBenefitDetailText2ForIndexPath(indexPath))
case 1:
return 0
case 2:
return 0
default:
return 0
}
}
And regarding your problem to expand few cells by for the very first time, make sure you set the collapsed property to true for those cells before reloading the table.
As a performance improvement, you can store the height value calculated for each expanded cell in a dictionary and return the value from the dictionary, to avoid the same calculation again and again.
Hope this helps you. If not, do share a sample project for more insight about your problem.
You can try this for String extension to calculate bounding rect
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height
}
}
Source: Figure out size of UILabel based on String in Swift
I am using CollapsibleTableView from here and modified it as per my requirement to achieve collapsible sections. Here is how it looks now.
Since there is a border for my section as per the UI design, I had chosen the section header to be my UI element that holds data in both collapsed and expanded modes.
Reason: I tried but couldn't get it working in this model explained below -
** Have my header elements in section header and details of each item in its cell. By default, the section is in collapsed state. When user taps on the header, the cell is toggled to display. As I said, since there is a border that needs to be shown to the whole section (tapped header and its cell), I chose section header to be my UI element of operation. Here is my code for tableView -
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections.count
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return sections[indexPath.section].collapsed! ? 0 : (100.0 + heightOfLabel2!)
case 1:
return 0
case 2:
return 0
default:
return 0
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier("header") as! CollapsibleTableViewHeader
if sections.count == 0 {
self.tableView.userInteractionEnabled = false
header.cornerRadiusView.layer.borderWidth = 0.0
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
header.amountLabel.hidden = true
header.titleLabel.text = "No_Vouchers".localized()
}
else {
header.amountLabel.hidden = false
header.cornerRadiusView.layer.borderWidth = 1.0
self.tableView.userInteractionEnabled = true
header.titleLabel.text = sections[section].name
header.arrowImage.image = UIImage(named: "voucherDownArrow")
header.setCollapsed(sections[section].collapsed)
let stringRepresentation = sections[section].items.joinWithSeparator(", ")
header.benefitDetailText1.text = stringRepresentation
header.benefitDetailText2.text = sections[section].shortDesc
header.benefitDetailText3.text = sections[section].untilDate
header.section = section
header.delegate = self
if sections[section].collapsed == true {
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
else {
if sections[section].isNearExpiration == true {
header.benefitAlertImage.hidden = false
header.benefitAlertText.hidden = false
}
else {
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
}
if appLanguageDefault == "nl" {
self.totalAmountLabel.text = "€ \(sections[section].totalAvailableBudget)"
}
else {
self.totalAmountLabel.text = "\(sections[section].totalAvailableBudget) €"
}
}
return header
}
Function to toggle collapse/expand -
I am using height values of the "dynamically changing" UILabels inside the section and then using those values to extend the border (using its layoutconstraint).
func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
let collapsed = !sections[section].collapsed
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
// Toggle collapse
sections[section].collapsed = collapsed
header.setCollapsed(collapsed)
// Toggle Alert Labels show and hide
if sections[section].collapsed == true {
header.cornerRadiusViewBtmConstraint.constant = 0.0
header.cornerRadiusViewTopConstraint.constant = 20.0
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
else {
heightOfLabel2 = header.benefitDetailText2.bounds.size.height
if sections[section].isNearExpiration == true {
header.benefitAlertImage.hidden = false
header.benefitAlertText.hidden = false
header.cornerRadiusViewBtmConstraint.constant = -100.0 - heightOfLabel2!
header.cornerRadiusViewTopConstraint.constant = 10.0
if let noOfDays = sections[section].daysUntilExpiration {
if appLanguageDefault == "nl" {
header.benefitAlertText.text = "(nog \(noOfDays) dagen geldig)"
}
else {
header.benefitAlertText.text = "(valable encore \(noOfDays) jour(s))"
}
}
}
else {
header.cornerRadiusViewBtmConstraint.constant = -80.0 - heightOfLabel2!
header.cornerRadiusViewTopConstraint.constant = 20.0
header.benefitAlertImage.hidden = true
header.benefitAlertText.hidden = true
}
}
// Adjust the height of the rows inside the section
tableView.beginUpdates()
for i in 0 ..< sections.count {
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic)
}
tableView.endUpdates()
}
The problem:
I need to have, few section headers in this table view to be expanded by default on the first launch of the view, based on some conditions. As I am calculating the height of the labels and using the heights to set for the border's top and bottom constraint, it has become difficult to show the expanded section header as per design.
The content comes out of the border since the height of my UILabel is being taken as 21 by default.
UPDATE: The row height changes only after I scroll through the view or when I toggle between collapse/expand
The Question:
How do I calculate the heights of the UILabels present in my Section header by the first time launch of the view? (That means, after my REST call is done, data is fetched and then I need to get the UIlabel height).
Currently, I am using heightOfLabel2 = header.benefitDetailText2.bounds.size.height
(Or)
Is there a better way to achieve this?
Thanks in advance!
Here's what I got working based on my understanding of the overall goals of OP. If I'm misunderstanding, the following is still a working example. Full working project is also linked below.
Goals:
Dynamically sized TableViewCells that are also
Collapsable to show/hide additional details
I tried a number of different ways, this is the only one that I could get working.
Overview
Design makes use of the following:
custom TableViewCells
Autolayout
TableView Automatic Dimension
So if you're not familiar with those (especially Autolayout, might want to review that first.
Dynamic TableViewCells
Interface Builder
Lay out your a prototype cell. It's easiest to increase the row height size. Start simply with just a few elements to make sure you can get it working. (even though adding into Autolayout can be a pain). For example, simply stack two labels vertically, full width of the layout. Make the top label 1 line for the "title" and the second 0 lines for the "details"
Important: To configure Labels and Text Areas to grow to the size of their content, you must set Labels to have 0 lines and Text Areas to not be scrollable. Those are the triggers for fit to contents.
The most important thing is making sure there is a constraint for all four sides of every element. This is essential to get the Automatic Dimensioning working.
CollapsableCell
Next we make a very basic custom class for that table cell prototype. Connect the labels to outlets in the custom cell class. Ex:
class CollapsableCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var detailLabel: UILabel!
}
Starting simply with two labels is easiest.
Also make sure that in Interface Builder you set the prototype cell class to CollapsableCell and you give it a reuse ID.
CollapsableCellViewController
On to the ViewController. First the standard things for custom TableViewCells:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "collapsableCell", for: indexPath) as! CollapsableCell
let item = data[indexPath.row]
cell.titleLabel?.text = item.title
cell.detailLabel?.text = item.detail
return cell
}
We've added functions to return the number of rows and to return a cell for a given Row using our custom Cell. Hopefully all straightforward.
Now normally there would be one more function, TableView(heightForRowAt:), that would be required, but don't add that (or take it out if you have it). This is where Auto Dimension comes in. Add the following to viewDidLoad:
override func viewDidLoad() {
...
// settings for dynamic resizing table cells
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 50
...
}
At this point if you set up the detail label to be 0 lines as described above and run the project, you should get cells of different sizes based on the amount of text you're putting in that label. That Dynamic TableViewCells done.
Collapsable Cells
To add collapse/expand functionality, we can just build off the dynamic sizing we have working at this point. Add the following function to the ViewController:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CollapsableCell else { return }
let item = data[indexPath.row]
// update fields
cell.detailLabel.text = self.isExpanded[indexPath.row] ? item.detail1 : ""
// update table
tableView.beginUpdates()
tableView.endUpdates()
// toggle hidden status
isExpanded[indexPath.row] = !isExpanded[indexPath.row]
}
Also add 'var isExpanded = Bool' to your ViewController to store the current expanded status for your rows (This could also be class variable in your custom TableViewCell).
Build and click on one of the rows, it should shrink down to only show the title label. And that's the basics.
Sample Project:
A working sample project with a few more fields and a disclosure chevron image is available at github. This also includes a separate view with a demo of a Stackview dynamically resizing based on content.
A Few Notes:
This is all done in normal TableViewCells. I know the OP was using header cells, and while I can't think of a reason why that wouldn't work the same way, there's no need to do it that way.
Adding and removing a subView is the method I originally thought would work best and be most efficient since a view could be loaded from a nib, and even stored ready to be re-added. For some reason I couldn't get this to resize after the subViews were added. I can't think of a reason it wouldn't work, but here is a solution that does.
If I understood your question correctly, what you want to do is to resize your tableHeaderView when you call toggleSection.
Therefore what you need to do for your tableHeaderView to resize is this
// get the headerView
let headerView = self.tableView(self.tableView, viewForHeaderInSection: someSection)
// tell the view that it needs to refresh its layout
headerView?.setNeedsDisplay()
// reload the tableView
tableView.reloadData()
/* or */
// beginUpdates, endUpdates
Basically what you would do is to place the above code snippet inside your function toggleSection(header: CollapsibleTableViewHeader, section: Int)
func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
...
// I'm not sure if this `header` variable is the headerView so I'll just add my code snippet at the bottom
header.setNeedsDisplay()
/* code snippet start */
// get the headerView
let headerView = self.tableView(self.tableView, viewForHeaderInSection: someSection)
// tell the view that it needs to refresh its layout
headerView?.setNeedsDisplay()
/* code snippet end */
// reload the tableView
// Adjust the height of the rows inside the section
tableView.beginUpdates()
// You do not need this
for i in 0 ..< sections.count {
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic)
}
// You do not need this
tableView.endUpdates()
}
Explanation: A tableView's headerView/footerView does not update its layout even if you call reloadData() and beginUpdates,endUpdates. You need to tell the view that it needs to update first and then you refresh the tableView
Finally you also need to apply these two codes
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
return estimatedHeight
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
In this method,
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return sections[indexPath.section].collapsed! ? 0 : (100.0 + heightOfLabel2!)
case 1:
return 0
case 2:
return 0
default:
return 0
}
}
instead of using heightOfLabel2, try implementing the following method to calculate heights specific to each cell(since we know the text to be filled, its font and label width, we can calculate the height of label),
func getHeightForBenefitDetailText2ForIndexPath(indexPath: NSIndexPath)->CGFloat
So your method should look like this,
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 0:
return sections[indexPath.section].collapsed! ? 0 : (100.0 + getHeightForBenefitDetailText2ForIndexPath(indexPath))
case 1:
return 0
case 2:
return 0
default:
return 0
}
}
And regarding your problem to expand few cells by for the very first time, make sure you set the collapsed property to true for those cells before reloading the table.
As a performance improvement, you can store the height value calculated for each expanded cell in a dictionary and return the value from the dictionary, to avoid the same calculation again and again.
Hope this helps you. If not, do share a sample project for more insight about your problem.
You can try this for String extension to calculate bounding rect
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height
}
}
Source: Figure out size of UILabel based on String in Swift
I'm having trouble adding rows to the UITableView upon UIButton click.
I have two custom-cell xibs - one that contains an UILabel, another one that contains an UIButton.
Data for the table cell is loaded from two dictionaries (answersmain and linesmain).
Here is the code for the UITableView main functions:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.linesmain["Audi"]!.count + 1
}
// 3
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row < 3){
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = linesmain["Audi"]![indexPath.row]
return cell
} else {
var celle:vwAnswers = self.tableView.dequeueReusableCellWithIdentifier("cell2") as! vwAnswers
celle.Answer.setTitle(answersmain["Good car"]![0], forState:UIControlState.Normal)
return celle
}}
What do I put here?
#IBAction func option1(sender: UIButton) {
// I need to add rows to the uitableview from two dictionaries into two different xibs
}
You can do the next:
var showingAll = false
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return showingAll ? self.linesmain["Audi"]!.count + 1 : 0
}
#IBAction func option1(sender: UIButton) {
showingAll = true
tableView.beginUpdates()
let insertedIndexPathRange = 0..<self.linesmain["Audi"]!.count + 1
var insertedIndexPaths = insertedIndexPathRange.map { NSIndexPath(forRow: $0, inSection: 0) }
tableView.insertRowsAtIndexPaths(insertedIndexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
}
You should take a look over the documentation here
There is this UITableView method called insertRowsAtIndexPaths:withRowAnimation: that inserts row at a specified indexPath.
You need to modify linesmain and answersmain by adding data to these and then call [self.tableView reloadData].
It would be better if you extract linesmain["Audi"] and answersmain["Good car"] and save them into different mutable arrays and modify those.
You need to do this in the func option1.