Show JSON data correctly in UITableView - ios

I've made an UITableView and filled it with JSON data I get inside my API. I get and place all correctly but when I scroll or delete a row everything gets messed up!
Labels and images interfere; this is my code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var dict = productsArrayResult[indexPath.row]
let cellImage = UIImageView(frame: CGRect(x: 5, y: 5, width: view.frame.size.width / 3, height: 90))
cellImage.contentMode = .scaleAspectFit
let productMainImageString = dict["id"] as! Int
let url = "https://example.com/api/DigitalCatalog/v1/getImage?id=\(productMainImageString)&name=primary"
self.downloadImage(url, inView: cellImage)
cell.addSubview(cellImage)
let cellTitle = UILabel(frame: CGRect(x: view.frame.size.width / 3, y: 5, width: (view.frame.size.width / 3) * 1.9, height: 40))
cellTitle.textColor = UIColor.darkGray
cellTitle.textAlignment = .right
cellTitle.text = dict["title"] as? String
cellTitle.font = cellTitle.font.withSize(self.view.frame.height * self.relativeFontConstantT)
cell.addSubview(cellTitle)
let cellDescription = UILabel(frame: CGRect(x: view.frame.size.width / 3, y: 55, width: (view.frame.size.width / 3) * 1.9, height: 40))
cellDescription.textColor = UIColor.darkGray
cellDescription.textAlignment = .right
cellDescription.text = dict["description"] as? String
cellDescription.font = cellDescription.font.withSize(self.view.frame.height * self.relativeFontConstant)
cell.addSubview(cellDescription)
return cell
}

You are adding subviews multiple times while dequeuing reusable cells. What you can do is make a prototype cell either in storyboard or as xib file and then dequeue that cell at cellForRowAtIndexPath.
Your custom class for cell will look similar to this where outlets are drawn from prototype cell.
Note: You need to assign Reusable Identifier for that prototype cell.
class DemoProtoTypeCell: UITableViewCell {
#IBOutlet var titleLabel: UILabel!
#IBOutlet var descriptionLabel: UILabel!
#IBOutlet var titleImageView: UIImageView!
}
Now you can deque DemoProtoTypeCell and use accordingly.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: DemoProtoTypeCell.self), for: indexPath) as! DemoProtoTypeCell
cell.titleImageView.image = UIImage(named: "demoImage")
cell.titleLabel.text = "demoTitle"
cell.descriptionLabel.text = "Your description will go here."
return cell
}

That's because you are adding subviews to reused (so that it may already have subviews added previously) cells.
Try to check if the cell has subviews and fill in information you need, if there're no subviews then you add them to the cell.
Option 1
if let imageView = cell.viewWithTag(1) {
imageView.image = //your image
} else {
let imageView = UIImageView(//with your settings)
imageView.tag = 1
cell.addSubview(imageView)
}
Option 2
Crete UITableViewCell subclass that already has all the subviews you need.

I have used below method to remove all subviews from cell:
override func prepareForReuse() {
for views in self.subviews {
views.removeFromSuperview()
}
}
But I have created UITableViewCell subclass and declared this method in it.
you can also do one thing as #sCha has suggested. Add tags to the subviews and then use the same method to remove subview from cell:
override func prepareForReuse() {
for view in self.subviews {
if view.tag == 1 {
view.removeFromSuperview()
}
}
}
Hope this helps.

I think the other answers already mentioned a solution. You should subclass the tableview cell and just change the values of your layout elements for each row.
But I want to explain why you get this strange behaviour.
When you call
tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
it tries to reuse an already created cell with the passed identifier #"cell". This saves memory and optimises the performance. If not possible it creates a new one.
So now we got a cell with layout elements already in place and filled with your data. Your code then adds new elements on top of the old ones. Thats why your layout is messed up. And it only shows if you scroll, because the first cells got no previous cells to load.
When you subclass the cell try to create the layout only once on first initialisation. Now you can pass all values to the respective layout element and let the tableview do its thing.

Try this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil
{
cell = UITableViewCell.init(style: UITableViewCellStyle.default, reuseIdentifier: "cell")
}
for subView in cell.subviews
{
subView.removeFromSuperview()
}
// Your Code here
return cell
}

Related

Can the same UITableviewcell be used in multiple UITableViews?

