I was wondering how one might go about creating a grouped UITableView with sections that have a drop shadow and rounded corners. Also, the section header is selectable, so when the user clicks the header the cells in that section should collapse; therefore, the shadow and rounded corners will need to change to the new size of the section, just the header. Does anybody know how to do what I am talking about? I have attached a UI mockup for reference as well as some code.
Here is a UI Mockup of what I wish to design:
This is the code I have to set up the tableView:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
return nil
} else if settings[section - 1].isEmpty == false {
let headerView = Bundle.main.loadNibNamed("HeaderTableViewCell", owner: self, options: nil)?.first as! HeaderTableViewCell
let settingCategories = ["Account", "Notifications", "Personalized Feed", "Color Theme"]
headerView.settingCategoryTitle.text = settingCategories[section - 1]
let totalCellHeight = CGFloat(settings[section - 1].count * 63)
let height = headerView.frame.height + totalCellHeight
print("Needed Height: \(height)")
headerView.clipsToBounds = false
headerView.headerBackground.frame.size.height = height
print("HeaderView Height: \(headerView.headerBackground.frame.size.height)")
headerView.headerBackground.layer.shadowRadius = 10
headerView.headerBackground.layer.shadowOffset = CGSize(width: 0, height: 1)
headerView.headerBackground.layer.shadowColor = UIColor.black.cgColor
headerView.headerBackground.layer.shadowOpacity = 0.1
headerView.headerBackground.backgroundColor = UIColor(named: "Background")
headerView.headerBackground.layer.cornerRadius = 10
headerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDropDown)))
return headerView
} else {
return nil
}
}
#objc func handleDropDown() {
print("Handle Drop Down")
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
return 0.0001
} else {
return 54
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
if section == 0 {
return 10
} else {
return 20
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return settings.count + 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
} else {
return settings[section - 1].count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingTitleCell", for: indexPath) as! SettingsTitleTableViewCell
cell.titleLabel.text = "Settings"
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingCell", for: indexPath) as! SettingTableViewCell
cell.setSetting(setting: settings[indexPath.section - 1][indexPath.row])
print("Index Path: \(indexPath.row) SettingCount: \(settings[indexPath.section - 1].count)")
return cell
}
}
That code produces this result:
I would love some help on how to make this work the way I have shown in the design. Thanks ahead of time for any help.
For collapse your header you can use this: https://github.com/jeantimex/ios-swift-collapsible-table-section
i used nested tableview to resolve it uibezierpath may be render incorrect layouts.
Related
I'm trying to make a filter. I got a tableview. When I click any section, it expands and collapses.
My problem is that when I open and close other sections after clicking on the checkboxes, unselected checkboxes in other sections appear as selected and selected ones are unselected. What should I do? Can you show me some code? Thanks!
https://ibb.co/0htP7Hz // Filter image
var hiddenSections = Set<Int>()
var filtersArray = Set<String>()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "FilterCell", for: indexPath) as? FilterTableViewCell else
{
fatalError("Product Group Cell not found")
}
guard let item = self.filterElementListVM.itemfilterviewmodelAtIndex(indexPath) else {
return UITableViewCell()
}
cell.setupCell(title: item.definition ?? "", buttonTag: item.id ?? 0, filterArray: self.filtersArray)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? FilterTableViewCell {
let selectedFilterItem = self.filterElementListVM.itemfilterviewmodelAtIndex(indexPath)
if cell.buttonCheck.isSelected {
self.filtersArray.remove(String(selectedFilterItem?.definition ?? ""))
} else {
self.filtersArray.insert(String(selectedFilterItem?.definition ?? ""))
}
cell.buttonCheckTap()
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let viewHeader = UIView.init(frame: CGRect.init(x: 0.0, y: 0.0, width: tableView.frame.size.width, height: 67.0))
viewHeader.backgroundColor = .white
let filterVM : FilterViewModel = self.filterElementListVM.filterViewModelAtIndex(section)
let viewFilterHeader : ViewFilter = ViewFilter.init(title: filterVM.definition,
rightImage: UIImage.init(named: "arrow_down")!, isPropertiesChanged: false, isArrowHidden: false)
viewFilterHeader.tag = section
let tap = UITapGestureRecognizer(target: self, action: #selector(hideSection(_:)))
viewFilterHeader.addGestureRecognizer(tap)
viewHeader.addSubview(viewFilterHeader)
viewFilterHeader.snp.makeConstraints { (make) in
make.top.equalTo(7.0)
make.bottom.equalTo(0.0)
make.leading.trailing.equalTo(0)
}
return viewHeader
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 67.0
}
#objc private func hideSection(_ sender: UITapGestureRecognizer? = nil) {
guard let section = sender?.view?.tag else { return }
func indexPathsForSection() -> [IndexPath] {
var indexPaths = [IndexPath]()
for row in 0..<self.filterElementListVM.numberOfRowsInSection(section) {
indexPaths.append(IndexPath(row: row,
section: section))
}
return indexPaths
}
if self.hiddenSections.contains(section) {
self.hiddenSections.remove(section)
self.tableviewFilter.insertRows(at: indexPathsForSection(),
with: .fade)
} else {
self.hiddenSections.insert(section)
self.tableviewFilter.deleteRows(at: indexPathsForSection(),
with: .fade)
}
}
Looks like your configuration works wrong and when you toggle your cell old data applied to your cell. So you need to clear all your data in prepareFroReuse() method inside your UIColelctionViewCell class
More information: https://developer.apple.com/documentation/uikit/uitableviewcell/1623223-prepareforreuse
So I am trying to set up my UITableViewController that has a headerview, footerview, and then multiple prototype cells in between them. When I go to run my code on my simulator, only the hearderview and footerview are displaying and the prototype cells are not. Here is my code:
import UIKit
class UploadTrackTableViewController: UITableViewController {
var headerView: UploadHeader!
var footerView: UploadFooter!
override func viewDidLoad() {
super.viewDidLoad()
headerView = tableView.tableHeaderView as! UploadHeader
footerView = tableView.tableFooterView as! UploadFooter
}
}
extension UploadTrackTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
} else if section == 1 {
return 1
} else if section == 2 {
return 1
} else {
return 1
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "trackTitleCell", for: indexPath) as! TrackTitleTableViewCell
return cell
} else if indexPath.section == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "genreCell", for: indexPath) as! SelectGenreTableViewCell
return cell
} else if indexPath.section == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "featuredArtistCell", for: indexPath) as! AddFeaturedArtistTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "audioCell", for: indexPath) as! SelectAudioFileTableViewCell
return cell
}
}
}
And here is a screenshot of how my controller looks on interface builder.
And here is a screenshot of the simulator.
Not sure if setting this up as a tableview is the best option maybe I should go for a scrollview with this UI but I am not sure what would work best.
here is what the view controller looks like now.
You can use only 1 section and UITableView has delegate functions of
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return 'your custom view'
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return 'your custom view'
}
You can return you own custom view of header and footer here.
Happy coding. :)
I have a tableview with 3 collapsible sections. users can only select rows in section 3 and when they select it goes green. However, when this section is collapsed all the selections are forgotten and when I re-open the section, usually the first row is always green (though it shouldn't be). Sometimes, other sections end up being green too when they shouldn't - not sure what I've got wrong?
// Number of table sections
func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
// Set the number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (self.expandedSectionHeaderNumber == section) {
// If markscheme, create the markscheme format
if (section == 2)
{
return self.markschemeRows.count
}
else
{
let arrayOfItems = self.sectionItems[section] as! NSArray
return arrayOfItems.count
}
} else {
return 0;
}
}
// Set titles for sections
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if (self.sectionNames.count != 0) {
return self.sectionNames[section] as? String
}
return ""
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44.0;
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat{
return 0;
}
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
//recast your view as a UITableViewHeaderFooterView
let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView
header.contentView.backgroundColor = UIColor.darkGray
header.textLabel?.textColor = UIColor.white
if let viewWithTag = self.view.viewWithTag(kHeaderSectionTag + section) {
viewWithTag.removeFromSuperview()
}
let headerFrame = self.view.frame.size
let theImageView = UIImageView(frame: CGRect(x: headerFrame.width - 32, y: 13, width: 18, height: 18));
theImageView.image = UIImage(named: "Chevron-Dn-Wht")
theImageView.tag = kHeaderSectionTag + section
header.addSubview(theImageView)
// make headers touchable
header.tag = section
let headerTapGesture = UITapGestureRecognizer()
headerTapGesture.addTarget(self, action: #selector(CaseViewController.sectionHeaderWasTouched(_:)))
header.addGestureRecognizer(headerTapGesture)
}
// Load the table data
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! CustomTableCell
let section = self.sectionItems[indexPath.section] as! NSArray
cell.textLabel?.textColor = UIColor.black
cell.selectionStyle = .none
//cell.backgroundColor = .white
// Get the data from different arrays depending on the section
if indexPath.section == 2 {
cell.textData?.text = markschemeRows[indexPath.row]
} else {
cell.textData?.text = section[indexPath.row] as! String
}
return cell
}
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if indexPath.section == 0{
if indexPath.row == 0{
return nil
}
}
else if indexPath.section == 1{
if indexPath.row == 0{
return nil
}
}
return indexPath
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
let cell = tableView.cellForRow(at: indexPath)
cell?.backgroundColor = .green
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let cell = tableView.cellForRow(at: indexPath)
if (cell?.backgroundColor == .green)
{
cell?.backgroundColor = .white
}
}
// MARK: - Expand / Collapse Methods
#objc func sectionHeaderWasTouched(_ sender: UITapGestureRecognizer) {
let headerView = sender.view as! UITableViewHeaderFooterView
let section = headerView.tag
let eImageView = headerView.viewWithTag(kHeaderSectionTag + section) as? UIImageView
if (self.expandedSectionHeaderNumber == -1) {
self.expandedSectionHeaderNumber = section
tableViewExpandSection(section, imageView: eImageView!)
} else {
if (self.expandedSectionHeaderNumber == section) {
tableViewCollapeSection(section, imageView: eImageView!)
} else {
let cImageView = self.view.viewWithTag(kHeaderSectionTag + self.expandedSectionHeaderNumber) as? UIImageView
tableViewCollapeSection(self.expandedSectionHeaderNumber, imageView: cImageView!)
tableViewExpandSection(section, imageView: eImageView!)
}
}
}
func tableViewCollapeSection(_ section: Int, imageView: UIImageView) {
let sectionData = self.sectionItems[section] as! NSArray
self.expandedSectionHeaderNumber = -1;
if (sectionData.count == 0) {
return;
} else {
UIView.animate(withDuration: 0.4, animations: {
imageView.transform = CGAffineTransform(rotationAngle: (0.0 * CGFloat(Double.pi)) / 180.0)
})
var indexesPath = [IndexPath]()
// If markscheme, different number needed
if (section == 2)
{
for i in 0 ..< markschemeRows.count {
let index = IndexPath(row: i, section: section)
indexesPath.append(index)
}
}
else
{
for i in 0 ..< sectionData.count {
let index = IndexPath(row: i, section: section)
indexesPath.append(index)
}
}
self.tableView!.beginUpdates()
self.tableView!.deleteRows(at: indexesPath, with: UITableView.RowAnimation.fade)
self.tableView!.endUpdates()
}
}
func tableViewExpandSection(_ section: Int, imageView: UIImageView) {
let sectionData = self.sectionItems[section] as! NSArray
if (sectionData.count == 0) {
self.expandedSectionHeaderNumber = -1;
return;
} else {
UIView.animate(withDuration: 0.4, animations: {
imageView.transform = CGAffineTransform(rotationAngle: (180.0 * CGFloat(Double.pi)) / 180.0)
})
var indexesPath = [IndexPath]()
// If markscheme, create the markscheme format
if (section == 2)
{
for i in 0 ..< markschemeRows.count {
let index = IndexPath(row: i, section: section)
indexesPath.append(index)
}
}
else
{
for i in 0 ..< sectionData.count {
let index = IndexPath(row: i, section: section)
indexesPath.append(index)
}
}
self.expandedSectionHeaderNumber = section
self.tableView!.beginUpdates()
self.tableView!.insertRows(at: indexesPath, with: UITableView.RowAnimation.fade)
self.tableView!.endUpdates()
}
}
The key thing to understand here is that the Cells are Reused when you say
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! CustomTableCell
dequeueReusableCell basically reuses a previously loaded UITableViewCell and in your case you changed the background color of the cell to green
To get a better understanding of the concept consider reading some articles like this one on Reusing Cells
Changes in Code
What you should do considering the above in mind
var backgroundColors = [UIColor](repeating: UIColor.white, count: 10)
you have to save the state of the colors in a model (ideally you should make a custom struct)
now in cellForRowAt add this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! CustomTableCell
.
.
cell.backgroundColor = backgroundColors[indexPath.row]
// **EDIT**
let cellShouldBeSelected = backgroundColors[indexPath.row] == .green
if cellShouldBeSelected {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
.
.
return cell
}
And your didSelectRowAt and didDeselectRowAt should update the model
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.backgroundColors[indexPath.row] = .green
tableView.reloadRows(at: [indexPath], with: .none)
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if (self.backgroundColors[indexPath.row] == .green) {
self.backgroundColors[indexPath.row] = .white
tableView.reloadRows(at: [indexPath], with: .none)
}
}
On second thought
Solution 2 (Recommended)
From you comments, i see you only need one selected cell at one time, assuming that keeping an array of backgroundColors is just a bad idea.
declare a int for the selected index
// -1 representing nothing is selected in the beginning
var selectedRow = -1
now your cellForRowAt will look like
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! CustomTableCell
.
.
if indexPath.section == 2, indexPath.row == self.selectedRow {
cell.backgroundColor = .green
} else {
cell.backgroundColor = .white
}
.
.
return cell
}
And your didSelectRowAt
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.section == 2 {
self.selectedRow = indexPath.row
tableView.reloadSections([2], with: .automatic)
}
}
And now you can remove didDeselectRowAt completely
I have a performance issue when HeaderCell becomes visible, it takes up to 40% of CPU on iPhone X and I see lags. This UITableView displays static data (TV guide). I see lags every time when I scroll up and down, "tableView.dequeueReusableCell()" doesn't help. If I use my ExpandableHeaderView() to create HeaderCell for supporting tapping and expanding section, I have the same issue. If expand section and scrolls inside elements, I do not see lags. Performance problem occurs only when HeaderCell appears on the screen. When user opens View Controller, he sees 3 programs for each channel (current and next two), if he taps on section name, he sees all programs for section (channel). HeaderCell contains only Label. I display the same data in the same way on my Android app and it works without any problems on simple Android device.
func numberOfSections(in tableView: UITableView) -> Int {
return Channels.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Channels[section].Programs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier)! as! TVGuideTableViewCell
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
let text = Channels[indexPath.section].Programs[indexPath.row].Name
let dateString = dateFormatter.string(from:Channels[indexPath.section].Programs[indexPath.row].StartDate!)
cell.label.text = text
cell.timeLabel.text = dateString
if (Channels[indexPath.section].Programs[indexPath.row].EndDate! < currentDate) {
cell.label.textColor = UIColor.gray
} else {
cell.label.textColor = UIColor.white
}
if (Channels[indexPath.section].Programs[indexPath.row].StartDate! < currentDate) {
cell.bell.isHidden = true
} else {
cell.bell.isHidden = false
}
if (Channels[indexPath.section].Programs[indexPath.row].EndDate! < currentDate) {
Channels[indexPath.section].Programs[indexPath.row].expandedRow = false
} else {
if Channels[indexPath.section].expandedCount < 3 {
Channels[indexPath.section].expandedCount += 1
Channels[indexPath.section].Programs[indexPath.row].expandedRow = true
}
}
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifierHeader) as! CustomHeaderTableViewCell
headerCell.headerLabel.text = Channels[section].Name
return headerCell
// let header = ExpandableHeaderView()
// header.customInit(title: Channels[section].Name, section: section, delegate: self as ExpandableHeaderViewDelegate)
// return header
}
func toggleSection(header: ExpandableHeaderView, section: Int) {
Channels[section].expanded = !Channels[section].expanded
Channels[section].expandedCount = 0
tableView.beginUpdates()
for i in 0 ..< Channels[section].Programs.count {
tableView.reloadRows(at: [IndexPath(row: i, section: section)], with: .automatic)
}
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 38
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if(Channels[indexPath.section].expanded) {
return 38
} else {
if (Channels[indexPath.section].Programs[indexPath.row].EndDate! < currentDate) {
return 0
} else {
if Channels[indexPath.section].Programs[indexPath.row].expandedRow {
return 38.0
} else {
return 0.0
}
}
}
}
class CustomHeaderTableViewCell: UITableViewCell {
#IBOutlet weak var headerLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
From your code, what I can see that the problem is not HeaderCell, the problem is DateFormatter object. Because everytime you scroll and cell appears a new object is created and just imagine that if there are 10 - 15 cells at a time then 10 - 15 reused / created and as you scroll or reloadData it again create a new object.
So, my advise is to create the DateFormatter object outside cellforRow and then just use it to get to get a dateString by calling it.
This is just one assumption, try this and project still appears do let me know!
i have this code for my tableview in tvOS project in swift 3, i want color cell when scroll it not focus! please can yon explain how do it?
this my code
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tvChannelsArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
// Show TV Channels names
cell.textLabel?.text = ((tvChannelsArray[(indexPath as NSIndexPath).row] as AnyObject).object(forKey: "channel") as! String)
cell.imageView?.image = UIImage(named: "\(cell.textLabel!.text!)")
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
channelsTableView.isHidden = false;
return 100
}
In your tableview delegate implement the UISsrollViewDelegate method ScrollViewDidScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
if let currentCellPath = tableView.indexPath(for: cell),
let selectedCellPath = tableView.indexPathForSelectedRow {
guard currentCellPath != selectedCellPath else {
continue
}
}
let red = Float(cell.frame.origin.y / scrollView.contentSize.height)
cell.contentView.backgroundColor = UIColor(colorLiteralRed: red, green: 0.5, blue: 0.25, alpha: 1)
}
}