Swift issue with inputAccessoryView - ios

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

Related

How to animate collection view layout change while using `layoutAttributesForElements`?

I made a custom collection view flow layout that can toggle (with animation) between "film-strip" and "list" layouts. But after adding some fancy animations to the edge cells, the toggle animation broke. Here's what it looks like currently, without those changes:
The animation is nice and smooth, right? Here's the current, working code (full demo project here):
enum LayoutType {
case strip
case list
}
class FlowLayout: UICollectionViewFlowLayout {
var layoutType: LayoutType
var layoutAttributes = [UICollectionViewLayoutAttributes]() /// store the frame of each item
var contentSize = CGSize.zero /// the scrollable content size of the collection view
override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
/// pass attributes to the collection view flow layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.item]
}
// MARK: - Problem is here
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
/// edge cells don't shrink, but the animation is perfect
return layoutAttributes.filter { rect.intersects($0.frame) } /// try deleting this line
/// edge cells shrink (yay!), but the animation glitches out
return shrinkingEdgeCellAttributes(in: rect)
}
/// makes the edge cells slowly shrink as you scroll
func shrinkingEdgeCellAttributes(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = layoutAttributes.filter { rect.intersects($0.frame) }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size) /// rect of the visible collection view cells
let leadingCutoff: CGFloat = 50 /// once a cell reaches here, start shrinking it
let trailingCutoff: CGFloat
let paddingInsets: UIEdgeInsets /// apply shrinking even when cell has passed the screen's bounds
if layoutType == .strip {
trailingCutoff = CGFloat(collectionView.bounds.width - leadingCutoff)
paddingInsets = UIEdgeInsets(top: 0, left: -50, bottom: 0, right: -50)
} else {
trailingCutoff = CGFloat(collectionView.bounds.height - leadingCutoff)
paddingInsets = UIEdgeInsets(top: -50, left: 0, bottom: -50, right: 0)
}
for attributes in rectAttributes where visibleRect.inset(by: paddingInsets).contains(attributes.center) {
/// center of each cell, converted to a point inside `visibleRect`
let center = layoutType == .strip
? attributes.center.x - visibleRect.origin.x
: attributes.center.y - visibleRect.origin.y
var offset: CGFloat?
if center <= leadingCutoff {
offset = leadingCutoff - center /// distance from the cutoff, 0 if exactly on cutoff
} else if center >= trailingCutoff {
offset = center - trailingCutoff
}
if let offset = offset {
let scale = 1 - (pow(offset, 1.1) / 200) /// gradually shrink the cell
attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return rectAttributes
}
/// initialize with a LayoutType
init(layoutType: LayoutType) {
self.layoutType = layoutType
super.init()
}
/// make the layout (strip vs list) here
override func prepare() { /// configure the cells' frames
super.prepare()
guard let collectionView = collectionView else { return }
var offset: CGFloat = 0 /// origin for each cell
let cellSize = layoutType == .strip ? CGSize(width: 100, height: 50) : CGSize(width: collectionView.frame.width, height: 50)
for itemIndex in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: itemIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let origin: CGPoint
let addedOffset: CGFloat
if layoutType == .strip {
origin = CGPoint(x: offset, y: 0)
addedOffset = cellSize.width
} else {
origin = CGPoint(x: 0, y: offset)
addedOffset = cellSize.height
}
attributes.frame = CGRect(origin: origin, size: cellSize)
layoutAttributes.append(attributes)
offset += addedOffset
}
self.contentSize = layoutType == .strip /// set the collection view's `collectionViewContentSize`
? CGSize(width: offset, height: cellSize.height) /// if strip, height is fixed
: CGSize(width: cellSize.width, height: offset) /// if list, width is fixed
}
/// boilerplate code
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
class ViewController: UIViewController {
var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var isExpanded = false
lazy var listLayout = FlowLayout(layoutType: .list)
lazy var stripLayout = FlowLayout(layoutType: .strip)
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
#IBAction func toggleExpandPressed(_ sender: Any) {
isExpanded.toggle()
if isExpanded {
collectionView.setCollectionViewLayout(listLayout, animated: true)
} else {
collectionView.setCollectionViewLayout(stripLayout, animated: true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = stripLayout /// start with the strip layout
collectionView.dataSource = self
collectionViewHeightConstraint.constant = 300
}
}
/// sample data source
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath) as! Cell
cell.label.text = "\(data[indexPath.item])"
cell.contentView.layer.borderWidth = 5
cell.contentView.layer.borderColor = UIColor.red.cgColor
return cell
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
}
Again, everything works perfectly, including the animation. So then, I tried to make the cells shrink as they neared the screen's edge. I overrode layoutAttributesForElements to do this.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes.filter { rect.intersects($0.frame) } /// delete this line
return shrinkingEdgeCellAttributes(in: rect) /// replace with this
}
Film-strip
List
The scale/shrink animation is great. However, when I toggle between the layouts, the transition animation is broken.
Before (return layoutAttributes.filter...)
After (return shrinkingEdgeCellAttributes(in: rect))
How can I fix this animation? Should I be using a custom UICollectionViewTransitionLayout, and if so, how?
Whew! This was a workout. I was able to modify your FlowLayout so that there are no hiccups in animation. See below.
It works!
Problem
This is what was happening. When you change layouts, the layoutAttributesForElements method in FlowLayout is called twice if the content offset of the collection view is anything but (0, 0).
This is because you have overridden 'shouldInvalidateLayout' to return true regardless of whether it is actually needed. I believe the UICollectionView calls this method on the layout before and after the layout change (as per the observation).
The side effect of this is that your scale transform is applied twice - before and after the animations to the visible layout attributes.
Unfortunately, the scale transform is applied based on the contentOffset of the collection view (link)
let visibleRect = CGRect(
origin: collectionView.contentOffset,
size: collectionView.frame.size
)
During layout changes the contentOffset is not consistent. Before the animation starts contentOffset is applicable to the previous layout. After the animation, it is relative to the new layout. Here I also noticed that without a good reason, the contentOffset "jumps" around (see note 1)
Since you use the visibleRect to query the set of Layout Attributes to apply the scale on, it introduces further errors.
Solution
I was able to find a solution by applying these changes.
Write helpers methods to transform the content offset (and dependent visibleRect) left by the previous layout to values meaningful for this layout.
Prevent redundant layout attribute calculates in prepare method
Track when and when not the layout is animating
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
// ...
}
// In View Controller,
isExpanded.toggle()
if isExpanded {
listLayout.reset()
listLayout.animating = true // <--
// collectionView.setCollectionViewLayout(listLayout)
} else {
stripLayout.reset()
stripLayout.animating = true // <--
// collectionView.setCollectionViewLayout(stripLayout)
}
Override targetContentOffset method to handle content offset changes (prevent jumps)
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
var layoutType: LayoutType
// ...
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard animating else {
// return super
}
// Use our 'graceful' content content offset
// instead of arbitrary "jump"
switch(layoutType){
case .list: return transformCurrentContentOffset(.fromStripToList)
case .strip: return transformCurrentContentOffset(.fromListToStrip)
}
}
// ...
The implementation of content offset transforming is as follows.
/**
Transforms this layouts content offset, to the other layout
as specified in the layout transition parameter.
*/
private func transformCurrentContentOffset(_ transition: LayoutTransition) -> CGPoint{
let stripItemWidth: CGFloat = 100.0
let listItemHeight: CGFloat = 50.0
switch(transition){
case .fromStripToList:
let numberOfItems = collectionView!.contentOffset.x / stripItemWidth // from strip
var newPoint = CGPoint(x: 0, y: numberOfItems * CGFloat(listItemHeight)) // to list
if (newPoint.y + collectionView!.frame.height) >= contentSize.height{
newPoint = CGPoint(x: 0, y: contentSize.height - collectionView!.frame.height)
}
return newPoint
case .fromListToStrip:
let numberOfItems = collectionView!.contentOffset.y / listItemHeight // from list
var newPoint = CGPoint(x: numberOfItems * CGFloat(stripItemWidth), y: 0) // to strip
if (newPoint.x + collectionView!.frame.width) >= contentSize.width{
newPoint = CGPoint(x: contentSize.width - collectionView!.frame.width, y: 0)
}
return newPoint
}
}
There are some minor details I left out in the comments and as a pull request to OP's demo project so anyone interested can study it.
The key take-aways are,
Use targetContentOffset when arbitrary changes in content offset occur in response to layout changes.
Be careful about incorrect query of layout attributes in layoutAttributesForElements. Debug your rects!
Remember to clear your cached layout attributes on the prepare() method.
Notes
The "jump" behavior is evident even before you introduced scale transforms as seen in your gif.
I sincerely apologize if the answer is lengthy. Or, The solution is not quite what you wanted. The question was interesting which is why I spent the whole day trying to find a way to help.
Fork and Pull request.
Thanks for your detailed investigation #Thisura Dodangoda
– it was instrumental in helping me solve a similar problem. For folks who end up here, I want to add a tiny detail in case you run into another issue that I did.
The UICollectionViewLayout API has 2 very similar methods:
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
This method Retrieves the point at which to stop scrolling
and
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint
This method Retrieves the content offset to use after an animated layout update or change
I had already implemented the first for some custom behaviour during scrolling, and I was trying to implement the solution posted by #Thisura Dodangoda in that method.
However, these are used for completely different purposes. You need to use the second method (without the velocity parameter) to implement the solution for layout changes.

