Collection View inside Table does not load immediately in swift 3 - uitableview

I add one table view in Home View Controller. Inside table view, I add Collection View.
An Image inside collection view at first time running the app. Then, goes to other links and come back to Home but image in the collection view does not show immediately until it scrolls at this second time.
After pulling the view several times, then Image appears.
In "Collection.swift" ,In ViewDidLoad() ,
DispatchQueue.main.async {
self.tableView.reloadData()
}
I also add in viewWillLayoutSubviews()
override func viewWillLayoutSubviews() {
debugPrint("viewWillLayoutSubviews")
DispatchQueue.main.async(execute: {() -> Void in
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
})
}
and I also reload the Collection View
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let mainThemeList = mainHomeThemeTable[(indexPath as NSIndexPath).row]
self.prefs.set(mainThemeList.main_associated_url, forKey: "associated_home_url")
let cell = tableView.dequeueReusableCell(withIdentifier: "homecell") as! HomeCategoryRowCell
DispatchQueue.main.async {
cell.categoryTitle.text = mainThemeList.main_name
cell.mainAssociatedURL.text = mainThemeList.main_associated_url
cell.categoryTitle.font = UIFont.boldSystemFont(ofSize: 17.0)
cell.collectionView.reloadData()
}
return cell
}
In Table View Cell (CollectionCell.swift) file ,
override func awakeFromNib() {
self.collectionView.reloadData()
self.collectionView.setNeedsLayout()
self.collectionView.layoutIfNeeded() }
override func layoutSubviews() {
super.layoutSubviews()
self.collectionView.reloadData()
self.collectionView.setNeedsLayout()
self.collectionView.layoutIfNeeded()
}
How can I do to load and show immediately in swift 3? Can anyone help me?

You cannot rely on the sequence of the calls. Currently you are trying to pass setIndex from the table view's cellForRowAtIndexPath to the collection view's delegate methods. A simple way to fix this is rather than using a single variable to pass the row number, instead pass it in the collection view's tag property. Then each collection view will know it's relevant row number. i.e.
In the table view's cellForRowAtIndexPath:
cell.collectionView.tag = indexPath.row
And then in the collection view's numberOfItemsInSection:
return self.model.sets[collectionView.tag].subsets!.count

Related

After tableView scrolled data puts in cells in wrong order

in my View:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TransactionTableCell", for: indexPath) as! TransactionTableCell
let newItem = getTransactionsInSection(section: sectionHeader[indexPath.section])[indexPath.row]
cell.configure(item: newItem)
}
in my TransactionTableCell
func configure(item: TransactionModel) {
guard let withdrawalBonuses = item.withdrawalBonuses,
withdrawalBonuses < 0,
let accruedBonuses = item.accruedBonuses,
accruedBonuses > 0 else {
configureWithOneOperation(item)//shows one line of operation
return
}
//show 2 lines of operations
firstOperationAmountLabel.text = "+\(Int(accruedBonuses))"
secondOperationAmountLabel.text = "\(Int(withdrawalBonuses))"
}
When I scroll the cell , second operation line is appears in wrong cells where its shouldn't be, even If I reload my table , that also has this problem.
You should use prepareForReuse() method
Simply just clear data of your labels:
override func prepareForReuse() {
super.prepareForReuse()
firstOperationAmountLabel.text = nil
secondOperationAmountLabel.text = nil
}
There are few things to check here.
Make sure you reset all fields before configure a new cell.
If you have created a cell using xib or storyboard, make sure you haven't filled labels with static text.
Is your guard statements passing for every item?
Else block for guard configures cell with a single operation, Is it handling all ui elements in cell?

Call reloadData() to refresh TableView - Reusable Code

I need the tableView to perform reloadData() after a row has been added via a textView. My tableViews all use the reusable code which works fine.
Below is my Reusable TableViewCode.
class ReusableSubtitleTable: NSObject, UITableViewDataSource, UITableViewDelegate{
let cell = "cell"
var dataArray = [String]()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) {
print("DataArray count from table view = \(dataArray.count)")
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let selfSizingCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SelfSizingCell
let num = indexPath.row
selfSizingCell.titleText.text = (stepText[num])
selfSizingCell.subtitleText.text = dataArray[num]
return selfSizingCell
}
}
The function below uses the reusable code to display the table which works fine.
class DetailViewController: UIViewController {
let step = 13
var tableView: UITableView!
let dataSource = ReusableSubtitleTable()
var selectedEntry: JournalEntry!
var dataModel = [String]()
var didSave = false
var coreDataManager = CoreDataManager()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = dataSource
tableView.dataSource = dataSource
dataSource.dataArray = dataModel
#IBAction func unwindToDetail( _ sender: UIStoryboardSegue) {
dataModel[10] = step11
didSave = coreDataManager.updateEntry(step11: step11, selectedEntry: selectedEntry)
}
}
The problem come in when a user wants to add to the last row. The user taps a button and is taken to the next controller which is a TextView. When user finishes their entry they tap the 'Save' button which returned to the DetailViewController via an unwind. The selectedEntry is saved and the dataModel updated. Now the table view needs to reload to display this added text.
I've tried adding tableView.ReloadData() after didSave. I've tried a Dispatch and tried saving the data before returning from the textView via the unwind but that doesn't work either.
I tried adding the below function to ReusableTableView and called it after the coredata update - there are no errors but it does not reload the table.
func doReload(){
tableView.reloadData()
}
I have verified that the data is saved and it does displays if I return to the summary controller and then go forward the DetailViewController.
Any help is appreciated.
Placing the UITableView reloadData() within either viewWillAppear or viewDidAppear should resolve this issue.
For example:
func viewDidAppear(_ animated: bool) {
super.viewDidAppear(animated)
tableView.reloadData()
}
This is because the view hierarchy isn't yet regarded as "visible" during the segue unwind and why you see it work by going back to reloading the view controller. The reloadData() function, for efficiency, only redisplays visible cells and at the time of the unwind the cells aren't "visible".
Apple Documentation - UITableView.reloadData():
For efficiency, the table view redisplays only those rows that are
visible.

