tableView does not scroll to bottom when first presented - ios

Upon first presenting my tableView controller I load data into my model array and then scroll the tableView to the bottom. However the tableView does not scroll all the way down. Just scrolls to an intermediate position. Subsequent scrolling using the same method always correctly scrolls to the bottom.
I have the following code in my viewDidLoad of the tableViewController:
override func viewDidLoad() {
super.viewDidLoad()
let fetchRequest: NSFetchRequest<Item> = NSFetchRequest(entityName: "Item")
do {
let fetchedResults = try context.fetch(fetchRequest)
if fetchedResults.count > 0 {
self.dataArray = fetchedResults
let indexPath = IndexPath(item: (dataArray.count - 1), section: 0)
self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: false)
}
} catch {
print("Could not fetch \(error)")
}
}
I am using an inputAccessoryView in my tableViewController. Additionally I am using automaticDimensions although I did try calculating each row height and it didn't make any difference.
Wondering why the tableView is scrolling to an arbitrary intermediate position and not the bottom. On all subsequent scrollToRowAt calls, the tableView correctly scrolls to the bottom.

Related

collection view list cell when the cell is added / deleted, how to update the indexPath of the cell?

I'm working on the collection view list cell and I was wondering how to update the indexPath when I use the diffable data source and snapshot on iOS14.
In my ViewController's viewdidload, I called configureDataSource method which registers cell information. Also, I have a displayEvents method that fetches Events data from the backend and displays them on the collection view cell. The eventDataSource holds an array of Events, so when the Event data are fetched, it updates the array so the ViewController can display data.
I also have an addEvent function which is called from EventActionModalViewController, like a user types the event name and sends .save API request to store the data in the database. It works fine, I mean successfully add data backend and display the new event on the collection view list. However, the problem is the collection view's indexPath is not updated.
For instance, if there are already two events in the collection view, their indexPath are [0,0] and [0,1]. (I printed them out in print("here is the indexPath: \(indexPath)"))
And after I add the new event, there are three events on the collection view, which is correct, but the indexPath becomes [0,0],[0,0] and [0,1]. So I think the indexPath of already displayed events are not updated. Is applying a snapshot not enough to update the indexPath of each cell? I was thinking even after applying snapshot, it still needs to reload the collection view to apply a new indexPath to the cell, or something similar.
Has anyone faced the same issue? If so, how did you apply the new indexPath to the cell?
I also have a function to delete the cell, but it doesn't update the indexPath as well.
Btw, I'm working on the app for iOS14, so cannot use one for iOS15...
private var eventDataSource = EventDataSource()
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView() // -> create collection view and set constraints
configureDataSource()
displayEvents()
}
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ListCell, Event> { cell, indexPath, Event in
cell.Event = Event
let moreAction = UIAction(image: Images.setting) { _ in
let vc = EventActionModalViewController();
vc.modalPresentationStyle = .overCurrentContext
print("here is the indexPath: \(indexPath)")
vc.indexPath = indexPath
self.tabBarController?.present(vc, animated: false, completion: nil)
}
let moreActionButton = UIButton(primaryAction: moreAction)
moreActionButton.tintColor = UIColor.ouchienLightGray()
let moreActionAccessory = UICellAccessory.CustomViewConfiguration(
customView: moreActionButton,
placement: .trailing(displayed: .whenEditing, at: { _ in return 0 })
)
cell.accessories = [
.customView(configuration: moreActionAccessory),
.disclosureIndicator(displayed: .whenNotEditing, options: .init(tintColor: .systemGray))
]
}
dataSource = UICollectionViewDiffableDataSource<Section, Event>(collectionView: collectionView) {
(collectionView, indexPath, Event) -> UICollectionViewCell? in
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: Event)
return cell
}
}
func displayEvents() {
eventDataSource.fetchEvents { [weak self] in
self?.applySnapshot(animatingDifferences: true)
}
}
func addEvent(EventTitle: String) {
eventDataSource.save(eventTitle: EventTitle, completion: { [weak self] in
self?.applySnapshot(animatingDifferences: true)
})
}
func applySnapshot(animatingDifferences: Bool = true) {
snapshot = NSDiffableDataSourceSnapshot<Section, Event>()
snapshot.appendSections([.List])
snapshot.appendItems(eventDataSource.Events)
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
}
The index path you pass in moreAction is the one captured at cell registration, which happens only if cell is created, but not recycled.
You need to call UICollectionView.indexPathForCell to get the right index at any time.

UICollectionView trigger cell animation when bottom of cell is visible

Using a UICollectionView, I am trying to trigger in each cell an animation that would add some views in it. The thing is that I need to trigger these animations only when the bottom of the cell passes the bottom of the screen.
Of course, iOS provides methods that can tell me when a cell is about to be displayed, but I want to trigger my animation really when the cell is fully visible on screen (otherwise user won't see the animation)
Here is what I do and that actually works but there is probably a better option :
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let cells = self.collectionView.visibleCells
for cell in cells {
if cell is PublicationSingleMediaCollectionViewCell {
guard let indexPath = collectionView.indexPath(for: cell) else {
return
}
guard let frame = self.collectionView.layoutAttributesForItem(at: indexPath)?.frame else {
return
}
if self.collectionView.bounds.contains(frame) {
let cell = cell as! PublicationSingleMediaCollectionViewCell
cell.animate()
}
}
}
}

