tags:

views:

11405

answers:

7
+1  A: 

I had to do a similar setup, but I basically custom wrote the whole thing. I'm not sure how you're going to get around the problem of 'handing off' touch events from the child UIScrollView to the parent UISscrollView when you reach the edge. You might try overriding UITouchesBegan:withEvent: in your parent UIScrollView, and dumping directly to the child. Good luck!

Ben Gottlieb
A: 

Thanks for the info Ben. I was afraid that might be the answer :) It would be nice if Apple provided some example code that showed how they implemented that part of the Photos app.

Ryan Brubaker
A: 

Hello Ryan / Ben, Can you guys help me. I am doing an app which has a section with UIScrollView (paging enabled) and many UIScrollViews having UIImageViews as subviews. I am having 2 problems.

  1. When there are more than 30 images i receive a memory warning and the app crashes. I think its because i use [UIImage imageNamed:myImageName]. So I tried using UIImage imageWithContentsOfFile. No memory issues but On scrolling the page does not scroll fluently, it puts a break (stucks) a second and continues. This happens whenever the Page comes to screen. How can i resolve this?
  2. Then i have the same issue as Ryan. How can i allow user to pan around the image when zoomed?

Waiting for a helping hand.

Thankyou Anoop

Hey, your helping hand has arrived, but it's probably too late. Anyway, if you are still interested, have a look at my solution.
Andrey Tarantsov
A: 

Hi Ryan,

I am also facing the same problem not able to pan around a zoomed-in image, even I have overridden the same methods as you did.

Pls. let me know how did you resolved it.

Thanks in advance, Regards, Sreelatha.

+5  A: 

Yay! I tried to approach the problem with just one UIScrollView, and I think I found a solution.

Before the user starts zooming (in viewForZoomingInScrollView:), I switch the scroll view into zooming mode (remove all additional pages, reset content size and offset). When the user zooms out to scale 1.00 (in scrollViewDidEndZooming:withView:atScale:), I switch back to paging view (add all pages back, adjust content size and offset).

Here's the code of a simple view controller that does just that. This sample switches between, zooms and pans three large UIImageViews.

Note that a single view controller with a handful of functions is all it takes, no need to subclass UIScrollView or something.

typedef enum {
 ScrollViewModeNotInitialized,           // view has just been loaded
 ScrollViewModePaging,                   // fully zoomed out, swiping enabled
 ScrollViewModeZooming,                  // zoomed in, panning enabled
 ScrollViewModeAnimatingFullZoomOut,     // fully zoomed out, animations not yet finished
 ScrollViewModeInTransition,             // during the call to setPagingMode to ignore scrollViewDidScroll events
} ScrollViewMode;

@interface ScrollingMadnessViewController : UIViewController <UIScrollViewDelegate> {
 UIScrollView *scrollView;
 NSArray *pageViews;
 NSUInteger currentPage;
 ScrollViewMode scrollViewMode;
}

@end

@implementation ScrollingMadnessViewController

- (void)setPagingMode {
 NSLog(@"setPagingMode");
 if (scrollViewMode != ScrollViewModeAnimatingFullZoomOut && scrollViewMode != ScrollViewModeNotInitialized)
  return; // setPagingMode is called after a delay, so something might have changed since it was scheduled
 scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset

 // reposition pages side by side, add them back to the view
 CGSize pageSize = scrollView.frame.size;

 NSUInteger page = 0;
 for (UIView *view in pageViews) {
  if (!view.superview)
   [scrollView addSubview:view];
  view.frame = CGRectMake(pageSize.width * page++, 0, pageSize.width, pageSize.height);
 }

 scrollView.pagingEnabled = YES;
 scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = NO;
 scrollView.contentSize = CGSizeMake(pageSize.width * [pageViews count], pageSize.height);
 scrollView.contentOffset = CGPointMake(pageSize.width * currentPage, 0);

 scrollViewMode = ScrollViewModePaging;
}

