UIScrollViewでメルカリのチュートリアル画面を再現してみた

アプリを開発しているとチュートリアル画面を実装する場面はよくあるかと思います.
そこで今回はフリマアプリ「メルカリ」のチュートリアル画面を再現してみようと思います.

メルカリをダウンロードし初期起動するとまずチュートリアル画面が表示されます.

チュートリアル画面の要素としては,

  • 画面をフリックすると次のページへ遷移する
  • インジケータをタップすると次のページへ遷移する
  • ボタンをタップすると次のページへ遷移する
  • 最後のページまで遷移するとボタンの文言が”次へ”から”さぁ、はじめよう!”に変わる

となります.

実装の主な要素としては,

  • UIScrollView
  • UIPageControl

となります.

実装環境

開発環境

  • OS Yosemite Version 10.10.5
  • Xcode Version 6.4

デバッグ環境

  • iPhone 5
  • iOS 8.1.3

Storyboard

まず,Storyboard 上の画面レイアウトです.
画面は2つに分かれます.

  • スクロール機能を有する親コントローラー
  • コンテンツを有する子コントローラー

レイアウト

初めに親コントローラーのレイアウトです.

最下部の SuperView に,親Viewの領域に合わせた形で ScrollView を配置します.
同様に SuperView に,Hight Equally(Multiplier = 0.2) で高さ比20%の BottomView を配置します.

次にUIPageControl と UIButton 用の領域を配置していきます.
先ほど配置した BottomView に,Hight Equally(Multiplier = 0.3)で高さ比30%の PageControlView を,Hight Equally(Multiplier = 0.7)で高さ比70%の ButtonView を配置します.

最後に PageControlView に UIPageControl を,ButtonView に UIButton をそれぞれ Vertical Center, Horizontal Center で配置します.

これで親コントローラーのレイアウトは完成です.

次に子コントローラーのレイアウトです.

最下部の SuperView に,Hight Equally(Multiplier = 0.4) で高さ比40%の UIImageView を配置します.
同様に SuperView に,Hight Equally(Multiplier = 0.4) で高さ比40%の TextParentView を配置します.
ここで余らせている20%の領域は親コントローラーの BottomView と対応しています.

次に各ページのタイトルとテキスト用の領域を配置していきます.
先ほど配置した TextParentView に,Hight Equally(Multiplier = 0.5)で高さ比50%の TitleView を,Hight Equally(Multiplier = 0.5)で高さ比50%の ContentView を配置します.

最後に Title, Content それぞれ View に,Vertical Center, Horizontal Center で UILabel を配置します.

以上で親・子コントローラーのレイアウトが完成しました.

UI要素の設定

次に各UI要素の設定をしていきます.

まず初めに親コントローラーの ButtomView, PageControlView, ButtonView の Background を透過色に設定します.これらの ViewはScrollView よりも上の階層に位置するため,透過色にしないと ScrollView が隠れてしまうためです.

次に親コントローラーのScrollViewの設定です.
Shows Horizontal / Vertical Indicator のチェックを外します.これにより垂直 / 水平方向のスクロールバーが表示されなくなります.
Paging Enabled をチェックします.これによりページ単位でスクロールが行えます.

最後に親・子コントローラー共通で,UILabel,UIButton の描画色を White にしておきます.

ソースコード

コード側も Storyboard と同様に親と子,2つの ViewController です.
初めに親コントローラーである TutorialParentViewController です.

親コントローラー

#import 

@interface TutorialParentViewController : UIViewController 

@end

UIViewControllerを継承し,UIScrollViewDelegateを実装します.

#import "TutorialParentViewController.h"
#import "TutorialContentViewController.h"

@interface TutorialParentViewController ()

@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIPageControl *pageControl;
@property (weak, nonatomic) IBOutlet UIButton *button;

// ViewControlerがGC時に解放されないようにするためのArray
@property (weak, nonatomic) NSMutableArray *viewControlers;

