Content offset of NSCollectionView - swift

I'm working with NSCollectionView and want to implant paging logic (loading additional content). For that I want to know when user have scrolled to bottom of collection view, because I used to add this approach in iOS apps. How I can do that? Or maybe I need to work with NSScrollView or NSClipView?

Maybe CollectioView have ScrollView delegate method.
Try this:
var lastOffsetY: CGFloat = 0
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == (scrollView.contentSize.height - scrollView.frame.size.height) {
loadMore()
}
}

Related

Limit the scroll for UITableView

I have a TableViewController:
As you see I have my own custom bar at the top.
The UITable View is just a static one, and I add a view at the top of UITableView.
The thing is when I scroll the TableView to top-side it become like bellow image, and I don't want it. is there any easy code that I can limit the scroll for the tableView?
since UITableView is a subclass of UIScrollView you can use this UIScrollViewDelegate method to forbid scrolling above the top border
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.tableView) {
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointZero;
}
}
}
Yo will need to set the bounce property of the uitableview to NO
UITableView *tableView;
tableView.bounces = NO;
Edit: Note also you can uncheck the bounces from interface builder too
Please check this answer for further details Disable UITableView vertical bounces when scrolling
I had the same problem and asked our UX-Designer, how it would be better to do. He said, that both strict solutions (prevent bouncing or allow it as it is) are bad. It's better to allow bouncing but only for some space
My solution was:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.tableView {
if scrollView.contentOffset.y < -64 {
scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -64), size: scrollView.frame.size), animated: false)
scrollView.scrollRectToVisible(CGRect(origin: CGPoint.zero, size: scrollView.frame.size), animated: true)
}
}
}
Where 64 was that "some space" for me. Code stops tableView at -64 from the top and brings it up with an animation.
Good luck!
If i understand correctly you have set-up your custom bar as part of your tableview. Put your custom bar in a separate view not in the tableview and put your tableview below custom bar when you are setting up your views. You need to create your custom view controller that will have your custom bar and your static table view.
You need to create your view controller object as type UIViewController and not UITableViewController. Then add the custom bar as a subview to self.view. Create a separate UITableView and add it below the custom bar. That should make custom bar static and table view scrollable.
Update:
In order to make the tableview static you need to set it as
tableView.scrollEnabled = NO:
Let me know if this works for you.
Swift version of Mattias's answer:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView == self.ordersTable) {
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPoint.zero;
}
}
}

make UIView in UIScrollView stick to the top when scrolled up

So in a UITableView when you have sections the section view sticks to the top until the next section overlaps it and then it replaces it on top. I want to have a similar effect, where basically I have a UIView in my UIScrollView, representing the sections UIView and when it hits the top.. I want it to stay in there and not get carried up. How do I do this? I think this needs to be done in either layoutSubviews or scrollViewDidScroll and do a manipulation on the UIVIew..
To create UIView in UIScrollView stick to the top when scrolled up do:
func createHeaderView(_ headerView: UIView?) {
self.headerView = headerView
headerViewInitialY = self.headerView.frame.origin.y
scrollView.addSubview(self.headerView)
scrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let headerFrame = headerView.frame
headerFrame.origin.y = CGFloat(max(headerViewInitialY, scrollView.contentOffset.y))
headerView.frame = headerFrame
}
Swift Solution based on EVYA's response:
var navigationBarOriginalOffset : CGFloat?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationBarOriginalOffset = navigationBar.frame.origin.y
}
func scrollViewDidScroll(scrollView: UIScrollView) {
navigationBar.frame.origin.y = max(navigationBarOriginalOffset!, scrollView.contentOffset.y)
}
If I recall correctly, the 2010 WWDC ScrollView presentation discusses precisely how to keep a view in a fixed position while other elements scroll around it. Watch the video and you should have a clear-cut approach to implement.
It's essentially updating frames based on scrollViewDidScroll callbacks (although memory is a bit hazy on the finer points).
Evya's solution works really well, however if you use Auto Layout, you should do something like this (The Auto Layout syntax is written in Masonry, but you get the idea.):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//Make the header view sticky to the top.
[self.headerView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_top).with.offset(scrollView.contentOffset.y);
make.left.equalTo(self.scrollView.mas_left);
make.right.equalTo(self.scrollView.mas_right);
make.height.equalTo(#(headerViewHeight));
}];
[self.scrollView bringSubviewToFront:self.headerView];
}

check if UIView is in UIScrollView visible state

