Detect UITableViewCells that appear on the screen - ios

I need to make a request when the user saw the UITableViewCell, I used func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) but this method call for each UITableViewCell, before it's displayed, but I need a delegate when UITableViewCell appeared in the screen.

What you need to do is detect when scrolling occurs by implementing the scrollViewDidScroll(_:) delegate method. When the delegate method is called you can determine which cells are visible by checking the tableView.indexPathsForVisibleRows property. Note that this property returns the IndexPath for all cells that are both fully visible and partially visible.
Assuming you that you are only concerned with cells that are fully visible, you can check the frame for each row to determine if it is in fact fully visible.
To determine which cells are newly visible, you need to keep track of cells that were previously visible and compare the two sets.
This code will let you know as soon as soon as a cell is displayed:
var previousVisiblePaths = Set<IndexPath>()
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let visiblePaths = tableView.indexPathsForVisibleRows?.filter ({
let rect = tableView.rectForRow(at: $0)
return tableView.bounds.contains(rect)
}).reduce(into: Set<IndexPath>(), { result, indexPath in
result.insert(indexPath)
}) else { return }
let newVisiblePaths = visiblePaths.subtracting(previousVisiblePaths)
if newVisiblePaths.count > 0 {
print("Just displayed: \(newVisiblePaths)")
}
previousVisiblePaths = visiblePaths
}

Related

How to narrow UITableView cell height if there is dynamic structure in Swift?

I have a tableView and cells. The Cells are loaded from a xib and they have a label with automatic height. I need to narrow one cell if the user taps on it.
I have tried hiding - doesn't work
I have tried removeFromSuperView()- doesn't work
Is there any alternative?
When setting up your tableViewCell store the height anchor you want to update
var yourLabelHeightAnchor: NSLayoutConstraint?
private func setupLayout() {
yourLabelHeightAnchor = yourLabel.heightAnchor.constraint(equalToConstant: 50)
// Deactivate your height anchor as you want first the content to determine the height
yourLabelHeightAnchor?.isActive = false
}
When the user clicks on a cell, notify the tableView that the cell is going to change, and activate the height anchor of your cell.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourTableViewCellIdentifier") as? YourCell
self.tableView.beginUpdates()
cell?.yourLabelHeightAnchor?.isActive = true
self.tableView.endUpdates()
}
Did you try to do something like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var result: CGFloat
if (indexPath.row==0) {
result = 50 }
else {result = 130}
return result
}
This is just an example where height is changed for the first row. I tested on my application and it gave result like this.

TableView empties when interacting

So I have 2 Views that are shown inside a UIView, based on what is selected on the SegmentViewController. I create dummy data, returning 20 row of a custom cell. This works great.
Everything is fine, till I interact with the TableView.
Bellow is my code:
GoalsViewController.swift
import UIKit
class GoalsViewController: UIViewController {
#IBOutlet weak var goalsTableView: UITableView!
let goalCellIdentifier = "goalCell"
override func viewDidLoad() {
super.viewDidLoad()
goalsTableView.delegate = self
goalsTableView.dataSource = self
goalsTableView.register(UINib(nibName: "GoalsViewCell", bundle: nil), forCellReuseIdentifier: goalCellIdentifier)
goalsTableView.reloadData()
}
}
extension GoalsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = goalsTableView.dequeueReusableCell(withIdentifier: goalCellIdentifier, for: indexPath) as! GoalsViewCell
cell.goalTitle.text = "aaaaa"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected \(indexPath.row)")
}
}
After any of the empty rows is selected, the didSelectRowAt is not called, so the cells are not there at all. I tried to find a solution, but I was only to find issues about empty lists, before being populated.
What could be the reason for the empty tableview?
I might be wrong here but one thing that I've noticed is that you are not implementing a function which sets the height of each cell.
// Specify the height of your cells
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100 // Or your given cell height.
}
So here is my theory: If you are using constraints something is missing and your cell's height can't be identified by constraints alone or you are not using constraints at all thus you must use heightForRowAt function to specify each cell's height.
I will explain what was the issue for people who probably did not know (like me).
So my UITableView is inside a UIView that changes based on what the user is selecting. In total I had 2 different Views that where switching. The reason that it was emptying it was because my parent ViewController, could not access the delegate for UITableView. To fix that, after adding a subview to the UIView, you need also to move the ViewController to the parent controller. In code it goes like this.
// Empty array of UIViewControllers
var views: [UIViewController]!
// Add the UIViewControllers to the array
views = [UIViewController()]
views.append(EventsViewController())
views.append(GoalsViewController())
for view in views {
// Needed to adjust the size of Subview to size of View
view.view.frame = containerView.bounds
// Add the subviews
containerView.addSubview(view.view)
}
// Bring the view in position 1 to the front of the UIView
containerView.bringSubviewToFront(views[1].view)
// Add Views[1] UIViewController as a child to the parent controller
self.addChild(views[1])
views[1].didMove(toParent: self)
// After done with everything with the UIViewController remove it
views[1].removeFromParent()
addChild Apple.com
didMove Apple.com

