読者です 読者をやめる 読者になる 読者になる

ぴよぴよエンジニアの日記

クラウドベンダーに勤める見習いSEの日記です。発言は私自身の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

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

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

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


f:id:Santea:20160123224651g:plain

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

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

となります.

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

  • UIScrollView
  • UIPageControl

となります.


開発環境

  • OS Yosemite Version 10.10.5
  • Xcode Version 6.4

デバッグ環境


Storyboard


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

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

レイアウト

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

f:id:Santea:20160123234029p:plain

最下部の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で配置します.

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



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

f:id:Santea:20160124001428p:plain

最下部の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が隠れてしまうためです.

f:id:Santea:20160124002826p:plain

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

f:id:Santea:20160124003545p:plain

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


コード


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

親コントローラー

#import <UIKit/UIKit.h>

@interface TutorialParentViewController : UIViewController <UIScrollViewDelegate>

@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);
}

f:id:Santea:20160207221815p:plain
子コントローラーをひとつなぎの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 <UIKit/UIKit.h>

@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;

子コントローラーの宣言文です.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [imageView setClipsToBounds:YES];
    
    imageView.image = [UIImage imageNamed:imageFilePath];
    labelTitle.text = contentTitle;
    labelContent.text = contentText;
    contentSuperView.backgroundColor = backgroundColor;
}

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

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

実装結果

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

まとめ

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

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

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

以上です.