How to prevent data overlapping in UITableView after scrolling - ios

My data displays correctly before any scrolling happens on the UITableView. Once I scroll I see data overlapping and from my understanding it is because cells get reused! I now understand the concept but not really understanding what my solution needs to be or how/where to implement one (sorry i am very new to swift). I found this Why do my UITableView cells overlap on scroll? but again don't really know how to apply it to my code.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.lineBreakMode = .byWordWrapping
let userNameLabel = UILabel()
userNameLabel.text = activeUsers[indexPath.row].userFirstName
userNameLabel.font = userNameLabel.font.withSize(10)
userNameLabel.sizeToFit()
let userProfilePic = UIImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
userProfilePic.image = activeUsers[indexPath.row].profilePicImage
userProfilePic.layer.borderWidth = 1.0
userProfilePic.layer.masksToBounds = false
userProfilePic.layer.borderColor = WhistleColors.primaryBorderColor.cgColor
userProfilePic.layer.cornerRadius = userProfilePic.frame.size.height / 2
userProfilePic.clipsToBounds = true
userProfilePic.contentMode = .scaleAspectFill
let userStackView = UIStackView(arrangedSubviews: [userProfilePic, userNameLabel])
userStackView.alignment = .center
userStackView.distribution = .fillEqually
userStackView.axis = .vertical
cell.addSubview(userStackView)
userStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
userStackView.centerXAnchor.constraint(equalTo: cell.contentView.centerXAnchor, constant: cell.frame.size.width / 2),
userStackView.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor, constant: cell.frame.size.height / 2),
]
)
userProfilePic.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
userProfilePic.widthAnchor.constraint(equalToConstant: 40),
userProfilePic.heightAnchor.constraint(equalToConstant: 40)
]
)
return cell
}

Concept of tableview is to reuse cells and it's subviews. You are creating subviews on each cell every time when it reloads. Check https://sahilpathania1997.medium.com/how-to-start-with-tableview-in-swift-bf273a8bbabe

I was able to fix my issue by watching this https://www.youtube.com/watch?v=Pu7B7uEzP18 and implementing a custom cell class with a prepareForReuse() method.
The first youtube video I ever watched around UITableViews and loading data never went over the concept of "reusable cells". Wondering how many issues like this I will encounter in the future because a concept was not discussed in my learning process! Oh well. Onward. Thanks all

dont use cellForRowAt for creating ui properties.
First of all I suggest you pLease use custom cell for UI creation.
Otherwise its overlap.

Related

asynchronously loading images in swift

I have an image that I want shown at the top of the view in a table view cell.
I have the following code that asynchronously loads that image and adds it to the cell:
if let image = value?["image"] {
let bookImageUrl = URL(string: image as! String)
let data = try? Data(contentsOf: bookImageUrl!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
if data != nil {
let imageView = UIImageView(image: UIImage(data: data!));
imageView.frame = CGRect(x: 0,
y: 95,
width: cell.frame.size.width,
height: 150)
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = false
cell.addSubview(imageView)
let label = UILabel(frame: CGRect(x: 10,
y: cell.frame.size.height-60,
width: cell.frame.size.width,
height: 50));
label.textColor = UIColor.black
label.backgroundColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 0.5)
label.font = UIFont(name: "CeraPro-Bold", size: 16)
label.text = " \(title)"
cell.addSubview(label)
}
}
}
Unfortunately, sometimes the image does not get loaded. Am I doing something wrong?
If you are using tableView.dequeueReusableCell(withIdentifier: "cellIdentifier", for: indexPath) in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell, here UITableView do reusing of those cells dequeueReusableCell(withIdentifier:)
So as with the problem you mentioned is with some times the images do not get shown, this might be because of the UITableView is reusing the cells, so, it will be better if you use else for every if statement. Or you can use something like, SDWebImage or Kingfisher for image caching.
Also note that, as the UITableView does reuse every cell so it will be better to remove all subviews or create a variable in class for 'UITableViewCell' before adding any view over programmatically, as in your code cell.addSubview(imageView) and cell.addSubview(label) the imageView and label is getting initialized and added over and over for every reuse and every URL check, Here what you can do is create a variable for label and imageview in the cell and initialize only if the variable is nil and assign the image afterwards.

Nest a UIScrollView in Swift

