Collection View inside UITableView reuse issue - ios

I have a TableView with a CollectionView inside.
So each Table cell is the delegate of the CollectionView.
The problem is that the collection View reloads on the main thread and the UITableView Cells are being reused.
That causes the collectionView cells to briefly present the labels of previous cells and then fade the changes.
How can I hide those labels when the tableView is initially passed through the return cell ?
The problem happens as collection.reload() is applied after the return cell of the UITableView in cell for row.
Here is a sample of the code.
UITableViewCell Code
let collectionDatasource = CollectionDataSource()
func configureCell(_ object: CustomClass) {
//Configure Object for tableView
self.presentedObject = object
self.nameLabel.text = object.name
//Set object and items on collection datasource
DispatchQueue.main.async {
let collectionItems = object.collectionItems ?? []
self.collectionDatasource.object = object
self.collectionDatasource.items = collectionItems
}
}
class CollectionDataSource: NSObject {
private var collectionView: UICollectionView?
var items: [CustomItem] = [] {
didSet {
DispatchQueue.main.async {
let changes = [CustomItem].changes(from: oldValue, to: self.items, identifiedBy: ==, comparedBy: <)
self.collectionView?.apply(changes: changes)
}
}
}
}

UICollectionView itself does its work asynchronously in contrast to UITableView. So, even if you will call reload collectionView directly in configureCell it will update the collection's cells after the table is set up (still can result in the mentioned issue). But you have added the "reload" call into DispatchQueue.main.async which makes the things worse.
You should skip using DispatchQueue.main.async if possible.
You should replace collectionView with a new one in configureCell. I suggest to add view into the cell of the same size and position as collection view and add collection view in the code. This will guarantee that previous collection view will never appear. 👍

Related

iOS: How to adjust uikit row height based on height of dynamic embedded swiftui view?

I'm working on a legacy app and trying to embed a swiftUI view inside a UIKit UITableView. The SwiftUI view is a LazyHGrid with a number of items based on a user's number of accounts. The idea is to display the first 1-3 items (if there are any), and a "More" button, which, when tapped, displays the rest of the items.
The SwiftUI piece is working correctly, and initially the tableView displays it correctly, like so:
three tiles working correctly. However, when I tap the "More" button, the other items pop in, but the row height in the table doesn't adjust, so they just overlap the other content in the table, like so: tiles overlapping other content.
Here's the host function that controls the swiftUI view:
func host(_ view: Content, parent: UIViewController) {
hostingController.rootView = view
hostingController.view.invalidateIntrinsicContentSize()
let requiresControllerMove = hostingController.parent != parent
if requiresControllerMove {
// remove old parent if exists
removeHostingControllerFromParent()
parent.addChild(hostingController)
}
if !contentView.subviews.contains(hostingController.view) {
contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
}
if requiresControllerMove {
hostingController.didMove(toParent: parent)
}
}
I register the cell in the viewDidLoad in the UIKit controller:
self.tableView.register(SwiftUIHostTableViewCell<BillingTileGridView>.self, forCellReuseIdentifier: "BillingTileGridViewCell")
(This code was already present in the viewDidLoad):
self.tableView.estimatedRowHeight = 60
self.tableView.rowHeight = UITableView.automaticDimension
And then I create the cell like so in the tableView cellForRowAt function:
if let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID, for: indexPath) as? SwiftUIHostTableViewCell<BillingTileGridView> {
let view = BillingTileGridView(viewModel: BillingTileGridViewModel(), adjustHeight: {
})
cell.host(view, parent: self)
return cell
}
I tried using a callback to re-load the table height for the button action, like this:
let view = BillingTileGridView(viewModel: BillingTileGridViewModel(), adjustHeight: {
tableView.beginUpdates()
tableView.endUpdates()
})
like I'd seen in questions like this one, Swift: How to reload row height in UITableViewCell without reloading data, but it didn't seem to do anything. Do I just need to calculate the height manually based on how many rows I have, and set the height explicitly to that?

