Swift - Data not displayed properly when using tableView.dequeueReusableCell - ios

When certain buttons are pressed in the app, their name, start and end time that they were pressed are displayed in a UITableView.
This worked fine when using a custom UITableViewCell but after setting up tableView.dequeueReusableCell instead, the UITableView is showing the first cell as a white empty cell when it is meant to show data. If more data is added, the first input which wasn't visible is now shown but the last input is missing/hidden.
I have found similar questions and implemented what seemed the main culprit but it didn't work for me.
timelineTableView.contentInsetAdjustmentBehavior = .never
timelineScrollViewContainer.contentInsetAdjustmentBehavior = .never
timelineTableView.contentOffset = .zero
I also tried to change the section height but to no avail either.
Worth mentioning that the data is not displaying properly in the UITableView but is still saving properly in the plist.
The UITableView is loaded during the ViewDidLoad as mentioned in other questions as it seems the issue of the error for some.
Doeanyonene have another solution? thanks for the help
cellForRowAt method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == timelineTableView {
//let cell = TimelineCell(frame: CGRect(x: 0, y: 0, width: 100, height: 40), title: "test", startTime: "test", endTime: "test", rawStart: "") // Used previously before using dequeueReusableCell
var cell: TimelineCell = tableView.dequeueReusableCell(withIdentifier: timelineCellId, for: indexPath) as! TimelineCell
if let marker = markUpPlist.arrayObjects.filter({$0.UUIDpic == endClipSelectedMarkerUUID}).first {
cell = TimelineCell(frame: CGRect(x: 0, y: 0, width: 100, height: 40), title: "test", startTime: "test", endTime: "test", rawStart: "")
cell.backgroundColor = marker.colour
cell.cellLabelTitle.text = marker.name
cell.cellUUID.text = marker.UUIDpic
if let timeline = chronData.rows.filter({$0.rowName == marker.name}).first {
if let start = timeline.clips.last?.str {
cell.cellStartTime.text = chronTimeEdited(time: Double(start))
cell.cellStartRaw.text = String(start)
}
if let end = timeline.clips.last?.end {
cell.cellEndTime.text = chronTimeEdited(time: Double(end))
}
}
}
return cell
}
TimelineCell.swift
class TimelineCell : UITableViewCell {
var cellLabelTitle: UILabel!
var cellStartTime: UILabel!
var cellEndTime: UILabel!
var cellStartRaw: UILabel!
var cellUUID: UILabel!
init(frame: CGRect, title: String , startTime: String, endTime: String, rawStart: String) {
super.init(style: UITableViewCell.CellStyle.default, reuseIdentifier: "timelineCellId")
backgroundColor = UIColor(red: 29/255.0, green: 30/255.0, blue: 33/255.0, alpha: 1.0)
cellLabelTitle = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
cellLabelTitle.translatesAutoresizingMaskIntoConstraints = false
cellLabelTitle.textColor = UIColor.black
addSubview(cellLabelTitle)
cellLabelTitle.widthAnchor.constraint(equalToConstant: 80).isActive = true
cellLabelTitle.heightAnchor.constraint(equalToConstant: 30).isActive = true
cellLabelTitle.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0).isActive = true
cellLabelTitle.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
cellStartTime = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
cellStartTime.translatesAutoresizingMaskIntoConstraints = false
cellStartTime.textColor = UIColor.black
addSubview(cellStartTime)
cellStartTime.widthAnchor.constraint(equalToConstant: 80).isActive = true
cellStartTime.heightAnchor.constraint(equalToConstant: 30).isActive = true
cellStartTime.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0).isActive = true
cellStartTime.leftAnchor.constraint(equalTo: cellLabelTitle.rightAnchor, constant: 10).isActive = true
cellEndTime = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
cellEndTime.translatesAutoresizingMaskIntoConstraints = false
cellEndTime.textColor = UIColor.black
addSubview(cellEndTime)
cellEndTime.widthAnchor.constraint(equalToConstant: 80).isActive = true
cellEndTime.heightAnchor.constraint(equalToConstant: 30).isActive = true
cellEndTime.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0).isActive = true
cellEndTime.leftAnchor.constraint(equalTo: cellStartTime.rightAnchor, constant: 10).isActive = true
cellStartRaw = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
cellUUID = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
addSubview(cellStartRaw)
addSubview(cellUUID)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
}
Create tableview
timelineTableView.frame = CGRect(x: 0, y: 0, width: sideView.frame.width, height: sideView.frame.size.height)
timelineTableView.delegate = self
timelineTableView.dataSource = self
timelineTableView.register(TimelineCell.self, forCellReuseIdentifier: timelineCellId)
timelineTableView.translatesAutoresizingMaskIntoConstraints = false
timelineTableView.separatorStyle = .none
timelineTableView.backgroundColor = Style.BackgroundColor
timelineTableView.contentInsetAdjustmentBehavior = .never
timelineScrollViewContainer.contentInsetAdjustmentBehavior = .never
timelineScrollViewContainer.addSubview(timelineTableView)
timelineTableView.contentOffset = .zero
So recapitulating, using the line below shows the data properly but the cells aren't reused properly.
let cell = TimelineCell(frame: CGRect(x: 0, y: 0, width: 100, height: 40), title: "test", startTime: "test", endTime: "test", rawStart: "")
Using the code below show a blank cell first and data not displayed properly but the cells are reused properly.
var cell: TimelineCell = tableView.dequeueReusableCell(withIdentifier: timelineCellId, for: indexPath) as! TimelineCell