What is the easiest and most elegant way to check if a UIView is visible on the current UIScrollView's contentView? There are two ways to do this, one is involving the contentOffset.y position of the UIScrollView and the other way is to convert the rect area?
If you're trying to work out if a view has been scrolled on screen, try this:
CGRect thePosition = myView.frame;
CGRect container = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.frame.size.width, scrollView.frame.size.height);
if(CGRectIntersectsRect(thePosition, container))
{
// This view has been scrolled on screen
}
Swift 5: in case that you want to trigger an event that checks that the entire UIView is visible in the scroll view:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(targetView.frame) {
// entire UIView is visible in scroll view
}
}
}
Implement scrollViewDidScroll: in your scroll view delegate and calculate manually which views are visible (e.g. by checking if CGRectIntersectsRect(scrollView.bounds, subview.frame) returns true.
updated for swift 3
var rect1: CGRect!
// initialize rect1 to the relevant subview
if rect1.frame.intersects(CGRect(origin: scrollView.contentOffset, size: scrollView.frame.size)) {
// the view is visible
}
I think your ideas are correct. if it was me i would do it as following:
//scrollView is the main scroll view
//mainview is scrollview.superview
//view is the view inside the scroll view
CGRect viewRect = view.frame;
CGRect mainRect = mainView.frame;
if(CGRectIntersectsRect(mainRect, viewRect))
{
//view is visible
}
José's solution didn't quite work for me, it was detecting my view before it came on screen. The following intersects code works perfect in my tableview if José's simpler solution doesn't work for you.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
Solution that takes into account insets
public extension UIScrollView {
/// Returns `adjustedContentInset` on iOS >= 11 and `contentInset` on iOS < 11.
var fullContentInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return adjustedContentInset
} else {
return contentInset
}
}
/// Visible content frame. Equal to bounds without insets.
var visibleContentFrame: CGRect {
bounds.inset(by: fullContentInsets)
}
}
if scrollView.visibleContentFrame.contains(view) {
// View is fully visible even if there are overlaying views
}

Making two UIScrollViews follow each others scrolling

How would I make two scroll views follow each others scrolling?
For instance, I have a scroll view (A) on the left of a screen, whose contents can scroll up and down, but not left and right. Scroll view B matches the up and down of A, but can also scroll left and right. Scroll view A is always on the screen.
-----------------------------------------------------------
| | |
| | |
| | |
| A | B |
| | |
| scrolls | |
| up & down | scrolls all directions |
| | |
-----------------------------------------------------------
How would I make it so the the up and down scrolling (of either view) also makes the other view scroll in the same up-down direction? Or is there another method to do this?
Set the delegate of scroll view A to be your view controller... then have...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
}
If you want both to follow each other, then set delegate for both of them and use...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
} else {
CGPoint offset = scrollViewA.contentOffset;
offset.y = scrollViewB.contentOffset.y;
[scrollViewA setContentOffset:offset];
}
}
The above can be refactored to have a method which takes in two scrollviews and matches one to the other.
- (void)matchScrollView:(UIScrollView *)first toScrollView:(UIScrollView *)second {
CGPoint offset = first.contentOffset;
offset.y = second.contentOffset.y;
[first setContentOffset:offset];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
[self matchScrollView:scrollViewB toScrollView:scrollViewA];
} else {
[self matchScrollView:scrollViewA toScrollView:scrollViewB];
}
}
Swift 3 Version:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == scrollViewA {
self.synchronizeScrollView(scrollViewB, toScrollView: scrollViewA)
}
else if scrollView == scrollViewB {
self.synchronizeScrollView(scrollViewA, toScrollView: scrollViewB)
}
}
func synchronizeScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
var offset = scrollViewToScroll.contentOffset
offset.y = scrolledView.contentOffset.y
scrollViewToScroll.setContentOffset(offset, animated: false)
}
I tried the Simon Lee's answer on iOS 11. It worked but not very well. The two scroll views was synchronized, but using his method, the scroll views would lost the inertia effect(when it continue to scroll after you release your finger) and the bouncing effect. I think it was due to the fact that setting the contentOffset through setContentOffset(offset, animated: false) method causes cyclic calls of the scrollViewDidScroll(_ scrollView: UIScrollView) delegate's method(see this question)
Here is the solution that worked for me on iOS 11:
// implement UIScrollViewDelegate method
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollViewA {
self.syncScrollView(self.scrollViewB, toScrollView: self.scrollViewA)
}
else if scrollView == self.scrollViewB {
self.syncScrollView(self.scrollViewA, toScrollView: scrollViewB)
}
}
func syncScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
var scrollBounds = scrollViewToScroll.bounds
scrollBounds.origin.y = scrolledView.contentOffset.y
scrollViewToScroll.bounds = scrollBounds
}
So instead of setting contentOffset we are using bounds property to sync the other scrollView with the one that was scrolled by the user. This way the delegate method scrollViewDidScroll(_ scrollView: UIScrollView) is not called cyclically and the scrolling happens very smooth and with inertia and bouncing effects as with a single scroll view.
Swift 5.4 // Xcode 13.1
What has worked flawlessly for me was the following:
Create a custom subclass of UIScrollView
Conform to UIGestureRecognizer delegate
Override the gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) GestureRecognizerDelegate method
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Both collection views need to have the same superview. Add both gesture recognizers to your superview
yourSuperview.addGestureRecognizer(scrollView1.panGestureRecognizer)
yourSuperview.addGestureRecognizer(scrollView2.panGestureRecognizer)
Hope this helps!
Guys i know the question is answered, but decided to share with you here my approach for solving a similar issue that i had, because i believe it is a pretty clean solution. Basically i had to make three collection views scroll together, and what i did, is i made a custom UICollectionView subclass called SharedOffsetCollectionView, that when you set this class to a collection view in storyboards or you instantiate it from code directly, you can have all instances scroll the same.
So with SharedOffsetCollectionView all collection view instances of this class in your app, will scroll the same always. In my opinion it is a clean solution because it requires adding zero logic in your view controllers, it is all contained in this external class, you just have to set the class of your collection view to be SharedOffsetCollectionView and you are done.
The same approach could easily be transferred to UITableViews and UIScrollViews
Hope that is helpful to some you. :)
My solution is written in:
Swift 5.2, XCode 11.4.1
The answer above all did not quite work our for me, since I run into a cyclic call of scrollViewDidScroll. This happend, because setting the content offset of a scroll view also calls scrollViewDidScoll. I solved it by putting a lock between it, which is set based on if a scroll view is being dragged by the user or not, so the syncing won't happen by setting the content offset:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.synchronizeScrollView(offSet: scrollView.contentOffset.y,
scrollViewAIsScrolling: scrollView == scrollViewA,
isScroller: scrollView.isDragging)
}
private enum Scroller {
case page, time
}
private var scroller: Scroller? = nil
func synchronizeScrollView(offSet: CGFloat, scrollViewAIsScrolling: Bool,
isScroller: scrollView.isDragging) {
let scrollViewToScroll = scrollViewAIsScrolling ? scrollViewB : scrollViewA
var offset = scrollViewToScroll.contentOffset
offset.y = offSet
scrollViewToScroll.setContentOffset(offset, animated: false)
}
This code can be refactored based on how many scroll views are used and based on who owns them. I won't recommend having one controller being the delegate of many scroll views. I would rather solve it with delegation.