I have a scrollview of an array of images, it works for the images to scroll left and right but I cannot scroll down, instead I am able to scroll my tableview cells...I need to be able to scroll downwards and not have my array of images fixed on the view, is there any ways?
the codes i used are :
override func viewDidLoad() {
super.viewDidLoad()
scrollView.frame = view.frame
imageArray = [UIImage(named:"adsImage3")!,UIImage(named:"adsImage2")!,UIImage(named:"adsImage")!]
navigationController?.navigationBar.barTintColor = UIColor.white
for i in 0..<imageArray.count {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.image = imageArray[i]
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width: self.scrollView.frame.width, height: 380)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(imageView)
}
}
as for my storyboard, it looks like:
if I were to put my scrollview in my tableview, my code would be
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: MainMenuRowSelectionTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MainMenuRowSelectionTableViewCell
var cellColors = ["#FFB3B3","#FFFFFF"]
cell.contentView.backgroundColor = UIColor(hexString: cellColors[indexPath.row % cellColors.count])
imageArray = [UIImage(named:"adsImage3")!,UIImage(named:"adsImage2")!,UIImage(named:"adsImage")!]
for i in 0..<imageArray.count {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.image = imageArray[i]
}
return cell
}
and my storyboard
this way,my scroll view wouldnt appear at all
I need to be able to scroll downwards and not have my array of images fixed on the view
A table view is a vertical scroll view. You should turn your horizontal scroll view of images into the header view for the table view (i.e. its tableHeaderView), and make the table view occupy the whole screen. Now you can scroll the table and you'll scroll the scroll view of images up out of the screen.
Yes, this can be done. A good reference for nested scroll views is Apple Documentation There is sample code available there as well.
In short, you will likely need to create:
A UIScrollView that fills the view and can scroll vertically,
A UIScrollView within that for the images and can scroll horizontally, and
Your UITableView also placed within the first scroll view.

Label on Row Separator - Swift Tableview - Hourly Calendar

I want to create an hourly calendar view that is relatively basic, but similar to Apple's native calendar view. How do you add labels to be in line with the row/cell separators, and not contained in a cell. Like this:
Is there a property that lets you add a label to the lines? Do the labels have to be placed outside of the table view? Or is there a separate table that occurs?
In terms of creating colored blocks to represent events on the calendar, what would be the best way to go about doing this? Would it just be a CGRect in a prototype cell? Would you need to create separate xib files?
Thanks in advance for the help, I am still new to learning Swift!
It's not possible (or technically, it would be possible, but the overhead is too high, considering your other options).
Instead of using cell separators, set separatorStyle = .none, and draw the line in the cell (e.g., as a UIView with view.height = 1 and view.backgroundColor = .grey) and normally add the label in the cell.
Basically the solution is very simple: disable standard separator lines, and rather draw separator inside the cell (bottom or top) along with the labels. That's how I've been doing things when the client asked for some custom fancy separators - I added a custom line at the bottom of the cell and used the rest of the cell's contentView as for the cell's content.
EDIT
You can use a following example to start with (note that this is just one of several different approaches how to manage it):
class TimeCellViewController: UITableViewController {
override func loadView() {
super.loadView()
// you can use UITableViewAutomaticDimension instead of static height, if
// there will be variable heights that you don't know upfront
// https://stackoverflow.com/a/18746930/2912282
// or mine:
// https://stackoverflow.com/a/47963680/2912282
tableView.rowHeight = 80
tableView.estimatedRowHeight = 80
tableView.separatorStyle = .none
// to allow scrolling below the last cell
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40))
tableView.register(TimeCell.self, forCellReuseIdentifier: "timeCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 24
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "timeCell", for: indexPath) as! TimeCell
if indexPath.row > 0 {
cell.topTime = "\(indexPath.row):00"
} else {
cell.topTime = ""
}
cell.bottomTime = "\(indexPath.row + 1):00"
return cell
}
}
class TimeCell: UITableViewCell {
// little "hack" using two labels to render time both above and below the cell
private let topTimeLabel = UILabel()
private let bottomTimeLabel = UILabel()
private let separatorLine = UIView()
var topTime: String = "" {
didSet {
topTimeLabel.text = topTime
}
}
var bottomTime: String = "" {
didSet {
bottomTimeLabel.text = bottomTime
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
contentView.addSubview(topTimeLabel)
contentView.addSubview(bottomTimeLabel)
contentView.addSubview(separatorLine)
topTimeLabel.textColor = UIColor.gray
topTimeLabel.textAlignment = .right
bottomTimeLabel.textColor = UIColor.gray
bottomTimeLabel.textAlignment = .right
separatorLine.backgroundColor = UIColor.gray
bottomTimeLabel.translatesAutoresizingMaskIntoConstraints = false
topTimeLabel.translatesAutoresizingMaskIntoConstraints = false
separatorLine.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
bottomTimeLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0),
bottomTimeLabel.centerYAnchor.constraint(equalTo: self.bottomAnchor),
bottomTimeLabel.widthAnchor.constraint(equalToConstant: 50),
topTimeLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0),
topTimeLabel.centerYAnchor.constraint(equalTo: self.topAnchor),
topTimeLabel.widthAnchor.constraint(equalToConstant: 50),
separatorLine.leftAnchor.constraint(equalTo: bottomTimeLabel.rightAnchor, constant: 8),
separatorLine.bottomAnchor.constraint(equalTo: self.bottomAnchor),
separatorLine.heightAnchor.constraint(equalToConstant: 1),
separatorLine.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0),
])
// if you use UITableViewAutomaticDimension instead of static height,
// you will have to set priority of one of the height constraints to 999, see
// https://stackoverflow.com/q/44651241/2912282
// and
// https://stackoverflow.com/a/48131525/2912282
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Set background color of UITableView programmatically not working

