I'm creating a chat app using swift. On my storyboard, I have one View Controller that is responsible for showing the messages. I have 3 swift files :
ChatingViewController : Class associated with the View Controller on the storyboard
CustomChatingTableViewController
CellChatingTableViewCell
Each message is displayed in a cell. I create the tableView programmatically.
- ChatingViewController
import UIKit
class ChatingViewController: UIViewController {
var messageController = [[String:String]]()
var tableViewController = CustomChatingTableViewController()
override func viewDidLoad() {
let sizeTableView = CGSize(width: view.frame.size.width - 2 * margin, height: view.frame.size.height - sizeTextField.height - 2 * margin - self.navigationController!.navigationBar.frame.size.height)
let originTableView = CGPoint(x: margin, y: 2 * margin + self.navigationController!.navigationBar.frame.size.height)
tableViewController.tableView.frame = CGRect(origin: originTableView, size: sizeTableView)
tableViewController.tableView.registerClass(CellChatingTableViewCell.self, forCellReuseIdentifier: "Cell")
tableViewController.data = messageController
tableViewController.tableView.separatorStyle = .None
view.addSubview(tableViewController.tableView)
}
- CustomChatingTableViewController
import UIKit
class CustomChatingTableViewController: UITableViewController {
var data:[[String:String]]!
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as CellChatingTableViewCell
cell.setCell(data[indexPath.row]["name"] as String!, date: data[indexPath.row]["date"] as String!, message: data[indexPath.row]["message"] as String!)
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 150
}
}
** -CellChatingTableViewCell**
import UIKit
class CellChatingTableViewCell: UITableViewCell {
var date = UILabel()
var message = UILabel()
func setCell(name:String,date:String,message:String){
let imageContainerMessage = UIImage(named: "orange.png")!.stretchableImageWithLeftCapWidth(24, topCapHeight: 15)
self.date.font = UIFont(name: "Arial", size: 10)
self.date.text = date
self.date.numberOfLines = 0
self.date.lineBreakMode = NSLineBreakMode.ByWordWrapping
let sizeDateLabelMax = CGSizeMake(self.frame.size.width, 9999)
let expectedSizeDate = self.date.sizeThatFits(sizeDateLabelMax)
self.date.frame = CGRect(origin: CGPoint.zeroPoint, size: expectedSizeDate)
self.message.font = UIFont(name: "Arial", size: 15)
self.message.text = message
self.message.numberOfLines = 0
self.message.lineBreakMode = NSLineBreakMode.ByWordWrapping
let sizeMessageLabelMax = CGSizeMake(self.frame.size.width, 9999)
let expectedSizeMessage = self.message.sizeThatFits(sizeMessageLabelMax)
self.message.frame = CGRect(origin: CGPoint(x: 15, y: 10), size: expectedSizeMessage)
var imageContainer = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: expectedSizeDate.height + 5), size:
CGSizeMake(expectedSizeMessage.width + 25, expectedSizeMessage.height + 25)))
imageContainer.image = imageContainerMessage
self.addSubview(self.date)
self.addSubview(imageContainer)
imageContainer.addSubview(self.message)
}
}
When I load the ViewController, everything does work fine but when I scroll the tableView, it turns horrible:
Before scrolling :
After scrolling :
Any suggestion?
Thanks in advance
In your cellForRowAtIndexPath, you are reusing cells. But then in your custom UITableViewCell, you are adding (self.addSubView) self.date and self.message multiple times (yikes), and adding new instances of imageContainer.
You should either clear the cells before re-adding them, or invalidate them with new data but do not do self.addSubView again.
Related
I'm fairly new to Swift and I'm trying to make a tableView appear in a popup as shown here.
I have the datasource and delegate set to self and I call reloadData() after fetching data from Cloud Firestore. The thing is, numberOfRowsInSection might get called once, but can't get called again. CellForRowAt never gets called.
Does this have to do with the fact that I made my tableView programmatically? Something like it doesn't think the frame is set or something, even though it is. The table does work if I just make the table in Xcode manually and link an outlet. The sad thing is I do the same thing in a different view controller, but in that view controller it does work and I can't find any differences in the code.
Here's the function that gets called when you press the button
#IBAction func ShowTeams(_ sender: Any) {
RefreshData()
let startPoint = CGPoint(x: self.btnShowTeams.frame.origin.x + 15, y: self.btnShowTeams.frame.origin.y + 23)
tblTeams = UITableView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 180))
tblTeams.register(UITableViewCell.self, forCellReuseIdentifier: "cellTeams")
let popover = Popover()
tblTeams.rowHeight = 35
tblTeams.contentInset = UIEdgeInsets(top: 15,left: 0,bottom: 0,right: 0)
tblTeams.separatorColor = UIColor(hexFromString: "13293d")
popover.show(tblTeams, point: startPoint)
}
Here are the functions that set up the tableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
if tableView == tblTeams{
print("this shows like once, and yes there's data in dataTeams")
return dataTeams.count
}else{
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == tblTeams{
print("this doesnt show")
let cell = tableView.dequeueReusableCell(withIdentifier: "cellTeams", for: indexPath)
cell.textLabel?.textAlignment = .center
cell.textLabel?.font = UIFont.systemFont(ofSize: 22, weight: UIFont.Weight.bold)
cell.accessoryType = .disclosureIndicator
cell.textLabel!.text = dataTeams[indexPath.row]
return cell
}else{
let cell = tableView.dequeueReusableCell(withIdentifier: "cellInvites", for: indexPath)
return cell
}
}
Here's the fetch data function
func RefreshData(){
let db = Firestore.firestore()
let uid = Auth.auth().currentUser!.uid
dataTeams = [String]()
var i = 1
while i <= 6 {
db.collection("teams").whereField("uid\(i)", isEqualTo: uid)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
self.dataTeams.append((document["username"] as? String)!)
}
print("the code does always make it here, I checked")
self.tblTeams.reloadData()
}
}
i = i+1
}
}
And the stuff at the top for good measure. Thank you!
import UIKit
import Firebase
import FirebaseFirestore
import GradientLoadingBar
import SCLAlertView
import Popover
class Game: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var btnShowTeams: UIButton!
var dataTeams = [String]()
var tblTeams: UITableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tblTeams.dataSource = self
tblTeams.delegate = self
RefreshData()
}
#IBAction func ShowTeams(_ sender: Any) {
RefreshData()
let startPoint = CGPoint(x: self.btnShowTeams.frame.origin.x + 15, y: self.btnShowTeams.frame.origin.y + 23)
tblTeams = UITableView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 180))
tblTeams.register(UITableViewCell.self, forCellReuseIdentifier: "cellTeams")
let popover = Popover()
tblTeams.rowHeight = 35
tblTeams.contentInset = UIEdgeInsets(top: 15,left: 0,bottom: 0,right: 0)
tblTeams.separatorColor = UIColor(hexFromString: "13293d")
popover.show(tblTeams, point: startPoint)
}
In the above code you're creating new table view every time you click your button. And the newly created tableview has nil data source and delegate (by default).
So either set data source or delegate in above method
tblTeams.dataSource = self
tblTeams.delegate = self
or reuse the existing table view by replacing
tblTeams = UITableView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 180))
with
tblTeams.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 180)
I would like to recalculate the height of a table view's footer based upon the table view's changing content size. When the table has zero rows the height of the footer will be at its maximum. As rows are added to the table the footer's height will be reduced until it reaches a minimum. What I am doing is using the footer to fill up the empty space that appears at the bottom of the table when there are zero or few rows. In addition to rows being added it is possible for the content size to change because the height (content) of an existing row has been changed.
Supposing that I have a view controller whose main view contains two subviews: a button and a table view. Clicking the button results in the data store being modified and the table's reloadData method being called. When/Where would I assign a new value to the table's tableFooterView.bounds.size.height?
I should also point out that I am using UITableViewAutomaticDimension. If, in the table's data source delegate method cellForRowAt, I print the cell heights I get:
Upper table cell height = 21.0
Upper table cell height = 21.0
Upper table cell height = 21.0
Upper table cell height = 21.0
Upper table cell height = 44.0
All 21 except for the last one, the new one. This must be due to the automatic dimensioning not yet having been applied.
Update:
I have tentatively arrived at the following solution (many thanks to all of the folks on this thread for the biggest part of the solution). I am tentative because the solution involves calling reloadData twice in order to deal with an issue with the contentSize. See this GitHub project for a demo of the contentSize issue.
class TableView: UITableView {
override func reloadData() {
execute() { super.reloadData() }
}
override func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) {
execute() { super.reloadRows(at: indexPaths, with: animation) }
}
private func execute(reload: #escaping () -> Void) {
CATransaction.begin()
CATransaction.setCompletionBlock() {
if self.adjustFooter() {
reload() // Cause the contentSize to update (see GitHub project)
self.layoutIfNeeded()
}
}
reload()
CATransaction.commit()
}
// Return true(false) if the footer was(was not) adjusted
func adjustFooter() -> Bool {
guard let currentFrame = tableFooterView?.frame else { return false }
let newHeight = calcFooterHeight()
let adjustmentNeeded = newHeight != currentFrame.height
if adjustmentNeeded {
tableFooterView?.frame = CGRect(x: currentFrame.minX, y: currentFrame.minY, width: currentFrame.width, height: newHeight)
}
return adjustmentNeeded
}
private let minFooterHeight: CGFloat = 44
private func calcFooterHeight() -> CGFloat {
guard let footerView = tableFooterView else { return 0 }
let spaceTaken = contentSize.height - footerView.bounds.height
let spaceAvailable = bounds.height - spaceTaken
return spaceAvailable > minFooterHeight ? spaceAvailable : minFooterHeight
}
}
UITableViewDelegate has method tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat which we can use to specifiy height of section footers. This method fires when we call reloadData() for table view or when screen orientation was changed, etc.
So you can implement this method to calculate a new height of the footer:
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
guard section == 0 else { return 0.0 } // assume there is only one section in the table
var cellsHeight: CGFloat = 0.0
let rows = self.tableView(tableView, numberOfRowsInSection: section)
for row in 0..<rows
{
let indexPath = IndexPath(item: row, section: section)
cellsHeight += self.tableView(tableView, heightForRowAt: indexPath)
}
let headerHeight: CGFloat = tableView.tableHeaderView?.frame.height ?? 0.0
let footerHeight = view.frame.height - headerHeight - cellsHeight
return footerHeight
}
I arrived at the following solution. Many thanks to all of the folks on this thread for the biggest part of the solution. The TableViewController.TableView class provides the desired functionality. The remainder of the code fleshes out a complete example.
//
// TableViewController.swift
// Tables
//
// Created by Robert Vaessen on 11/6/18.
// Copyright © 2018 Robert Vaessen. All rights reserved.
//
// Note: Add the following to AppDelegate:
//
// func application(_ application: UIApplication,
// didFinishLaunchingWithOptions launchOptions:
// [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// window = UIWindow(frame: UIScreen.main.bounds)
// window?.makeKeyAndVisible()
// window?.rootViewController = TableViewController()
// return true
// }
import UIKit
class TableViewController: UIViewController {
class TableView : UITableView {
override func reloadData() {
execute() { super.reloadData() }
}
override func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) {
execute() { super.reloadRows(at: indexPaths, with: animation) }
}
private func execute(reload: #escaping () -> Void) {
CATransaction.begin()
CATransaction.setCompletionBlock() {
print("Reload completed")
_ = self.adjustFooter()
}
print("\nReload begun")
reload()
CATransaction.commit()
}
private func adjustFooter() -> Bool {
guard let footerView = tableFooterView else { return false }
func calcFooterHeight() -> CGFloat {
var heightUsed = tableHeaderView?.bounds.height ?? 0
for cell in visibleCells { heightUsed += cell.bounds.height }
let heightRemaining = bounds.height - heightUsed
let minHeight: CGFloat = 44
return heightRemaining > minHeight ? heightRemaining : minHeight
}
let newHeight = calcFooterHeight()
guard newHeight != footerView.bounds.height else { return false }
// Keep the origin where it is, i.e. tweaking just the height expands the frame about its center
let currentFrame = footerView.frame
footerView.frame = CGRect(x: currentFrame.origin.x, y: currentFrame.origin.y, width: currentFrame.width, height: newHeight)
return true
}
}
class FooterView : UIView {
override func draw(_ rect: CGRect) {
print("Drawing footer")
super.draw(rect)
}
}
private var tableView: TableView!
private let cellReuseId = "TableCell"
private let data: [UIColor] = [UIColor(white: 0.4, alpha: 1), UIColor(white: 0.5, alpha: 1), UIColor(white: 0.6, alpha: 1), UIColor(white: 0.7, alpha: 1)]
private var dataRepeatCount = 1
override func viewDidLoad() {
super.viewDidLoad()
func createTable(in: UIView) -> TableView {
let tableView = TableView(frame: CGRect.zero)
tableView.separatorStyle = .none
tableView.translatesAutoresizingMaskIntoConstraints = false
`in`.addSubview(tableView)
tableView.centerXAnchor.constraint(equalTo: `in`.centerXAnchor).isActive = true
tableView.centerYAnchor.constraint(equalTo: `in`.centerYAnchor).isActive = true
tableView.widthAnchor.constraint(equalTo: `in`.widthAnchor, multiplier: 1).isActive = true
tableView.heightAnchor.constraint(equalTo: `in`.heightAnchor, multiplier: 0.8).isActive = true
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseId)
return tableView
}
func addHeader(to: UITableView) {
let header = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 50))
to.tableHeaderView = header
let color = UIColor.black
let offset: CGFloat = 64
let add = UIButton(type: .system)
add.setTitle("Add", for: .normal)
add.layer.borderColor = color.cgColor
add.layer.borderWidth = 1
add.layer.cornerRadius = 5
add.tintColor = color
add.contentEdgeInsets = UIEdgeInsets.init(top: 8, left: 8, bottom: 8, right: 8)
add.addTarget(self, action: #selector(addRows), for: .touchUpInside)
add.translatesAutoresizingMaskIntoConstraints = false
header.addSubview(add)
add.centerXAnchor.constraint(equalTo: to.centerXAnchor, constant: -offset).isActive = true
add.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true
let remove = UIButton(type: .system)
remove.setTitle("Remove", for: .normal)
remove.layer.borderColor = color.cgColor
remove.layer.borderWidth = 1
remove.layer.cornerRadius = 5
remove.tintColor = color
remove.contentEdgeInsets = UIEdgeInsets.init(top: 8, left: 8, bottom: 8, right: 8)
remove.addTarget(self, action: #selector(removeRows), for: .touchUpInside)
remove.translatesAutoresizingMaskIntoConstraints = false
header.addSubview(remove)
remove.centerXAnchor.constraint(equalTo: header.centerXAnchor, constant: offset).isActive = true
remove.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true
}
func addFooter(to: UITableView) {
let footer = FooterView(frame: CGRect(x: 0, y: 0, width: 0, height: 50))
footer.layer.borderWidth = 3
footer.layer.borderColor = UIColor.red.cgColor
//footer.contentMode = .redraw
to.tableFooterView = footer
}
tableView = createTable(in: view)
addHeader(to: tableView)
addFooter(to: tableView)
view.backgroundColor = .white
tableView.backgroundColor = .black // UIColor(white: 0.2, alpha: 1)
tableView.tableHeaderView!.backgroundColor = .cyan // UIColor(white: 0, alpha: 1)
tableView.tableFooterView!.backgroundColor = .white // UIColor(white: 0, alpha: 1)
}
#objc private func addRows() {
dataRepeatCount += 1
tableView.reloadData()
}
#objc private func removeRows() {
dataRepeatCount -= dataRepeatCount > 0 ? 1 : 0
tableView.reloadData()
}
}
extension TableViewController : UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard section == 0 else { fatalError("Unexpected section: \(section)") }
return dataRepeatCount * data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
cell.textLabel?.textAlignment = .center
cell.backgroundColor = data[indexPath.row % data.count]
cell.textLabel?.textColor = .white
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
I am an android application developer and new to iOS programming and my very first challenge is to build a 2-way scrolling table in iOS. I am getting many solutions with UICollectionView inside UITableView. But in my case rows will scroll together, not independent of each other. There are more than 15 columns and 100+ rows with text data in the table.
I have achieved the same in Android by using a ListView inside a HorizontalScrollView. But yet to find any solution in iOS. Any help is greatly appreciated.
EDIT: I have added a couple of screens of the android app where the table is scrolled horizontally.
So you want this:
You should use a UICollectionView. You can't use UICollectionViewFlowLayout (the only layout that's provided in the public SDK) because it is designed to only scroll in one direction, so you need to implement a custom UICollectionViewLayout subclass that arranges the elements to scroll in both directions if needed.
For full details on building a custom UICollectionViewLayout subclass, you should watch these: videos from WWDC 2012:
Session 205: Introducing Collection Views
Session 219: Advanced Collection Views and Building Custom Layouts
Anyway, I'll just dump an example implementation of GridLayout here for you to start with. For each IndexPath, I use the section as the row number and the item as the column number.
class GridLayout: UICollectionViewLayout {
var cellHeight: CGFloat = 22
var cellWidths: [CGFloat] = [] {
didSet {
precondition(cellWidths.filter({ $0 <= 0 }).isEmpty)
invalidateCache()
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: totalWidth, height: totalHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// When bouncing, rect's origin can have a negative x or y, which is bad.
let newRect = rect.intersection(CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight))
var poses = [UICollectionViewLayoutAttributes]()
let rows = rowsOverlapping(newRect)
let columns = columnsOverlapping(newRect)
for row in rows {
for column in columns {
let indexPath = IndexPath(item: column, section: row)
poses.append(pose(forCellAt: indexPath))
}
}
return poses
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return pose(forCellAt: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
private struct CellSpan {
var minX: CGFloat
var maxX: CGFloat
}
private struct Cache {
var cellSpans: [CellSpan]
var totalWidth: CGFloat
}
private var _cache: Cache? = nil
private var cache: Cache {
if let cache = _cache { return cache }
var spans = [CellSpan]()
var x: CGFloat = 0
for width in cellWidths {
spans.append(CellSpan(minX: x, maxX: x + width))
x += width
}
let cache = Cache(cellSpans: spans, totalWidth: x)
_cache = cache
return cache
}
private var totalWidth: CGFloat { return cache.totalWidth }
private var cellSpans: [CellSpan] { return cache.cellSpans }
private var totalHeight: CGFloat {
return cellHeight * CGFloat(collectionView?.numberOfSections ?? 0)
}
private func invalidateCache() {
_cache = nil
invalidateLayout()
}
private func rowsOverlapping(_ rect: CGRect) -> Range<Int> {
let startRow = Int(floor(rect.minY / cellHeight))
let endRow = Int(ceil(rect.maxY / cellHeight))
return startRow ..< endRow
}
private func columnsOverlapping(_ rect: CGRect) -> Range<Int> {
let minX = rect.minX
let maxX = rect.maxX
if let start = cellSpans.firstIndex(where: { $0.maxX >= minX }), let end = cellSpans.lastIndex(where: { $0.minX <= maxX }) {
return start ..< end + 1
} else {
return 0 ..< 0
}
}
private func pose(forCellAt indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let pose = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let row = indexPath.section
let column = indexPath.item
pose.frame = CGRect(x: cellSpans[column].minX, y: CGFloat(row) * cellHeight, width: cellWidths[column], height: cellHeight)
return pose
}
}
To draw the separating lines, I added hairline views to each cell's background:
class GridCell: UICollectionViewCell {
static var reuseIdentifier: String { return "cell" }
override init(frame: CGRect) {
super.init(frame: frame)
label.frame = bounds.insetBy(dx: 2, dy: 2)
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.addSubview(label)
let backgroundView = UIView(frame: CGRect(origin: .zero, size: frame.size))
backgroundView.backgroundColor = .white
self.backgroundView = backgroundView
rightSeparator.backgroundColor = .gray
backgroundView.addSubview(rightSeparator)
bottomSeparator.backgroundColor = .gray
backgroundView.addSubview(bottomSeparator)
}
func setRecord(_ record: String) {
label.text = record
}
override func layoutSubviews() {
super.layoutSubviews()
let thickness = 1 / (window?.screen.scale ?? 1)
let size = bounds.size
rightSeparator.frame = CGRect(x: size.width - thickness, y: 0, width: thickness, height: size.height)
bottomSeparator.frame = CGRect(x: 0, y: size.height - thickness, width: size.width, height: thickness)
}
private let label = UILabel()
private let rightSeparator = UIView()
private let bottomSeparator = UIView()
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here's my demo view controller:
class ViewController: UIViewController {
var records: [[String]] = (0 ..< 20).map { row in
(0 ..< 6).map {
column in
"Row \(row) column \(column)"
}
}
var cellWidths: [CGFloat] = [ 180, 200, 180, 160, 200, 200 ]
override func viewDidLoad() {
super.viewDidLoad()
let layout = GridLayout()
layout.cellHeight = 44
layout.cellWidths = cellWidths
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.isDirectionalLockEnabled = true
collectionView.backgroundColor = UIColor(white: 0.95, alpha: 1)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.register(GridCell.self, forCellWithReuseIdentifier: GridCell.reuseIdentifier)
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return records.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return records[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GridCell.reuseIdentifier, for: indexPath) as! GridCell
cell.setRecord(records[indexPath.section][indexPath.item])
return cell
}
}
I've been having this problem for weeks and I think I am not able to solve it.
I have a tableView which has custom tableViewCells and they are filled with data. The tableViewCell has two UILabels and one UIView.
The problem appears when I scroll several times the drawings on the UIViews are overlapped one with another, I think they are redrawn. I know that this behavior is because of the reuse of the cells but I can't even locate the origin of the problem.
UIViews show perfectly when opening the app
UIViews get overlapped after scrolling
My UITableView's cellForRowAtIndexPath is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let CellIdentifier = "Cell"
let cell: CustomTableViewcell = self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! CustomTableViewcell
//self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier) as! CustomTableViewcell
cell.prepareForReuse()
self.createUIForCell(cell)
self.configureCell(cell, indexPath: indexPath)
print("\t\(parsedData[indexPath.row].stationName): \(parsedData[indexPath.row].freeBikes)")
return cell
}
func createUIForCell(cell: CustomTableViewcell) {
cell.distanceLabel.textColor = UIColor.whiteColor()
cell.distanceLabel.backgroundColor = UIColor.clearColor()
cell.bikeStationLabel.textColor = UIColor.whiteColor()
cell.bikeStationLabel.backgroundColor = UIColor.clearColor()
}
func configureCell(cell: CustomTableViewcell, indexPath: NSIndexPath) {
let stations = parsedData[indexPath.row]
if indexPath.row % 2 == 0 {
cell.stackView.arrangedSubviews.first!.backgroundColor = cellBackgroundColor1
cell.progressView.backgroundColor = cellBackgroundColor2
} else {
cell.stackView.arrangedSubviews.first!.backgroundColor = cellBackgroundColor2
cell.progressView.backgroundColor = cellBackgroundColor1
}
cell.progressView.getWidth(stations.freeBikes, freeDocks: stations.freeDocks)
cell.getLocation(stations.latitude, longitude: stations.longitude)
cell.getParameters(stations.freeBikes, freeDocks: stations.freeDocks)
cell.bikeStationLabel.text = stations.stationName
if stations.distanceToLocation != nil {
if stations.distanceToLocation! < 1000 {
cell.distanceLabel.text = String(format: "%.f m", stations.distanceToLocation!)
} else {
cell.distanceLabel.text = String(format: "%.1f km", stations.distanceToLocation! / 1000)
}
} else {
cell.distanceLabel.text = ""
}
}
For the before mentioned UIVIew inside my custom cell I have created a separated class for it to handle the drawing and it looks like this:
import UIKit
class ProgressViewBar: UIView {
var freeBikes = 0
var freeDocks = 0
var text: UILabel? = nil
var text2: UILabel? = nil
func getWidth(freeBikes: Int, freeDocks: Int) {
self.freeBikes = freeBikes
self.freeDocks = freeDocks
}
override func drawRect(rect: CGRect) {
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: rect.width * (CGFloat(freeBikes) / (CGFloat(freeBikes) + CGFloat(freeDocks))), height: frame.height), cornerRadius: 0)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(shapeLayer)
drawText(rect)
}
func drawText(rect: CGRect) {
if freeBikes != 0 {
text?.removeFromSuperview()
text = UILabel(frame: CGRect(x: 2, y: 0, width: 20, height: 20))
text!.text = String("\(freeBikes)")
text!.textAlignment = NSTextAlignment.Left
text!.font = UIFont(name: "HelveticaNeue-Thin", size: 17)
text!.backgroundColor = UIColor.clearColor()
text!.textColor = UIColor(red: 1/255, green: 87/255, blue: 155/255, alpha: 1)
self.addSubview(text!)
}
text2?.removeFromSuperview()
text2 = UILabel(frame: CGRect(x: rect.width * (CGFloat(freeBikes) / (CGFloat(freeBikes) + CGFloat(freeDocks))) + 2, y: 0, width: 20, height: 20))
text2!.text = String("\(freeDocks)")
text2!.textAlignment = NSTextAlignment.Left
text2!.font = UIFont(name: "HelveticaNeue-Thin", size: 17)
text2!.backgroundColor = UIColor.clearColor()
text2!.textColor = UIColor.whiteColor()
self.addSubview(text2!)
}
}
The view needs two parameters to be drawn and I pass them using the func getWidth(freeBikes: Int, freeDocks: Int) method calling it from the `ViewController. If any other piece of code is needed you can look at the repo.
The problem is that when reusing the cell you call drawRect once again and don't clear the already drawn rect. Clear it before drawing another one.
My issue is that the last cell in my TableView is below the screen view and to see it you must scroll up and hold your position. At a neutral position where you dont scroll up, you cant see the last cell. Everything seemed fine until i changed the size of the cells. Here is my code:
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
//MARK : Properties
var tableView = UITableView()
var items: [String] = ["Age", "Gender", "Smoking Hx", "Occup. -Ag", "Family Hx", "Chronic Lung Disease Radiology", "Chronic Lung Disease Hx", "Nodule Border", "Nodule Location", "Satellite Lesions", "Nodule Pattern Cavity", "Nodule Size"]
var navigationBar = NavigationBar()
var gender = GenderView()
override func viewDidLoad() {
super.viewDidLoad()
//Create TableView
tableView.frame = CGRectMake(0, self.view.bounds.height * 0.097, self.view.bounds.width, self.view.bounds.height);
tableView.delegate = self
tableView.dataSource = self
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.view.addSubview(tableView)
//Create Navigation Bar with custom class
self.navigationBar = NavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height * 0.097))
self.view.addSubview(navigationBar)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
//Cell wont turn grey when selected
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return self.view.bounds.height * 0.095
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You selected cell #\(indexPath.row)!")
}
}
The only thing i could think of causing this issue is that instead of me creating a navigation bar, i created a "navigationBar" using a custom UIView() class. I then start the table view at the bottom of the navigation bar. Any idea how to fix this?
Here is the NavigationBar.swift:
class NavigationBar: UIView {
var navigationBar = UIView()
var header = UILabel()
var lineBorder = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.frame = frame
setUpView()
}
func setUpView(){
//Create Navigation Bar
navigationBar.frame = CGRectMake(0, 0, self.bounds.width, self.bounds.height)
navigationBar.backgroundColor = UIColor.whiteColor()
self.addSubview(navigationBar)
//Create Line Border
lineBorder.frame = CGRectMake(0, self.bounds.height, self.bounds.width, self.bounds.height * 0.005)
lineBorder.backgroundColor = UIColor.grayColor()
self.addSubview(lineBorder)
header.frame = CGRectMake(0, 0, 200, 200)
header.font = UIFont(name: "Helvetica", size: 17)
header.text = "Nodule Risk Calculator"
//header.backgroundColor = UIColor.blackColor()
self.addSubview(header)
header.translatesAutoresizingMaskIntoConstraints = false
header.centerHorizontallyTo(navigationBar, padding: 0)
header.centerVerticallyTo(navigationBar, padding: 9)
}
func hide(){
self.removeFromSuperview()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
tableView.frame = CGRectMake(0, self.view.bounds.height * 0.097, self.view.bounds.width, self.view.bounds.height);
has problem. The origin.y is not 0, and you still feed the whole height. So the table view will have some area below the screen
Try this:
tableView.frame = CGRectMake(0, self.view.bounds.height * 0.097, self.view.bounds.width,
self.view.bounds.height - self.view.bounds.height * 0.097);
self.navigationBar = NavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height * 0.097))
self.view.addSubview(navigationBar)
var yAxis : Float = self.navigationBar.frame.height
var tblHeight : Float = self.view.bounds.height - yAxis
tableView.frame = CGRectMake(0, yAxis, self.view.bounds.width, tblHeight)
self.view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")