- (void)setZoomingMode {
 NSLog(@"setZoomingMode");
 scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset

 CGSize pageSize = scrollView.frame.size;

 // hide all pages besides the current one
 NSUInteger page = 0;
 for (UIView *view in pageViews)
  if (currentPage != page++)
   [view removeFromSuperview];

 // move the current page to (0, 0), as if no other pages ever existed
 [[pageViews objectAtIndex:currentPage] setFrame:CGRectMake(0, 0, pageSize.width, pageSize.height)];

 scrollView.pagingEnabled = NO;
 scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = YES;
 scrollView.contentSize = pageSize;
 scrollView.contentOffset = CGPointZero;

 scrollViewMode = ScrollViewModeZooming;
}

- (void)loadView {
 CGRect frame = [UIScreen mainScreen].applicationFrame;
 scrollView = [[UIScrollView alloc] initWithFrame:frame];
 scrollView.delegate = self;
 scrollView.maximumZoomScale = 2.0f;
 scrollView.minimumZoomScale = 1.0f;

 UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"red.png"]];
 UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"green.png"]];
 UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"yellow-blue.png"]];

 // in a real app, you most likely want to have an array of view controllers, not views;
 // also should be instantiating those views and view controllers lazily
 pageViews = [[NSArray alloc] initWithObjects:imageView1, imageView2, imageView3, nil];

 self.view = scrollView;
}

- (void)setCurrentPage:(NSUInteger)page {
 if (page == currentPage)
  return;
 currentPage = page;
 // in a real app, this would be a good place to instantiate more view controllers -- see SDK examples
}

- (void)viewDidLoad {
 scrollViewMode = ScrollViewModeNotInitialized;
 [self setPagingMode];
}

- (void)viewDidUnload {
 [pageViews release]; // need to release all page views here; our array is created in loadView, so just releasing it
 pageViews = nil;
}

- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setPagingMode) object:nil];
 CGPoint offset = scrollView.contentOffset;
 NSLog(@"scrollViewDidScroll: (%f, %f)", offset.x, offset.y);
 if (scrollViewMode == ScrollViewModeAnimatingFullZoomOut && ABS(offset.x) < 1e-5 && ABS(offset.y) < 1e-5)
  // bouncing is still possible (and actually happened for me), so wait a bit more to be sure
  [self performSelector:@selector(setPagingMode) withObject:nil afterDelay:0.1];
 else if (scrollViewMode == ScrollViewModePaging)
  [self setCurrentPage:roundf(scrollView.contentOffset.x / scrollView.frame.size.width)];
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)aScrollView {
 if (scrollViewMode != ScrollViewModeZooming)
  [self setZoomingMode];
 return [pageViews objectAtIndex:currentPage];
}

- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
 NSLog(@"scrollViewDidEndZooming: scale = %f", scale);
 if (fabsf(scale - 1.0) < 1e-5) {
  if (scrollView.zoomBouncing)
   NSLog(@"scrollViewDidEndZooming, but zoomBouncing is still true!");

  // cannot call setPagingMode now because scrollView will bounce after a call to this method, resetting contentOffset to (0, 0)
  scrollViewMode = ScrollViewModeAnimatingFullZoomOut;
  // however sometimes bouncing will not take place
  [self performSelector:@selector(setPagingMode) withObject:nil afterDelay:0.2];
 }
}

@end

Runnable sample project is available at http://github.com/andreyvit/ScrollingMadness/ (if you don't use Git, just click Download button there). A README is available there, explaining why the code was written the way it is.

(The sample project also illustrates how to zoom a scroll view programmatically, and has ZoomScrollView class that encapsulates a solution to that. It is a neat class, but is not required for this trick. If you want an example that does not use ZoomScrollView, go back a few commits in commit history.)

P.S. For the sake of completeness, there's TTScrollView — UIScrollView reimplemented from scratch. It's part of the great and famous Three20 library. I don't like how it feels to the user, but it does make implementing paging/scrolling/zooming dead simple.