@property (nonatomic) NSInteger numberOfPage;
@property (nonatomic) NSArray *imageFilePathes;
@property (nonatomic) NSArray *contentTitles;
@property (nonatomic) NSArray *contentText;
@property (nonatomic) NSArray *backgroundColors;

@end

@implementation TutorialParentViewController

@synthesize scrollView;
@synthesize pageControl;
@synthesize button;

@synthesize viewControlers;

@synthesize numberOfPage;
@synthesize imageFilePathes;
@synthesize contentTitles;
@synthesize contentText;
@synthesize backgroundColors;

TutorialViewController.m の宣言部です.UIScrollView, UIPageControl, UIButton を宣言しています.
また,子コントローラーの配列を宣言しています.これは子コントローラーがGC時に解放されてしまい,動作が不安定になることを防ぐためです.

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self initBootTutorialContents];
    
    // ナビゲーションバーを非表示にする
    [self.navigationController setNavigationBarHidden:YES animated:YES];
    
    // UIScrollViewのサイズを 縦:画面サイズ、横:画面サイズ * ページ数 に設定
    scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * numberOfPage, scrollView.frame.size.height);
    scrollView.delegate = self;
    
    // ページ数、初期ページを設定
    pageControl.numberOfPages = numberOfPage;
    pageControl.currentPage = 0;
    
    // 丸型ボタンを設定する
    button.layer.borderColor = [UIColor whiteColor].CGColor;
    button.layer.borderWidth = 1.0f;
    button.layer.cornerRadius = button.frame.size.height/2.0;
    
    for(int i = 0; i < pageControl.numberOfPages; i++){
        
        // ContentViewを生成
        TutorialContentViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"TutorialContentViewController"];
        vc.imageFilePath = imageFilePathes[i];
        vc.contentTitle = contentTitles[i];
        vc.contentText = contentText[i];
        vc.backgroundColor = backgroundColors[i];
        
        [scrollView addSubview:vc.view];
        
        // ViewControlerが解放されないようにインスタンスを保持
        [viewControlers addObject:vc];
    }
    
    [self setupScrollViews];
}

TutorialViewController.m の viewDidLoad です.

- (void)initBootTutorialContents
{
    
    numberOfPage = 5;
    
    imageFilePathes = @[@"haruka.jpg",
                    @"chihaya.jpg",
                    @"miki.jpg",
                    @"kotori.jpg",
                    @"baneP.jpg"];
    
    contentTitles = @[@"天海春香",
               @"如月千早",
               @"星井美希",
               @"音無小鳥",
               @"赤羽根P"];
    
    contentText = @[@"プロデューサーさん!\nドームですよっ!ドームっ!",
                @"...くっ!",
                @"おはようなのーっ!",
                @"ダメよ、小鳥イィ~",
                @"俺は忘れないからな\n今日のこのステージを...!"];
    
    backgroundColors = @[[UIColor colorWithRed:0.843 green:0.243 blue:0.220 alpha:1.0],
                         [UIColor colorWithRed:0.231 green:0.573 blue:0.863 alpha:1.0],
                         [UIColor colorWithRed:0.922 green:0.506 blue:0.110 alpha:1.0],
                         [UIColor colorWithRed:0.180 green:0.710 blue:0.318 alpha:1.0],
                         [UIColor colorWithRed:0.80 green:0.361 blue:0.729 alpha:1.0]];
}

子コントローラーの中身の要素を生成するメソッドです.

- (void)setupScrollViews
{
    UIView *view = nil;
    
    NSArray *subviews = [scrollView subviews];
    float currentLocation = 0;
    
    // ContentViewの位置を画面の横幅分右にずらしていく
    for (view in subviews)
    {
        CGRect frame = view.frame;
        frame.origin = CGPointMake(currentLocation, 0);
        view.frame = frame;
        
        currentLocation += [[UIScreen mainScreen] bounds].size.width;
    }
    scrollView.contentSize = CGSizeMake(currentLocation, scrollView.frame.size.height);
}