UIScrollView, reaching the bottom of the scroll view

I know the Apple documentation has the following delegate method:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; // called when scroll view grinds to a halt
However, it doesn't necessarily mean you are at the bottom. Cause if you use your finger, scroll a bit, then it decelerates, but you are not actually at the bottom of your scroll view, then it still gets called. I basically want an arrow to show that there is more data in my scroll view, and then disappear when you are at the bottom (like when it bounces). Thanks.
I think what you might be able to do is to check that your contentOffset point is at the bottom of contentSize. So you could probably do something like:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
float bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height;
if (bottomEdge >= scrollView.contentSize.height) {
// we are at the end
}
}
You'll likely also need a negative case there to show your indicator when the user scrolls back up. You might also want to add some padding to that so, for example, you could hide the indicator when the user is near the bottom, but not exactly at the bottom.
So if you want it in swift, here you go:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
//reach bottom
}
if (scrollView.contentOffset.y < 0){
//reach top
}
if (scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)){
//not top and not bottom
}
}
I Think #bensnider answer is correct, But not exart. Because of these two reasons
1. - (void)scrollViewDidScroll:(UIScrollView *)scrollView{}
This method will call continuously if we check for if (bottomEdge >= scrollView.contentSize.height)
2 . In this if we go for == check also this condition will valid for two times.
(i) when we will scroll up when the end of the scroll view touches the bottom edge
(ii) When the scrollview bounces back to retain it's own position
I feel this is more accurate.
Very few cases this codition is valid for two times also. But User will not come across this.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y == roundf(scrollView.contentSize.height-scrollView.frame.size.height)) {
NSLog(#"we are at the endddd");
//Call your function here...
}
}
The accepted answer works only if the bottom contentInset value is non-negative. A slight evolution would consider the bottom of the contentInset regardless of it's sign:
CGFloat bottomInset = scrollView.contentInset.bottom;
CGFloat bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height - bottomInset;
if (bottomEdge == scrollView.contentSize.height) {
// Scroll view is scrolled to bottom
}
Actually, rather than just putting #bensnider's code in scrollViewDidScroll, this code (written in Swift 3) would be better performance-wise:
func scrollViewDidEndDragging(_ scrollView: UIScrollView,
willDecelerate decelerate: Bool) {
if !decelerate {
checkHasScrolledToBottom()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
checkHasScrolledToBottom()
}
func checkHasScrolledToBottom() {
let bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height
if bottomEdge >= scrollView.contentSize.height {
// we are at the end
}
}
It work in Swift 3:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == (scrollView.contentSize.height - scrollView.frame.size.height) {
loadMore()
}
}
See what items are currently displayed in the UIView, using something like indexPathsForVisibleRows and if your model has more items than displayed, put an arrow at the bottom.