Scroll to bottom as new content comes in - ios

So I have a UICollectionView that sort of houses a chat function in my app. As users send new messages they are brought in to the app via an observer pattern. However when new content comes in. It ends up below the keyboard and never adjust itself up. When I toggle the keyboard I have a function like this which seems to accomplish this goal. However, when I try similar code in my completion handler it fails and gives me this message.
'NSInvalidArgumentException', reason: 'attempt to scroll to invalid index path: {length = 2, path = 0 - 9}'
*** First throw call stack:
fileprivate func tryObserveComments(){
print(eventKey)
commentHandle = ChatService.observeMessages(forChatKey: eventKey) { (ref, newComments) in
self.commentRer = ref
self.comments.append(newComments!)
self.adapter.performUpdates(animated: true)
let item = self.collectionView.numberOfItems(inSection: self.collectionView.numberOfSections - 1) - 1
let insertionIndexPath = IndexPath(item: item, section: self.collectionView.numberOfSections - 1)
self.collectionView.scrollToItem(at: insertionIndexPath, at: UICollectionViewScrollPosition.top, animated: true)
}
}
Also for some reason in the strange instance that it does work. Some of the keyboard is partially covering the cell

Try it using performBatchUpdates, i think it will work as you excepted
fileprivate func tryObserveComments(){
print(eventKey)
commentHandle = ChatService.observeMessages(forChatKey: eventKey) { (ref, newComments) in
self.commentRer = ref
self.comments.append(newComments!)
self.adapter.performUpdates(animated: true)
self.collectionView.performBatchUpdates({
let item = self.collectionView.numberOfItems(inSection: self.collectionView.numberOfSections - 1) - 1
let insertionIndexPath = IndexPath(item: item, section: self.collectionView.numberOfSections - 1)
}, completion: { (Bool) in
self.collectionView.scrollToItem(at: insertionIndexPath, at: UICollectionViewScrollPosition.top, animated: true)
})
}
}

Related

Animate CollectionViewCell after deleting item

For my CollectionView I have this animation inside willDisplay :
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Add animations here
let animation = AnimationFactory.makeMoveUpWithFade(rowHeight: cell.frame.height, duration: 0.5, delayFactor: 0.1)
let animator = Animator(animation: animation)
animator.animate(cell: cell, at: indexPath, in: collectionView)
}
This is how the animation works (I implemented it for CollectionView) if you need it for more info.
Probelm:
Inside my project the user can create and delete an item.
Right now the collectionView is not animating after deleting even though I am calling reloadData:
extension MainViewController: DismissWishlistDelegate {
func dismissWishlistVC(dataArray: [Wishlist], dropDownArray: [DropDownOption]) {
self.dataSourceArray = dataArray
self.dropOptions = dropDownArray
self.makeWishView.dropDownButton.dropView.tableView.reloadData()
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: nil)
}
}
This is where I call the delegate inside my other ViewController:
func deleteTapped(){
let alertcontroller = UIAlertController(title: "Wishlist löschen", message: "Sicher, dass du diese Wishlist löschen möchtest?", preferredStyle: .alert)
let deleteAction = UIAlertAction(title: "Löschen", style: .default) { (alert) in
DataHandler.deleteWishlist(self.wishList.index)
self.dataSourceArray.remove(at: self.currentWishListIDX)
self.dropOptions.remove(at: self.currentWishListIDX)
// change heroID so wishlist image doesnt animate
self.wishlistImage.heroID = "delete"
self.dismiss(animated: true, completion: nil)
// update datasource array in MainVC
self.dismissWishlistDelegate?.dismissWishlistVC(dataArray: self.dataSourceArray, dropDownArray: self.dropOptions)
}
let cancelAction = UIAlertAction(title: "Abbrechen", style: .default) { (alert) in
print("abbrechen")
}
alertcontroller.addAction(cancelAction)
alertcontroller.addAction(deleteAction)
self.present(alertcontroller, animated: true)
}
When creating the animation works just fine. This is how my createDelegateFunction looks like this:
func createListTappedDelegate(listImage: UIImage, listImageIndex: Int, listName: String) {
// append created list to data source array
var textColor = UIColor.white
if Constants.Wishlist.darkTextColorIndexes.contains(listImageIndex) {
textColor = UIColor.darkGray
}
let newIndex = self.dataSourceArray.last!.index + 1
self.dataSourceArray.append(Wishlist(name: listName, image: listImage, wishData: [Wish](), color: Constants.Wishlist.customColors[listImageIndex], textColor: textColor, index: newIndex))
// append created list to drop down options
self.dropOptions.append(DropDownOption(name: listName, image: listImage))
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: {
(result) in
// scroll to make newly added row visible (if needed)
let i = self.theCollectionView.numberOfItems(inSection: 0) - 1
let idx = IndexPath(item: i, section: 0)
self.theCollectionView.scrollToItem(at: idx, at: .bottom, animated: true)
})
}
Animation of IUCollectionView elements insertion and deletion is correctly done using finalLayoutAttributesForDisappearingItem(at:) and initialLayoutAttributesForAppearingItem(at:)
Little excerpt from Apple's documentation on finalLayoutAttributesForDisappearingItem(at:)
This method is called after the prepare(forCollectionViewUpdates:) method and before the finalizeCollectionViewUpdates() method for any items that are about to be deleted. Your implementation should return the layout information that describes the final position and state of the item. The collection view uses this information as the end point for any animations. (The starting point of the animation is the item’s current location.) If you return nil, the layout object uses the same attributes for both the start and end points of the animation.
I solved the problem by removing performBatchUpdates ... I have no idea why it works now and what exactly performBatchUpdates does but it works so if anyone wants to explain it to me, feel free :D
The final function looks like this:
func dismissWishlistVC(dataArray: [Wishlist], dropDownArray: [DropDownOption], shouldDeleteWithAnimation: Bool, indexToDelete: Int) {
if shouldDeleteWithAnimation {
self.shouldAnimateCells = true
self.dataSourceArray.remove(at: self.currentWishListIDX)
self.dropOptions.remove(at: self.currentWishListIDX)
// reload the collection view
theCollectionView.reloadData()
} else {
self.shouldAnimateCells = false
self.dataSourceArray = dataArray
self.dropOptions = dropDownArray
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: nil)
}
self.makeWishView.dropDownButton.dropView.tableView.reloadData()
}

