swift infinite scroll up collectionView - ios

Almost everyone can find examples with infinite collectionView load more data when collectionView scrolls to bottom, but maybe anybody known how infinite scroll collectionView from bottom to top?
Like a chat history load more when user scrolls up.
For example.
I have a list with 100000 items and with one of conditions. The app should show to a user items from 9500 to 9600 and when user scrolls up, the app should add to the collectionView items from 9400 to 9500 at the begin of collectionView, and when user scrolls down - the app should add to the collectionView to the end of collectionView and etc.
I've googled and have tried reverse logic for bottom infinite collectionView, but it unsuccessful.
CollectionView just scrolled up to the first item on the list.
Any ideas or tips?

The result you are describing is expected. Since you scrolled to top the content offset is at zero. And when you added new items the old ones were pushed to the bottom. It will not happen the other way around; when you scroll to bottom and add items beneath them the scroll position stays the same.
To be honest it is all about perspective. The position actually stays the same in both cases (or none) but depends on how you look at it. When you append items at bottom the position should stay the same looking from top (which is a default scenario). But when you add items at top the position stays the same from bottom.
So the solution in your case is that when you add items at the bottom you should change scroll from top:
let distanceFromTop = scrollView.contentOffset.y - 0.0;
// Reload data here
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0 + distanceFromTop)
which basically means "do nothing at all".
But when you are appending items at the top you need to compute it from bottom:
let distanceFromBottom = scrollView.contentSize.height - scrollView.contentOffset.y;
// Reload data here
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentSize.height - distanceFromBottom)
You should note that you can do this sideways as well. Appending items on the right side would again be transparent. But adding them on the left should be like:
let distanceFromRight = scrollView.contentSize.width - scrollView.contentOffset.x;
// Reload data here
scrollView.contentOffset = CGPoint(x: scrollView.contentSize.width - distanceFromRight, y: scrollView.contentOffset.y)
I am using scrollView as this happens the same to every subclass of it. The same logic can be applied to table view or collection view.
Another note here is that estimated row heights or similar functionalities may cause anomalies when not done correctly. In such cases you may need to compute the offset a bit more smartly.

Related

How to access scrollbar position in swift

I'm trying to access the position of a scrollbar in scrollview so that I can attach some UI elements to it. The only method I've found to access this is scrollViewDidScroll(_ scrollView:).
This gives me the scrollView.contentOffset.y property, but I can't seem to use it during runtime to attach my UI to it. I think this gives me the entire scrollview length though, and not the position of the scrollbar.
I'm basically trying to do exactly what's circled in red in this screenshot. (KakaoTalk message)
The simplest way I've found to get the relative position of the scrollBar position is to find it's current percentage in the scrollView, and use that value to find the percent the scrollBar is at within the Bounds.
ScrollView position: (scrollView.contentOffset.y / scrollView.contentSize.height)
Which is basically Current Scrolling Position / Total Height of ScrollView
Let's call it scrollPercent.
let scrollPercent = (scrollView.contentOffset.y / scrollView.contentSize.height)
That will give you a % value between 0 and 1 (0% and 100%).
You can take the the scrollPercent and multiply it by the Parent View's max height view.bounds.size.height, to get the approximate Y value of the scrollbar within the view.
let scrollBarPosition = scrollPercent * (view.bounds.size.height)
This can be used as the Y value for your UI element.
When you are doing this, be sure to check that scrollView.contentSize.height is > 0, because it starts as zero, and if you try to divide by that, the number will reach infinite and your app will crash.
The final solution looks like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollBarPosition: CGFloat = (scrollView.contentOffset.y / scrollView.contentSize.height) * (view.bounds.size.height)
if scrollView.contentSize.height > 0 {
dateScrollbar.snp.remakeConstraints { (make) in
make.centerY.equalTo(scrollBarPosition)
make.trailing.equalToSuperview().inset(110)
}
}
}
Note: I am using an autolayout library called SnapKit for setting autolayout constraints.
EDIT: The above solution works, but it will also extend your new UI off-screen at the top and bottom of your scrollView.
To fix this: Add a subview to the parentView, and make the subView's height the denominator of the scrollBarPosition. This will act like a "track" for any UI like the picture to slide against, and stay within its bounds.
let scrollBarTrack = UIView()
view.addSubView(scrollBarTrack)
//Put it wherever you want, with the height being equal to whatever you want, and the width can be something like 1.
and then update your scrollBarPosition
let scrollBarPosition = scrollPercent * (scrollBarTrack.bounds.size.height)
It should stay within whatever those bounds are :)