What I want to ask you is "Can one UITableviewcell be used for multiple tableview like viewholder that can use anytime for recyclerview in android?" what I used to do is in one viewcontroller I have a tableview with a custom Cell and gave its identifier as normal but if I trying to use another uitableview in another Viewcontroller with that cell that inside the previous tableview, it always gives me a blank cell with white background. is there a way to use it like that?
EDIT: Here is what my tableview look like when i've already set cellforrow for it already.
Click to view and here what my cell look like Click to view cell and here are my code for different cell in a tableview, It'll work if i use use those 2 cell in current tableview
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0{
let cell = self.mytable.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HistoryItemTableCell
let view = UIView(frame: CGRect(x: 0, y: cell.frame.maxY, width: cell.frame.size.width, height: cell.frame.size.height))
view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
cell.selectedBackgroundView = view
let order = OrderItemObj
cell.num_of_day.text = "\(order.ticket_type.name)"
cell.ref_num.text = order.order_tx_number
cell.quantity.text = order.number_tickets
cell.price.text = "$\(order.ticket_type.price).00 USD"
if order.status == "unpaid"{
cell.ic_status.image = UIImage(named: "ic_status_unpaid")
}else{
cell.ic_status.image = UIImage(named: "ic_status_paid")
}
cell.start_date.text = "\(order.start_date)"
cell.end_date.text = "\(order.expired_date)"
return cell
}else{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! OrderDetailTicketCell
let t = listTicket[indexPath.row]
cell.dob.text = t.dob
cell.gender.text = t.sex
cell.nation.text = t.nationality
let url = URL(string: t.photo)
cell.imageN.kf.setImage(with: url)
return cell
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 3
}else{
return self.listTicket.count
}
}
override func viewDidLoad(){
super.viewDidLoad()
mytable.register(HistoryItemTableCell.self, forCellReuseIdentifier: "cell")
ViewHistoryItem()
mytable.dataSource = self
mytable.delegate = self
}
Yes you can. You have to register it again for the new tableView. It is just like how you create variables using the same type. This is also a class which can be used to create objects. Doesn't matter where you want to use it.
On the other hand if you are asking if instances of the same cell which are present in a tableView can be reused in another tableView, then the answer is no, because they have only been registered for that particular tableView.

Overlapping Buttons due to Cell Reuse

I am having some trouble with the dequeue reusable cell function in my UITableView. The tableview has a couple of cells, each of which contains a button.
When I scroll, the cell is recreated, and new buttons begin to overlap old buttons (until I have a bunch of the same buttons in the same cell). I've heard that your supposed to use the removeFromSuperview function to fix this, but i'm not really sure how to do it.
Here is a picture of my app:
And here is the cellForRowAtIndexPath (where the problem is occurring)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
let nameLabel: UILabel = {
let label = UILabel()
label.text = "Sample Item"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let actionButton = YSSegmentedControl(
frame: CGRect.zero,
titles: [
"No",
"Yes"
])
The reason you are seeing multiple buttons appear is because the cellForRowAtIndexPath: method is called every time a new table cell is needed. Since you are likely creating the button in that method body, it is getting recreated every time the cell is reused and you'll see them stack on top like that. The correct way to use dequeueReusableCell: with custom elements is to create a subclass of a UITableViewCell and set that as the class of your table cell in your storyboard. Then when you call dequeueReusableCell: you'll get a copy of your subclass that will contain all of your custom code. You will need to do a type conversion to get access to any of that custom code like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
if let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as? MyCustomCellClass {
cell.nameLabel.text = "Sample item"
}
// This return path should never get hit and is here only as a typecast failure fallback
return tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath);
}
Your custom cell subclass would then look something like this:
class MyCustomCellClass: UITableViewCell {
#IBOutlet var nameLabel: UILabel!
#IBOutlet var actionButton: UIButton!
#IBAction func actionButtonPressed(_ sender: UIButton) {
//Do something when this action button is pressed
}
}
You can add new label/button in cellForRowAtIndexPath, but you need to make sure there is no existing label/button before you create and add new ones. One way to do it is to set a tag to the label/button, and before you generate new label/button, check if the view with the tag is already in the cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
if let label = cell.viewWithTag(111) as? UILabel
{
label.text = "Second Labels"
}
else{
let label = UILabel()
label.tag = 111
label.text = "First Labels"
cell.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.frame = CGRect(x:0, y:10, width: 100, height:30)
}
return cell
}

Number cells for UITableView

