UICollectionView not scrolling to bottom on the first few items - ios

I've been trying to create a chat interface that I can reuse. I'm almost done with the implementation, but there's something that keeps bugging me about it. If I start loading in messages like in the gif when I first load the interface you can see that after the 4th message there are 3 messages that don't scroll to the bottom. With the 8th being the first one that does finally scroll. This varies according to the screen size. On the iPhone 6s testing device it reaches the 9th message being the one that scrolls.
I'm using content inset as the method to keep the collectionview visible with the following code being run every time the frame of the UIToolbar at the bottom changes
toolBar.inputAccessoryViewFrameChanged = {(rect: CGRect) in Void()
let navigationAndStatusHeight = self.navigationController != nil && self.navigationController!.navigationBar.isTranslucent ? self.navigationController!.navigationBar.frame.size.height + UIApplication.shared.statusBarFrame.height : 0
self.collectionView.contentInset = UIEdgeInsets(top: navigationAndStatusHeight + 8, left: 8, bottom: UIScreen.main.bounds.height - rect.origin.y + 8, right: 8)
self.collectionView.scrollIndicatorInsets.bottom = UIScreen.main.bounds.height - rect.origin.y
}
This code is run every time a new message is inserted:
func insertNewMessage(){
self.collectionView.performBatchUpdates({
self.collectionView.insertItems(at: [NSIndexPath(item: self.numberOfMessages() - 1, section: 0) as IndexPath])
}) { (Bool) in
self.scrollToBottom(animated: true)
}
}
with the scrollToBottom function being:
func scrollToBottom(animated: Bool){
guard self.numberOfMessages() > 0 else{
return
}
self.collectionView.scrollToItem(at: IndexPath(item: self.numberOfMessages() - 1, section: 0), at: UICollectionViewScrollPosition.top , animated: animated)
}
I'm currently running on this version of XCode Version 8.1 beta (8T29o) & iOS 10.1(14B55c)

The problem maybe when the collection view content size is too small, scrollToItem doesn't work properly. Try use this code
func scrollToBottomAnimated(animated: Bool) {
guard self.collectionView.numberOfSections > 0 else{
return
}
let items = self.collectionView.numberOfItems(inSection: 0)
if items == 0 { return }
let collectionViewContentHeight = self.collectionView.collectionViewLayout.collectionViewContentSize.height
let isContentTooSmall: Bool = (collectionViewContentHeight < self.collectionView.bounds.size.height)
if isContentTooSmall {
self.collectionView.scrollRectToVisible(CGRect(x: 0, y: collectionViewContentHeight - 1, width: 1, height: 1), animated: animated)
return
}
self.collectionView.scrollToItem(at: NSIndexPath(item: items - 1, section: 0) as IndexPath, at: .bottom, animated: animated)
}

Related

How to prevent the last table cell from overlapping with the keyboard