UICollectionView with full page cells VS UIScrollView with child view controllers

I'm trying to create a ViewController which would have swipe-able (android like tabs) pages. These pages themselves will have scrollviews(vertical) inside them and multiple views which would be added dynamically depending on type of response (different network calls for each page). I can't use a PageViewController as I want the pages to take up only half the screen.
Issues with CollectionView -
If the cells would get reused (or removed from memory), how would I maintain the state of the cell's UI and store that data (especially difficult for views as each page might have different type of view in them)
Issues with ScrollView -
I'm worried if there would be memory issues if all page view controllers would be in memory with each view in it
PS - data in each page would be 4-10 stackviews each containing 2-10 images/labels OR just one collectionview
PSS - Total number of tabs wouldn't exceed 10, minimum would be 1
I'd implemented it with collectionView cause it should be really more resource effective. But then we need to cache states of view controllers. Here is the example
Let's say you have controller A which contains collectionView with cell with your child controllers. Then in cell for row
....
var childrenVC: [Int: UIViewController] = [:]
....
// cell for row
let cell: ChildControllerCell = collectionView.dequeueReusableCell(for: indexPath)
if let childController = childrenVC[indexPath.row] {
cell.contentView.addSubview(childController.view)
childController.view.frame = cell.contentView.frame
} else {
let childViewController = ChildViewController()
addChildViewController(childViewController)
childViewController.didMove(toParentViewController: self)
cell.contentView.addSubview(childController.view)
childController.view.frame = cell.contentView.frame
childrenVC[indexPath.row] = childViewController
cell.childVC = childViewController
}
return cell
....
class ChildControllerCell: UICollectionViewCell {
var childVC: UIViewController?
override func prepareForReuse() {
super.prepareForReuse()
if !contentView.subviews.isEmpty {
childVC?.willMove(toParentViewController: nil)
childVC?.view.removeFromSuperview()
childVC?.removeFromParentViewController()
}
}
}

how to hide the image/label in StoryBoard to show up when we run the app?

I am making a table view app which retrieves the data from the Firebase. when making the user interface in the storyboard, I am using dummy image and label to visualize my app.
but when i run the app which consists of dynamic table view, those dummy images and label also shows up before immediately replaced by the actual data that i download from the Firebase storage.
can I set those images and labels to not show up when i run the app but still available in the storyboard?
thanks in advance :)
If they're just dummies, you can get rid of them when your view loads, before it appears onscreen:
override func viewDidLoad() -> Void{
super.viewDidLoad()
dummy.removeFromSuperview()
}
Whenever you want to hide/show an UIView:
myView.isHidden = true // hide
myView.isHidden = false // show
I assume what you need is to hide the views in viewWillAppear and then show them when necessary.
In your custom cell class, define something to hide the unwanted views:
func hideDummyViews() {
// do some stuff to hide what you don't want, e.g.
myEnclosingStackView.isHidden = true
}
In your table view controller, in the cellForRowAt indexPath func:
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell ..
if yourDataSource[indexPath.row].isCompletelyLoaded {
// do your fancy dynamic cell layout
} else {
// show the basic version (minus any dummy views)
cell.hideDummyViews()
}
return cell
You can choose your preferred method for hiding the items (isHidden for each view, removing, adjusting constraints). I prefer to embed any disappearing views in a stack view and then use isHidden = true on the enclosing stack. This keeps things organized in your storyboard/XIB file and neatly recalculates constraints for the hidden stacks.
It seems that you want to show some empty (or incomplete) cells until database content arrives and then you will reload each cell as you process new entries in the datasource. This answer will initially give you a set of cells appearing as per your storyboard/XIB, minus the hidden dummy elements. Then as items in your datasource are loaded fully, you can reload the cells.
By the way, it seems like a lot of work to carefully layout these dummy views for "visualization" and then never show them in the app. Why not have some user-friendly place holders or progress indicators showing and then animate in the real/dynamic views as the data arrives?
I assume you download the Image from FireBase and does't want the dummy Image to appear
(Why dont you declare an empty ImageView and an empty Label!). Try setting an Image array in the viewController. Or you can use a struct array if you want a Image and label text together.
var arrImage=[UIImage]()
var arrLblTxt=[String]()
In view did load append your Demo Image's if required.
arrImage.append(UIImage("Demo"))
arrLblTxt.append("Demo")
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tbl_AssignEmployees.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as YourCell
if arrImage[IndexPath.row] != UIImage("Demo")
{
cell.ImageView.Image = arrImage[indexPath.row]
cell.textLabel!.text = arrLblTxt[indexPath.row]
}
else
{
cell.ImageView.Image = nil
cell.textLabel!.text = arrLblTxt[indexPath.row]
}
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsetsZero
cell.layoutMargins = UIEdgeInsetsZero
return cell
}
Then After you download your Image From FireBase,
Reset your Image array with the new Images downloaded from fireBase
Then reload your tableView. tableView.reloadData