UICollectionView ScrollToItem With Insets

I have a horizontally scrolling uicollectionview with insets I am setting through the uicollectionviewdelegateflowlayout method insetForSectionAt.
In trying to use the method .scrollToItem the scroll doesn't seem to take into account of the inset and only scrolls a part of the way.
Is there a suggested approach to account for the inset and have the scroll go all the way? It is just one section and I am trying to scroll from item 0 to item 1.
Another approach is to manually modify content offset based on a custom calculation.
let customPoint = CGPoint(x: customX, y: customY)
collectionView.setContentOffset(customPoint, animated: true)
Where customX and customY are calculated based on cell size and cell count + other factors such as insetForSection.

Best way to add multiple diagonal connection lines between TableViewCells

I'm creating an app that needs to show a tableview like below image
Similar colored circles are to be matched with a line.
Which view i can add the lines?
Or need to create a new view above tableview? But still my tableview needs to be scrolled.
How can i achieve this?
Update for Bounty
I want to implement the same with incliend lines between neighbouring circles. How to achieve the same?
Demonstration below:
create design like this
Based on your requirement just hide upper line and lower line of circle
You need to create collection view in tableview cell. In collection view you create one cell. Design the same user interface like your design. Show and hide the view with matching of rule. It will not affect tableview scrolling. and with this approach you can also provide scroll in collection view cell. i can provide you coded solution if you able to provide me more information. Thanks
You can use this Third Party LIb
You need to use a combination of collection view and a table view to give support for all devices.
1.Create one collection view cell with following layout
Hide upper and lower lines as per your need
Add collection view in table view cell and managed a number of cells in collection view depending upon the current device width and item's in between spacing.
You can create a vertical label without text, set the background color with black and place it behind the circle in view hierarchy and set a width of the label as per your requirement. Then you can hide unhide the label whenever you want.
P.S.: Make sure to hide your cell separator.
I have created a demo project. You can find it here. I tried to match your requirements. You can update the collection view settings to handle the hide and show of labels.
Let me know if you have any questions.
Hope this help.
Thanks!
To connect any circle with any other circle in the cell above / below, it will be easier and cleaner to create the connection lines dynamically rather than building them into the asset as before. This part is simple. The question now is where to add them.
You could have the connection lines between every two cells be contained in the top or bottom cell of each pair, since views can show content beyond their bounds.
There's a problem with this though, regardless of which cell contains the lines. For example, if the top cell contains them, then as soon as it is scrolled up off screen, the lines will disappear when didEndDisplayingCell is called, even though the bottom cell is still completely on screen. And then scrolling slightly such that cellForRow is called, the lines will suddenly appear again.
If you want to avoid that problem, then here is one approach:
One Approach
Give your table view and cells a clear background color, and have another table view underneath to display a new cell which will contain the connection lines.
So you now have a background TVC, with a back cell, and a foreground TVC with a fore cell. You add these TVC's as children in a parent view controller (of which you can set whatever background color you like), disable user interaction on the background TVC, and peg the background TVC's content offset to the foreground TVC's content offset in an observation block, so they will stay in sync when scrolling. I've done this before; it works well. Use the same row height, and give the background TVC a top inset of half the row height.
We can make the connection lines in the back cell hug the top and bottom edges of the cell. This way circles will be connected at their centre.
Perhaps define a method in your model that calculates what connections there are, and returns them, making that a model concern.
extension Array where Element == MyModel {
/**
A connection is a (Int, Int).
(0, 0) means the 0th circle in element i is connected to the 0th circle in element j
For each pair of elements i, j, there is an array of such connections, called a mesh.
Returns n - 1 meshes.
*/
func getMeshes() -> [[(Int, Int)]] {
// Your code here
}
}
Then in your parent VC, do something like this:
class Parent_VC: UIViewController {
var observation: NSKeyValueObservation!
var b: Background_TVC!
override func viewDidLoad() {
super.viewDidLoad()
let b = Background_TVC(model.getMeshes())
let f = Foreground_TVC(model)
for each in [b, f] {
self.addChild(each)
each.view.frame = self.view.bounds
self.view.addSubview(each.view)
each.didMove(toParent: self)
}
let insets = UIEdgeInsets(top: b.tableView.rowHeight / 2, left: 0, bottom: 0, right: 0)
b.tableView.isUserInteractionEnabled = false
b.tableView.contentInset = insets
self.b = b
self.observation = f.tableView.observe(\.contentOffset, options: [.new]) { (_, change) in
let y = change.newValue!.y
self.b.tableView.contentOffset.y = y // + or - half the row height
}
}
}
Then of course there's your drawing code. You could make it a method of your back cell class (a custom cell), which will take in a mesh data structure and then draw the lines that represent it. Something like this:
class Back_Cell: UITableViewCell {
/**
Returns an image with all the connection lines drawn for the given mesh.
*/
func createMeshImage(for mesh: [(Int, Int)]) -> UIImage {
let canvasSize = self.contentView.bounds.size
// Create a new canvas
UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0)
// Grab that canvas
let canvas = UIGraphicsGetCurrentContext()!
let spacing: CGFloat = 10.0 // whatever the spacing between your circles is
// Draw the lines
for each in mesh {
canvas.move(to: CGPoint(x: CGFloat(each.0) * spacing, y: 0))
canvas.addLine(to: CGPoint(x: CGFloat(each.1) * spacing, y: self.contentView.bounds.height))
}
canvas.setStrokeColor(UIColor.black.cgColor)
canvas.setLineWidth(3)
canvas.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
You'd probably want to create a Mesh class and store the images in that model, to avoid redrawing.

Paging with UITableView Sections

My goal is basically to create a tableView with paging enabled that uses sections to determine the amount of pages, and which page you're on.
Example: 3 sections
1 section: 3 cells
2nd section: 14 cells - Scrolls to the very bottom of the section as a normal tableView would, when it hits the bottom, if you user continues swiping, goes to next page.
3rd section: 8 cells - Scrolls to bottom and stops.
So the first page would have 3 cells, then blank space, if you started to scroll up then the next section would appear, then that section header would snap to the top and you will have changed pages
The second page has many cells more than can fit on a single screen at one time, the user scrolls to the bottom of these, once the bottom is reached, can continue to scroll to reveal the third section header, then that header will snap to the top, etc
I know how to create a pages tableView, it is the paging out the tableview using sections rather than view height that I do not know is possible
I had to do something similar to your design.
I needed the first section to take up the entre screen and the second one to be an infinite scroll, with paging between Section 1 and Section 2 when scrolling down. Also, when scrolling up, I wanted the scroll to stop before the top section.
I know it is slightly different to your case, but you could quite easily figure out the height of each section and apply it similarly to my design.
here is how I did it :
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
if (lastContentOffset > offsetY) {
tableNode.view.pagingEnabled = offsetY < view.frame.height
}
lastContentOffset = offsetY
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
tableNode.view.pagingEnabled = offsetY < view.frame.height
}
Note that the scrollViewDidEndDecelerating is important to re-enable paging when scrolling DOWN.
Also notice I am only setting the tableNode.view.pagingEnabled when the user scrolls UP in the scrollViewDidScroll function.
Hope this helps anyone looking for this in the future, as I hope for you #YichenBman you have come up with a solution by now.