I have a chat app that displays the messages in a table view. When I invoke the keyboard, I want:
The table view to scroll to the bottom
I want there to be no overlap between any messages and the keyboard.
#objc private func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let keyBoardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let keyboardViewEndFrame = view.convert(keyBoardFrame!, from: view.window)
let keyboardHeight = keyboardViewEndFrame.height
tableView.scrollToBottom() { indexPath in
let rectofCell = self.tableView.rectForRow(at: indexPath)
let lastCellFrame = self.tableView.convert(rectofCell, from: self.view.window)
if lastCellFrame.origin.y + lastCellFrame.size.height > keyboardViewEndFrame.origin.y {
let overlap = lastCellFrame.origin.y + lastCellFrame.size.height - keyboardViewEndFrame.origin.y
self.tableView.frame.origin.y = -overlap
}
}
}
}
extension UITableView {
func scrollToBottom(animated: Bool = true, completion: ((IndexPath) -> Void)? = nil) {
let sections = self.numberOfSections
let rows = self.numberOfRows(inSection: sections - 1)
if (rows > 0){
let indexPath = IndexPath(row: rows - 1, section: sections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
completion?(indexPath)
}
}
}
The value for rectOfCell shows (0.0, 5305.333518981934, 375.0, 67.33333587646484) and the converted value (0.0, 9920.000185648601, 375.0, 67.33333587646484) and the table view disappears out of the screen.
I only want to move the table view upward if the last message overlaps with the eventual position of the keyboard. For example, when the keyboard is invoked (either when the table view is already at the bottom or not at the bottom), the table view shouldn't move upward if the appearance of the keyboard doesn't cover the messages (i.e., there is only one message).
You don't need to manage tableView.frame for such a thing. You just need to make sure that when keyboard appears, tableView adds necessary contentInset value from bottom so that user can still see all the content inside tableView with the keyboard still on screen.
All you need is this -
// when keyboard appears
let insets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
// when keyboard disappears
let insets: UIEdgeInsets = .zero
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets

how to make a collection view to scroll horizontally having infinite scroll in both directions left, as well as right in iOS swift

I want a horizontal scrollable collection view to scroll in both side(left and right), now it has continuous scrolling only in right direction, how to implement in left side ?
let itemcount = array?.count ?? 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == DemoCollectionView {
let offSet = scrollView.contentOffset.x
let width = scrollView.frame.width
let horizontalCenter = width / 2
let currentPage = Int(offSet + horizontalCenter) / Int(width)
if currentPage >= itemCount / 2 {
itemCount += array?.count ?? 0
DemoCollectionView.reloadData()
}
}
}
You can do it using "scrollToItemAtIndexPath" method. Swipe direction you can get from scrollview delegate methods.
Scroll right to left
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: dataArray.count - 1, inSection: 0), atScrollPosition: .Right, animated: false)
Scroll left to right
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0), atScrollPosition: .left, animated: false)
You can also apply a transformation on collection view to get similar.

UICollectionView scroll bug

I have made a full screen collection view with paging turned on:
Here's how I configure it in my code:
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0.0
layout.minimumLineSpacing = 0.0
layout.itemSize = UIScreen.main.bounds.size
layout.scrollDirection = UICollectionViewScrollDirection.horizontal
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0)
allQuotesCollectionView.collectionViewLayout = layout
And in my Storyboard:
The issue I'm having is when scrolling for the first time screen loaded it has bug in scroll animation:
After this bug happens, then scroll becomes smooth.
How can I fix this? I have also used Github Framework but it didn't help. I need any possible solution to this issue.
Will be grateful for any help, many thanks.
After battling with this trouble for 3 or 4 days finally found reason:
My mistake was here:
override func viewDidLayoutSubviews() {
let midIndexPath = IndexPath(row: infiniteSize / 2, section: 0)
allQuotesCollectionView.scrollToItem(at: midIndexPath, at: .centeredHorizontally, animated: false)
}
After I moved this code into viewDidAppear method - everything started to work and bug disappeared.
So, my final code looks like this right now:
override func viewDidAppear(_ animated: Bool) {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0.0
layout.minimumLineSpacing = 0.0
layout.itemSize = UIScreen.main.bounds.size
layout.scrollDirection = UICollectionViewScrollDirection.horizontal
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0)
allQuotesCollectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
allQuotesCollectionView.collectionViewLayout = layout
allQuotesCollectionView.isPagingEnabled = true
let midIndexPath = IndexPath(row: infiniteSize / 2, section: 0)
allQuotesCollectionView.scrollToItem(at: midIndexPath, at: .centeredHorizontally, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
UIView.transition(with: self.view, duration: 1, options: .transitionCrossDissolve, animations: {
self.allQuotesCollectionView.isHidden = false
})
})
}
I guess you need to setup collection view after screen has loaded with paging on, mainly starting index.
Hope this will help someone in future!

Swift issue with inputAccessoryView