I have added a UIView using storyboard and subclassing it. Within this view, I am adding a UITableView programmatically. Here is the code to create the tableview and add it:
private func commonInit() {
self.backgroundColor = .clear
self.categoryTableView = UITableView(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
categoryTableView?.delegate = self
categoryTableView?.dataSource = self
self.categoryTableView?.backgroundView = nil
self.categoryTableView?.backgroundColor = .yellow
self.categoryTableView?.isScrollEnabled = false
self.categoryTableView?.allowsMultipleSelection = true
self.addSubview(categoryTableView!)
}
This is what it looks like. I'm expecting the background of the tableview to be yellow (it is white in the screenshot)
I have also set the cells background color to clear which seems to be working. When I look at the UI hierarchy, it is pretty obvious that the White is coming from the the tableview.
I feel like this should be really strait forward. The strange part is the lines self.categoryTableView?.isScrollEnabled = false and self.categoryTableView?.allowsMultipleSelection = true both seem to be working, but the background color changing isn't.
I got the exactly same problem and found that just changing the tableView.backgroundColor after tableView.backgroundView = nil doesn't work.
My case also was a programmatically created UITableView inside an UIView.
The solution is to add a backgroundView to tableView and change the property backgroundColor of backgroundView object.
Swift 5 / iOS 12.x
Changing tableView background color
For any color other than .clear, the above should work:
self.tableView.backgroundView = UIView() //Create a backgroundView
self.tableView.backgroundView!.backgroundColor = .lightGray //choose your background color
Changing tableViewCells background color
Going a little further, some may find that the tableView background color didn't appear as intended because of the background colors of the UITableViewCell instances. A simple solution to make sure the cells have a transparent background:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.contentView.backgroundColor = UIColor.clear
cell.backgroundColor = .clear
}
I tested this in a playground, and the result was as expected.
import UIKit
import XCTest
import PlaygroundSupport
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
PlaygroundPage.current.liveView = view
view.backgroundColor = UIColor.blue
let tableView = UITableView(frame:CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
tableView.backgroundColor = UIColor.yellow
view.addSubview(tableView)
The result:
My guess is something else is afoot here. Maybe something with the sizing of the frames, or where you are calling the commonInit() method?

Add subview on tableView cells causes a sad patchwork

I want to give a chat aspect to a table view of messages in my iPhone app.
In order to perform a quality render I add two subviews: the text and the "container" which is just a view with background color.
Even if it works the first time, when I scroll, it becomes really messy because it keeps adding lots of subviews.
Here you can see it when clean
And then when it has become messy
Here is the function to handle the transform, it's called when scrolling.
func configChatCell(cell: UITableViewCell, text: String, color:UIColor)
{
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping
cell.textLabel?.textColor = UIColor.whiteColor()
let fixedWidth = cell.bounds.width - 150
let textView: UITextView = UITextView(frame: CGRect(x: 0, y: 0, width: 10, height: CGFloat.max))
textView.text = text
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.max))
var newFrame = textView.frame
newFrame.size = CGSize(width: min(newSize.width, fixedWidth), height: newSize.height)
textView.sizeThatFits(newFrame.size)
textView.frame = newFrame
textView.backgroundColor = UIColor.clearColor()
self.rowHeight = textView.frame.height+20
let view = UIView()
view.backgroundColor = color
print(textView.frame.height+10)
view.frame = CGRect(x: 0, y: 5, width: textView.frame.width+50, height: textView.frame.height+10)
view.layer.cornerRadius = 5
cell.backgroundColor = UIColor.clearColor()
cell.contentView.addSubview(view)
cell.contentView.addSubview(textView)
cell.contentView.sendSubviewToBack(view)
}
If I remove the subviews each time I scroll, nothing appears on screen.
Can somebody help me to find a solution? Or is there any other way to do this?
Thanks in advance!
I quickly wrote up something for this.
It starts with the ChatCell
class ChatCell: UITableViewCell {
var messageLabel: UILabel? {
didSet {
messageLabel?.text = message
}
}
var message: String? {
didSet {
messageLabel?.text = message
}
}
class func messageCell(withText text: String, leading: Bool = true) -> ChatCell {
let cell = ChatCell()
cell.message = text
// Make the container
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(container)
container.topAnchor.constraintEqualToAnchor(cell.contentView.topAnchor, constant: 8).active = true
container.bottomAnchor.constraintEqualToAnchor(cell.contentView.bottomAnchor, constant: -8).active = true
if leading {
container.leadingAnchor.constraintEqualToAnchor(cell.contentView.leadingAnchor, constant: leading ? 8 : 8*8).active = true
container.trailingAnchor.constraintLessThanOrEqualToAnchor(cell.contentView.trailingAnchor, constant: leading ? -8*8 : -8).active = true
} else {
container.leadingAnchor.constraintGreaterThanOrEqualToAnchor(cell.contentView.leadingAnchor, constant: leading ? 8 : 8*8).active = true
container.trailingAnchor.constraintEqualToAnchor(cell.contentView.trailingAnchor, constant: leading ? -8*8 : -8).active = true
}
// Make the messageLabel.
let messageLabel = UILabel()
messageLabel.numberOfLines = 0
messageLabel.textColor = UIColor.whiteColor()
messageLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(messageLabel)
// Add constraints.
messageLabel.topAnchor.constraintEqualToAnchor(container.topAnchor, constant: 8).active = true
messageLabel.bottomAnchor.constraintEqualToAnchor(container.bottomAnchor, constant: -8).active = true
messageLabel.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: 8).active = true
messageLabel.trailingAnchor.constraintEqualToAnchor(container.trailingAnchor, constant: -8).active = true
cell.messageLabel = messageLabel
container.backgroundColor = UIColor(red:0.19, green:0.70, blue:1.00, alpha:1.00)
container.layer.cornerRadius = 12.0
return cell
}
}
The cell also includes support for leading and trailing messages, for back and forth conversation. Perhaps make an array of tuples like this:
let messages: [(message: String, leading: Bool)] = [("Hello", true), ("My name is John Doe and this works quite well", false), ("I would agree", true)]
Then in your tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell you could do this:
let cell = ChatCell.messageCell(withText: messages[indexPath.row].message, leading: messages[indexPath.row].leading)
return cell
Let me know if this works for you. I tested it in a Playground and it works as expected
Assuming that your configureChatCell is called from tableView:cellForRowAtIndexPath:, then #Paulw11 is right; cells are reused, so you should only make changes that are unique to that row in the table. In your example, the only calls that you should be making in your method are textView.text = text and the ones to resize the textView to fit. Everything else should go in a dynamic cell prototype in the storyboard or, if you want to do everything in code (which I have a bad feeling you do), then put the rest of the configuration in a UITableViewCell subclass, then register that subclass with your table view.
I write something like this. It's simple but can elucidate solution
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseid", forIndexPath: indexPath)
// Configure the cell...
let contentView = cell.contentView
let subviews = contentView.subviews
for view in subviews {
if 100 == view.tag {
let label = view as! UILabel
label.text = self.datas[indexPath.row]
} else if 200 == view.tag {
view.layer.cornerRadius = 10.0
}
}
return cell
}
the key point is config every thing in tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
the view in code with tag 200 is a background view has same frame with label, I use layout constraint in storyboard to make sure its size and position.

Resources