How to scroll to newly inserted row only when already scrolled to bottom of the table?

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)
}
}
})
}

Swift - How to jump to the top using becomeFirstResponder

Here's what I want to do , when I click on my button I want to jump the top but when I click on the button my app crashes.
#IBAction func addToCart(_ sender: UIButton) {
if quantitylbl.text == "0" {
dismiss(animated: true)
}
// Save in to userdefault
let section = sender.tag / 100
let row = sender.tag % 100
let dict = arrayCategory[section] as? NSDictionary
let subLimit = getStringValueFromAPIValue(apiValue: dict?.value(forKey: "limit"), defaultValue: "")
let cell = self.tableView.cellForRow(at: IndexPath(row: row, section: section)) as! CustomizationDropDownTableViewCell
if self.section_quantity[section] < Int(truncating: subLimit.numberValue!) {
cell.btnDropdown.becomeFirstResponder()
}
}
let subLimit = getStringValueFromAPIValue(apiValue: dict?.value(forKey: "limit"), defaultValue: "")
let cell = self.tableView.cellForRow(at: IndexPath(row: row, section: section)) as! CustomizationDropDownTableViewCell
I'm not sure what exact problem makes your App crash. Try to make some breakpoints in your code. I think there might be a problem with the optional issues.
The subLimit is optional, so make sure it has a value when forced unwrapping. Int(truncating: subLimit.numberValue!)
Also, use the forced form of the type cast operator (as!) only when you are sure that the downcast will always succeed.
See Apple's Swift document. https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html

Scroll down tableView in swift?

I have a table view which received data from a real-time database. These data are added from the bottom of the table view and so on this table view has to scroll down itself to show new data.
I've found a method to do it however I'm not satisfied because the scroll always start from the top of the list. Not very beautiful.
Here is the code of this method :
func tableViewScrollToBottom(animated: Bool) {
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
let numberOfSections = self.clientTable.numberOfSections
let numberOfRows = self.clientTable.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.clientTable.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Bottom, animated: animated)
}
})
}`
Is there a way to modify this method in order to scroll only from the previous position ?
The issue is probably how the rows were inserted into the table. For example, if you add rows to the end using something like this, you get a very smooth UI:
#IBAction func didTapAddButton(sender: AnyObject) {
let count = objects.count
var indexPaths = [NSIndexPath]()
// add two rows to my model that `UITableViewDataSource` methods reference;
// also build array of new `NSIndexPath` references
for row in count ..< count + 2 {
objects.append("New row \(row)")
indexPaths.append(NSIndexPath(forRow: row, inSection: 0))
}
// now insert and scroll
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .None)
tableView.scrollToRowAtIndexPath(indexPaths.last!, atScrollPosition: .Bottom, animated: true)
}
Note, I don't reload the table, but rather call insertRowsAtIndexPaths. And I turned off the animation because I know they're off screen, and I'll then scroll to that row.

Keep track of taps on a button?

Is there a way to keep count of how many times a user taps a button? i am hoping to perform a segue after the user taps a button so many times. As of right now I am having my backend keep track of how many taps.
#IBAction func nextbuttontest(sender: AnyObject) {
let button = sender as! UIButton
let view = button.superview!
let cell = view.superview as! NewFeedControllerCell
let indexPath = tableView.indexPathForCell(cell)
let row = indexPath?.row
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: row!+1, inSection: 0),
atScrollPosition: UITableViewScrollPosition.Top,
animated: false)
}
You can create integer variable inside you view controller, and use it as a counter -> that is, when user taps increment it and if you have enough taps perform a segue or anything else.
For instance:
var tapCounter = 0
as a declaration in your view controller.
func tapGestureForElement(gest:UIGestureRecognizer){
tapCounter = tapCounter + 1
if(tapCounter == 123){
//performSegue
}
}
EDIT::
for the second part:
if(row!+1 == yourArray.count){ // or == 10, as you mentioned in comments, but I think that is a bad practice
//do nothing or do something, up to you :)
{
else{
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: row!+1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)
}
Edit 2:
you can even do something better, you can check if you reached end of the rows in section:
if( row!+1 == tableView. numberOfRowsInSection(indexPath.section)
Last part is not tested, but that should be it.
Like NickCatib said, add a tapCounter property to your view controller. However, instead of manually firing the segue when the tap count reaches the desired value, override shouldPerformSegueWithIdentifier:sender: so that it won't return YES for that particular segue until the tap count has been reached.

Resources