So, UITableView supports essentially "infinite" scrolling. There' may be a limit but that sucker can scroll for a looonnnggg time. I would like to mimic this behavior with a UIScrollView but there are two fundamental impediments:
1) scrollView.contentSize is fixed at creation time.
2) zooming can blow any lazy-loading scheme all to hell since it can cause infinte data explosion.
Have others out there pondered this idea? Yah, I know, we are essentially talking about re-creating Google Maps here. Any insights would be much appreciated.
Cheers,
Doug
I've just finished implementing the infitine scroll for me.
In my Implementation I have UITableViewCell with a scrollView and Navigationbuttons. The scrollView contains x views all with the same width. views are alined horizontally and paging is enabled.
scrollView.clipsToBounds = YES;
scrollView.scrollEnabled = YES;
scrollView.pagingEnabled = YES;
scrollView.showsHorizontalScrollIndicator = NO;
My codelogic is like the following:
In my initialization function I
create all the views (for the scrollview) and
put them into an array and
add them to the scrollView
Then I call a function that calculates in a loop the positions for each view (each time you detect a scroll this function will need to be called too). It always takes the first element of the array and sets the frame to (0,0,...,...), the second with (i*width,0,....,....) and so on. The function beeing called looks like this:
- (void)updateOffsetsOfViews{
int xpos = 0;
for (int i=0; i<[views count]; i++) {
UIImageView *_view = [views objectAtIndex:i];
CGRect aFrame = _view.frame;
aFrame.origin.x = xpos;
aFrame.origin.y = 0.0;
_view.frame = aFrame;
xpos += viewWidth;
}
float center = 0;
if(fmod([views count],2) == 1){
center = viewWidth * ([views count]-1)/2;
}else {
center = viewWidth * [views count]/2;
}
[scrollView setContentOffset:CGPointMake(center, 0)];
lastOffset = center;
}
Then (still in the initialization process) I add an observer
[scrollView addObserver:self forKeyPath:#"contentOffset" options:0 context:nil];
so each time something in the scrollView changes I get the (observeValueForKeyPath)-function called, which looks like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
UIImageView *_viewFirst = (UIImageView *)[views objectAtIndex:0];
if ( fmod([scrollView contentOffset].x,viewWidth) == 0.0) {
if ([scrollView contentOffset].x > lastOffset) {
[views removeObjectAtIndex:0];
[views addObject:_viewFirst];
[self updateOffsetsOfViews];
}else if ([scrollView contentOffset].x < lastOffset) {
UIImageView *_viewLast = (UIImageView *)[views lastObject];
[views removeLastObject];
[views insertObject:_viewLast atIndex:0];
[self updateOffsetsOfViews];
}
}
}
And in dealloc or viewDidUnload (depends on how you implement it) don't forget to remove the observer.
[scrollView removeObserver:self forKeyPath:#"contentOffset"];
Hope this helps, you might notice some overhead, but in my implementation I also support like scrolling 5 pages (well... unlimited) at once and autoanimated scrolling etc. so you might see something that could be thrown away.
While it's impossible to have a truly infinite UIScrollView, there are some simple tricks you can use to emulate that behavior.
Handling the fixed contentSize: have some fixed-size view handled by your scroll view, and at launch or instantiation, set the content offset so that you're seeing the middle of the handled view. Then just watch the content offset (using KVO or some other method), and if you near any edge, update the content of the view with a new set of content (offset appropriately) and reset the scroll view's contentOffset property to be back in the middle.
Handling zooming: do something similar, only this time watch the zoom factor on the scroll view. Whenever it gets to a certain point, do some manipulation to whatever data you're presenting so that it appears zoomed, then reset the zoom factor to 1.0. For example, if you're scrolling an image and it gets zoomed to appear twice as large, programmatically apply some kind of transform to make the image twice as large, then reset the scroll view's zoom factor to 1.0. The image will still appear zoomed in, but the scroll view will be able to continue zooming in further as necessary. (Google Maps takes this one step further where it lazy-loads more detailed views as the user zooms - you may or may not choose to implement this.)
The StreetScroller sample project from Apple demonstrates how to perform infinite scrolling in a UIScrollView.
Bear in mind that when the scroll is animated, contentOffset changes many times, not just page by page, but with each step in the animation.
Perhaps setting contentSize to some gigantic value and then moving a limited number of underlying views around to track the view position as in the Tiling sample will do the trick.
To mitigate the possibility of eventually reaching an edge and having to abruptly recenter the view (which cancels any scrolling currently in motion), the view can be recentered when it is stationary, from time to time.
Anyway, that's what I'm about to try.
Related
I am trying to implement a view with a mainScrollView. Inside that mainScrollView I have 10 UIView, each one is having subScrollView.Now, I want to add many (around 150 to 200) views(imageview,label,textview,uiview,webview,etc) inside each subScrollView.
something like this:
Timeline ww2 and
History of Jazz
I have tried following:
Method 1:
Lazy loading for each subScrollView to add views.
code for this:
-(void)loadInnerScrollViewLazily:(int)tag pageNo:(int)pageNo{
/// this array will contain all the views for subScrollView
NSMutableArray *array=[NSMutableArray array];
array=[self.tlScrollContentArray objectAtIndex:tag];
if (pageNo < 0)
return;
if (pageNo >= [array count])
return;
UIView *controller = [array objectAtIndex:pageNo];
CGRect frame = [[array objectAtIndex:0] frame];
frame.origin.x = 350;
frame.origin.y = 0;
controller.frame = frame;
for (UIView* view in [mainScrollView subviews]) {
for (UIScrollView* scroll in [view subviews]) {
if ([scroll isKindOfClass:[UIScrollView class]]) {
if (scroll.tag == tag+11) {
[scroll addSubview:controller]; /// adds view in particular subScrollView
}
}
}
}
}
This method is called in scrollViewDidScroll method, which checks perticular scrollView and removes view which is out of screen and then it will add views depending upon the condition.
This code is slowing down app as scrolling rate increases.
Method 2:
With the reference of this tutorial:
Add three UIView(each with subScrollView) in to mainScrollView. Updating content of subScrollView depending upon the condition.
But this solution is also slowing down my app as it is updating subScrollView every time it appears.
Is there any library or code available to manage this.
Any help will be appreciated.
Thanks in Advance!
I think your code has many, many errors, and you would need to post your complete source to be helped. It's unclear if you are ever removing images from your scrollview. If not, that is your problem.
Look at Apple's sample PhotoScroller code: http://developer.apple.com/library/ios/#samplecode/PhotoScroller/Introduction/Intro.html
This dequeues and manages the ImageViews on the scrollview... I implemented something similar based on this.
I have a grouped UITableView with custom cells (created by subclassing UITableViewCell). I add subviews and insert sublayers just like this:
[self.contentView addSubview:myUILabel];
and
[self.contentView.layer insertSublayer:myCALayer];
When entering editing mode for deleting rows, the cells move right and myUILabel and myCALayer go beyond the borders of the cell, which looks ugly.
I tried this:
Grouped UITableView with custom UILabels in Editing Mode
... but it didn't help.
What "kind of" worked is to override setEditing: in my custom cell
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
CGRect oldBounds = self.myCALayer.bounds;
CGRect newBounds = oldBounds;
CGPoint oldPosition = self.myCALayer.position;
CGPoint newPosition = oldPosition;
//move right
if (editing && !self.showingDeleteConfirmation) {
newBounds.size.width -=32;
newPosition.x -= 15;
}
//move back left
else if (self.editing) {
newBounds.size.width += 32;
newPosition.x += 15;
}
self.myCALayer.bounds = newBounds;
self.myCALayer.position = newPosition;
[super setEditing:editing animated:animated];
}
... however, the animations are not performed synchronously. What happens is while the cell moves right when entering editing mode, first the width of the layer shrinks, and then the position changes. Although the layer finally fits the cell, the animation looks bad.
Thanks for your advice!
Not sure about your layers, but you should be able to set a smart autoresizingMask on your subviews so they re-layout appropriately when the size of contentView is altered by the tableview (on entry to edit mode).
More here.
Edit: with a little looking, I think CALayer's anchorPoint property may help with your sublayers. (more here)
Alternatively (and this is a bit of a hack, and could hurt performance somewhat), you could set your custom layers on a UIView w/ an appropriately set autoresizingMask.
I am new to iOS development. Can anyone give me a working sample project/ tutorial of paging based scrollview? I need to load more data in my table view from a webservice as user scrolls down.
I have spent a lot of time, but could not find anything that suits my need. Any help will be greatly appreciated.
I was able to figure out the solution of my problem. Now I am trying to detect scroll up and down based on the y offset difference. The problem is it always reads the drag to be downwards even if it is upward. This is how I am trying to detect the scroll direction:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
CGPoint scrollOffSet = scrollView.contentOffset;
NSLog(#"Current Content offset: , y = %f", scrollOffSet.y);
NSLog(#"Previous offset , y = %f", self.previousYOffset);
if(scrollOffSet.y > self.previousYOffset)
{
self.previousYOffset = scrollOffSet.y;
NSLog(#"Scrolled Down");
self.nextPageNumber++;
//Here I send the request to fetch data for next page number
}
else {
NSLog(#"Scrolled up");
self.previousYOffset = scrollOffSet.y;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint scrollOffSet = scrollView.contentOffset;
NSLog(#"Current Content offset: y = %f", scrollOffSet.y);
}
The problem is: either i scroll up or down, it will always display "scrolled down", thus it goes to fetch data for next page number even if it was not intended to request. Plus, the y offset positions it returns on up/down scroll are also very confusing. Please guide me how to accurately detect an upside scroll?
Do you mean a "swipable" UIScrollView?
Paging a UIScrollView is quite easy. First make sure you have the pagingEnabled property set to YES. The UIScrollView will use it's own frame as the size of a page (like a viewport).
Use the contentSize property to determine the scrollable area.
Example:
//scrollView is a UIScrollView instance
int pageCount = 3;
scrollView.pagingEnabled = YES;
scrollView.contentSize = CGSizeMake(scrollView.frame.width*pageCount, scrollView.frame.height);
I'm trying to put a custom view inside a UITableViewCell which of course lives within a UITableView. I want to make this custom view accessible so I need to make it a UIAccessibilityContainer (since it contains several visual elements that aren't implemented as their own UIViews).
When I do this, the location of the elements get all messed up whenever the table scrolls. While paging through the elements using VoiceOver, it will automatically scroll the table to attempt to center the selected element on screen, but then the outline of where VoiceOver thinks the element is no longer lines up with where it is visually.
Note in the screenshot that the inspector says "Row 4, element 2" but the highlighted area is some random place in Row 7 since that happens to be where Row 4 was before it auto-scrolled the table.
My thought is that I might have to use UIAccessibilityPostNotification() to post a layout change when the table view scrolls, but I don't have to do that when I don't use a UIAccessibilityContainer and it feels like I shouldn't have to do it and that the system should be handling this for me - but the fact that UIAccessibilityElement needs to have it's accessibilityFrame set in screen coordinates does seem to throw a wrinkle into things. (Bonus question: Why the heck is the API designed that way? Why not define the frame relative to the element's container or something like that? Arg.)
Here's the custom view's implementation just in case there's something in here which is causing the problem. For the full project (Xcode 4), click here.
#implementation CellView
#synthesize row=_row;
- (void)dealloc
{
[_accessibleElements release];
[super dealloc];
}
- (void)setRow:(NSInteger)newRow
{
_row = newRow;
[_accessibleElements release];
_accessibleElements = [[NSMutableArray arrayWithCapacity:0] retain];
for (NSInteger i=0; i<=_row; i++) {
UIAccessibilityElement *element = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
element.accessibilityValue = [NSString stringWithFormat:#"Row %d, element %d", _row, i];
[_accessibleElements addObject:element];
[element release];
}
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[[UIColor lightGrayColor] setFill];
UIRectFill(self.bounds);
[[UIColor blackColor] setFill];
NSString *info = [NSString stringWithFormat:#"Row: %d", _row];
[info drawAtPoint:CGPointZero withFont:[UIFont systemFontOfSize:12]];
[[[UIColor whiteColor] colorWithAlphaComponent:0.5] setFill];
NSInteger x=0, y=0;
for (NSInteger i=0; i<=_row; i++) {
CGRect rect = CGRectMake(12+x, 22+y, 30, 30);
UIAccessibilityElement *element = [_accessibleElements objectAtIndex:i];
element.accessibilityFrame = [self.window convertRect:[self convertRect:rect toView:self.window] toWindow:nil];
UIRectFill(rect);
x += 44;
if (x >= 300) {
x = 0;
y += 37;
}
}
}
- (BOOL)isAccessibilityElement
{
return NO;
}
- (NSInteger)accessibilityElementCount
{
return [_accessibleElements count];
}
- (id)accessibilityElementAtIndex:(NSInteger)index
{
return [_accessibleElements objectAtIndex:index];
}
- (NSInteger)indexOfAccessibilityElement:(id)element
{
return [_accessibleElements indexOfObject:element];
}
#end
Edit: I should note that I've tried variations that update the element's accessibilityFrame in -indexOfAccessibilityElement: and -accessibilityElementAtIndex: with the idea that VoiceOver will request the element somehow whenever it needs it and that'd be a nice time to update things. However that doesn't seem to work, either. I was kind of hoping maybe VoiceOver would automatically request things to redraw, but that also doesn't seem to work. (The idea of putting the location setting code in -drawRect: comes from something I remember seeing at WWDC about this, but it was unclear to me if that was "best practice" or just happened to be convenient.)
I've solved the problem you described by adding some side effects to the accessibility methods and with collaboration from the table scroll delegates. Inside the drawRect method I calculate the local coordinates of the rectangle, so I don't need to convert the coordinates there, simply calculate them with regards to the cell's top left corner.
Then, I modified the accessor to update the frame with a side effect like this (note the y resetting):
- (id)accessibilityElementAtIndex:(NSInteger)index
{
UIAccessibilityElement *element = [accesible_items_ get:index];
CGRect rect = element.accessibilityFrame;
rect.origin.y = 0;
element.accessibilityFrame = [self.window
convertRect:rect fromView:self];
return element;
}
While this works fine for the initial view, you still get the displaced frames when the user scrolls, so in your table view controller implement the following scrolling delegate:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// This loop has a side effects, see the cell accesor code.
for (id cell in self.tableView.visibleCells)
for (int f = 0; [cell accessibilityElementAtIndex:f]; f++);
UIAccessibilityPostNotification(
UIAccessibilityLayoutChangedNotification, nil);
NSLog(#"Layout changed after scrollViewDidScroll");
}
Depending on the contents of your table not all cells may respond to the accessibility method, so you could first query each cell with respondsToSelector to avoid sending unexpected messages.
I would also post UIAccessibilityLayoutChangedNotification at the end of the cell setter creating the UIAccessibilityElement objects, or you will get log messages saying that your elements disappeared or could not be found.
These changes make the scroll work when iterating the elements one through one with the rotor, but you may get still odd results if the user scrolls with a triple finger gesture. That's because by default tableViews scroll a screen page at a time, which may not happen to have the same element boundaries as your cells, and the rotor selects a cell half visible. Depending on the scrolling direction and other UI elements, the half visible cell could overlap controls the rotor gets confused with. You need to implement paged scrolling to control avoid this behavior.
Is this problem affecting the usability of the app in VoiceOver mode? When I've played with VoiceOver on both Mac OS and iOS, the highlight boxes (especially in Web views) frequently become unmatched with their onscreen objects. If the app is still usable in VoiceOver, I'd call this a known bug and fix it if somebody complains.
After all, most of the blind people I know aren't looking at the highlight boxes.
I have the UIScrollView with pagingEnabled set to YES, and programmatically scroll its content to bottom:
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.y = scrollView.contentSize.height - scrollView.frame.size.height;
[scrollView setContentOffset:contentOffset animated:YES];
it scrolls successfully, but after that, on single tap its content scrolls up to offset that it has just before it scrolls down. That happens only when I programmaticaly scroll scrollView's content to bottom and then tap. When I scroll to any other offset and then tap, nothing is happened.
That's definitely not what I'd like to get. How that should be fixed?
Much thanks in advance!
Timur.
This small hack prevents the UIScrollView from scrolling when tapped. Looks like this is happening when the scroll view has paging enabled.
In your UIScrollView delegate add this method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
scrollView.pagingEnabled = self.scrollView.contentOffset.x < (self.scrollView.contentSize.width - self.scrollView.frame.size.width);
}
This disables the paging when the scroll view reaches the right end in horizontal scrolling (my use case, you can adapt it to other directions easily).
I just figured out what causes this problem, and how to avoid it. If you having pagingEnabled set to YES on a scroll view, you must set the contentOffset to be a multiple of the scroll view's visible size (i.e. you should be on a paging boundary).
Concrete example:
If your scroll view was (say) 460 pixels high with a content area of 920, you would need to set the content offset to EITHER 0 or 460 if you want to avoid the "scroll to beginning on tap" problem.
As a bonus, the end result will probably look better since your scroll view will be aligned with the paging boundaries. :)
The following workaround did help (assume that one extends UIScrollView with a category, so 'self' refers to its instance):
-(BOOL) scrolledToBottom
{
return (self.contentSize.height <= self.frame.size.height) ||
(self.contentOffset.y == self.contentSize.height - self.frame.size.height);
}
Then, one should sometimes turn pagingEnabled off, just at the position where scroll view reaches its bottom. In the delegate (pagingEnabled is initialy on of course, since the problem occurs only when it is enabled):
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.pagingEnabled == YES)
{
if ([scrollView scrolledToBottom] == YES)
scrollView.pagingEnabled = NO;
}
else
{
if ([scrollView scrolledToBottom] == NO)
scrollView.pagingEnabled = YES;
}
}
This seems to be a bug:
UIScrollView doesn't remember the position
I have tested this on iOS 4.2 (Simulator) and the issue remains.
When scrolling a ScrollView I would suggest using
[scrollView scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];
Where the rect is the position you're after. (In this case the rect would be the top of the scrollview).
Changing the content offset is not the correct way of scrolling a scrollview.