I was wondering how I might go about implementing a scrolling parallax effect similar to what is seen in TodoMovies 3?
In TodoMovies 3, the background of (what I think is) a UITableViewCell moves faster than the scrolling of the page, making a really awesome effect.
How would you go about detecting the scroll of the TableView's scrollview and also adjust the background image of the cell in a performant way?
Or is the effect impossible to achieve with UITableView?
I was able to achieve the effect by placing this code in my UITableViewController
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (TVTableViewCell *cell in [self.tableView visibleCells]) {
[cell adjust:(cell.frame.origin.y - scrollView.contentOffset.y)];
}
}
And this code in my UITableViewCell
- (void)adjust:(CGFloat)offset {
CGRect frame = self.image.frame;
frame.origin.y = (offset / 10.0);
self.image.frame = frame;
}
Modified from oleb.net/blog/2014/05/parallax-scrolling-collectionview. Thanks mustafabesnili for the link.
Related
I am using the following code to find the visible cells in my tableview. Everytime the user scrolls only one UITableViewCell is visible to the user but the code below returns two visible rows because this is how the UITableView functioning when user scrolls to a cell.
NSArray *visibleCells = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *cellIndexPath in visibleCells)
{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:cellIndexPath];
if ([cell isKindOfClass:[C8SubmittedContentTableViewCell class]]) {
[submittedContentTableViewCell play];
}
}
I run the code above at
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
But as i mentioned UITableView returns two visible cells. How would i know which one of them is really visible to the user? Because if i have two or three videos in a row i need to start playing only the one that is really visible to the user and at the same time stop playing the previous one.
Any thoughts or help appreciated!
Here is possible way:
CGRect cellRect = [self.tableView rectForRowAtIndexPath:cellIndexPath];
CGRect visibleRect;
visibleRect.origin = self.tableView.contentOffset;
visibleRect.size = self.tableView.bounds.size;
BOOL isCellVisible = CGRectContainsRect(visibleRect, cellRect);
Your layout is not clear so possible CGRectIntersectsRect might be needed, or even manual rect to rect matching (say, if cell rect is always bigger then table clip area, the percentage of overlapping should be used).
Use delegate's willDisplayCell for starting video playing and didEndDisplayingCell for stopping.
I have a custom cell which should be spaced from the edges of the display. For that I use this:
- (void)setFrame:(CGRect)frame {
frame.origin.x += kCellSidesInset;
frame.size.width -= 2 * kCellSidesInset;
[super setFrame:frame];
}
I do have a button that hides/shows the bottom view of a stacked view inside the cell. For which I use this code:
- (IBAction)showCardDetails:(id)sender {
UITableView *cellTableView = (UITableView*)[[[[sender superview] superview] superview] superview ];
[cellTableView beginUpdates];
self.details.hidden = !self.details.hidden;
[cellTableView endUpdates];
// [cellTableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationBottom];
// [cellTableView reloadData];
}
However when the table is updated to reflect the change the right padding becomes allot bigger. Then when I scroll a bit it gets fixed.
As much as I could visually judge it is like 3 times. Maybe it adds two more kCellSidesInset on the right but I doubt it.
Why is this happening and how can it be fixed? Maybe it can be avoid by instead of giving inset to the cell giving it to the UITableView (I have some trouble figuring how to do this).
PS. All the code is inside the CustomCell.m. I am open for a suggestion to a better way of getting the UITableView inside the action. Should I use selector in the CustomTableViewController.m to implement the action there when the cell is added?
EDIT: From what I can see the re rendering of the cells goes trough three phases.
Phase one, a couple of these:
Phase two, it updates the view cells:
And here everything looks good for now. The view that I want to hide/show is hidden/shown and all is good but then some sort of cleanup breaks the layout:
I solved the problem by refactoring the setFrame method to use the superview's frame of the cell as a reference point for the cell frame
- (void)setFrame:(CGRect)frame {
frame.origin.x = self.superview.frame.origin.x + kCellSidesInset;
frame.size.width = self.superview.frame.size.width - 2 * kCellSidesInset;
[super setFrame:frame];
}
Currently I have a UITableView with a resizing UITextView in it. The cell is resizing automatically using beginUpdates/endUpdates, but when it does it the table view stutters (See the gif below).
The end result is a UITableViewCell that has a textview in it that resizes based on it's content. Here is the code within the custom UITableViewCell class that causes the UITableView to update itself.
- (void)textViewDidChange:(UITextView *)textView {
// This is a category on UITableViewCell to get the [self superView] as the UITableView
UITableView *tableView = [self tableView];
if (tableView){
[tableView beginUpdates];
[tableView endUpdates];
}
}
Here are the things that I have already tried:
Get the current contentOffset and resetting it after the endUpdates but didn't work
Disabling scrolling on the UITableView before updates and then enabling afterwards
I tried returning NO always from - (BOOL)textViewShouldEndEditing:(UITextView *)textView
My UITableView cell height is using UITableViewAutomaticDimension. Any other ideas or thoughts are welcome.
Here is a sample of what it looks like:
I am not looking to use any libraries so please no suggestions for that.
Thanks
Edit: Solution
Found Here: I do not want animation in the begin updates, end updates block for uitableview?
Credit to #JeffBowen for a great find (although hacky it is workable and allows me to still implement the UITableViewDelegate methods for supporting iOS 7). Turn animations off prior to performing update and then enable after update to prevent the UITableView from stuttering.
[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];
If you don't need to use the Delegate methods and want a less hacky solution for iOS 8+ only then go with #Massmaker's answer below.
Just disable animation before calling beginUpdates and re-enable it after calling endUpdates.
[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];
Definitely a hack but works for now. Credit to my friend Beau who pointed me to this.
My solution (for iOS 8) was first set in my viewController viewDidLoad
self.tableView.rowHeight = UITableViewAutomaticDimension;
// this line is needed to cell`s textview change cause resize the tableview`s cell
self.tableView.estimatedRowHeight = 50.0;
then, combining this article solution in Swift and some dirty thoughts
I`ve set in my cell a property, called
#property (nonatomic, strong) UITableView *hostTableView;
and in cell-s -(void) textViewDidChange:(UITextView *)textView
CGFloat currentTextViewHeight = _textContainer.bounds.size.height;
CGFloat toConstant = ceilf([_textContainer sizeThatFits:CGSizeMake(_textContainer.frame.size.width, FLT_MAX)].height);
if (toConstant > currentTextViewHeight)
{
[_hostTableView beginUpdates];
[_hostTableView endUpdates];
}
then in viewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
textCell.hostTableView = self.tableView;
I don't find a way to achieve it because:
When you trigger [tableView endUpdates], table recalculate contentSize and re-set it. And this cause resetting contentOffset to default value.
This is behaviour inherited from UIScrollView, and I tried to avoid it via:
Subclassing UITableView and overriding setContentOffset: and setContentOffset:animated functions. But they don't called when table view change contentSize.
Subclassing UITableView, overriding setContentSize: function and setting contentOffset to old value, after content size updating, but it not work for me
using KVO, and setting old value for contentOffset right after it reset, but anyway I have this animated issue
setting scrollEnabled to NO and scrollToTop to NO, but it also not help
If anybody find solution for this problem welcome.
Maybe possible solution - disable autolayout: iOS: setContentSize is causing my entire UIScrollView frame to jump
UPDATE: I find solution: direct changing cell height, content size and content offset:
This works for me (table view is delegate of cell's UITextView)
- (void)textViewDidChange:(UITextView *)textView
{
CGFloat textHeight = [textView sizeThatFits:CGSizeMake(self.width, MAXFLOAT)].height;
if (self.previousTextHeight != textHeight && self.previousTextHeight > 0) {
CGFloat difference = textHeight - self.previousTextHeight;
CGRect cellFrame = self.editedCell.frame;
cellFrame.size.height += difference;
self.editedCell.frame = cellFrame;
self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height + difference);
self.contentOffset = CGPointMake(self.contentOffset.x, self.contentOffset.y + difference);
}
self.editedNote.comments = textView.text;
}
In Textview value change delegate, use this code to resize particular cell without any flickering and all.
But before that, make sure you have used dynamic tableview cell height.
UIView.setAnimationsEnabled(false)
DispatchQueue.main.async {
self.tableView.beginUpdates()
self.tableView.endUpdates()
self.tableView.layer.removeAllAnimations()
}
UIView.setAnimationsEnabled(true)
I don't really know how to explain what I want, so here's an picture :
I have a view with a lot of subviews (gray lines). Then the background (blue) is a picture (UIImageView + Blur effect), so I need it to stay and not to scroll. Behind the background, there's a view (orange). I want the picture (blue) to scroll only when the subviews (gray) are at the bottom (3rd picture).
Should I use embed scrollviews, or can I get this effect with only one UIScrollView ? If multiple scrollviews, does someone have an example ?
Thanks a lot for your help
Use only one scroll view, and use its delegate method 'scrollViewDidScroll' to read the content offset. Use this value to translate your background image.
It's not necessary to use 2 scroll views. But I would use it since I prefer setting contentOffset of the background scroll view than setting it's frame directly.
The idea is that you implement scrollViewDidScroll: delegate method for the front scroll view. And track the contentOffset to check whether the scrolling has reached the end of it's content by checking whether scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height or not.
If it has, then you offset the location of the background view with the amount of offset that exceed the content size.
Please see the code below. You can skip all the code and only look at scrollViewDidScroll:.
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.backgroundScrollView.userInteractionEnabled = NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Item %ld", (long)indexPath.row];
return cell;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat backgroundVerticalOffset = MAX(scrollView.contentOffset.y + scrollView.bounds.size.height - scrollView.contentSize.height, 0);
CGPoint backgroundContentOffset = CGPointMake(0.0, backgroundVerticalOffset);
self.backgroundScrollView.contentOffset = backgroundContentOffset;
// If you decide not to use two scroll views, then please use backgroundContentOffset to set the backgroundView.frame.origin.y instead
// e.g. backgroundView.frame.origin.y = -backgroundContentOffset;
}
The result shown below:
P.S. I used blue background view instead of an image view and everything else with clear background color.
I have a UICollectionView with a grid of images. When you tap on one, it opens up the grid and shows a subview with some details. Like this:
I open up the grid in my UICollectionViewLayout by adjusting the UICollectionViewLayoutAttributes and setting a translation on the transform3D property for all cells below the current row of the selected item. This works really nicely, and is a much better animation and a simpler approach than my first attempt at inserting another cell into the grid which is a different size to the others.
Anyway... it works most of the time, but then after continued use I see old images on the collection view. They are like ghost cells. I can't click them, it's like they haven't been removed from the collection view properly, and they sit on top of the cells preventing taps and just being a nuisance. Like this:
Any ideas why these cells are doing this?
EDIT:
I'd like to add, I think it only happens when i scroll the collection view really fast. I've written my own UICollectionViewFlowLayout replacement to test if it still happens. It does.
EDIT 2:
The 3d transforms or layout have nothing to do with this. It must be a bug in UICollectionView. I can exploit by just scrolling really fast, letting come to a standstill and then querying the views that are on screen. There are often double the number of cells, but they are hidden as they are stacked on top of each other. My implementation above reveals them because of the translation i do.
This can really hurt performance too.
See my answer for a solution.
My second edit of my question details why this is happenening, and here is my workaround. It's not bullet proof, but it works in my case, and if you experience somethign similar you could tweak my solution:
- (void) removeNaughtyLingeringCells {
// 1. Find the visible cells
NSArray *visibleCells = self.collectionView.visibleCells;
//NSLog(#"We have %i visible cells", visibleCells.count);
// 2. Find the visible rect of the collection view on screen now
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
//NSLog(#"Rect %#", NSStringFromCGRect(visibleRect));
// 3. Find the subviews that shouldn't be there and remove them
//NSLog(#"We have %i subviews", self.collectionView.subviews.count);
for (UIView *aView in [self.collectionView subviews]) {
if ([aView isKindOfClass:UICollectionViewCell.class]) {
CGPoint origin = aView.frame.origin;
if(CGRectContainsPoint(visibleRect, origin)) {
if (![visibleCells containsObject:aView]) {
[aView removeFromSuperview];
}
}
}
}
//NSLog(#"%i views shouldn't be there", viewsShouldntBeThere.count);
// 4. Refresh the collection view display
[self.collectionView setNeedsDisplay];
}
and
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self removeNaughtyLingeringCells];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self removeNaughtyLingeringCells];
}
A quick further comment to bandejapaisa's: under iOS 6 only, I found that UICollectionView also had a habit of bungling animated transitions. The original cells would remain where they were, copies would be made and then the copies would be animated. Usually on top of the originals but not always. So a simple bounds test wasn't sufficient.
I therefore wrote a custom subclass of UICollectionView that does the following:
- (void)didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
//
// iOS 6 contains a bug whereby it fails to remove subviews, ever as far as I can make out.
// This is a workaround for that. So, if this is iOS 6...
//
if(![UIViewController instancesRespondToSelector:#selector(automaticallyAdjustsScrollViewInsets)])
{
// ... then we'll want to wait until visibleCells has definitely been updated ...
dispatch_async(dispatch_get_main_queue(),
^{
// ... then we'll manually remove anything that's a sub of UICollectionViewCell
// and isn't currently listed as a visible cell
NSArray *visibleCells = self.visibleCells;
for(UIView *view in self.subviews)
{
if([view isKindOfClass:[UICollectionViewCell class]] && ![visibleCells containsObject:view])
[view removeFromSuperview];
}
});
}
}
Obviously it's a shame that 'is this iOS 6' test can't be a little more direct but it's hidden off in a category in my actual code.
A Swift UICollectionView extension version of bandejapaisa's answer:
extension UICollectionView {
func removeNaughtyLingeringCells() {
// 1. Find the visible cells
let visibleCells = self.visibleCells()
//NSLog("We have %i visible cells", visibleCells.count)
// 2. Find the visible rect of the collection view on screen now
let visibleRect = CGRectOffset(bounds, contentOffset.x, contentOffset.y)
//NSLog("Rect %#", NSStringFromCGRect(visibleRect))
// 3. Find the subviews that shouldn't be there and remove them
//NSLog("We have %i subviews", subviews.count)
for aView in subviews {
if let aCollectionViewCell = aView as? UICollectionViewCell {
let origin = aView.frame.origin
if (CGRectContainsPoint(visibleRect, origin)) {
if (!visibleCells.contains(aCollectionViewCell)) {
aView.removeFromSuperview()
}
}
}
}
// 4. Refresh the collection view display
setNeedsDisplay()
}
}