Identify when the UITableView loads the visible set of rows in iOS

I am looking for the event that gets fired when the tableview loads the rows to fit the screen. I know tableview loads only the number of rows that fit the screen. I want to execute a set of code when the rows that fit the screen are loaded.
Any pointers on how to determine this?
I think you'd need to implement UITableViewDelegate and override the WillDisplay(UITableView, UITableViewCell, NSIndexPath) method. Be sure to set the delegate of your tableview to the class that implements UITableViewDelegate.
Unfortunately WillDisplay is called per cell, not per row.
I am not good at c#, so please translate this from Swift. I've added another solution down below which is, sort of a manual calculation of visible rows in tableview.
The Recommended Solution:
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let indexPaths = tableView.indexPathsForVisibleRows {
if indexPath == indexPaths.last {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}
}
The isFirstTime (Very important) flag will restrict your specific "set of code" (self.visibleCellsLoaded()) to execute only once. You can remove it if you want it to be executed every time you scroll - which apparently negates the purpose of your question.
Another solution that also works:
Here we manually calculate and get what tableView.indexPathsForVisibleRows returns us (Result is mostly similar to the previous method)
fileprivate func getLastVisibleIndexPath() -> IndexPath {
//This samples the first indexPath only, so this works only with rows that have static height, not with UITableViewAutomaticDimension.
let firstIndexPath = IndexPath.init(row: 0, section: 0)
let tableViewHeight = self.tableView.bounds.height
let rowHeight = tableView.rectForRow(at: firstIndexPath).size.height
let numberOfVisibleRows = tableViewHeight / rowHeight
return IndexPath.init(row: Int(numberOfVisibleRows - 1), section: 0)
}
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath == getLastVisibleIndexPath() {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}

Pass message from ChildViewController toParentViewController before ParentViewController's delegate methods are called