indexPath is nil when passing cell from delegate to UITableViewController

I have a button called addSet at the end of each section of my tableView, it is used as a footerView and it is supposed to tell the UITableViewController of when it is pressed and in which section. My code for the custom table view cell is as follows
import UIKit
class FooterTableViewCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
var footerDelegate:FooterTableViewCellDelegate?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func addSetIsPressed(_ sender: AnyObject) {
print("Add Set is pressed")
footerDelegate?.didAddSetIsPressed(cell:self)
}
}
protocol FooterTableViewCellDelegate {
func didAddSetIsPressed(cell:FooterTableViewCell)
}
And in my TableViewController, I implement it like so
func didAddSetIsPressed(cell: FooterTableViewCell) {
let indexPath = tableView.indexPath(for: cell)
print("Index path is \(indexPath)")
}
I want to get the indexPath (the section specifically) when the user taps my button, however it always returns nil. What am I doing wrong?
To put things in context. I am using this cell as a footerView, so the cell is implemented like so
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "footerCell") as! FooterTableViewCell
cell.footerDelegate = self
return cell
}
so it isn't implemented in cellForRow at indexPath like it would normally be
Thanks in advance.
The thing is you put the cell FooterTableViewCell as a viewForFooterInSection,
so it's not used as a UITableViewCell in the UITableView, so the UITableView is not holding the indexPath of this UITableViewCell "Cause i said previously, the cell's view only is used as a footerView"
You need to add the button inside the cell that's being rendered on the UITableView. "The one that's being returned in the tableView(_:cellForRowAt:) method"
On a side note i noticed that you have a variable named footerDelegate in your cell, it needs to be weak to avoid memory leaks as you assign your TableViewController as this delegate,
so the UITableViewCell holds a strong reference of the TableViewController that leads to memory leak cause also in the view hierarchy the TableViewController contains the UITableView as a subView.
I found out how to do it, in order to detect the section in which the button was tapped. There must be an outlet reference in the FooterCell and in the tableViewController, in viewForFooter in Section, just add the following line
cell.addSetOutlet.tag = section

Dynamic table view cell height for embedded collection view that is filled after dequeue of table view cell

I have a collection view inside a table view cell at row = 1, that is loaded with content from Firebase Database after the cell is already dequeued. Therefore, using AutoDimensions on the cell at heightForRowAt: doesn't work since at that time, there is no content within the collection view. The collection view is anchored to all sides of the cell. What's the best way of updating the table view cell height at that specific index path when the collection view has loaded all of the data. Thanks!
In the VC containing the table view, I've set up a func retrieving a list of users from the DB that is called in viewDidLoad(), and I've set up protocols to reload the collectionView that is located in the tableView cell class.
fileprivate func retrieveListOfUsers() {
print ("fetching users from database")
let ref = Database.database().reference()
guard let conversationId = conversation?.conversationId else {return}
ref.child("conversation_users").child(conversationId).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionaries = snapshot.value as? [String:AnyObject] else {return}
for (key, _) in dictionaries {
let userId = key
Database.fetchUsersWithUID(uid: userId, completion: { (user) in
self.users.append(user)
DispatchQueue.main.async {
self.reloadDelegate.reloadCollectionView()
}
})
}
}, withCancel: nil)
}
This is the code for dequeuing the cell. I've removed the code for the first cell in this post since it's irrelevant and didn't want to make you read the unnecessary code.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Code for cell in indexPath.row = 0
if indexPath.row == 1 {
let usersCell = tableView.dequeueReusableCell(withIdentifier: usersCellId, for: indexPath) as! UsersTableViewCell
usersCell.backgroundColor = .clear
usersCell.selectionStyle = .none
usersCell.collectionView.delegate = self
usersCell.collectionView.dataSource = self
self.reloadDelegate = usersCell
usersCell.users = users
return usersCell
}
}
If I return UITableView.automaticDimension the collection view doesn't show. It only shows if I set the func to return a constant like 200. I've set estimatedRowHeight and rowHeight in viewDidLoad().
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return UITableView.automaticDimension
}
return UITableView.automaticDimension
}
After you load the data source for the collection view, you can update specific rows in your table like this:
UIView.performWithoutAnimation {
tableView.reloadRows(at: [indexPath], with: .none)
}
For the row height - try to remove the method implementation, it should be enough if you already set a value for estimatedRowHeight.

Showing and hiding a view only on a specific cell of a table view

I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.

Resources