I have a tableView which has a custom inputAccessoryView, and I have tableView.keyboardDismissMode set to .interactive. I subclassed UIView, and I create this class as my custom inputAccessoryView.
There is a textView in the view, and I have it resize automatically until a certain number of lines is reached. This is my code for that:
override var intrinsicContentSize: CGSize {
DispatchQueue.main.async {
let path = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
self.tableView.scrollToRow(at: path, at: .bottom, animated: true)
}
sendButton.frame.origin.y = self.frame.maxY - 5 - sendButton.frame.height
return CGSize(width: self.bounds.width, height: (textView.numberOfLines() < 10 ? textView.text.sizeForWidth(width: textView.frame.width, font: textView.font!).height : 254) + textView.frame.origin.y * 2)
}
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
if textView.numberOfLines() < 10 { // I defined numberOfLines() elsewhere
textView.isScrollEnabled = false
} else {
textView.isScrollEnabled = true
}
invalidateIntrinsicContentSize()
}
The .async {} block is for scrolling to the bottom of the tableView again once the inputAccessoryView height increases.
My problem is that when I drag down on the tableView to hide the keyboard, the tableView runs the scrolling animation (the one in the async block) when I do not want it to scroll to the bottom. Here is a video of what is happening.
I have been struggling with getting this to work how I want for a couple days now. Could anyone please explain to me why it is not working like I want it to and how I can fix it?
Thanks a lot in advance!
I solved my own problem! Hooray!
To do this, I created a public value in the ViewController for if the tableView is currently being dragged/scrolled. I used the UIScrollView delegate methods to set if the tableView is currently being scrolled:
scrollViewWillBeginDragging(_:)
and
scrollViewDidEndDragging(_:)
I then changed intrinsicContentSize to this:
override var intrinsicContentSize: CGSize {
if viewController.isDragging == false && textView.isFirstResponder {
DispatchQueue.main.async {
let path = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
self.tableView.scrollToRow(at: path, at: .bottom, animated: true)
}
}
sendButton.frame.origin.y = self.frame.maxY - 5 - sendButton.frame.height
return CGSize(width: self.bounds.width, height: (textView.numberOfLines() < 10 ? textView.text.sizeForWidth(width: textView.frame.width, font: textView.font!).height : 254) + textView.frame.origin.y * 2)
}

UITableView Restrictive Scrolling Around Sections

I'm attempting to find a good way to restrict user scrolling around certain sections within my UITableView. For instance, when section 3 is selected (by my logic) I would like to have it locked at the top of the screen so you cannot scroll away from it until it is unlocked, but the important part is I still want to keep the responsive feel of the UIScrollView (ie. the bounce interactivity).
I attempted to recreate the logic on my own as seen below (which works pretty well, but doesn't have the same bounce feel I am looking for):
override func scrollViewDidScroll(scrollView: UIScrollView) {
let sectionRect = tableView.rectForSection(activeHeaderSection)
let topY = sectionRect.origin.y
let bottomY = topY + sectionRect.height
let frameSize = self.tableView.frame.size.height - self.navigationController!.navigationBar.frame.size.height
let translation = scrollView.panGestureRecognizer.translationInView(scrollView)
if (scrollView.contentOffset.y + frameSize > bottomY) && (frameSize < sectionRect.size.height) && (translation.y < 0) {
UIView.animateLinear(0.7, initialSpringVelocity: 0, animations: { self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: self.checkTuple[self.activeHeaderSection].pickChecks.count-1, inSection: self.activeHeaderSection), atScrollPosition: .Bottom, animated: false) }, completion: nil)
} else if (frameSize > sectionRect.size.height) || (scrollView.contentOffset.y < topY) {
UIView.animateLinear(0.7, initialSpringVelocity: 0, animations: { self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: self.activeHeaderSection), atScrollPosition: .Top, animated: false) }, completion: nil)
}
}
So I did some searching around here and some question pointed towards restricting the scrollView's contentView height and position (ie. something like this) :
override func scrollViewDidScroll(scrollView: UIScrollView) {
var activeHeaderSection = 4
let sectionRect = tableView.rectForSection(activeHeaderSection)
tableView.contentOffset.y = sectionRect.origin.y // Set once when locked
tableView.contentSize.height = sectionRect.size.height
tableView.clipsToBounds = false
}
But this also isn't really giving me the behaviour I want as it always sticks to the top of my UITableView (with the correct height) but does not scroll down to the proper section. I'd prefer to use something simple like the second method if possible, I'm not sure if there is an even easier method for accomplishing something like this so I thought I'd ask here.
I think this
tableView.contentOffset.y = sectionRect.origin.y
tableView.contentSize.height = sectionRect.size.height
should be:
tableView.contentInset.top = -sectionRect.origin.y
tableView.contentSize.height = sectionRect.origin.y + sectionRect.size.height

Resources