P.P.S. The real Photo app by Apple has pre-SDK code and uses pre-SDK classes. One can spot two classes derived from pre-SDK variant of UIScrollView inside PhotoLibrary framework, however it is not immediately clear what they do (and they do quite a lot). I can easily believe this effect used to be harder to achieve in pre-SDK times.

Andrey Tarantsov
A: 

Hi,

I find this really interesting but I'm trying with text instead. Does anyone have any idea of how it can work but with text? of course no scrolling or zooming (horizontally or vertically).
Something like an eReader, if my text go outside the screen it will split in another page.. ( instant repagination).

Thanks, ludo

ludo
You should better ask this as a new question, not post it as an answer to this question. More people would see it that way and would try to answer. The "Ask Question" button is in the top right.
sth
thank you~ I will do it now
ludo
A: 

This is the Andrey's code with c# translation for monotouch developers... First you need to edit your xib file.. Insert 3 view controllers and make outlets like _page1, _page2, _mainpage.. and link these outlets with views. note you must referance to view controller's view outlet with _mainpage view. (sorry for my english)

    public partial class Test_Details_Controller : UIViewController
{
 private UIPageControl _pageCont;

 private UIScrollView _scView;
    private Object[] _pageViews;
    private int _currentPageIndex;        
 private bool _rotationInProgress;


 void InitializeAfterLoad ()
 { 

  this.Title = "Test";

  this._pageCont = CreatePageControll();

 }

 private UIPageControl CreatePageControll()
 {
  UIPageControl pageControll = new UIPageControl( new RectangleF( 146,348, 38, 20 ) );
  pageControll.BackgroundColor = UIColor.Red;
  pageControll.Alpha = 0.7f;

  return pageControll;
 }

 private void UpdatePageControll(UIPageControl cont, int current, int pages, UIView showed)
 {
  cont.CurrentPage = current;
  cont.Pages = pages;
  cont.UpdateCurrentPageDisplay();

  UIPageControl.AnimationsEnabled = true;
  UIPageControl.BeginAnimations(string.Empty, this.Handle);
  cont.Frame = new RectangleF(showed.Frame.Location.X 
                               , cont.Frame.Location.Y , pageSize().Width, cont.Frame.Height);   
  UIPageControl.CommitAnimations();

 }

 private UIView loadViewForPage(int pageIndex){
  UIView _view = null;
  switch ( pageIndex ) {
  case 1:
   _view = this._page1;
  break;
  case 2:
   _view = this._page2;
  break;
  default:
   _view = this._page1;
  break;
  }
  return _view;
 }

 private int numberOfPages(){
  return (int)this._pageViews.Count();
 }

 private UIView viewForPage( int pageIndex ){
  UIView pageView;
  if(this._pageViews.ElementAt( pageIndex ) == null)
  {
   pageView = loadViewForPage( pageIndex );
   _pageViews[ pageIndex ] = pageView;
  }
  else{
   pageView = (UIView)_pageViews[ pageIndex ];
  }

  _scView.AddSubview( pageView );

  return pageView;    
 }

 private SizeF pageSize(){
  return this._scView.Frame.Size;
 }

 private bool isPageLoaded( int pageIndex ){
  return this._pageViews.ElementAt( pageIndex ) != null;
 }

 private void layoutPage( int pageIndex ){

  SizeF pageSize = this.pageSize();
  ((UIView)this._pageViews[pageIndex]).Frame = new RectangleF( pageIndex * pageSize.Width,0, pageSize.Width, pageSize.Height );
  this.viewForPage( pageIndex ); 

 }

 private void loadView(){
  this._scView = new UIScrollView();
  this._scView.Delegate = new ScViewDelegate( this );
  this._scView.PagingEnabled = true;
  this._scView.ShowsHorizontalScrollIndicator = false;
  this._scView.ShowsVerticalScrollIndicator = false;
  this._scView.Layer.BorderWidth = 2;
  this._scView.AddSubview( _pageCont );
  this.View = this._scView;

 }

 public override void ViewDidLoad ()
 {
  base.ViewDidLoad ();
  InitializeAfterLoad ();
  this._pageViews = new Object[]{ _page1, _page2 };
  this.loadView(); 
 }   