Change cellForRowAt to look like this. I'm guessing as to how your chronData structure relates to the source table, so it's mostly using your original logic.
You need to blank out fields that should be empty, as otherwise they will retain state as you scroll.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: TimelineCell = tableView.dequeueReusableCell(withIdentifier: timelineCellId, for: indexPath) as! TimelineCell
let marker = markUpPListFiltered
cell.backgroundColor = marker.colour
cell.cellLabelTitle.text = marker.name
cell.cellUUID.text = marker.UUIDpic
if let timeline = chronData.rows.filter({$0.rowName == marker.name}).first {
if let start = timeline.clips.last?.str {
cell.cellStartTime.text = chronTimeEdited(time: Double(start))
cell.cellStartRaw.text = String(start)
}
else
{
cell.cellStartTime.text = ""
cell.cellStartRaw.text = ""
}
if let end = timeline.clips.last?.end {
cell.cellEndTime.text = chronTimeEdited(time: Double(end))
}
else
{
cell.cellEndTime.text = ""
}
}
return cell
}
It assumes there is an array for your filtered sorted data called markupPListFiltered. Prepare this in viewDidLoad or somewhere else. You haven't shown the other datasource methods so I'll assume you can change these as needed, e.g.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return markupPListFiltered.count
}
TimelineCell needs the c'tor to have the data removed. You should consider using a storyboard to design your cells and link up the widgets with the outlets (see any of the hundreds of tutorials on table views, I'd recommend Ray Wenderlich as a good starter source).

Related

Tableview disappears when scrolling