UITableView doesn't scroll automatically if the datasource changes

I have a UITableView with two arrays as datasource. I also have one UISegmentedControl to switch between these arrays. I use the segmentedControlChanged method to call the reloadData method.
This works fine.
But there is one problem.
For example arrayOne has 1 item and arrayTwo has 10 items.
So if I switch to the arrayTwo and scroll to the bottom of the tableview and after that switch back to arrayOne the tableview stays scrolled down and i don't see any item. But if I start scrolling, the tableview "jumps" automatically to the first item.
So my question is, how can i trigger this behaviour automatically when the tabledata source changes?
You can use any one of following two lines after reloading table
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Top, animated: true)
or
mainTableView.setContentOffset(CGPoint.zero, animated: true)
Firstly i don't know how you change data source of tableview but i use this method for my data source switches.
I create 3 arrays;
var mainArr = [String]()
var sortedArr = [String]()
var arrToReturn : [String] {
if segmentedController.selectedIndex == 0 {
return mainArr
}
else {
return sortedArr
}
}
I always return arrToReturn for tableView data source.
When it comes to your scrolling issue, it sounds like you are reloading tableView before changing its dataSource.
After that when you start scrolling, tableView reloads cells and can't find old items and automatically sets its scrolling position to start of screen.
Also do you have any code about setting tableviews scrolling position even if it is irrelevant? If you have you should check it.

How can I identify a table row in iOS?

I made a prototype cell and I have data from a database to load into the cells in a table.
Each cell has a label and 3 buttons just like this:
If I click on a button for example "Meets Standard", how can I identify in which row I tapped the button?
So for example when I press the "Meets Standard" button at a given row I'd like to change the background color of that row to red. How can I do it?
I have a CustomCell.swift class where I configure the prototype cell and a TableView.swift class where I configure the table.
Try control dragging the button to your CustomCell.swift class and typing a name and selecting "Action" on the popup menu. Then, inside the generated method, you can call self.backgroundColor = UIColor.redColor() or perform any other operations that you'd like.
Edit:
Here's what I think you should do to change the height:
Make a boolean in your cell class called "expanded" or something. Then, go into your table view class and implement the heightForRowAtIndexPath method. In that method, retrieve the cell and check if it's expanded, and if so, return a larger height. Now, to make it reload, you will need to store a reference to the table view in each cell, as it says here: Reference from UITableViewCell to parent UITableView?
In that clicked method you already made, where the background is set to red, you will need to call:
tableView.beginUpdates()
tableView.endUpdates()
where tableview is the weak reference you already stored. Also, in that method, you need to add self.expanded = true of course.
Edit 2:
You know what, maybe it's easier to just do this:
weak var _tableView: UITableView!
...
func tableView() -> UITableView! {
if _tableView != nil {
return _tableView
}
var view = self.superview
while view != nil && !(view?.isKindOfClass(UITableView))! {
view = view?.superview
}
self._tableView = view as! UITableView
return _tableView
}

Resources