I'm facing a problem with my scrollview (horizontal, paging enabled, can be 50 pages or more)
what i want to do is, load content onto the scrollView's content-View while the user is scrolling horizontally (because initially loading all the content takes way to long).
what i do is:
everytime a scrollViewDidScroll happens i check if the loading of additional content is necessary. in case it is, a new "page" is being created and added onto the scrollViews contentview at the correct position.
the problem is:
that while it preloads a page, the "scrolling-movement" gets disturbed. the screen doesn't exactly flicker, but the smoothness of the scrolling animation suffers heavily.
in code that is:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self preLoadPageNumber:self.currentPageIndex + 2];}
-(void)preLoadPageNumber:(int)pageNumber{
NSMutableArray *tmpMovies;
VoDPage *pageView;
if(pageNumber < [self getAmountOfPages])
{
pageView = [pageViewControllers objectAtIndex:pageNumber];
if((NSNull*)pageView == [NSNull null])
{
tmpMovies = [[NSMutableArray alloc] initWithCapacity:6];
for(int o = ((pageNumber) * 6); o < ((pageNumber+1)*6); ++o)
{
#try {
[tmpMovies addObject:[movies objectAtIndex:o]];
}
#catch (NSException * e) {
break;
}
}
pageView = [[VoDPage alloc] initWithContent:tmpMovies andNavCtrl:navCtrl];
pageView.view.frame = CGRectMake(((pageNumber) * 320)-8 , 0, 320, 332);
if(editModeEnabled)
[pageView enableEditMode];
[scrollView addSubview:pageView.view];
[pageViewControllers replaceObjectAtIndex:pageNumber withObject:pageView];
}
}
}
probably I have to make some design changes here. I'm sure some of you have faced similar problems in the past.
Any help / tipps or links are greatly appreciated
thanks in advance for your help
sam
Ok i somewhat figured out a workaround that does the trick.
i changed the preLoadPageNumber- Method to look like this:
-(void)preLoadPageNumber:(NSNumber*)param
simply because i call a helper method from within ScrollViewDidScroll and said helperMethod looks like this:
-(void)performPreLoadAfterDelay
{
[self performSelector:#selector(preLoadPageNumber:) withObject:[NSNumber numberWithInt:self.currentPageIndex +1] afterDelay:0.001];
}
Calling performSelector from within the ScrollViewDidScroll Method doesn't work when scrolling very fast ("blank" pages appear)..
I don't really know why, but the trick with the helperMethod worked.
Annimation is smooth now and the "blank-page"-problem dissappeared.
Related
The same behavior of UICollectionView as described here has been led to this question. Even though I decided to post my own one, because I did further investigations, I didn't want to post in a comment or in edit of the question mentioned above.
What happens?:
When large cells being displayed in a UICollectionView with a UICollectionViewFlowLayout, after scrolling the collection view to a certain offset, the cells will disappear.
When scrolling further until another cell comes into visible area, the vanished/hidden cell becomes visible again.
I tested with a vertical scrolling collection view and full-width-cells, but I'm rather sure, that it would also happen with similar setups for horizontal scrolling.
What are large cells?:
The described behavior happens with cells higher than twice the display height (960.f + 1.f on 3,5 inch displays, 1136.f + 1.f on 4 inch).
What exactly happens?:
When the scrolling offset of the collection view exceeds cell.frame.origin.y + displayHeightOfHardware the cells hidden property is set to YES and -collectionView:didEndDisplayingCell:forItemAtIndexPath: gets called (e.g. the first cell changes to hidden when scrollingOffset.y reaches 481.f on 3,5-inch-iPhone).
As described above, when scrolling until next cell comes into view, the hidden cell gets displayed again (i.e. hidden property changes to NO) and furthermore, when scrolling far enough the cell will never vanish again, when it shouldn't, no matter where you scroll to.
This changes when working with cells larger than triple-display-height (1441.f/1705.f). Those show the same behavior, but it stays the same, no matter how far they're being scrolled up and down.
What else?:
The situation can not be fixed by overriding -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds to return YES.
The cells cannot being forced to display with setting the hidden property to NO programmatically after they were hidden (in didEndDisplayingCell for example)
So, whats the question?:
I'm pretty sure, that this is a bug in UICollectionView/Controller/Cell/Layout and I'll submit a TSI at Apple. But for the meantime: Has anyone any ideas for a quick hack solution?
i have a VERY dirty and internal solution for this problem:
#interface UICollectionView ()
- (CGRect)_visibleBounds;
#end
#interface MyCollectionView : UICollectionView
#end
#implementation MyCollectionView
- (CGRect)_visibleBounds {
CGRect rect = [super _visibleBounds];
rect.size.height = [self heightOfLargestVisibleCell];
return rect;
}
- (float)heightOfLargestVisibleCell {
// do your calculations for current max cellHeight and return it
return 1234;
}
#end
I have a workaround that seems to be working for me and should not run amok of Apple's rules for iOS applications.
The key is the observation that the large cells bounds are the issue. I've worked around that by ensuring that one edge of the cell is within the viewable area of the scrollable content region. You'll obviously need to subclass the UICollectionViewFlowLayout class or UICollectionViewLayout depending on your needs and make use of the contentOffset value to track where you are in the UIScrollView.
I also had to ensure:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
returns YES or face a runtime exception indicating the layout was invalid. I keep the edge of the larger cell bound to the left edge in my case. This way you can avoid the erroneous bounds intersection detection for these larger cells.
This does create more work depending on how you would like the contents of the cell to be rendered as the width/height of the cell is being updated as you scroll. In my case, the subviews within the cell are relatively simple and do not require a lot of fiddling with.
As requested here is an example of my layoutAttributesInRect
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];
NSArray *vertical = myVerticalCellsStore.cells;
NSInteger startRow = floor(rect.origin.y * (vertical.count)/ (vertical.count * verticalViewHeight + verticalViewSpacing * 2));
startRow = (startRow < 0) ? 0 : startRow;
for (NSInteger i = startRow; i < vertical.count && (rect.origin.y + rect.size.height >= i * verticalViewHeight); i++) {
NSArray *horizontals = myHorizontalStore.horizontalCells;
UICollectionViewLayoutAttributes *verticalAttr = [self layoutAttributesForSupplementaryViewOfKind:#"vertical" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
if (CGRectIntersectsRect(verticalAttr.frame, rect)) {
[attributes addObject:verticalAttr];
}
BOOL foundAnElement = NO;
for (NSInteger j = 0 ; j < horizontals.count; j++) {
MYViewLayoutAttributes *attr = (MyViewLayoutAttributes *)[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:j inSection:i]];
if (CGRectIntersectsRect(rect, attr.frame)) {
[attributes addObject: attr];
foundAnElement = YES;
}
else if (foundAnElement) {
break;
}
}
}
return attributes;
}
This is my sanitized code. Basically I calculate about were the first cell should be based on the cell height. In my case that is fixed, so the calculation is pretty easy. But my horizontal elements have various widths. So the inner loop is really about figuring out the right number of horizontal cells to include in the attributes array. There I'm using the CGRectIntersectsRect to determine if the cell intersects. Then the loop keeps going until the intersection fails. And if at least one horizontal cell has been found the loop will break. Hope that helps.
My solution is basically the same as Jonathan's but in a category, so you don't have to use your own subclass.
#implementation UICollectionView (MTDFixDisappearingCellBug)
+ (void)load {
NSError *error = nil;
NSString *visibleBoundsSelector = [NSString stringWithFormat:#"%#isib%#unds", #"_v",#"leBo"];
if (![[self class] swizzleMethod:NSSelectorFromString(visibleBoundsSelector) withMethod:#selector(mtd_visibleBounds) error:&error]) {
FKLogErrorVariables(error);
}
}
- (CGRect)mtd_visibleBounds {
CGRect bounds = [self mtd_visibleBounds]; // swizzled, no infinite loop
MTDDiscussCollectionViewLayout *layout = [MTDDiscussCollectionViewLayout castedObjectOrNil:self.collectionViewLayout];
// Don`t ask me why, but there's a visual glitch when the collection view is scrolled to the top and the max height is too big,
// this fixes it
if (bounds.origin.y <= 0.f) {
return bounds;
}
bounds.size.height = MAX(bounds.size.height, layout.maxColumnHeight);
return bounds;
}
#end
I found that this issue only occurred when using a subclassed UICollectionViewLayoutAttributes and when that attribute class did not have a correct isEqual: method.
So for example:
#implementation COGridCollectionViewLayoutAttributes
- (id)copyWithZone:(NSZone *)zone
{
COGridCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.isInEditMode = _isInEditMode;
return attributes;
}
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![[other class] isEqual:[self class]]) {
return NO;
}
if ([((COGridCollectionViewLayoutAttributes *) other) isInEditMode] != [self isInEditMode]) {
return NO;
}
return [super isEqual:other];
}
#end
Worked but originally I had:
return YES;
This is on iOS 7.
I have UIScrollView with a lot of rows (~100) and I implemented dequeueReusableRow method for fast allocating and adding my subviews (rows). Everything work fine, but if I scroll very fast with decelerate some view don't added to scrollView on time only later.
- (UIView *)dequeueReusableRow
{
UIView *view = [reusableRows anyObject];
if(view)
{
[[view retain] autorelease];
[reusableRows removeObject:view];
}else{
view = [[UIView alloc] init....
}
return view;
}
- (void)addVisibleRows
{
UIView *row = [self dequeueReusableRow];
row.frame = ....
[scrollView addSubview:row]
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self addVisibleRows];
[self removeInvisibleRows];
}
Please, don't propose me use UITableView because structure of accordion looks like:
section
- section
-- section
--- row
- section
section
- row
dequeueReusableRow is not part of UIScrollViewController, so I assume dequeueReusableRow is something you implemented yourself. If so, can you show that code? It is likely where we can help with any inefficiencies.
Also, if your scrollview contains rows, why not just use a UITableView which will do much of the work for you? I know you asked not to propose it - can you explain why you need to use a ScrollView so we can help you better?
It's very hard to tell from that code snippet. Some more details would be appreciated.
One minor suggestion in the meantime: Call removeInvisibleRows before addVisibleRows
Background -
I am using automatic reference counting on a project. The root view is a Table View (Master / Detail setup) showing a list of "slide shows". Click on a table cell and you are taken to the detail view which consists of a Scroll view with views (viewController.view) in it (this is the "slide show"). Each slide show has a front cover and back cover (same view controller formatted differently) that sandwich an variable number of pages. Here is the code to load the slide show:
- (void)loadScrollView
{
// The front and back cover are set in Interface Builder because they
// are reused for every slide show, just labels are changed.
[self.scrollView addSubview:self.frontCoverViewController.view];
[self.frontCoverViewController setCoverTitle:_data.name creationDate:_data.creationDate isFrontCover:YES];
[self.pagesArray addObject:self.frontCoverViewController];
for (int i = 0; i < [self getTotalNumberOfComps]; i++)
{
PageViewController *pageView = [[PageViewController alloc] init];
pageView.data = [_compsArray objectAtIndex:i];
[_scrollView addSubview:pageView.view];
pageView.data.imgView = pageView.imageView;
pageView.slideShowViewController = self;
[_pagesArray addObject:pageView];
}
[self.scrollView addSubview:self.backCoverViewController.view];
[self.backCoverViewController setCoverTitle:_data.name creationDate:_data.creationDate isFrontCover:NO];
[self.pagesArray addObject:self.backCoverViewController];
[self.scrollView bringSubviewToFront:_frontCoverViewController.view];
[self setCurrentPage:0];
}
Problem -
So Im trying to reuse this slide show view controller so I need to nil and recreate the pages in the middle because each slide show has a different number of slides. Note a slide [PageViewController] is just a view with an ImageView in it. It has more functionality so we need the controller however the main display of the V.C. is the ImageView. I have created the following method to "empty" the slide show before running loadScrollView again with new data. Here is the empty method:
- (void)saflyEmptyScrollView
{
for (int i = 0; i < [self.pagesArray count]; i++)
{
if (i == 0 && i == ([self.pagesArray count]-1)) {
CoverViewController *cover = (CoverViewController*)[self.pagesArray objectAtIndex:i];
[cover.view removeFromSuperview];
} else {
PageViewController *page = (PageViewController*)[self.pagesArray objectAtIndex:i];
[page.view removeFromSuperview];
page = nil;
}
}
self.pagesArray = nil;
self.pagesArray = [[NSMutableArray alloc] init];
}
Big Question -
My main question is do I need to set the ImageView of each of these pages to nil? Or does setting the page itself to nil also free up the memory used by the ImageView/Labels/etc that are used in that view controller?
I tried adding self.imageView = nil; to the PageViewController's viewDidUnload and viewWillUnload methods (one at a time not in both) and I realized that setting page = nil does not call the pages Unload methods. Am I freeing up memory correctly.
I've read a lot of articles but Im still not sure if Im managing memory in the best way possible. Thanks so much for the help!
Generally, you shouldn't have to set things to nil. And in this specific case, the setting things to nil is doing nothing.
The line page = nil; is redundant, because the variable page goes out of scope immediately afterwards anyway. ARC knows this and doesn't need you to set it to nil.
And self.pagesArray = nil; is redundant because you follow it with self.pagesArray = [[NSMutableArray alloc] init];. The second line on its own will suffice.
I have an app which I add a subview to (and remove the same subview based on user interactions). I am looking for a way to check whether the subview is present or not at any given time.
For example:
In the current view (UIView *viewA) I add a subview (UIView *viewB). I then want a way of checking whether viewB is being displayed at any given time.
Sorry if this isn't very clear, it's quite hard to describe.
an UIView stores its superview and is accessible with the superview-property just try
if([viewB superview]!=nil)
NSLog(#"visible");
else
NSLog(#"not visible");
But the better approach is to use the hidden-property of UIView
I went through the same issue and consulted Apple Documentation and came up with this elegant solution:
if([self.childView isDescendantOfView:self.parentView])
{
// The childView is contained in the parentView.
}
I updated to Swift4, Thanks a lot to #Shinnyx and #thomas.
if viewB.superview != nil{
print("visible")
}
else{
print("not visible")
}
if selfView.isDescendant(of: self.parentView) {
print("visible")
}
else{
print("not visible")
}
func isDescendant(of: UIView)
Returns a Boolean value indicating whether the receiver is a subview of a given view or identical to that view.
Here's a method I put in the appDelegate so that I can display the entire subview hierarchy from any point.
// useful debugging method - send it a view and it will log all subviews
// can be called from the debugger
- (void) viewAllSubviews:(UIView *) topView Indent:(NSString *) indent {
for (UIView * theView in [topView subviews]){
NSLog(#"%#%#", indent, theView);
if ([theView subviews] != nil)
[self viewAllSubviews:theView Indent: [NSString stringWithFormat:#"%# ",indent]];
}
}
call it with a string with one character and it will indent for you. (i.e. [appDelegate viewAllSubviews:self.view Indent:#" "];)
I find it useful to clear the debug pane first, then call this from the debugger, and copy it into a text editor like BBEdit that will show the indents.
You can call it using the mainWindow's view and see everything on your screen.
I'm using TTLauncherView as a sort of home screen for my app and I only have one page's worth of icons. How can I make it so the TTLauncherView won't let you drag icons to "the next page"? I want to set a maximum number of pages (in this case one.)
(EDIT: long story short, I subclassed beginEditing, see the answer below.)
I see where why it adds an extra page when beginEditing is called, but I don't want to edit the framework code. (That makes it hard to update to newer versions.) I'd also prefer not to subclass and override that one method, if I have to rely on how it's implemented. (I'm not against subclassing or adding a category if it's clean.)
I tried setting scrollView.scrollEnabled to NO in the callback method launcherViewDidBeginEditing in my TTLauncherViewDelegate, but that doesn't work while it's in editing mode and I don't know why.
I tried adding a blocker UIView to the scrollview to intercept the touch events by setting userInteractionEnabled=NO, which works OK. I still have to disable the dragging of TTLauncherItems to the next page somehow.
I also tried setting the contentSize of the scrollview to it's bounds in launcherViewDidBeginEditing, but that didn't seem to work either.
Is there a better way?
Tried to block gestures:
- (void)setLauncherViewScrollEnabled:(BOOL)scrollEnabled {
if (scrollEnabled) {
[self.scrollViewTouchInterceptor removeFromSuperview];
self.scrollViewTouchInterceptor = nil;
} else {
// iter through the kids to get the scrollview, put a gesturerecognizer view in front of it
UIScrollView *scrollView = [launcherView scrollViewSubview];
self.scrollViewTouchInterceptor = [UIView viewWithFrame:scrollView.bounds]; // property retains it
UIView *blocker = self.scrollViewTouchInterceptor;
[scrollView addSubview:scrollViewTouchInterceptor];
[scrollView sendSubviewToBack:scrollViewTouchInterceptor];
scrollViewTouchInterceptor.userInteractionEnabled = NO;
}
}
For reference: TTLauncherView.m:
- (void)beginEditing {
_editing = YES;
_scrollView.delaysContentTouches = YES;
UIView* prompt = [self viewWithTag:kPromptTag];
[prompt removeFromSuperview];
for (NSArray* buttonPage in _buttons) {
for (TTLauncherButton* button in buttonPage) {
button.editing = YES;
[button.closeButton addTarget:self action:#selector(closeButtonTouchedUpInside:)
forControlEvents:UIControlEventTouchUpInside];
}
}
// Add a page at the end
[_pages addObject:[NSMutableArray array]];
[_buttons addObject:[NSMutableArray array]];
[self updateContentSize:_pages.count];
[self wobble];
if ([_delegate respondsToSelector:#selector(launcherViewDidBeginEditing:)]) {
[_delegate launcherViewDidBeginEditing:self];
}
}
I think overriding beginEditing in TTLauncherView is your best bet. Since you'd only really be touching one method (and only a few lines in that method), upgrading it when the time comes shouldn't be too bad. Since that method explicitly adds the extra page, I'm not sure how you'd get around it w/o editing that specific piece of code
As Andrew Flynn suggested in his answer, I was able to make it work by subclassing and overriding the beginEditing method to remove the extra page TTLauncherView adds when it goes into editing mode.
One problem I'm having is I can't figure out how to remove the warning I get for calling the (private) method updateContentSize on my subclass. I tried casting it to id, but that didn't remove the warning. Is it possible?
edit: I was able to remove the warning by using performSelector to send the message to the private class. (I had previously create a category method performSelector:withInt that wraps NSInvocation so that I can pass primitives via performSelector methods, which makes this very convenient.)
// MyLauncherView.h
#interface MyLauncherView : TTLauncherView {
NSInteger _maxPages;
}
#property (nonatomic) NSInteger maxPages;
#end
// MyLauncherView.m
#implementation MyLauncherView
#synthesize maxPages = _maxPages;
- (void)beginEditing {
[super beginEditing];
// ignore unset or negative number of pages
if (self.maxPages <= 0) {
return;
}
// if we've got the max number of pages, remove the extra "new page" that is added in [super beginEditing]
if ([_pages count] >= self.maxPages ) {
[_pages removeLastObject];
[self updateContentSize:_pages.count]; // this has a compiler warning
// I added this method to NSObject via a category so I can pass primitives with performSelector
// [self performSelector:#selector(updateContentSize:) withInt:_pages.count waitUntilDone:NO];
}
}