 private void currentPageIndexDidChange(){
  this.layoutPage( _currentPageIndex );

  if(_currentPageIndex+1 < this.numberOfPages()){
   this.layoutPage( _currentPageIndex + 1 );
  }
  if(_currentPageIndex >0){
   this.layoutPage( _currentPageIndex - 1 );
  }

  this.UpdatePageControll( _pageCont, _currentPageIndex, this.numberOfPages(), ((UIView)this._pageViews[_currentPageIndex]) );
  this._scView.BringSubviewToFront( _pageCont );


  this.NavigationController.Title = string.Format( "{0} of {1}", _currentPageIndex + 1, this.numberOfPages() );
 }

 private void layoutPages(){
  SizeF pageSize = this.pageSize();
  this._scView.ContentSize = new SizeF( this.numberOfPages() * pageSize.Width, pageSize.Height );
  // move all visible pages to their places, because otherwise they may overlap
  for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
   if(this.isPageLoaded( pageIndex ))
    this.layoutPage( pageIndex );
  }
 }

 public override void ViewWillAppear (bool animated)
 {
  this.layoutPages();
  this.currentPageIndexDidChange();
  this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
 }

 class ScViewDelegate : UIScrollViewDelegate
 {
  Test_Details_Controller id;
  public ScViewDelegate ( Test_Details_Controller id )
  {
   this.id = id;
  }
  public override void Scrolled (UIScrollView scrollView)
  {

   if(id._rotationInProgress)
    return;// UIScrollView layoutSubviews code adjusts contentOffset, breaking our logic

   SizeF pageSize = id.pageSize();
   int newPageIndex = ((int)id._scView.ContentOffset.X + (int)pageSize.Width / 2) / (int)pageSize.Width;
   if( newPageIndex == id._currentPageIndex )
    return;

   id._currentPageIndex = newPageIndex;
   id.currentPageIndexDidChange();
  }
 }

 public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
 {
  return toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown;
 }

 public override void WillRotate (UIInterfaceOrientation toInterfaceOrientation, double duration)
 {   
  _rotationInProgress = true;
  // hide other page views because they may overlap the current page during animation
  for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
   if(this.isPageLoaded( pageIndex ))
    this.viewForPage( pageIndex ).Hidden = ( pageIndex != _currentPageIndex );
  }
 }
 public override void WillAnimateRotation (UIInterfaceOrientation toInterfaceOrientation, double duration)
 {
 // resize and reposition the page view, but use the current contentOffset as page origin
 // (note that the scrollview has already been resized by the time this method is called)
  SizeF pageSize = this.pageSize();
  UIView pageView = this.viewForPage( _currentPageIndex );
  this.viewForPage( _currentPageIndex ).Frame = new RectangleF( this._scView.ContentOffset.X, 0, pageSize.Width, pageSize.Height );   
 }
 public override void DidRotate (UIInterfaceOrientation fromInterfaceOrientation)
 {
  base.DidRotate (fromInterfaceOrientation);

  // adjust frames according to the new page size - this does not cause any visible changes
  this.layoutPages();
  this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );

  //unhide
  for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
   if( this.isPageLoaded( pageIndex ) )
    this.viewForPage( pageIndex ).Hidden = false;
  }

  _rotationInProgress = false;
 }    

 public override void DidReceiveMemoryWarning ()
 {
  //SuperHandle = DidReceiveMemoryWarning();
  if(this._pageViews != null)
  {
   // unload non-visible pages in case the memory is scarse
   for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
    if( pageIndex < _currentPageIndex - 1 || pageIndex > _currentPageIndex + 1 )
     if( this.isPageLoaded(pageIndex) ){
      UIView pageview = (UIView)this._pageViews[ pageIndex ];
      this._pageViews[ pageIndex ] = null;
      pageview.RemoveFromSuperview();
     }
   }
  }
 }
 public override void ViewDidUnload ()
 {
  this._pageViews = null;
  this._scView = null;
 }  
}
Ahmet Göktaş