UICollectionViewController with dynamic header (like Reddit App)

I am looking for a way to implement a header view that automatically hides once you start scrolling down and immediately shows itself once the user starts scrolling up.
Usually, I always post some code, but now I am a little bit lost on how to implement such behaviour.
My view layout:
UICollectionViewController with paging enabled for horizontal
scrolling (has two items)
The UICollectionViewCell fills the entire vertical space. Each UICollectionViewCell hosts a UITableView for vertical scrolling. I assume that I have to use the UITableView vertical scrolling position to adjust the frame of the menu bar.
Video: https://imgur.com/a/Rdu3wko
What would be the best way to implement such a behaviour?
If you want to use a UICollectionView, just grab the delegate, see which direction the user is scrolling, and hide/show the header as needed. Here's an example to get you started:
class ViewController: UIViewController {
// Variable to save the last scroll offset.
private var lastContentOffset: CGFloat = 0
private lazy var header: UIView = {
let header = UIView()
header.translatesAutoresizingMaskIntoConstraints = false
header.backgroundColor = .red
header.widthAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
header.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
return header
}()
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.backgroundColor = .white
collectionView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: 2000.0)
// Setting bounces to false - otherwise the header will disappear when we go past the top and are sprung back.
collectionView.bounces = false
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(collectionView)
collectionView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
collectionView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
collectionView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: 2000.0)
// Make sure you either add the header subview last, or call self.view.bringSubviewToFront(header)
self.view.addSubview(header)
// Constrain the header so it's just sitting on top of the view. To make it visible, we'll use a transform.
header.bottomAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
header.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
// Header starts visible.
header.layoutIfNeeded()
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.frame.height)
}
func revealHeader() {
// Set the duration below to how quickly you want header to appear/disappear.
UIView.animate(withDuration: 0.3) {
self.header.transform = CGAffineTransform(translationX: 0.0, y: self.header.frame.height)
}
}
func hideHeader() {
UIView.animate(withDuration: 0.3) {
self.header.transform = .identity
}
}
}
extension ViewController: UICollectionViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (lastContentOffset > scrollView.contentOffset.y) {
// Scrolled up: reveal header.
revealHeader()
}
else if (lastContentOffset < scrollView.contentOffset.y) {
// Scrolled down: reveal header.
hideHeader()
}
lastContentOffset = scrollView.contentOffset.y
}
}
EDIT: Noticed the functionality of the Reddit header is a bit different. If you want the thing to scroll dynamically (i.e. by the amount you have scrolled down by as opposed to appear all at once) replace that delegate function with this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (lastContentOffset > scrollView.contentOffset.y) {
// Scrolled up: reveal header.
let difference = lastContentOffset - scrollView.contentOffset.y
if header.transform.ty < (header.frame.height - difference) {
// Header hasn't been fully revealed yet, bring it down by the amount we've scrolled up.
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.transform.ty + difference)
} else {
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.frame.height)
}
}
else if (lastContentOffset < scrollView.contentOffset.y) {
// Scrolled down: reveal header.
let difference = scrollView.contentOffset.y - lastContentOffset
if header.transform.ty > difference {
self.header.transform = CGAffineTransform(translationX: 0.0, y: header.transform.ty - difference)
} else {
self.header.transform = CGAffineTransform(translationX: 0.0, y: 0.0)
}
}
lastContentOffset = scrollView.contentOffset.y
}
This functionality is possible in UITableView set parallax header otherwise UIScrollView parallax animation.