CollectionView Programatic Scrolling

What i am trying to do is scrolling my CollectionView to the very bottom as soon as my content has been loaded.
Here is my code;
func bindSocket(){
APISocket.shared.socket.on("send conversation") { (data, ack) in
let dataFromString = String(describing: data[0]).data(using: .utf8)
do {
let modeledMessage = try JSONDecoder().decode([Message].self, from: dataFromString!)
self.messages = modeledMessage
DispatchQueue.main.async {
self.collectionView.reloadData()
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .bottom, animated: false)
}
} catch {
//show status bar notification when socket error occurs
}
}
}
but it is totally not working.
By the way i'm using InputAccessoryView for a sticky data input bar like in iMessage and using collectionView.keyboardDismissMode = .interactive
thanks,
What I am think about that you maybe call the func before your collectionView getting visible cells or call reload collection view and then scroll items immediately, at this point scroll to indexPath will not make sense, since the collection view's content size is not yet updated.
scrollToItem(at:at:animated:)
Scrolls the collection view contents until the specified item is visible.
try this:
self.collectionView.reloadData()
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
let itemAttributes = self.collectionView.layoutAttributesForItem(at: indexPath)
self.collectionView.scrollRectToVisible(itemAttributes!.frame, animated: true)

UICollectionView reloadData not working