I am trying number cell from 0-X for a uitableview. Currently I am using a label to do it and I just want the labels text to be 0,1,2,3,4,...X.
//label for the cell
let cellFrame: CGRect = CGRectMake(0, 0, 90, 32)
var cellLabel = UILabel(frame: cellFrame)
cellLabel.textAlignment = .Center
cellLabel.font = UIFont.MDFont.regularFourteen
cellLabel.text = ("\(indexPath.row)")
I have tried this approach and it just does 0,1,2,3,4 but when I scroll it prints 0,1,2,3,4 on top of the other numbers as well.
This is the whole code for the function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
if cell == null {
//label for the cell
let cellFrame: CGRect = CGRectMake(0, 0, 90, 32)
var cellLabel = UILabel(frame: cellFrame)
cellLabel.textAlignment = .Center
cellLabel.font = UIFont.MDFont.regularFourteen
cellLabel.text = ("\(indexPath.row)")
cell.contentView.addSubview(cellLabel)
}
//print("Table data is \(tableData[5])")
return cell
}
I think there is issue related reusable cell. kindly try below code. may it help you.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
if cell == null {
//label for the cell
let cellFrame: CGRect = CGRectMake(0, 0, 90, 32)
var cellLabel = UILabel(frame: cellFrame)
cellLabel.textAlignment = .Center
cellLabel.font = UIFont.MDFont.regularFourteen
cellLabel.text = ("\(indexPath.row)")
cell.contentView.addSubview(cellLabel)
}
else {
for obj : AnyObject in cell.contentView.subviews {
if let label = obj as? UILabel {
label.text = ("\(indexPath.row)")
}
}
}
//print("Table data is \(tableData[5])")
return cell
}
Try this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
if let cellLabel = cell.contentView.viewWithTag(10) as? UILabel {
cellLabel.text = ("\(indexPath.row)")
}
else {
let cellLabel = UILabel(frame: CGRectMake(0, 0, 90, 32))
cellLabel.tag = 10
cellLabel.textAlignment = .Center
cellLabel.font = UIFont.MDFont.regularFourteen
cellLabel.text = ("\(indexPath.row)")
cell.contentView.addSubview(cellLabel)
}
//print("Table data is \(tableData[5])")
return cell
}
When you compare variables you have to use nil.
Also in your case if the cell was nil, you should have created one. Because as you have it you try to add a label to a cell that doesn't exist.
See, when your table is loaded, your cells are nil for the visible region of screen... Here, its 4 cells, so your cell is nil condition will be satisfied for all 4 cells and you will get 4 cells having 0,1,2,3 text. But now when you scroll down, your cell will be reused and it wont go inside the condition so, it will show the old value that you've added in one of the above cells and hence will get wrong values.. You can use a simple approach:
Give tag (say, 1234) to your label when you allocate it when your cell is nil and access it outside the if condition, using that tag get that label and set text to it outside the if condition.. So, it will be called everytime and you will get 0,1,2,3,....X as you wanted...
Replace cellForRowAtIndexPath with this.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell : UITableViewCell = UITableViewCell(frame: CGRectMake(0, 0, tableView.frame.size.width, 44))
let cellLabel = UILabel(frame: CGRectMake(0, 0, 90, 32))
cellLabel.tag = 10
cellLabel.textAlignment = .Center
cellLabel.text = ("\(indexPath.row)")
cell.addSubview(cellLabel)
return cell
}
Hope this helps you. :)
I think the problem is getting your row index using indexPath.row. Just create a local variable with the index thats added to the cell in the function cellForRowAtIndexPath() before the cell is returned. Then just reset the cellIndex=0 before calling reloadData(). I believe that would work
i.e
var cellIndex = 0
func ..cellForRowAtIndexPath ...()
{
//set the index to the cell label.
cell.label = cellIndex++
return cell
}

Retaining button content in UICollectionView after scrolling with swift