Why in UITableView, cellForRowAtIndexPath is called before heightForRowAtIndexPath

I have breakpoints in both the two methods. The cellForRowAtIndexPath is always called before heightForRowAtIndexPath.
The problem is I have a logic code in cellForRowAtIndexPath that depends on the row height.
Currently, the row height I get inside cellForRowAtIndexPath is always 0.
Is there a way to let heightForRowAtIndexPath to be called before cellForRowAtIndexPath?
If not, how to call heightForRowAtIndexPatch explicitly in my code?
EDIT:
Here is my code for cellForRowAtIndexPath:
let slideshowCell = tableView.dequeueReusableCell(withIdentifier: "SlideShowCell") as! SlideShowTableViewCell
slideshowCell.delegate = self
slideshowCell.news = featuredNews
return slideshowCell
and here is my code for heightForRowAtIndexPatch:
let filterApplied = categoryNews.count != filteredCategoryNews.count
if featuredNews.count == 0 || filterApplied
{
return 0
}
else
{
return 530
}
and when I set featuredNews to slideshowCell.news, I run this method in the slideshowCell.news's didSet property observer:
private func populateSlideshowScrollView()
{
let imagesCount = CGFloat(slideshowNews.count)
// set the scrollView's content size to the size of all the images
scrollView.contentSize = CGSize(width: scrollView.frame.width * imagesCount, height: scrollView.frame.height)
// Remove subviews of ScrollView before populating it with new images
for subview in scrollView.subviews
{
subview.removeFromSuperview()
}
// populate the scrollView with images form the 'slideshowNews' property
for i in 0..<slideshowNews.count
{
let imgView = UIImageView(frame: CGRect(x: scrollView.frame.width * CGFloat(i),
y: scrollView.bounds.origin.y,
width: scrollView.frame.width,
height: scrollView.frame.height))
imgView.contentMode = .scaleAspectFill
imgView.clipsToBounds = true
imgView.image = slideshowNews[i].image
scrollView.addSubview(imgView)
}
// set scrollview to the first page from the right, only the first two times this method called.
// After that the current page of page control should be in the correct position
if numberOfRefresh < 2
{
scrollView.setContentOffset(CGPoint(x: scrollView.frame.width * (imagesCount - 1), y: 0), animated: false)
newsTitleLabel.text = slideshowNews.last?.title
sourceLabel.text = slideshowNews.last?.source.name
numberOfRefresh += 1
}
scrollToPage(page: pageControl.currentPage, animated: false) // to update scrollView's contentOffset
}
The problem is that scrollView.frame.height will always return zero, because heightForRowAtIndexPath is not yet called at this point.

