Is it possible to animate each item individually in a UICollectionView when I transition to a child View Controller? I have a UICollectionViewController and when an item is selected I want all items surrounding that item to bounce away from the selected item.
What you describe sounds like a layoutTransition in your UICollectionView. My suggestion would be to create a new layout by subclassing UICollectionViewLayout and override
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
There you can modify the frames of your items to create the effect you are looking for. In particular, you could keep track of the indexPath that was selected and remain that item unaffected. For the rest you could do something like this:
CGRect selectedItemFrame = [[super layoutAttributesForItemAtIndexPath:self.selectedIndexPath] frame];
CGFloat xOffset = attributes.frame.origin.x <= selectedItemFrame.origin.x ? -400.0 : 400.0;
CGFloat yOffset = attributes.frame.origin.y <= selectedItemFrame.origin.y ? -600.0 : 600.0;
CGRect newFrame = CGRectOffset(attributes.frame, xOffset, yOffset);
attributes.frame = newFrame;
Then, just trigger the transition from one layout to the other.
Related
I'd like to implement a "zoom" effect on a paging UIScrollView that I've created, but I am having a lot of difficulty. My goal is that as a user begins to scroll to the next page, the current page zooms out to become a little bit smaller. As the next page comes into view, it zooms in until it becomes its full size. The closest thing I could find to an example was this...
https://www.pinterest.com/pin/147141112804210631/
Can anyone give me some pointers on how to accomplish this? I've been banging my head against a wall for the last 3 days on this.
I would recommend using the scrollView.contentOffset.y of your paginated UIScrollView to keep track of the scroll and to use that value to animate the transform of your views inside the UIScrollView.
So add your paginated scrollview and make self as delegate.
paginatedScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [[self view] bounds].size.width, [[self view] bounds].size.height-paginatedScrollViewYOffset)];
[self.view addSubview:paginatedScrollView];
paginatedScrollView.pagingEnabled = YES;
[paginatedScrollView setShowsVerticalScrollIndicator:NO];
[paginatedScrollView setShowsHorizontalScrollIndicator:NO];
[paginatedScrollView setAlwaysBounceHorizontal:NO];
[paginatedScrollView setAlwaysBounceVertical:YES];
paginatedScrollView.backgroundColor = [UIColor clearColor];
paginatedScrollView.contentSize = CGSizeMake([[self view] bounds].size.width, [[self view] bounds].size.height*2); //this must be the appropriate size depending of the number of pages you want to scroll
paginatedScrollView.delegate = self;
Then use the delegate method scrollViewDidScroll to keep track of the scrollView.contentOffset.y
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(#"Scroll Content Offset Y: %f",scrollView.contentOffset.y);
//use here scrollView.contentOffset.y as multiplier with view.transform = CGAffineTransformMakeScale(0,0) or with view.frame to animate the zoom effect
}
Use this Code scrollview its zoom in when scroll next page, the code is given below,
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GridCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectCell" forIndexPath:indexPath];
cell.myscrollview.minimumZoomScale = 5.0;
cell.myscrollview.zoomScale = 5.0;
cell.myscrollview.contentSize = cell.contentView.bounds.size;
return cell;
}
if you change the zoom scale value its automatically zoom in or zoom out to be showed when scroll next or previous page.
hope its helpful.
I actually just posted an answer to a very similar question, where somebody tried to achieve this effect using a UICollectionView. The link to my answer is here: https://stackoverflow.com/a/36710965/3723434
Relevant piece of code I will post here:
So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the pages' size based on their distance from the center of the screen.
For every page, calculate the distance of its center to the center of yourScrollView
The center of yourScrollView can be found using this nifty method: CGPoint point = [self.view convertPoint:yourScrollView.center toView:*yourScrollView];
Now set up a rule, that if the page's center is further than x away, the size of the page is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size, 2.
then you can use the following if/else idea:
if (distance > x) {
page.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
page.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the page's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
I've done some work on stylized app guide page before.
For Me, I would use CADisplayLink to track the contentOffset.x of the scrollView, associate the value with your animation process. Don't put your views on the scrollView, put them on an overlay view of this scrollView.
This solution follows the philosophy: Fake it before you make it.
Based on CADisplayLink and physics simulation of UIScrollView, you will get smooth animation. Believe me.
What you really want isn't a UIScrollView, it's a UICollectionView with a custom layout. UICollectionViewLayoutAttributes has a transform property that you can set.
Say for example, in layoutAttributesForElementsInRect::
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElementsInRect(rect) else {
return nil
}
return attributes.map { attribute -> UICollectionViewLayoutAttributes in
if attribute.frame.origin.y < 0 {
let scale = -attribute.frame.origin.y / attribute.frame.height
attribute.transform = CGAffineTransformMakeScale(scale, scale)
}
return attribute
}
}
Here, you're filtering by if the element is on the screen (so non-visible elements won't be counted) and checking to see if the y offset is less than 0. If it is, you take the difference between the negated y value and the item's height and turn that into a proportional scale.
You can do it however you want, if you want the scale to be between 1 and 0.5 for example. I like this way of doing things over mucking around with a scroll view.
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.
Objective-C beginner here.
My app structure is as follows:
TabBarController -> NavigationController -> UIViewController -> Table View.
In the table view you can find post objects which I would like to refresh and load more.
If user scrolls from top to bottom I want to delete all posts and then load 5 newest posts.
If user reaches the end of the list I want to load 5 older post.
I tried some suggestions from here and other sites but nothing seems to work.
Thanks for the comment. That helped and no I did not found that post. What I have right now is:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
float reload_distance = -100;
if(offset.y (h + reload_distance)) {
NSLog(#"load more rows");
}
}
The code block deletes a if statement before the one that you can see. How is that possible?
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?
In my UICollectionView, one of the cells contains an animation will go out of the cell's bounds, and now it is covered by other cells nearby. Is it possible to make this special cell on top of others, so it can present animation perfectly?
Thanks.
I had this same need
Specifically, I want to present a modal view controller based on the cell within the collection view that was selected. You can see this sort of behavior in iBooks when you are looking at the library. Select a "book" from the collection, and it "pops out" and opens to the book.
Simply scaling the cell "as is" may have it overlaid by other cells, which is not desirable.
To get this to work, you simply remember the cell's superview (it may not be the UICollectionView directly!) and its original center point. Then you remove it from its superview, add it to your main view (optionally bringing it to the front), perform your animation. In the completion block, reset your transform to Identity (no transform), remove it from your main view, and add it back to its original parent and center point.
I have example code below, but there are a few caveats:
I use IVARS for animation state information because I use the information in multiple methods in my actual implementation
I'm also using QuartzCore 3D transformations due to additional transformations my code does that is outside the scope of this answer
I added an "animating" property to my cell to have it alter its appearance during the animation (so it will scale nicer)
Finally, I switched my code to do a standard modal presentation at the end in this example, rather than calling a non-standard presentation controller which I did in my actual app for the actual launching of the next VC in the scene
There are probably other tweaks you will need to do as well, but the basic technique I outlined above will definitely work.
// Assume: UIView *cell == cell we are animating
// Assume: UIViewController *vc == View controlelr we will launch
// Assume: IVAR CGPoint _animationCellOrigCenter
// Assume: IVAR UIView *_animationCellParent
// Assume: IVAR UIView *_animationCell;
// Save "before" state before animating:
_animationCellOrigCenter = cell.center;
_animationCellParent = cell.superview;
_animationCell = cell;
// Where is the cell currently in the main view?
CGPoint cellViewLoc = [self.view convertPoint:_animationCellOrigCenter fromView:_animationCellParent];
// OK. Hoist the cell into the main view:
[cell removeFromSuperview];
cell.center = cellViewLoc;
[self.view addSubview:cell];
[self.view bringSubviewToFront:cell];
CGSize cellSize = cell.bounds.size;
CGSize newSize = CGSizeMake(600,600); // YMMV
[cell.superview bringSubviewToFront:cell];
// Get ready to do some animating:
CGSize viewSize = collectionView.bounds.size;
CGPoint destination = CGPointMake(viewSize.width / 2, viewSize.height / 2);
CGFloat scaleX = newSize.width / cellSize.width;
CGFloat scaleY = newSize.height / cellSize.height;
cell.animating = YES;
[UIView animateWithDuration:.2
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
cell.layer.transform = CATransform3DMakeScale(scaleX, scaleY, 1);
cell.center = destination;
}
completion:^(BOOL finished) {
[self presentViewController:vc
animated:YES
completion:^{
// Put everything back the way we found it!
[_animationCell removeFromSuperview];
[_animationCellParent addSubview:_animationCell];
_animationCell.center = _animationCellOrigCenter;
_animationCell.layer.transform = CATransform3DIdentity;
_animationCell.animating = NO;
}
];
}
];
This is fairly straightforward. Just add the cell to the collectionview's scrollview before you expand it on top of the others, and then remove it when you're no longer wanting it on top. Below I've done this and then added an animation on each cell itself to have it expand on top the others. I've left that animation out for simplicity.
override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.cellForItemAtIndexPath(indexPath)
self.collectionView?.addSubview(cell!)
return true
}
override func collectionView(collectionView: UICollectionView, shouldDeselectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.cellForItemAtIndexPath(indexPath)
if (cell?.superview != nil) {
cell?.removeFromSuperview()
}
return true
}