I have a tableView that displays hidden cells when the user scrolls. Not sure why this behavior is happening.
In viewDidLoad()
watchListTable = UITableView(frame: CGRect(x: self.view.frame.width * 0.25, y: 0, width: self.view.frame.width * 0.75, height: 300)) //height = 200
watchListTable.isHidden = true
watchListTableFrame = CGRect(x: self.view.frame.width * 0.25, y: 0, width: self.view.frame.width * 0.75, height: 300)
watchListTableFrameHide = CGRect(x: self.view.frame.width * 0.25, y: 0, width: self.view.frame.width * 0.75, height: 0)
watchListTable.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
watchListTable.register(UITableViewCell.self, forCellReuseIdentifier: "closeCell")
watchListTable.dataSource = self
watchListTable.delegate = self
watchListTable.CheckInterfaceStyle()
watchListTable.roundCorners(corners: .allCorners, radius: 8)
watchListTable.backgroundColor = .systemGray6
//remove the bottom line if there is only one option
watchListTable.tableFooterView = UIView()
view.addSubview(watchListTable)
Once the user taps on a button, the table expands in an animatable fashion.
//watchlist won't animate properly on the initial setup. So we set it to be
hidden, then change the frame to be 0, unhide it, and then animate it. Only will
be hidden on the initial setup.
if(watchListTable.isHidden == true)
{
watchListTable.isHidden = false
watchListTable.frame = watchListTableFrameHide
}
UIView().animateDropDown(dropDown: watchListTable, frames:
self.watchListTableFrame)
watchListTable.reloadData()
In func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if(indexPath.row >= watchListStocks.count)
{
let cell = tableView.dequeueReusableCell(withIdentifier: "closeCell",
for: indexPath as IndexPath)
cell.selectionStyle = .none
cell.textLabel?.text = indexPath.row == watchListStocks.count + 1 ?
"Close List" : "Create New Watchlist"
cell.textLabel?.textColor = .stockOrbitTeal
cell.textLabel?.textAlignment = .center
cell.backgroundColor = .systemGray6
cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right:
.greatestFiniteMagnitude)
return cell
}
else
{
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for:
indexPath as IndexPath)
cell.selectionStyle = .none
if(indexPath.row == 0)
{
cell.layer.cornerRadius = 8
cell.layer.maskedCorners = [.layerMinXMinYCorner,
.layerMaxXMinYCorner]
}
else
{
cell.layer.cornerRadius = 8
cell.layer.maskedCorners = [.layerMinXMaxYCorner,
.layerMaxXMaxYCorner]
cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right:
.greatestFiniteMagnitude)
cell.directionalLayoutMargins = .zero
}
let label = UITextView()
label.frame = CGRect(x: 0, y: 0, width: cell.frame.width * 0.45, height:
cell.frame.height)
label.text = watchListStocks[indexPath.row].listName
label.textColor = .stockOrbitTeal
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18, weight: UIFont.Weight.medium)
label.backgroundColor = .systemGray5
label.delegate = self
label.tag = indexPath.row
cell.addSubview(label)
cell.backgroundColor = .systemGray5
cell.layer.cornerRadius = 8
return cell
}
When I scroll, all cells are hidden. I see that they are created in cellForRowAt, however, they do not appear on my screen. Why are the cells being hidden? I have searched all over stackoverflow.
You shouldn't add subviews inside cellForRowAt. When you call dequeueReusableCell, at first it'll create new cells, but when you start scrolling it'll start returning cells that were dismissed earlier, means they already have UITextView subview, and you're adding one more on top of that.
cell returned by dequeueReusableCell doesn't have to have final size already, that's why you can't use cell.frame.width to calculate your subview size, I think that's may be the reason you can't see it.
What you need to do: create a UITableView subclass, something like this:
class MyCell: UITableViewCell {
let label = UITextView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupCell()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupCell()
}
func setupCell() {
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18, weight: UIFont.Weight.medium)
label.backgroundColor = .systemGray5
contentView.addSubview(label)
}
override func layoutSubviews() {
super.layoutSubviews()
label.frame = CGRect(x: 0, y: 0, width: contentView.frame.width * 0.45, height: contentView.frame.height)
}
}
Here you're adding a subview during initialisation only once and update label frame each time cell size gets changed. Don't forget to add this class to your cell in the storyboard and let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath) as! MyCell, so you can set delegate to text field, etc.
If this won't help, check out View Hierarchy to see what's actually going on there
So after many hours, I figured it out...
I had called this function in viewDidLoad()
watchListTable.roundCorners(corners: .allCorners, radius: 8)
Which made my table hidden after I scrolled. I removed this line of code, and the table is now completely visible when scrolling.

finding a tableView cells superView