I'm trying to make a UICollectionView that has infinite scrolling of buttons and the button's background is populated base on the result of http request to a server.
let reuseIdentifier = "Cell"
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
let categoryApiUrl = "url"
let categoryImageField = "field"
class BrowseViewController: UICollectionViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var categoryImgUrl:[String] = []
var buttonList:[UIButton] = []
func setupView(){
self.title = "Browse"
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.itemSize = CGSize(width: screenWidth/2-15, height: screenHeight/3.5)
collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView!.dataSource = self
collectionView!.delegate = self
collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
collectionView!.backgroundColor = UIColor.whiteColor()
self.view.addSubview(collectionView!)
}
func setupButton(cell: UICollectionViewCell, cellNumber: Int){
var button = UIButton.buttonWithType(UIButtonType.System) as UIButton
button.frame = CGRectMake(0, 0, screenWidth/2-15, screenHeight/3.5)
button.backgroundColor = UIColor.orangeColor()
button.setTitle("Category", forState: UIControlState.Normal)
button.addTarget(self, action: "btnClicked:", forControlEvents: UIControlEvents.TouchUpInside)
buttonList.append(button)
cell.addSubview(button)
}
override func viewDidLoad(){
super.viewDidLoad()
let url = NSURL(string: categoryApiUrl)
let request = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {(response, dataValue, error) in
let json = JSON(data: dataValue)
for(var i = 0; i < json.count; i++){
self.categoryImgUrl.append(json[i]["CATEGORY_IMAGE"].stringValue)
let imageUrl = self.categoryImgUrl[i]
let url = NSURL(string: imageUrl)
let data = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
self.buttonList[i].setBackgroundImage(image, forState: .Normal)
}
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
//#warning Incomplete method implementation -- Return the number of sections
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//#warning Incomplete method implementation -- Return the number of items in the section
return 10;
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as UICollectionViewCell
let cellNumber = indexPath.row as Int
setupButton(cell, cellNumber: cellNumber)
// Configure the cell
return cell
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height {
numberOfItemsPerSection += 6
self.collectionView!.reloadData()
}
}
}
Currently, the code is able to pull the image from the server and populate it as the button's background image.
However, since I made this collection view scrollable. When I scroll the view down and then back up, the background image of the previous buttons disappear.
I did some research but couldn't find a solution to it. The reason that the button disappears is because IOS only loads the cell that is visible on screen. So when I scroll down and then scroll back up, the previous cells are consider as "New Cells". Therefore the background image that was in it are now gone.
Questions:
Does anyone have an idea on how to retain the previous buttons even if we scroll down and then scroll back up? In addition, with my current code, I added the image onto the button inside the http request because the http request is always the last execution that finishes. Is there anyway to change the code so then the http request will be finish before the cells get loaded?
I would suggest to create uicollectionview in interface builder, subclass uicollectionviewcell, add one dynamic cell to collectionview, change its class to collectionviewcell you subclassed, drop uibutton on it, and everytime cell is being created, you would download the image.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as myCollectionViewCell
let cellNumber = indexPath.row as Int
//downloadimghere
cell.myButton.setBackgroundImage(downloadedImg, forState: .Normal)
return cell
}
This would download image everytime cell is being created. For more info you should checkout "lazy image loading". I think this is a better approach to your problem.
Now to your code, first of all you are not using your buttonList array, everytime cell is being created you create a new button and place it there, so you are not reusing already created buttons. If you fixed this, it might work like you wanted.
Here is another problem, since collectionview is reusing cells, everytime you create a button and place it on cell, it stays there, so basically now you are creating button on button. So if you want this to work correctly and have only one button on your cell, you need to remove previous button from the cell before you create it, you can do this in cellForItemAtIndexPath.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as myCollectionViewCell
//something like this
for view in cell.subviews(){
if view == <UIButton>{
view.removeFromSuperview()
}
}
return cell
}
There might be some syntax errors in my code, I didnt test it, but you get the idea how to do it.

Button in cell is being added when it is not supposed to be

So I have a settings button and it is only supposed to be added to the cell with the current users name. However, it seems to being randomly added to a cell. In the statement in which I create the button it is only being created once however it is being added to multiple cells. I have attached images of the problem and ps the current username is "test", so the settings button should not be in the same cell as the matt short user. Thanks and below is the attached code of the function in which i am creating and adding the button subview.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("chatCell") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "chatCell")
}
cell!.selectionStyle = UITableViewCellSelectionStyle.None
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
var sectionTitle = self.friendSectionTitles[indexPath.section]
var friendArray: [String]! = friendDict[sectionTitle]
var friend = friendArray[indexPath.row]
if sectionTitle == "me" && friend == PFUser.currentUser().username {
var settingsButton: UIButton = UIButton.buttonWithType(UIButtonType.System) as UIButton
settingsButton.frame = CGRectMake(screenWidth - 100 , 5, 50, 30)
settingsButton.addTarget(self, action: "settingsButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside)
settingsButton.setTitle("Settings", forState: UIControlState.Normal)
cell!.addSubview(settingsButton)
}
cell!.textLabel!.text = friend
return cell!
}
In the statement in which I create the button it is only being created once however it is being added to multiple cells
Because cells, once created, are reused for other rows of the table. If you don't want the button to appear in a reused cell, you will need (in your implementation of cellForRowAtIndexPath:) to detect its presence and remove it (or at least hide it) for every row that is not supposed to have it.

Resources