2012年4月27日

【iPhone】ページ送りを実装する

最近サボリ気味のdommyです。

最近のUIはどんどん進化していまして、
clearのUIなんかは大好きなんですが、使い道が無いので残念です。

Android 2.xのアプリでも、android-support-packageを使用して、
FragmentとViewPagerを利用し、横にスワイプして画面遷移するUIが増えてきました。

iPhoneでもGoogle+やらmixiが実装していますので、
どうやるのかざっくりと検証してみましたら、
UIScrollVIew

UIPageControl
を使用する事で実装可能でしたので、
コードを紹介します。

参考:wannabegeek / PageViewController

再生するとこんな感じ



こちらのソースコードはGitHubに公開しました。
iPhoneでページ送りを実装するソースコード公開

今回作成するアプリは、1つのRootViewController(PagerViewController)に、
UIScrollViewとUIPageControlがあり、

UIScrollViewに
Document1ViewController
Document2ViewController
Document3ViewController
のviewを表示するように実装します。

InterfaceBuilderでUIScrollViewとUIPageControlを適当に配置したと仮定し、
下記コードで実装していきます。
PagerViewController.m
/**
 * @author dommy <shonan.shachu at gmail.com>
 * @version 1.0.0 updated on 2012-04-25
 */
#import "PagerViewController.h"

@interface PagerViewController ()
@property (assign) BOOL pageControlUsed;
@property (assign) NSUInteger page;
@property (assign) BOOL rotating;
- (void)loadScrollViewWithPage:(int)page;
@end

@implementation PagerViewController

@synthesize scrollView;
@synthesize pageControl;
@synthesize pageControlUsed = _pageControlUsed;
@synthesize page = _page;
@synthesize rotating = _rotating;

// UIViewControllerの配列を保存しておくNSMutableArray
@synthesize controllers;
// UIViewController
@synthesize controller1, controller2, controller3;

- (void)viewDidLoad
{
    [super viewDidLoad];
 // ベースとなるscrollViewの設定を決める
 [self.scrollView setPagingEnabled:YES];
 [self.scrollView setScrollEnabled:YES];
 [self.scrollView setShowsHorizontalScrollIndicator:NO];
 [self.scrollView setShowsVerticalScrollIndicator:NO];
 [self.scrollView setDelegate:self];

        // UIVIewControllerの初期化をする
        controller1 = [[Document1ViewController alloc] initWithNibName:@"Document1ViewController" bundle:nil];
        controller2 = [[Document1ViewController alloc] initWithNibName:@"Document2ViewController" bundle:nil];
        controller3 = [[Document1ViewController alloc] initWithNibName:@"Document3ViewController" bundle:nil];

        // UIViewControllerを保存する配列の設定
        controllers = [[NSMutableArray alloc] initWithObjects:controller1, controller2, controller3, nil];
}

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers {
 return NO;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
 return YES;
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 [viewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
 _rotating = YES;
}

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {

 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 [viewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];

 self.scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * [controllers count], scrollView.frame.size.height);
 NSUInteger page = 0;
 for (viewController in controllers) {
  CGRect frame = self.scrollView.frame;
  frame.origin.x = frame.size.width * page;
  frame.origin.y = 0;
  viewController.view.frame = frame;
  page++;
 }
 
 CGRect frame = self.scrollView.frame;
    frame.origin.x = frame.size.width * _page;
    frame.origin.y = 0;
 [self.scrollView scrollRectToVisible:frame animated:NO];

}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
 _rotating = NO;
 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 [viewController didRotateFromInterfaceOrientation:fromInterfaceOrientation];
}

- (void)viewWillAppear:(BOOL)animated {
 [super viewWillAppear:animated];

 for (NSUInteger i =0; i < [controllers count]; i++) {
  [self loadScrollViewWithPage:i];
 }

 self.pageControl.currentPage = 0;
 _page = 0;
 [self.pageControl setNumberOfPages:[controllers count]];

 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 if (viewController.view.superview != nil) {
  [viewController viewWillAppear:animated];
 }

 self.scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * [controllers count], scrollView.frame.size.height);
}