I am trying to create a range slider that has labels representing the sliders handle value. I have the slider enabled but when I try to add the labels to the sliders subview, my app crashes with the error
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
The slider is inside of a tableViewCell and I am initializing this cell inside of the tableView VC with the code below,
if indexPath.section == 2 {
let costRangeCell = AgeRangeCell(style: .default, reuseIdentifier: nil)
let contentView = costRangeCell.rangeSlider.superview!
// my declaration of contentView is where my app is crashing.
costRangeCell.rangeSlider.minimumValue = 0
costRangeCell.rangeSlider.maximumValue = 100
costRangeCell.rangeSlider.lowValue = 0
costRangeCell.rangeSlider.highValue = 100
costRangeCell.rangeSlider.minimumDistance = 20
let lowLabel = UILabel()
contentView.addSubview(lowLabel)
lowLabel.textAlignment = .center
lowLabel.frame = CGRect(x:0, y:0, width: 60, height: 20)
let highLabel = UILabel()
contentView.addSubview(highLabel)
highLabel.textAlignment = .center
highLabel.frame = CGRect(x: 0, y: 0, width: 60, height: 20)
costRangeCell.rangeSlider.valuesChangedHandler = { [weak self] in
let lowCenterInSlider = CGPoint(x:costRangeCell.rangeSlider.lowCenter.x, y: costRangeCell.rangeSlider.lowCenter.y - 30)
let highCenterInSlider = CGPoint(x:costRangeCell.rangeSlider.highCenter.x, y: costRangeCell.rangeSlider.highCenter.y - 30)
let lowCenterInView = costRangeCell.rangeSlider.convert(lowCenterInSlider, to: contentView)
let highCenterInView = costRangeCell.rangeSlider.convert(highCenterInSlider, to: contentView)
lowLabel.center = lowCenterInView
highLabel.center = highCenterInView
lowLabel.text = String(format: "%.1f", costRangeCell.rangeSlider.lowValue)
highLabel.text = String(format: "%.1f", costRangeCell.rangeSlider.highValue)
}
costRangeCell.rangeSlider.addTarget(self, action: #selector(handleMinAgeChange), for: .valueChanged)
let minAge = user?.minSeekingCost ?? SettingsViewController.defaultMinSeekingCost
costRangeCell.rangeLabel.text = " $\(minAge)"
return costRangeCell
}
Is there a different way for me to gain access to the cells range slider superView?
ageRange class,
class AgeRangeCell: UITableViewCell {
let rangeSlider: AORangeSlider = {
let slider = AORangeSlider()
slider.minimumValue = 20
slider.maximumValue = 200
return slider
}()
let rangeLabel: UILabel = {
let label = costRangeLabel()
label.text = "$ "
return label
}()
class costRangeLabel: UILabel {
override var intrinsicContentSize: CGSize {
return .init(width: 80, height: 50)
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.isUserInteractionEnabled = true
let overallStackView = UIStackView(arrangedSubviews: [
UIStackView(arrangedSubviews: [rangeLabel, rangeLabel]),
])
overallStackView.axis = .horizontal
overallStackView.spacing = 16
addSubview(overallStackView)
overallStackView.anchor(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor, padding: .init(top: 16, left: 16, bottom: 16, right: 16))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
AORangeSlider is a custom Slider.
Took a look at AORangeSlider...
You want to implement your label tracking inside your custom cell... not in your controller class.
Here's a simple implementation, based on the code you supplied in your question:
class AgeRangeCell: UITableViewCell {
let rangeSlider: AORangeSlider = {
let slider = AORangeSlider()
slider.minimumValue = 0
slider.maximumValue = 100
return slider
}()
let lowLabel = UILabel()
let highLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
lowLabel.textAlignment = .center
lowLabel.frame = CGRect(x:0, y:0, width: 60, height: 20)
highLabel.textAlignment = .center
highLabel.frame = CGRect(x: 0, y: 0, width: 60, height: 20)
[rangeSlider, lowLabel, highLabel].forEach {
contentView.addSubview($0)
}
rangeSlider.translatesAutoresizingMaskIntoConstraints = false
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
rangeSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
rangeSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
rangeSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
rangeSlider.heightAnchor.constraint(equalToConstant: 40.0),
])
// avoid auto-layout complaints
let c = rangeSlider.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
rangeSlider.valuesChangedHandler = { [weak self] in
guard let `self` = self else {
return
}
let lowCenterInSlider = CGPoint(x:self.rangeSlider.lowCenter.x, y: self.rangeSlider.lowCenter.y - 30)
let highCenterInSlider = CGPoint(x:self.rangeSlider.highCenter.x, y: self.rangeSlider.highCenter.y - 30)
let lowCenterInView = self.rangeSlider.convert(lowCenterInSlider, to: self.contentView)
let highCenterInView = self.rangeSlider.convert(highCenterInSlider, to: self.contentView)
self.lowLabel.center = lowCenterInView
self.highLabel.center = highCenterInView
self.lowLabel.text = String(format: "%.1f", self.rangeSlider.lowValue)
self.highLabel.text = String(format: "%.1f", self.rangeSlider.highValue)
}
}
}
class RangeTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// register your "slider" cell
tableView.register(AgeRangeCell.self, forCellReuseIdentifier: "ageRangeCell")
// register any other cell classes you'll be using
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "plainCell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 2 {
return 1
}
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "ageRangeCell", for: indexPath) as! AgeRangeCell
cell.rangeSlider.minimumValue = 0
cell.rangeSlider.maximumValue = 100
cell.rangeSlider.lowValue = 0
cell.rangeSlider.highValue = 100
cell.rangeSlider.minimumDistance = 20
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: "plainCell", for: indexPath)
cell.textLabel?.text = "\(indexPath)"
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Section Header: \(section)"
}
}
That code will produce this:

My UITableView crashes when I add a gradient background?

I have a UITableView under a UIViewController with a custom cell. The text for the UILabel in each cell is held in an array of strings. I’m coding in Swift Playgrounds, and when I run the Playground with an empty array it works fine (there aren’t an cells, of course, but the playground does run). When I populate the array, I get the error “there as a problem running this page... check your code...”. When I step through the code, it gets stuck at the line:
view.addSubview(gradientView)
What am I doing wrong?
import UIKit
import PlaygroundSupport
class ViewController: UITableViewController {
// Array that holds menu items
var menuItems = ["option 1","option 2","option 3"]
override func viewDidLoad() {
super.viewDidLoad()
// Add a gradient background
// Set height, width to view height, width
var gradientView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
let gradientLayer:CAGradientLayer = CAGradientLayer()
gradientLayer.frame.size = gradientView.frame.size
// Set colors
gradientLayer.colors = [UIColor(red: 253/255, green: 94/255, blue: 172/255, alpha: 1).cgColor, UIColor(red: 121/255, green: 73/255, blue: 242/255, alpha: 1).cgColor]
// Skew gradient (diagonally)
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
// Rasterize to improve performance
gradientLayer.shouldRasterize = true
//Add gradient
gradientView.layer.addSublayer(gradientLayer)
view.addSubview(gradientView)
// Reguster custom cell
tableView.register(MenuItemCell.self, forCellReuseIdentifier: "cell_1")
// Turn off seperators
tableView.separatorStyle = .none
// Set header height
tableView.sectionHeaderHeight = 75
}
// Custom header
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?{
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 75))
customView.backgroundColor = .clear //UIColor(white: 0.9, alpha: 1)
let button = UIButton(type: .custom)
button.setTitle("😁", for: .normal)
button.frame = CGRect(x: 20, y: 20, width: 50, height: 50)
button.layer.cornerRadius = 25
button.layer.shadowRadius = 8.0
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 0, height: 0)
button.layer.shadowOpacity = 0.5
let blur = UIVisualEffectView(effect: UIBlurEffect(style:
UIBlurEffect.Style.light))
blur.frame = button.bounds
blur.isUserInteractionEnabled = false //This allows touches to forward to the button.
button.insertSubview(blur, at: 0)
customView.addSubview(button)
return customView
}
#objc func updateView(){
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menuItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell_1", for: indexPath) as! MenuItemCell
cell.selectionStyle = .none
//cell.messageLabel.text = textMessages[indexPath.row]
cell.bubbleBackgroundView.backgroundColor = UIColor(white: 0.9, alpha: 1)
return cell
}
}
class MenuItemCell: UITableViewCell {
let optionLabel = UILabel()
let bubbleBackgroundView = UIView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
bubbleBackgroundView.layer.shadowOpacity = 0.35
bubbleBackgroundView.layer.shadowRadius = 6
bubbleBackgroundView.layer.shadowOffset = CGSize(width: 0, height: 0)
bubbleBackgroundView.layer.shadowColor = UIColor.black.cgColor
bubbleBackgroundView.layer.cornerRadius = 25
bubbleBackgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(bubbleBackgroundView)
addSubview(optionLabel)
optionLabel.numberOfLines = 0
optionLabel.translatesAutoresizingMaskIntoConstraints = false
// lets set up some constraints for our label
let constraints = [optionLabel.topAnchor.constraint(equalTo: topAnchor, constant: 32),
optionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32),
optionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32),
optionLabel.widthAnchor.constraint(equalToConstant: 250),
bubbleBackgroundView.topAnchor.constraint(equalTo: optionLabel.topAnchor, constant: -16),
bubbleBackgroundView.leadingAnchor.constraint(equalTo: optionLabel.leadingAnchor, constant: -16),
bubbleBackgroundView.bottomAnchor.constraint(equalTo: optionLabel.bottomAnchor, constant: 16),
bubbleBackgroundView.trailingAnchor.constraint(equalTo: optionLabel.trailingAnchor, constant: 16),
]
NSLayoutConstraint.activate(constraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
PlaygroundPage.current.liveView = ViewController()

How to fix wrong indexPath returned by didSelectRowAt?

I have a UITableView; without a tableHeaderView tapping a row and triggering didSelectRowAt returns the correct index path.
When I set the tableHeaderView property, didSelectRowAt either does not fire or returns the tappedRow + 2. Where am I going wrong?
Here is my code
class MenuController: UIViewController {
// Mark -- Properties
var tableView: UITableView!
var delegate: HomeControllerDelegate?
var headerView: HeaderView? = nil
var user: User? = nil
// Mark -- Init
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
if let user = self.user {
populateMenuHeader(email: user.email, firstName: user.firstName, lastName: user.lastName, imageUrl: user.imageUrl)
}
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//updateHeaderViewHeight(for: tableView.tableHeaderView)
}
func updateHeaderViewHeight(for header: UIView?) {
guard let headerView = headerView else { return }
headerView.frame.size.height = 170
}
func populateMenuHeader(email: String, firstName: String, lastName: String, imageUrl: String) {
headerView?.emailLabel?.text = email
headerView?.nameLabel?.text = "\(firstName) \(lastName)"
let request = ImageRequest(
url: URL(string: imageUrl)!,
processors: [
ImageProcessor.Resize(size: CGSize(width: 70, height: 70)),
ImageProcessor.Circle()
]
)
Nuke.loadImage(with: request, into: headerView!.imageView!)
}
// Mark -- Handlers
func configureTableView() {
// Create Material Header
headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 170))
//headerView?.heightAnchor.constraint(equalToConstant: 170).isActive = true
headerView?.translatesAutoresizingMaskIntoConstraints = false
tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self
tableView.tableHeaderView = headerView
tableView.sectionHeaderHeight = 170
tableView.register(MenuOptionCell.self, forCellReuseIdentifier: reuseIdentifier)
tableView.backgroundColor = .darkGray
tableView.separatorStyle = .none
tableView.rowHeight = 80
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
}
extension MenuController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! MenuOptionCell
let menuOption = MenuOption(rawValue: indexPath.row)
cell.descriptionLabel.text = menuOption?.description
cell.iconImageView.image = menuOption?.image
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let row = indexPath.row
print("tapped row: \(row)")
let menuOption = MenuOption(rawValue: row)
delegate?.handleMenuToggle(forMenuOption: menuOption)
}
}
class CustomView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: 0, y: bounds.height))
context.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
context.strokePath()
}
}
}
class HeaderView : UIView {
var imageView: UIImageView? = nil
var nameLabel: UILabel? = nil
var emailLabel: UILabel? = nil
override init(frame: CGRect) {
super.init(frame: frame)
imageView = UIImageView()
imageView?.translatesAutoresizingMaskIntoConstraints = false
nameLabel = UILabel()
nameLabel?.translatesAutoresizingMaskIntoConstraints = false
nameLabel?.font = UIFont(name: "Avenir-Light", size: 20)
nameLabel?.text = "Test name"
nameLabel?.textColor = .white
emailLabel = UILabel()
emailLabel?.translatesAutoresizingMaskIntoConstraints = false
emailLabel?.textColor = .white
emailLabel?.font = UIFont(name: "Avenir-Light", size: 15)
emailLabel?.text = "testemail#gmail.com"
self.addSubview(imageView!)
self.addSubview(nameLabel!)
self.addSubview(emailLabel!)
let lineView = CustomView(frame: CGRect(x: 0, y: frame.height - 1, width: frame.width, height: 1))
self.addSubview(lineView)
imageView?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
imageView?.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true
imageView?.widthAnchor.constraint(equalTo: widthAnchor, constant: 70).isActive = true
imageView?.heightAnchor.constraint(equalTo: heightAnchor, constant: 70).isActive = true
nameLabel?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
nameLabel?.topAnchor.constraint(equalTo: imageView!.bottomAnchor, constant: 10).isActive = true
emailLabel?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
emailLabel?.topAnchor.constraint(equalTo: nameLabel!.bottomAnchor, constant: 5).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The problem appears to be due to the fact that you are not properly setting the height of the header view. The documentation for tableHeaderView states:
When assigning a view to this property, set the height of that view to a nonzero value. The table view respects only the height of your view's frame rectangle; it adjusts the width of your header view automatically to match the table view's width.
Update your header view code:
Change:
headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 170))
//headerView?.heightAnchor.constraint(equalToConstant: 170).isActive = true
headerView?.translatesAutoresizingMaskIntoConstraints = false
...
tableView.sectionHeaderHeight = 170
to just:
headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 170))
That's it. No need to mess with constraints. No need to set the unrelated section height. Just give the header view's frame the desired height.