Its getting called in viewDidLoad, after fetching the data used.
After some print debugging it looks like it calls all the appropriate delegeate methods, if no data is changed. If there has been some data changed, cellForItemAt does not get called.
Reloading the whole section works fine. But gives me an unwanted animation. Tried disabling UIView animation before, and enabling after reloading section, but still gives me a little animation.
collectionView.reloadSections(IndexSet(integer: 0))
Here is my current situation, when using reloadData()
The UICollectionViewController is a part of a TabBarController.
I'm using custom UICollectionViewCells. The data is loaded from CoreData.
First time opening the tab, its works fine.
After updating the favorites item in another tab, and returning to this collectionView, its not updated. But if i select another tab, and go back to this one, its updated.
var favorites = [Item]()
override func viewWillAppear(_ animated: Bool) {
collectionView!.register(UINib.init(nibName: reuseIdentifier, bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
if let flowLayout = collectionView!.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 1,height: 1)
}
loadFavorites()
}
func loadFavorites() {
do {
print("Load Favorites")
let fetch: NSFetchRequest = Item.fetchRequest()
let predicate = NSPredicate(format: "favorite == %#", NSNumber(value: true))
fetch.predicate = predicate
favorites = try managedObjectContext.fetch(fetch)
if favorites.count > 0 {
print("Favorites count: \(favorites.count)")
notification?.removeFromSuperview()
} else {
showEmptyFavoritesNotification()
}
print("Reload CollectionView")
collectionView!.reloadData(
} catch {
print("Fetching Sections from Core Data failed")
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("Get number of section, \(favorites.count)")
if favorites.count > 0 {
return favorites.count
} else {
return 0
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("Get cell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SubMenuCell
let menuItem = favorites[indexPath.row]
cell.title = menuItem.title
cell.subtitle = menuItem.subtitle
return cell
}
Console Print/Log
Starting the app, going to the CollectionView tab where there are no favorites:
Load Favorites
Get number of section, 0
Get number of section, 0
Reload CollectionView
Get number of section, 0
Switching tab, and adding and setting an Item object's favorite to true, then heading back to the CollectionView tab:
Load Favorites
Favorites count: 1
Reload CollectionView
Get number of section, 1
The datamodel has 1 item, reloading CollectonView, cellForRowAtIndex not called?
Selecting another tab, random which, then heading back to the CollectionView tab, without changing any data.
Load Favorites
Favorites count: 1
Reload CollectionView
Get number of section, 1
Get cell
Now my Item shows up in the list. You can see from the Get Cell that cellForItemAt is called aswell. Nothing has changed between these last two loggings, just clicked back/fourth on the tabs one time.
Forgot to mention, but this code IS working fine in the simulator.
On a read device its just not giving me any error, just not showing the cell (or calling the cellForItemAt)
After some more debugging i got an error when the items got reduced (instead of increasing like i've tried above).
UICollectionView received layout attributes for a cell with an index path that does not exist
This led med to iOS 10 bug: UICollectionView received layout attributes for a cell with an index path that does not exist
collectionView!.reloadData()
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.layoutSubviews()
This solved my problem.
I am using autoresizing cells, but i guess this does not explain why cellForItemAt did not get called.
in my case, my collectionview was in a stackview, i didnt make any constraints, when i added the necessary constraints it worked.
Just check collectionview's constraints.
Ok, maybe I can help someone =)
I do this:
Load data from API
Then after load data call DispatchQueue.main.async like this
DispatchQueue.main.async {
self.pointNews = pointNews
}
In self.pointNews didSet I try to do collectionView.reloadData(), but it work only, if I call DispatchQueue.main.async in didSet
DispatchQueue.main.async {
self.collectionView.reloadData()
}
var paths: [IndexPath] = []
if !data.isEmpty {
for i in 0...salesArray.count - 1 {
paths.append(IndexPath(item: i, section: 0))
}
collectionView.reloadData()
collectionView.reloadItems(at: paths)
}
This is the code helped me, for solving this kind of issue.
Post your code.
One possibility is that you are using `URLSession and trying to tell your collection view to update from the it's delegate method/completion closure, not realizing that that code is run on a background thread and you can't do UI calls from background threads.
I faced the same problem with self resizing cells and above solution works as well but collection view scroll position resets and goes to top. Below solution helps to retain your scroll position.
collectionView.reloadData()
let context = collectionView.collectionViewLayout.invalidationContext(forBoundsChange: collectionView.bounds)
context.contentOffsetAdjustment = CGPoint.zero
collectionView.collectionViewLayout.invalidateLayout(with: context)
collectionView.layoutSubviews()
i was facing also this kind of issue so finally i am able to solved using this line of code
[self.collectionView reloadData];
[self.collectionView reloadItemsAtIndexPaths:#[[NSIndexPath indexPathWithIndex:0]]];
I had the same issue. I was updating the height of the UICollectionView based on the content and reloadData() stops working when you set the height of the collection view to zero. After setting a minimum height. reloadData() was working as expected.

UIWebView's entire contents embedded in UITableView

I've been struggling with this for two days, so I come hat in hand to the wise people of the internet.
I am showing an article as part of my UITableView. For this to display correctly, I need to give the delegate a height for the cell, which I want to be the same as the UIWebView's height, so I can disable scroll on the WebView and display the web content in its entirety as a static cell.
My first approach was to render it in the heightForRowAtIndexpathmethod, but this did obviously not work as I need the wait for the UIWebViewDelegate to tell me when the web view is fully loaded and has a height. After a while I found a working solution, which used the web view delegate to refresh the cell height when the web view was loaded.
The works fine until the screen size changes. Either from rotate or from full-screening my UISplitView. I forced an update on it in the didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation), but this causes it to flash about 10 times before settling into the correct height. I logged this change, and it seems the WebView is calling itself multiple times, causing a loop.
As seen in this log, starting from when I rotated the screen.
It flashes once every time it reloads, and as you can see, it reloads itself a bunch of times.
So. I need a way to show an entire web views content inside a uitableview, and reliably get the height when the screen size changes. If anyone has managed this in any way before, please tell me. I will give a bounty and my firstborn child to anyone who can resolve this, as it's driving me insane.
Here's my relevant code.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 4:
//Content
print("Height for row called")
return CGFloat(webViewHeight)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch (indexPath.row){
//HTML Content View
case 4:
let cell = tableView.dequeueReusableCellWithIdentifier("ContentCell", forIndexPath: indexPath)
var contentCell = cell as? ContentCell
if contentCell == nil {
contentCell = ContentCell()
}
contentCell?.contentWebView.delegate = self
contentCell?.contentWebView.scrollView.userInteractionEnabled = false
contentCell?.contentWebView.loadHTMLString((post?.contentHTML)!, baseURL: nil)
print("Cell For row at indexpath called")
return contentCell!
}
func webViewDidFinishLoad(webView: UIWebView) {
updateHeight()
}
func updateHeight(){
let webView = (self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 4, inSection: 0)) as! ContentCell).contentWebView
if self.webViewHeight != Double(webView.scrollView.contentSize.height) {
print("Previous WV Height = \(self.webViewHeight), New WV Height = \(webView.scrollView.contentSize.height)")
self.webViewHeight = Double(webView.scrollView.contentSize.height)
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .Automatic)
} else {
return
}
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
print("rotated")
self.updateHeight()
//tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .Automatic)
}
I solved this by changing the .Automatic to .None in the row change animation. Its still a bad solution, but at least it doesn't flicker anymore.
I would recommend that you calculate the web view height independently of the table view and store the dimension as part of the data itself and use it return in heightForRowAtIndexPath call. Its a easier that way since you don't have to deal with calculating the table height during table view display. When the html content is not loaded use a standard height and a message for the web view.
I don't see a problem in your implementation. Trey few things
There are few things you can check
func webViewDidFinishLoad(webView: UIWebView) {
updateHeight()
//This function may get called multiple times depending on webpage.
}
//Use
self.tableView.beginUpdates()
self.tableView.endUpdates()
//Instead of
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .None)
func updateHeight(){
let webView = (self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 4, inSection: 0)) as! ContentCell).contentWebView
if self.webViewHeight != Double(webView.scrollView.contentSize.height)
{
print("Previous WV Height = \(self.webViewHeight), New WV Height = \(webView.scrollView.contentSize.height)")
self.webViewHeight = Double(webView.scrollView.contentSize.height)
self.tableView.beginUpdates()
self.tableView.endUpdates()
// tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .None)
} else {
return
}
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation)
{
print("rotated")
let webView = (self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 4, inSection: 0)) as! ContentCell).contentWebView
webView.reload();
}
You will need to reload webview on orientation change.

Resources