- (void)viewDidAppear:(BOOL)animated {
 [super viewDidAppear:animated];
 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 if (viewController.view.superview != nil) {
  [viewController viewDidAppear:animated];
 }
}

- (void)viewWillDisappear:(BOOL)animated {
 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 if (viewController.view.superview != nil) {
  [viewController viewWillDisappear:animated];
 }
 [super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated {
 UIViewController *viewController = [controllers objectAtIndex:self.pageControl.currentPage];
 if (viewController.view.superview != nil) {
  [viewController viewDidDisappear:animated];
 }
 [super viewDidDisappear:animated];
}

- (void)loadScrollViewWithPage:(int)page {
    if (page < 0)
        return;
    if (page >= [controllers count])
        return;
    
 // replace the placeholder if necessary
    UIViewController *controller = [controllers objectAtIndex:page];
    if (controller == nil) {
  return;
    }

 // add the controller's view to the scroll view
    if (controller.view.superview == nil) {
        CGRect frame = self.scrollView.frame;
        frame.origin.x = frame.size.width * page;
        frame.origin.y = 0;
        controller.view.frame = frame;
        [self.scrollView addSubview:controller.view];
    }
}

- (IBAction)changePage:(id)sender {
    int page = ((UIPageControl *)sender).currentPage;
 
 // update the scroll view to the appropriate page
    CGRect frame = self.scrollView.frame;
    frame.origin.x = frame.size.width * page;
    frame.origin.y = 0;
    
 UIViewController *oldViewController = [controllers objectAtIndex:_page];
 UIViewController *newViewController = [controllers objectAtIndex:self.pageControl.currentPage];
 [oldViewController viewWillDisappear:YES];
 [newViewController viewWillAppear:YES];

 [self.scrollView scrollRectToVisible:frame animated:YES];

 // Set the boolean used when scrolls originate from the UIPageControl. See scrollViewDidScroll: above.
    _pageControlUsed = YES;
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
 UIViewController *oldViewController = [controllers objectAtIndex:_page];
 UIViewController *newViewController = [controllers objectAtIndex:self.pageControl.currentPage];
 [oldViewController viewDidDisappear:YES];
 [newViewController viewDidAppear:YES];

 _page = self.pageControl.currentPage;
}

#pragma mark -
#pragma mark UIScrollViewDelegate methods

- (void)scrollViewDidScroll:(UIScrollView *)sender {
    // We don't want a "feedback loop" between the UIPageControl and the scroll delegate in
    // which a scroll event generated from the user hitting the page control triggers updates from
    // the delegate method. We use a boolean to disable the delegate logic when the page control is used.
    if (_pageControlUsed || _rotating) {
        // do nothing - the scroll was initiated from the page control, not the user dragging
        return;
    }
 
    // Switch the indicator when more than 50% of the previous/next page is visible
    CGFloat pageWidth = self.scrollView.frame.size.width;
    int page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
 if (self.pageControl.currentPage != page) {
  UIViewController *oldViewController = [controllers objectAtIndex:self.pageControl.currentPage];
  UIViewController *newViewController = [controllers objectAtIndex:page];
  [oldViewController viewWillDisappear:YES];
  [newViewController viewWillAppear:YES];
  self.pageControl.currentPage = page;
  [oldViewController viewDidDisappear:YES];
  [newViewController viewDidAppear:YES];
  _page = page;
 }
}

// At the begin of scroll dragging, reset the boolean used when scrolls originate from the UIPageControl
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    _pageControlUsed = NO;
}

// At the end of scroll animation, reset the boolean used when scrolls originate from the UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    _pageControlUsed = NO;
}

参照先はstoryboardを使っていますので、
iOS4系向けにInterface Builderで作り変えています。
self.childViewControllersは使わず、NSMutableArrayにcontrollerを記録し、
そちらを参照しております。
これはiOS4系向けの実装になります。