Problems with complex UITableViewCell

I'm trying to implement a custom complex UITableViewCell. My data source is relatively simple, but I could have some multiple elements.
class Element: NSObject {
var id: String
var titles: [String]
var value: String
init(id: String, titles: [String], value: String) {
self.id = id
self.titles = titles
self.value = value
}
}
I have an array of elements [Element] and, as you can see, for each element titles could have multiple string values. I must use the following layouts:
My first approach was to implement a dynamic UITableViewCell, trying to add content inside self.contentView at runtime. Everything is working, but it's not so fine and as you can see, reusability is not handled in the right way. Lag is terrible.
import UIKit
class ElementTableCell: UITableViewCell {
var titles: [String]!
var value: String!
var width: CGFloat!
var titleViewWidth: CGFloat!
var cellHeight: Int!
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:)")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
}
func drawLayout() {
titleViewWidth = (width * 2)/3
cellHeight = 46 * titles.count
for i in 0 ..< titles.count {
let view = initTitleView(title: titles[i], width: titleViewWidth, yPosition: CGFloat(cellHeight * i))
self.contentView.addSubview(view)
}
self.contentView.addSubview(initButton())
}
func initTitleView(title: String, width: CGFloat, yPosition: CGFloat) -> UIView {
let titleView: UILabel = UILabel(frame:CGRect(x:0, y:Int(yPosition), width: Int(width), height: 45))
titleView.text = title
return titleView
}
func initButton(value: String) -> UIButton {
let button = UIButton(frame:CGRect(x: 0, y: 0, width: 70, height:34))
button.setTitle(value, for: .normal)
button.center.x = titleViewWidth + ((width * 1)/3)/2
button.center.y = CGFloat(cellHeight/2)
return priceButton
}
}
And the UITableView delegate method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ElementTableCell(style: .default, reuseIdentifier: "ElementTableCell")
cell.width = self.view.frame.size.width
cell.titles = elements[indexPath.row].titles
cel.value = elements[indexPath.row].value
cell.drawLayout()
return cell
}
Now I'm thinking about a total different approach, such as using a UITableView Section for each element in elements array and a UITableViewCell for each title in titles. It could work, but I'm concerned about the right button.
Do you have any suggestion or other approach to share?
I solved changing application UI logic in order to overcome the problem. Thank you all.
Here's some code you can play with. It should work just be creating a new UITableView in a Storyboard and assigning it to BoxedTableViewController in this file...
//
// BoxedTableViewController.swift
//
import UIKit
class BoxedCell: UITableViewCell {
var theStackView: UIStackView!
var containingView: UIView!
var theButton: UIButton!
var brdColor = UIColor(white: 0.7, alpha: 1.0)
// "spacer" view is just a 1-pt tall UIView used as a horizontal-line between labels
// when there is more than one title label
func getSpacer() -> UIView {
let newView = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 1))
newView.backgroundColor = brdColor
newView.translatesAutoresizingMaskIntoConstraints = false
newView.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
return newView
}
// "label view" is a UIView containing on UILabel
// embedding the label in a view allows for convenient borders and insets
func getLabelView(text: String, position: Int) -> UIView {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
let newLabel = UILabel()
newLabel.font = UIFont.systemFont(ofSize: 15.0)
newLabel.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
newLabel.textColor = .black
newLabel.layer.borderWidth = 1
newLabel.layer.borderColor = brdColor.cgColor
newLabel.numberOfLines = 0
newLabel.text = text
newLabel.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(newLabel)
newLabel.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0).isActive = true
newLabel.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0).isActive = true
var iTop: CGFloat = 0.0
var iBot: CGFloat = 0.0
// the passed "position" tells me whether this label is:
// a Single Title only
// the first Title of more than one
// the last Title of more than one
// or a Title with a Title above and below
// so we can set up proper top/bottom padding
switch position {
case 0:
iTop = 16.0
iBot = 16.0
break
case 1:
iTop = 12.0
iBot = 8.0
break
case -1:
iTop = 8.0
iBot = 12.0
break
default:
iTop = 8.0
iBot = 8.0
break
}
newLabel.topAnchor.constraint(equalTo: v.topAnchor, constant: iTop).isActive = true
newLabel.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -iBot).isActive = true
return v
}
func setupThisCell(rowNumber: Int) -> Void {
// if containingView is nil, it hasn't been created yet
// so, create it + Stack view + Button
// else
// don't create new ones
// This way, we don't keep adding more and more views to the cell on reuse
if containingView == nil {
containingView = UIView()
containingView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(containingView)
containingView.layer.borderWidth = 1
containingView.layer.borderColor = brdColor.cgColor
containingView.backgroundColor = .white
containingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
containingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0).isActive = true
containingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6.0).isActive = true
containingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6.0).isActive = true
theStackView = UIStackView()
theStackView.translatesAutoresizingMaskIntoConstraints = false
containingView.addSubview(theStackView)
theStackView.axis = .vertical
theStackView.spacing = 4.0
theStackView.alignment = .fill
theStackView.distribution = .fill
theButton = UIButton(type: .custom)
theButton.translatesAutoresizingMaskIntoConstraints = false
containingView.addSubview(theButton)
theButton.backgroundColor = .blue
theButton.setTitleColor(.white, for: .normal)
theButton.setTitle("The Button", for: .normal)
theButton.setContentHuggingPriority(1000, for: .horizontal)
theButton.centerYAnchor.constraint(equalTo: containingView.centerYAnchor, constant: 0.0).isActive = true
theButton.trailingAnchor.constraint(equalTo: containingView.trailingAnchor, constant: -8.0).isActive = true
theStackView.topAnchor.constraint(equalTo: containingView.topAnchor, constant: 0.0).isActive = true
theStackView.bottomAnchor.constraint(equalTo: containingView.bottomAnchor, constant: 0.0).isActive = true
theStackView.leadingAnchor.constraint(equalTo: containingView.leadingAnchor, constant: 0.0).isActive = true
theStackView.trailingAnchor.constraint(equalTo: theButton.leadingAnchor, constant: -8.0).isActive = true
}
// remove all previously added Title labels and spacer views
for v in theStackView.arrangedSubviews {
v.removeFromSuperview()
}
// setup 1 to 5 Titles
let n = rowNumber % 5 + 1
// create new Title Label views and, if needed, spacer views
// and add them to the Stack view
if n == 1 {
let aLabel = getLabelView(text: "Only one title for row: \(rowNumber)", position: 0)
theStackView.addArrangedSubview(aLabel)
} else {
for i in 1..<n {
let aLabel = getLabelView(text: "Title number \(i)\n for row: \(rowNumber)", position: i)
theStackView.addArrangedSubview(aLabel)
let aSpacer = getSpacer()
theStackView.addArrangedSubview(aSpacer)
}
let aLabel = getLabelView(text: "Title number \(n)\n for row: \(rowNumber)", position: -1)
theStackView.addArrangedSubview(aLabel)
}
}
}
class BoxedTableViewController: UITableViewController {
let cellID = "boxedCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(BoxedCell.self, forCellReuseIdentifier: cellID)
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1250
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! BoxedCell
// Configure the cell...
cell.setupThisCell(rowNumber: indexPath.row)
return cell
}
}
I'll check back if you run into any problems with it (gotta run, and haven't fully tested it yet -- and ran out of time to comment it - ugh).
You can also use tableview as tableviecell and adjust cell accordingly.
u need to layout cell in func layoutsubviews after set data to label and imageview;
Yes, split ElementTableCell to section with header and cells is much better approach. In this case you have no need to create constraints or dealing with complex manual layout. This would make your code simple and make scrolling smooth.
The button you use can be easily moved to the reusable header view
Is you still want to keep it in one complete cell, where is a way to draw manually the dynamic elements, such as titles and separators lines. Manually drawing is faster as usual. Or remove all views from cell.contentView each time you adding new. But this way is much more complicated.
Greate article about how to make UITableView appearence swmoth:
Perfect smooth scrolling in UITableViews

Resources