I'm trying to achieve next behavior:
I add few items in the beginning of UICollectionView
Items are added with proper animation, but I keep content offset on current center cell (it will be first cell before adding new items).
How I'm doing it now:
[self.collectionView performBatchUpdates:^{
CGPoint contentOffset = self.collectionView.contentOffset;
contentOffset.x += kMyCollectionViewCellWidth * firstIndex;
((MyCollectionViewLayout *)self.collectionView.collectionViewLayout).proposedContentOffset = contentOffset;
[self.collectionView insertItemsAtIndexPaths:indexesToAdd];
} completion:nil];
And in MyCollectionViewLayout.m:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {
if (self.proposedContentOffset.x != NSNotFound) {
proposedContentOffset = self.proposedContentOffset;
CGPoint zeroPoint = CGPointZero;
zeroPoint.x = NSNotFound;
self.proposedContentOffset = zeroPoint;
}
return proposedContentOffset;
}
Though, on slow devices I can see weird animation of adding this cells. New cells appears above old ones and moves left, when I want them appear in the left immediately or at least under old cells.
Related
I've got a huge legacy project on objective-c and trying to implement new features before we starting to move it to swift.
So I'm working on chat right now and creating "Search" like in telegram.
After we got 200status from server we got 41 message in chat. Im set user screen to the middle of this messages, and need to implement pagination in both sides.
I've successfully added both features, and need to resolve last problem:
When user scroll UP or down to the another messages, I've got check in scrollViewDidScroll:
CGPoint offset = scrollView.contentOffset;
CGSize size = scrollView.contentSize;
CGFloat height = _tableView.frame.size.height;
if (offset.y > size.height - height) {
if((isLoadingMessages == NO) && (loadedMessagesUp % MAX_LOADING_MESS == 0)){
_isFromSearch = false;
NSInteger create = [[[_messagesArray lastObject] objectForKey:#"create"] integerValue];
_dateForPagination = create;
scrollDirectionDown = false;
[self loadMessages];
NSLog(#"pagination up");
}
}
if(offset.y < 10) {
if((isLoadingMessages == NO) && (loadedMessagesDown % MAX_LOADING_MESS == 0)){
_isFromSearch = false;
NSInteger create = [[[_messagesArray firstObject] objectForKey:#"create"] integerValue];
_dateForPagination = create;
scrollDirectionDown = true;
[self loadMessages];
NSLog(#"pagination down");
}
}
So, the question is:
When I've scrolledUP, my [_tableVIew contentOffset] calculated automatically (or I can't find where it happened, but I've check all project with cmd+f).
But when user scrollDOWN, to the new messages, table view added messages with method:
if (scrollDirectionDown) {
_paginationMessagesArray = [[[_paginationMessagesArray reverseObjectEnumerator]allObjects]mutableCopy];
[_paginationMessagesArray addObjectsFromArray: _messagesArray];
_messagesArray = _paginationMessagesArray;
}
And after this dropped my tableview to the [tableView offset] = 0 (equal indexPath.row = 0). So this automatically calls method with pagination and again dropped page to first element.
I think that I need to set new contentOffset after [tableview reloadData]. But I can't calculate it.
Can somebody help: how to calculate offset from to tableView minY to the tableView currentY ? Or I do everything wrong?
I suppose that I can't find some method in this terrible project, but I hope you can help to me with this situation.
in this picture I need to calculate offset from bottom of black square to the button of blue square. Because after [tableView reloadData] black square (which is screen of the phone) immediately drops to the bottom of blue Screen and turning pagination again. And it repeating and repeating..
THanks a lot, mates!
I found answer by changing my search in google :D 3 days I've spent on this.
UIScrollView disable vertical bounce only at bottom
I'm a beginner at objective C learning to program, and also beginner at asking questions on this site, please bear with me.
I am currently trying to draw a column of boxes (UIControls) on the screen, and be able to scroll them upward or downward infinitely. So when one goes off the bottom of the screen its shifted to the bottom and reused.
I know there must be a lot of mistakes in the code. But the gist of what I am trying to do is: The boxes are all in an array (imArray). When a box scrolls off the bottom of the screen its taken off the end of the array, and inserted at the beginning. Then the box inserts itself graphically into the top of the column.
The first if statement deals with scrolling off the bottom of the screen, and it works fine. But the second if statement, where i try to do the opposite with similar code works only when i scroll slowly, when i scroll quickly the spacing between boxes becomes uneven, and sometimes a box just locks up on the screen and stops moving.
Any help is appreciated, and I will try to provide any more clarity that may be needed.
-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint pt = [touch locationInView:self];
int yTouchEnd = pt.y;
int yTouchChange = yTouchEnd - yTouchStart;
//iterate through all boxes in imArray
for(int i = 0; i < self.numberOfSections; i++)
{
//1. get box
STTimeMarker *label = self.imArray[i];
//2. calculate new label transform
label.transform = CGAffineTransformTranslate(label.startTransform, 0, yTouchChange);
CGRect frame = label.frame;
//3. if the box goes out of the screen on the bottom
if (frame.origin.y > [[UIScreen mainScreen]bounds].size.height)
{
//1. move box that left the screen to to beginning of array
[self.imArray removeObjectAtIndex:i];
[self.imArray insertObject:label atIndex:0];
//2. get y value of box closest to top of screen.
STTimeMarker *labelTwo = self.imArray[1];
CGRect frameTwo =labelTwo.frame;
//3. put box that just left the screen in front of the box I just got y value of.
frame.origin.y = frameTwo.origin.y - self.container.bounds.size.height/self.numberOfSections;
label.frame=frame;
}
//1. if the box goes out of the frame on the top
// (box is 40 pixels tall)
if (frame.origin.y < -40)
{
[self.imArray removeObjectAtIndex:i];
[self.imArray addObject:label];
STTimeMarker *labelTwo = self.imArray[self.numberOfSections-1];
CGRect frameTwo =labelTwo.frame;
frame.origin.y = frameTwo.origin.y + self.container.bounds.size.height/self.numberOfSections;
label.frame=frame;
}
}
return YES;
}
If I understand what you are trying to do correctly, I think you want to come at this a different way. Your data model (the array) does not need to change. All that is changing as you scroll is the view, what is displaying on screen. The simplest way to achieve the appearance of an infinite scroll would be to use a UITableView and give it some large number of cells. Then your cellForRowAtIndexPath: method will return the cell for the correct position using the mod operator (%). Untested code:
- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {
return 99999;
}
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
NSInteger moddedRow = indexPath.row % [self.imArray count];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kSomeIdentifierConst forIndexPath:[NSIndexPath indexPathForRow:moddedRow inSection:indexPath.section]];
return [self configureCellWithData:self.imArray[moddedRow]];
}
This may not be sufficient for your purposes if you need true infinite scroll, but should work for most purposes.
I am implementing an infinite-scrolling calendar. My issue is that I would like to set the current month as the title in the navigation bar and it should update while scrolling - once you pass the section header view the title should update in the nav bar.
A possible solution would be to set the view title in the method called - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath so that, when I calculate a new section Header, it also updates the title. The problem with this is that the title changes when the new section is at the bottom of the page.
Is there a way to know the "current section" of UICollectionView once the user has scrolled to it? Or can you think of a way to improve my current solution?
To help the readers of this post, I posted my own sample code for this question at this GitHub repo.
I have been pondering an algorithm that would allow you to know when the user has scrolled past a section header in order to update the title, and after some experimentation I have figured out how to implement the desired behavior.
Essentially, every time the scroll position changes you need to know what section the user is on and update the title. You do this via scrollViewDidScroll on the UIScrollViewDelegate - remembering a collection view is a scroll view. Loop over all the headers and find the one that's closest to the current scroll position, without having a negative offset. To do that, I utilized a property that stores an array of each section header's position. When a header is created, I store its position in the array at the appropriate index. Once you've found the header that's closest to your scroll position (or the index location of said header), simply update the title in the navigation bar with the appropriate title.
In viewDidLoad, fill the array property with NSNull for each section you have:
self.sectionHeaderPositions = [[NSMutableArray alloc] init];
for (int x = 0; x < self.sectionTitles.count; x++) {
[self.sectionHeaderPositions addObject:[NSNull null]];
}
In collectionView:viewForSupplementaryElementOfKind:atIndexPath:, update the array with the position of the created header view:
NSNumber *position = [NSNumber numberWithFloat:headerView.frame.origin.y + headerView.frame.size.height];
[self.sectionHeaderPositions replaceObjectAtIndex:indexPath.section withObject:position];
In scrollViewDidScroll:, perform the calculations to determine which title is appropriate to display for that scroll position:
CGFloat currentScrollPosition = self.collectionView.contentOffset.y + self.collectionView.contentInset.top;
CGFloat smallestPositiveHeaderDifference = CGFLOAT_MAX;
int indexOfClosestHeader = NSNotFound;
//find the closest header to current scroll position (excluding headers that haven't been reached yet)
int index = 0;
for (NSNumber *position in self.sectionHeaderPositions) {
if (![position isEqual:[NSNull null]]) {
CGFloat floatPosition = position.floatValue;
CGFloat differenceBetweenScrollPositionAndHeaderPosition = currentScrollPosition - floatPosition;
if (differenceBetweenScrollPositionAndHeaderPosition >= 0 && differenceBetweenScrollPositionAndHeaderPosition <= smallestPositiveHeaderDifference) {
smallestPositiveHeaderDifference = differenceBetweenScrollPositionAndHeaderPosition;
indexOfClosestHeader = index;
}
}
index++;
}
if (indexOfClosestHeader != NSNotFound) {
self.currentTitle.text = self.sectionTitles[indexOfClosestHeader];
} else {
self.currentTitle.text = self.sectionTitles[0];
}
This will correctly update the title in the nav bar once the user scrolls past the header for a section. If they scroll back up it will update correctly as well. It also correctly sets the title when they haven't scrolled past the first section. It however doesn't handle rotation very well. It also won't work well if you have dynamic content, which may cause the stored positions of the header views to be incorrect. And if you support jumping to a specific section, the user jumps to a section whose previous section's section header hasn't been created yet, and that section isn't tall enough such that the section header is underneath the nav bar (the last section perhaps), the incorrect title will be displayed in the nav bar.
If anyone can improve upon this to make it more efficient or otherwise better please do and I'll update the answer accordingly.
change below line in method viewForSupplementaryElementOfKind :
self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section]];
to this:
if(![[self dateForFirstDayInSection:indexPath.section-1] isKindOfClass:[NSNull class]]){
self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section-1]];
}
Hope it will help you.
Yes, the problem is that footer and header are not exists in visibleCells collection. There is other way to detect scroll for section header/footer. Just add a control there and find the rect for it. Like this:
func scrollViewDidScroll(scrollView: UIScrollView) {
if(footerButton.tag == 301)
{
let frame : CGRect = footerButton.convertRect(footerButton.frame, fromView: self.view)
//some process for frame
}
}
No solution here is fulfilling, so I came up with my own that I want to share, fully. If you use this, you have to make some pixel adjustments, though.
extension MyViewControllerVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.myCollectionView {
let rect = CGRect(origin: self.myCollectionView.contentOffset, size: self.cvProductItems.bounds.size)
let cellOffsetX: CGFloat = 35 // adjust this
let cellOffsetAheadY: CGFloat = 45 // adjust this
let cellOffsetBehindY: CGFloat = 30 // adjust this
var point: CGPoint = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY + cellOffsetAheadY) // position of cell that is ahead
var indexPath = self.myCollectionView.indexPathForItem(at: point)
if indexPath?.section != nil { // reached next section
// do something with your section (indexPath!.section)
} else {
point = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY - cellOffsetBehindY) // position of cell that is behind
indexPath = self.myCollectionView.indexPathForItem(at: point)
if indexPath?.section != nil { // reached previous section
// do something with your section (indexPath!.section)
}
}
}
}
}
UICollectionView inherits UIScrollView, so we can just do self.myCollectionView.delegate = self in viewDidLoad() and implement the UIScrollViewDelegate for it.
In the scrollViewDidScroll callback we will first get the point of a cell below, adjust cellOffsetX and cellOffsetAheadY properly, so your section will be selected when the cell hits that point. You can also modify the CGPoint to get a different point from the visible rect, i.e for x you can also use rect.midX / rect.maxX and any custom offset.
An indexPath will be returned from indexPathForItem(at: GCPoint) when you hit a the cell with those coordinates.
When you scroll up, you might want to look ahead, possibly ahead your UICollectionReusableView header and footer, for this I also check the point with negative Y offset set in cellOffsetBehindY. This has lower priority.
So, this example will get the next section once you pass the header and the previous section once a cell of the previous section is about to get into view. You have to adjust it to fit your needs and you should store the value somewhere and only do your thing when then current section changes, because this callback will be called on every frame while scrolling.
I want to change the height of my UITableView based on the scrolled content. Right now I do it by getting the scrollViewDidScroll event and then getting scrolled value and then change the height. Here is my code (for simplification I omitted the irrelevant code):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
float currentOffset = scrollView.contentOffset.y
double delta = currentOffset - storedOffset;
if(delta != 0)
{
delta = abs(delta);
CGRect newListFrame = myTableView.frame;
float newListHeight = newListFrame.size.height + delta;
newListFrame.size.height = newListHeight;
myTableView.frame = newListFrame;
}
storedOffset = currentOffset;
}
But this approach is wrong because with this approach my UITableView's content is scrolled only a little bit and that's not what I want. I just want to get the value of that list that would be scrolled without actually scrolling it. Is there any way to do that? I thing I could get raw finger moved event but can I get it on UITableVIew? Can I do something like this using a UITableView method?
I have a UICollectionView. It scrolls horizontally, has only a single row of items, and behaves like a paging UIScrollView. I'm making something along the lines of the Safari tab picker, so you can still see the edge of each item. I only have one section.
If I delete an item that is not the last item, everything works as expected and a new item slides in from the right.
If I delete the last item, then the collection view's scroll position jumps to the N-1th item (doesn't smoothly animate), and then I see the Nth item (the one I deleted) fade out.
This behaviour isn't related to the custom layout I made, as it occurs even if I switch it to use a plain flow layout. I'm deleting the items using:
[self.tabCollectionView deleteItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:index inSection:0]]];
Has anyone else experienced this? Is it a bug in UICollectionView, and is there a workaround?
I managed to get my implementation working using the standard UICollectionViewFlowLayout. I had to create the animations manually.
First, I caused the deleted cell to fade out using a basic animation:
- (void)tappedCloseButtonOnCell:(ScreenCell *)cell {
// We don't want to close our last screen.
if ([self screenCount] == 1u) {
return;
}
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration
animations:^{
// Fade out the cell.
cell.alpha = 0.0f;
}
completion:^(BOOL finished) {
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
UIViewController *screen = [self viewControllerAtIndex:indexPath.item];
[self removeScreen:screen animated:YES];
}];
}
Next, I caused the collection view to scroll to the previous cell. Once I've scrolled to the desired cell, I remove the deleted cell.
- (void)removeScreen:(UIViewController *)screen animated:(BOOL)animated {
NSParameterAssert(screen);
NSInteger index = [[self.viewControllerDictionaries valueForKeyPath:kViewControllerKey] indexOfObject:screen];
if (index == NSNotFound) {
return;
}
[screen willMoveToParentViewController:nil];
if (animated) {
dispatch_time_t popTime = DISPATCH_TIME_NOW;
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index
inSection:0];
// Disables user interaction to make sure the user can't interact with
// the collection view during the time between when the scroll animation
// ends and the deleted cell is removed.
[self.collectionView setUserInteractionEnabled:NO];
// Scrolls to the previous item, if one exists. If we are at the first
// item, we just let the next screen slide in from the right.
if (index > 0) {
popTime = dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC);
NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:index - 1
inSection:0];
[self.collectionView scrollToItemAtIndexPath:targetIndexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
animated:YES];
}
// Uses dispatch_after since -scrollToItemAtIndexPath:atScrollPosition:animated:
// doesn't have a completion block.
dispatch_after(popTime, dispatch_get_main_queue(), ^{
[self.collectionView performBatchUpdates:^{
[self.viewControllerDictionaries removeObjectAtIndex:index];
[self.collectionView deleteItemsAtIndexPaths:#[indexPath]];
[screen removeFromParentViewController];
[self.collectionView setUserInteractionEnabled:YES];
} completion:NULL];
});
} else {
[self.viewControllerDictionaries removeObjectAtIndex:index];
[self.collectionView reloadData];
[screen removeFromParentViewController];
}
self.addPageButton.enabled = YES;
[self postScreenChangeNotification];
}
The only part that is slightly questionable is the dispatch_after(). Unfortunately, -scrollToItemAtIndexPath:atScrollPosition:animated: does not have a completion block, so I had to simulate it. To avoid timing problems, I disabled user interaction. This prevents the user from interacting with the collection view before the cell is removed.
Another thing I had to watch for is I have to reset my cell's alpha back to 1 due to cell reuse.
I hope this helps you with your Safari-style tab picker. I know your implementation is different from mine, and I hope that my solution works for you too.
I know this has an answer already but I implemented this in a slightly different way that doesn't require dispatching after a set interval.
In your delete method you would do a check to determine if the last item was being deleted. If it was call the following:
if(self.selection == self.assets.count-1 && self.selection != 0){
isDeleting = YES;
[collection scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.selection-1 inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
Assuming selection is the selected item you are deleting. This will scroll to the item to the left of it. Note the if statement checking that this is not the only item. If it were the call would crash as there is no -1 row.
Then you can implement the following method which is called when the scroll animation is complete. I simply set isDeleting to no in the deleteObjectInCollection method and it all seems to work.
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
if(isDeleting){
[self deleteObjectInCollection];
}
}
I hope this helps.
This will work:
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutIfNeeded()
And here's yet another solution (targetIndexPath is the indexPath of the cell to be removed). This code can simply be placed in a removeCellAtIndexPath:(NSIndexPath*)targetIndexPath method and you're done (assuming my adaptations from my code to public code are correct, otherwise ask me and I'll try to help).
// (Here it's assumed that self.collectionView.collectionViewLayout has been assigned a UICollectionViewFlowLayout before; note the word "FLOW" in that class name)
UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*) self.collectionView.collectionViewLayout;
// Find index path of the last cell in collection view:
NSIndexPath* lastIndexPath = [self lastItemIndexPath];
// Extend content area temporarily to fill out space left by the last row after deletion, if last row is visible:
BOOL lastRowWasVisible = NO;
if ([self.collectionView icn_cellAtIndexPathIsVisible:lastIndexPath]) {
lastRowWasVisible = YES;
// Adapt section spacing to temporarily fill out the space potentially left after removing last cell:
CGFloat cellWithLineSpacingHeight = 79.0f + 8.0f; // Height of my cell + one line spacing
layout.sectionInset = UIEdgeInsetsMake(0.0f, 0.0f, cellWithLineSpacingHeight, 0.0f);
}
// Remove the cell:
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:targetIndexPath]];
} completion:^(BOOL finished) {
// Only scroll if we had the last row visible:
if (lastRowWasVisible) {
NSIndexPath* lastItemIndexPath = [self lastItemIndexPath];
// Run a custom scroll animation for two reasons; 1. that way we can reset the insets when animation is finished, and 2. using the "animated:YES" option lags here for some reason:
[UIView animateWithDuration:0.3f animations:^{
[self.collectionView scrollToItemAtIndexPath:prevItemIndexPath atScrollPosition:UICollectionViewScrollPositionBottom animated:NO];
} completion:^(BOOL finished) {
// Reset the space placeholder once having scrolled away from it:
layout.sectionInset = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f);
}];
}
}];
The icn_cellAtIndexPathIsVisible: method is just a category on UICollectionView:
- (BOOL) icn_cellAtIndexPathIsVisible:(NSIndexPath*)indexPath {
BOOL __block wasVisible = NO;
[self.indexPathsForVisibleItems enumerateObjectsUsingBlock:^(NSIndexPath* ip, NSUInteger idx, BOOL *stop) {
if ([ip isEqual:indexPath]) {
wasVisible = YES;
*stop = YES;
}
}];
return wasVisible;
}
Update:
This only works with one section.
A cheap solution is to add another 0px cell as the last cell and never remove it.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if indexPath.row < collectibles.count - 1 { // dont delete the last one. just because the animation isn't working
collectibles.removeAtIndex(indexPath.row)
collectionView.deleteItemsAtIndexPaths([indexPath])
}
}
I was facing a similar issue with a single line horizontal scrolling through images collection view. The issue disappeared when i removed my code for setting the collection view's contentSize it seems to handle this automagically.
Not a particularly verbose answer, but i hope it helps.