Swift: paging by screen doesn't work for two column layout collection view

I have a collectionview controller with custom 2-columns layout:
class CVLayout: UICollectionViewFlowLayout {
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
override var itemSize: CGSize {
set {
}
get {
let numberOfColumns: CGFloat = 2
let itemWidth = (self.collectionView!.frame.width - (numberOfColumns - 1)) / numberOfColumns
return CGSize(width: itemWidth, height: itemWidth)
}
}
func setupLayout() {
minimumInteritemSpacing = 1
minimumLineSpacing = 1
scrollDirection = .horizontal
}
}
In my controller I set
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
collectionView?.isScrollEnabled = true
collectionView?.isPagingEnabled = true
}
I have 7 items, 6 items are displayed on the screen and I expect to see one item on the next page, but instead I see 4 (3 from the previous page)
I tried to play with number of sections and items in section, but it doesn't help. What I've found from other topics is that default paging should be by screen, not by cell (in my case a column). What am I doing wrong?
If I understand your question, I do not think you are doing anything wrong per se. The content size of the collection view is determined (in part) by the item size. Right now that is approximately the width of 3 items (or a page and a half of content). I think you need to override the collection view's content size property to reflect full pages of content (in width) to achieve the desired effect.
override var collectionViewContentSize: CGSize {
if let collectionView = collectionView {
let numberOfItems = collectionView.numberOfItems(inSection: 0)
let pages = numberOfItems/2
let size = CGSize(width: collectionView.frame.width * CGFloat(pages), height: collectionView.frame.height)
return size
}
return CGSize(width: 0, height: 0)
}
Please see this answer How to expand UICollectionView contentSize when paging enable?. It is a bit old but I think it is trying to solve the same issue.
First of all UICollectionViewFlowLayout doesn't support paging
There are two solution that i think
first
use UIScrollView add 7 items simply with paging option
second
use UICollectionView modifying your UICollectionViewFlowLayout
subclass UICollectionViewFlowLayout to support paging
ex)
In Swift3
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
you can find more information searching UICollectionViewFlowLayout with Paging

UICollectionView not scrolling to bottom on the first few items

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

Resources