子コントローラーをひとつなぎのViewとなるように設定します.
ページの位置 = (ページ数 - 1) * 画面幅 です.

// スクロールのイベント
- (void)scrollViewDidScroll:(UIScrollView *)sender {
    
    // 縦スクロールしないようにするための設定
    [scrollView setContentOffset: CGPointMake(scrollView.contentOffset.x, 0)];

    // UIPgaeControlのページ数を設定   
    CGFloat pageWidth = scrollView.frame.size.width;
    pageControl.currentPage = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
    
    [self setButtonText:pageControl.currentPage];
}

UIScrollView のスクロール時のイベント処理です.
setContentOffset(x,y) で,y = 0 に固定にすることで縦方向のスクロールをできないようにします.

// ページ遷移時の処理
- (IBAction)changePage:(id)sender
{
    CGRect frame = scrollView.frame;
    // xを画面サイズ * ページ数分だけ右にずらす
    frame.origin.x = frame.size.width * pageControl.currentPage;

    // yは0で固定
    frame.origin.y = 0;
    [scrollView scrollRectToVisible:frame animated:YES];
    
    [self setButtonText:pageControl.currentPage];
}

UIPageControl に Bind するページ遷移のイベントです.
スクロール時のイベントと同様に y = 0 に固定し縦方向のスクロールを防いでいます.

-(void)setButtonText:(NSInteger)pageNumber{
    
    if(pageNumber == pageControl.numberOfPages - 1){
        [button setTitle:@" さぁ始めよう! " forState:UIControlStateNormal];
    } else{
        [button setTitle:@"  次へ  " forState:UIControlStateNormal];
    }
}

UIButtonのテキストを変更する処理です.

- (IBAction)nextToPage:(id)sender{
    
    if(pageControl.currentPage < pageControl.numberOfPages - 1){
        pageControl.currentPage++;
        [self changePage:NULL];
        
    } else {
        // 画面遷移など
        [self.navigationController setNavigationBarHidden:NO animated:NO];
    }
}

UIButton に Bind するページ遷移処理です.
最終ページ時のみ処理を変更し,画面遷移などを行います.

// ステータスバーを非表示にする
- (BOOL)prefersStatusBarHidden
{
    return YES;
}

全画面表示にするために,ステータスバーを非表示にします.

以上で親コントローラーの実装は完了です.

子コントローラー

#import 

@interface TutorialContentViewController : UIViewController

@property NSString *contentTitle;
@property NSString *contentText;
@property UIColor *backgroundColor;
@property NSString *imageFilePath;

@end

UIViewControllerを継承します.
プロパティとしてタイトル文字,コンテンツ文字,背景色,画像のファイルパスを定義します.

#import "TutorialContentViewController.h"

@interface TutorialContentViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIView *contentSuperView;
@property (weak, nonatomic) IBOutlet UILabel *labelTitle;
@property (weak, nonatomic) IBOutlet UILabel *labelContent;

@end

@implementation TutorialContentViewController

@synthesize contentTitle;
@synthesize contentText;
@synthesize backgroundColor;
@synthesize imageFilePath;

@synthesize contentSuperView;
@synthesize imageView;
@synthesize labelTitle;
@synthesize labelContent;

子コントローラーの viewDidLoad です.
プロパティのインスタンスをそれぞれの View に設定しています.

以上で子コントローラーの実装は完了です.

実装結果

実装結果になります.
各Viewのレイアウトはもう少し調整できそうですが,おおよそ挙動は再現できていると思います.

まとめ

今回はメルカリのチュートリアル画面を UIScrollView を用いて再現してみました.

UIPageViewController を用いる手段も考えられますが,静的で簡易な画面ならば UIScrollView を用いた方が比較的簡単に実装できます.

今回は子要素を UIViewController で実装しましたが,Viewで実装するパターンなど色々と応用もできると思います.

以上です.