I have a CollectionView that has an input accesory view at the bottom containing a text field and other elements. The app works like a messaging app: you write something and then you send it, at which point the message is added to the collection view.
The problem is that, with the code I've implemented, the "auto scroll" stars working when there is a certain number of messages (items in the collection view), 16-18 is the amount of items you need to add before the auto-scroll stars to work. This is the code I've implemented so far:
DispatchQueue.main.async {
self.collectionView?.reloadData()
self.inputTextField.text = ""
if self.messages.count > 0 {
let indexpath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexpath, at: .bottom, animated: true)
}
}
That code is in the textFieldShouldReturn method.
This is what I get when I put a breakpoint in the line "self.collectionView?..."
Related
I'm working with a UICollectionView as UIViewRepresentable. It should be updated when either current item is changed (to scroll to the current item and highlight it), or the source data was updated (it can add/remove one item). The first case works perfectly, but the second one can cause lags and even crash the app with this exception: "Attempted to scroll the collection view to an out-of-bounds item (9) when there are only 9 items in section 0". It's caused when scrolling after updating the source data (adding/removing item).
Here is the code for updateUIView function:
func updateUIView(_ uiView: UICollectionView, context: Context) {
uiView.reloadData()
if !sections.isEmpty, currentSectionId != context.coordinator.currentSectionIndex && uiView.numberOfItems(inSection: 0) == sections.count {
context.coordinator.currentSectionIndex = currentSectionId
guard let currentSection = sections.firstIndex(where: { $0.id == currentSectionId }) else { return }
let indexPath = IndexPath(row: currentSection, section: 0)
uiView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
And here are the variables of my UIViewRepresentable:
struct SectionScrollView: UIViewRepresentable {
#Binding var sections: [MenuSection]
#Binding var currentSectionId: Int
It's a menu that can be navigated through categories so scrolling main menu (also UIViewRepresentable UICollectionView) triggers currentSectionId that scrolls section collection view to the current section. And pressing current section in section collection view triggers main collection view to scroll to the beginning current section.
Also, making an item favorite adds additional section (if there is no favorites) and removes it (if user removes the last item from favorites). And here is when the exception appear. If the user scrolls main collection view after making item favorite, the app could crash (or could not).
It seems that reloadData() doesn't always work or work as expected.
So, what could be wrong here?
P.S. When I add the code below to updateUIView, the scrolling sometimes stops until another favorite dish isn't added/removed:
uiView.reloadData() // This was already there
if sections.count != uiView.numberOfItems(inSection: 0) {
uiView.reloadData()
}
I'm building an app where rows containing messages are inserted at the end of a table
messages.append(message)
let indexPath:IndexPath = IndexPath(row:(messages.count - 1), section:0)
tableView.insertRows(at: [indexPath], with: .none)
I then scroll to the bottom of the table
DispatchQueue.main.async {
let indexPath = IndexPath(
row: self.numberOfRows(inSection: self.numberOfSections - 1) - 1,
section: self.numberOfSections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
Like other messaging apps, I would like to modify this so that the autoscrolling only happens if you're already scrolled at the end of the table instead of every time a new message gets inserted.
I've tried several techniques like detecting if the last cell is full visible https://stackoverflow.com/a/9843146/784637, or detecting when scrolled to the bottom https://stackoverflow.com/a/39015301/784637.
However my issue is that because scrollToRow sets animated:true, if a new message comes in but the previous message which came a split second before is still being scrolled down to via scrollToRow, then the autoscrolling to newest message and subsequent messages doesn't occur - ex. the last cell won't be fully visible until the animation is complete, or detecting if you're scrolled to to the bottom will be false until the animation is complete.
Is there any way I can get around this without setting animated: false?
What I would do is insert the row in a batch operation to make use of its completion handler, which serializes the UI update. Then I would check to see which rows are visible to the user and if the last couple of rows are, I think the user is close enough to the bottom of the conversation to force a down scroll.
let lastRow = IndexPath(row: messages.count - 1, section: 0)
DispatchQueue.main.async {
self.tableView.performBatchUpdates({
self.tableView.insertRows(at: [lastRow], with: .bottom)
}, completion: { (finished) in
guard finished,
let visiblePaths = self.tableView.indexPathsForVisibleRows else {
return
}
if visiblePaths.contains([0, messages.count - 2]) || visiblePaths.contains([0, messages.count - 1]) { // last 2 rows are visible
DispatchQueue.main.async {
self.tableView.scrollToRow(at: lastRow, at: .bottom, animated: true)
}
}
})
}
I am developing an iOS app which has different forms which is populated into a UITableview based on users selection. Each form has different fields like Textfield, DatePicker, PickerView. So I used a single TableViewCell (nib) to hold all these and show or hide the items based on question.
There is save function defined which will save values when user enters to an array.
My issue is, at times my tableview scrolls as if the index goes out of control. like when I select any textfield, Tableview scrolls to top. I have removed all keyboard observer methods and checked, still it is happening.
Below is my save function code:
func saveFormValue(mystr: String) {
//selectedIndex is a global variable (NSIndexPath)
let dict = sections[selectedIndex!.section].questions![selectedIndex!.row]
dict.answer = mystr
sections[selectedIndex!.section].questions![selectedIndex!.row] = dict
let answer = updated_answer.init(ID: ID, form_id: selected_form_id, form_name: formName, section_id: dict.section_id ?? "",question_id: dict.question_id ?? "", question_name: dict.question_name!,question_type:dict.question_type!)
updatedArray.append(answer)
self.tableView.reloadData()
}
This is the code in textfieldDidBeginEditing function (how selectedIndexPath is initialized):
guard let index = tableView.indexPath(for: cell) else {
return
}
selectedIndex = index as NSIndexPath
I have added delegate for cell, and one thing I noticed is, this issue is happening whenever I press pickerview or datepicker once. I couldn't see this issue If I only touch textField cells only.
Please let me know for any further details.
Try this code hope this helps to you.
if let thisIndexPath = tableView.indexPath(for: cell) {
tableView.scrollToRow(at: thisIndexPath, at: .top, animated: false)
}
On textfield delegate method textFieldDidBeginEditing use the following code:
func textFieldDidBeginEditing(_ textField: UITextField) {
let indexParh = NSIndexPath(row: textField.tag, section: 0)
self.constTBL_Bottom.constant = 260
self.tblViewObj.scrollToRow(at: indexParh as IndexPath, at: .middle, animated: false)
}
Also you need to manage the table bottom constant. When you resigning your keyboard set table view constant to 0
Hope this will work :)
Yes, there are many similar/equal questions, but none worked for me.
I've a UITableView with some sections. The rows of these sections are created conform user put information in the table, so I want to auto-scroll when user type a especific field, but I don't know which IndexPath I must use to use the function:
UITableView.scrollToRow(at: IndexPath, at: UITableViewScrollPosition.middle, animated: true )
The problem is that I've section without rows, so I can't use the function above without especific a row in IndexPath. (In this case, the section without rows is the 3rd).
I already tried this:
let sectionRect : CGRect = tableViewOrder.rect(forSection: 3)
tableViewOrder.scrollRectToVisible(sectionRect, animated: true)
and
tableViewOrder.scrollToRow(at: IndexPath(row: NSNotFound, section: 3), at: UITableViewScrollPosition.middle, animated: true)
Both not works.
You can try
tableViewOrder.contentOffset = CGPoint(x:0,y: tableViewOrder.contentSize.height - tableViewOrder.frame.height)
When trying to create collapsible UICollectionView sections, I update the number of items in the section dependent on its state. However, doing it this way, I reload the section which also reloads the section header aswell, and I get a very weird behavior when animating my image in the section header.
Essentially, reloading the section header when changing section items enables the UICollectionView to update the items but the section animate looks and behaves strange.
Without calling reloadSection, it allows for the proper animation but the items do not load.
self?.collectionView?.performBatchUpdates({
let indexSet = IndexSet(integer: section)
self?.collectionView?.reloadSections(indexSet)
}, completion: nil)
What is the fix for this?
You may try to extract the sequence of IndexPath in a specific section, then call reloadItems on such sequence, doing so:
extension UICollectionView {
func reloadItems(inSection section:Int) {
reloadItems(at: (0..<numberOfItems(inSection: section)).map {
IndexPath(item: $0, section: section)
})
}
}
so your code might be something like:
var updateSection = 0 // whatever
collectionView.performBatchUpdates({
// modify here the collection view
// eg. with: collectionView.insertItems
// or: collectionView.deleteItems
}) { (success) in
collectionView.reloadItems(inSection: updateSection)
}