Scenario:
I have 2 VC -
ChildViewController
It has a tableView which displays a list of items. I need to pass the tableView.contentSize.height value, after the table is populated to my ParentVC. For that I am using delegate as
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableVieww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath)
cell.textLabel?.text = "heyy"
hght.constant = tableVieww.contentSize.height
if flag == true
{
delegate.tableHeight(tableVieww.contentSize.height)
print(tableVieww.contentSize.height)
flag = false
}
return cell
}
ParentViewController
It has a tableView with one cell. This cell is showing view of a childVC i.e nwVC. I want to change the cell height depending upon the height of my ChildVC's tableView.
I am adding the childVC's view by the following code & I know this is the wrong place to do so but I am not getting how,where and what to do, to get the childViewController's function to be called before the ParentViewController's functions?
vc3 = self.storyboard?.instantiateViewControllerWithIdentifier("nwVC") as? nwVC//newVC is ChildViewController
vc3!.view!.frame = cell.myview.bounds
vc3!.didMoveToParentViewController(self)
cell.myview.addSubview(vc3!.view)//UIView inside the cell
vc3!.delegate=self
Problem -
The delegate methods of ParentViewController's tableView gets called before the childViewController's function's are called for which I cannot update my rowHeight as per the childVC's table content.
Finally,I figured out something that works but still I want suggestions from iOS dev's viewing this question.
Mark: I could not perform the loading of childVC's functions before the loading of ParentVC's table view delegate functions but I did something which works quite good.
In my ParentVC's
override func viewWillAppear(animated: Bool) {
vc3 = self.storyboard?.instantiateViewControllerWithIdentifier("nwVC") as? nwVC
addChildViewController(vc3!)
vc3!.didMoveToParentViewController(self)
vc3!.delegate=self
}
//childVC's delegate function implementation
func tableHeight(height: CGFloat) {
height = height//I get the table view height from the childVC,height is a variable declared as var height = 200.0(it can be any value > 0)
print(ht)
self.tableVIeww.reloadData()//reload my tableView
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return hgt//at first call it returns the default value but in the 2nd call it returns the value sent by childVC
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableVIeww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath) as! myTVC
cell.backgroundColor = UIColor.greenColor()
vc3?.view.frame = cell.myview.bounds
cell.myview.addSubview((vc3?.view)!)//myView is the view in the cell's content view pinned to its edges.
return cell
}
Pro's & Con's
Pro's
The biggest advantage is that you get the works to be done.
Con's
As you can see that ChildVC's view is added 2 times (1 with the default cell size of height variable & the 2nd time when the table reloads). I feel that this might hamper the performance slightly & if Data is dynamic it might process for a bit long.
Please feel free to suggest...

Display cells of tableview gradually using autolayout?

I have a tableview inside my UIViewController to display comments. The height of this tableview depends on the number and the size of comments. The cells are dynamic.
I use autolayout, so my tableview has a height constraint. I set this constraint programmatically :
override func viewDidLayoutSubviews() {
self.heightCommentsTableView.constant = self.commentsTableView.contentSize.height + 50
}
It works if I display all the comments at once.
BUT, I would like display the comments 5 per 5, using this method : load more for UITableView in swift because of performance issue to display my view
(all my comments are loaded before pushing the view)
I noticed when I set my constraint, it calls cellForRowAtIndexPath for all the comments, not only the first 5.
I don't know how to do.
EDIT
var allCommentsArray: NSMutableArray = []
var elements: NSMutableArray = []
var range = 5
var currentPage = 0
var nextpage = 0
override func viewDidLoad() {
super.viewDidLoad()
allCommentsArray = NSMutableArray(array: comments)
elements.addObjectsFromArray(allCommentsArray.subarrayWithRange(NSMakeRange(0, range)))
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return comments.count
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
self.nextpage = self.elements.count - 5
if indexPath.row == nextpage {
self.currentPage++
self.nextpage = self.elements.count - 5
self.elements.addObjectsFromArray(self.allCommentsArray.subarrayWithRange(NSMakeRange(self.currentPage, self.range)))
tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CommentCustomTableViewCell", forIndexPath: indexPath) as! CommentCustomTableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
The table view will call tableView(_:cellForRowAtIndexPath:) for every visible cell it is about to create, the amount of it will determine based on how many sections and how many cells in each section. You let the table vie know these amounts by implementing the tableView(_:numberOfRowsInSection:) and numberOfSectionsInTableView(_:) methods of UITableViewDataSource. So, if you want to control how many cells could possibly be created and visible at a given time, you'd have to manage that state in your data source by adding and removing according to whatever logic you desire. In the answer that you linked, you can see that he is called elements.addObjectsFromArray to progressively add more elements in batches.

Resources