Limiting vertical movement of UIAttachmentBehavior inside a UICollectionView

I have a horizontal UICollectionView with a custom UICollectionViewFlowLayout that has a UIAttachmentBehavior set on each cell to give it a bouncy feel when scrolling left and right. The behavior has the following properties:
attachmentBehavior.length = 1.0f;
attachmentBehavior.damping = 0.5f;
attachmentBehavior.frequency = 1.9f;
When a new cell is added to the collection view it's added at the bottom and then animated to its position also using a UIAttachmentBehavior. Naturally it bounces up and down a bit till it rests in its position. Everything is working as expected till now.
The problem I have starts appearing when the collection view is scrolled left or right before the newly added cell has come to rest. The adds left and right bounciness to the up and down one the cell already has from being added. This results in a very weird circular motion in the cell.
My question is, is it possible to stop the vertical motion of a UIAttachmentBehavior while the collection view is being scrolled? I've tried different approaches like using multiple attachment behaviors and disabling scrolling in the collection view till the newly added cell has come to rest, but non of them seem to stop this.
One way to solve this is to use the inherited .action property of the attachment behavior.
You will need to set up a couple of variables first, something like (going from memory, untested code):
BOOL limitVerticalMovement = TRUE;
CGFloat staticCenterY = CGRectGetHeight(self.collectionView.frame) / 2;
Set these as properties of your custom UICollectionViewFlowLayout
When you create your attachment behavior:
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
attachment.damping = 1.0f;
attachment.frequency = 1.5f;
attachment.action = ^{
if (!limitVerticalMovement) return;
CGPoint center = item.center;
center.y = staticCenterY;
item.center = center;
};
Then you can turn the limiting function on and off by setting limitVerticalMovement as appropriate.
Have you tried manually removing animations from cells with CALayer's removeAllAnimations?
You'll want to remove the behaviour when the collection view starts scrolling, or perhaps greatly reduce the springiness so that it comes to rest smoothly, but quickly. If you think about it, what you're seeing is a realistic movement for the attachment behaviour you've described.
To keep the vertical bouncing at the same rate but prevent horizontal bouncing, you'd need to add other behaviours - like a collision behaviour with boundaries to the left and right of each added cell. This is going to increase the complexity of the physics a little, and may affect scrolling performance, but it would be worth a try.
Here's how I managed to do it.
The FloatRange limits the range of the attachment, so if you want it to go all the way up and down the screen you just set really large numbers.
This goes inside func recognizePanGesture(sender: UIPanGestureRecognizer) {}
let location = sender.location(in: yourView.superview)
var direction = "Y"
var center = CGPoint(x: 0, y: 0)
if self.direction == "Y" {center.y = 1}
if self.direction == "X" {center.x = 1}
let sliding = UIAttachmentBehavior.slidingAttachment(with: youView, attachmentAnchor: location, axisOfTranslation: CGVector(dx: center.x, dy: center.y))
sliding.attachmentRange = UIFloatRange(minimum: -2000, maximum: 2000)
animator = UIDynamicAnimator(referenceView: self.superview!)
animator.addBehavior(sliding)
If you're using iOS 9 and above then sliding function within attachment class will work perfectly for that job:
class func slidingAttachmentWithItem(_ item: UIDynamicItem,
attachmentAnchor point: CGPoint,
axisOfTranslation axis: CGVector) -> Self
it can be used easily, and it's very effective for sliding Apple documentation
I've resorted to disabling scrolling in the collection view for a specific amount of time after a new cell is added, then removing the attachment behavior after that time has passed using its action property, then adding a new attachment behavior again immediately.
That way I make sure the upwards animation stops before the collection view is scrolled left or right, but also the left/right bounciness is still there when scrolling.
Certainly not the most elegant solution but it works.

Resources