iOSアプリで初回起動時にアプリケーションの説明などのチュートリアルを設ける事がありますが、 よく見かけるパターンが何個かあると思います

Chardin.js のように実際の画面で、説明したい部分に説明を載せる感じの方法。

workshirt/WSCoachMarksView のようなスポットライトあてて説明するのも似たようなパターンです。

Chardin js 2013 07 11 16 21 34

もう一つは、これが実装もしやすいのでよく見かける気がしますが、
チュートリアル用の画面を用意して表示するタイプ。

evernoteのアプリなどもそうですが、PageControlを使ってスワイプして進めるタイプが多いです。

2013 06 19 15 56 12

これは、StoryBoardで実際にそれぞれの画面を作って、
NavigationControllerで遷移するような仕組みを作ればいいだけなので、作りやすいのです。 

後は、CMPopTipView等のツールチップを表示するものを使って必要な要素にツールチップで説明を表示するパターンです。

最初の方法と似たような感じですが、ツールチップを無視して進めたりする場合が多いので緩い感じのチュートリアルです。

NewImage

今回は、最後のツールチップを使ったチュートリアルをどういう感じで実装すればキレイにできるかを少し考えたメモです。
ツールチップのUI自体はCMPopTipViewのライブラリを利用します。

実装

サンプルは azu/tooltip-navigation-app においてあります。

動作は以下のような感じです

先に、CMPopTipViewの簡単な説明をしておきます。

    self.popTipView = [[CMPopTipView alloc] initWithMessage:tutorialData.message];
    self.popTipView.delegate = self;
    [self.popTipView presentPointingAtView:表示箇所にしたいView inView:self.view animated:YES];

表示箇所にしたいView に、あるボタンのViewを渡してあげれば、そのボタンに対してself.popTipViewのツールチップが表示されるという感じです。
つまり、表示するにはViewの参照を示してあげる必要があります。 

プロジェクトの関係してるファイルは以下のような感じです

├── Config
│   ├── UserDefaults.h // UserDefaultを管理
│   └── UserDefaults.m
├── Main
│   ├── MainModel.h // 結局使わなかった…
│   ├── MainModel.m
│   ├── MainViewController.h // メイン画面
│   └── MainViewController.m
├── Tutorial
│   ├── TutorialData.h // チュートリアルデータのモデル
│   ├── TutorialData.m
│   ├── TutorialDataManager.h // チュートリアルの管理
│   └── TutorialDataManager.m

これを作る際に気をつけていた事は、

  • 表示するツールチップの内容と場所は一箇所で管理したい
  • MainViewControllerがチュートリアルの内容に関するデータを持たない事
  • TutorialDataManagerでツールチップの内容を管理したい
  • しかし、TutorialDataManagerがViewの参照を直接持たない事

MainViewControllerで全部持ってしまうが一番単純ですが、コントローラーが肥大化するので避けたいと思います。
しかし、TutorialDataManagerでツールチップの表示内容と場所(Viewの参照)を管理したいのですが、
TutorialDataManagerがViewの参照を直接持つのは良くない感じがしたので、代わりに以下のようなenumを定義して参照の代わりにこちらを管理するようにしました。

typedef NS_ENUM(NSUInteger, MainViewOutletType){
    MainView_InformationButton,
    MainView_FirstButton,
    MainView_SecondButton,
    MainView_ThirdButton,
};

TutorialDataManagerでは以下のようにツールチップの内容と場所を配列にして管理するようにしました。 (この並び順にツールチップを出していって、tutorialTypeは一度出たツールチップは再度出ないようにするための識別子)

@[
        [TutorialData dataWithMessage:@"ここをタップする再度ヘルプを見られます" outletType:MainView_InformationButton tutorialType:TUTORIAL_STEP1],
        [TutorialData dataWithMessage:@"一番目のボタンです" outletType:MainView_FirstButton tutorialType:TUTORIAL_STEP2],
        [TutorialData dataWithMessage:@"二番目のボタンです" outletType:MainView_SecondButton tutorialType:TUTORIAL_STEP3],
        [TutorialData dataWithMessage:@"三番目のボタンです" outletType:MainView_ThirdButton tutorialType:TUTORIAL_STEP4],
];

これで、TutorialDataManagerは実際のViewの参照を知らなくても済むようになりました。

MainViewControllerに、outletTypeに対する実際のViewを返すようにすればデータとViewの対応関係が取れます。

- (id)viewForMainViewOutletType:(MainViewOutletType) type {
    switch (type) {
        case MainView_InformationButton:
            return self.informationButtonItem;
        case MainView_FirstButton:
            return self.firstButton;
        case MainView_SecondButton:
            return self.secondButton;
        case MainView_ThirdButton:
            return self.thirdButton;
    }
    return nil;
}

次はツールチップの表示方法について

一度出たツールチップは表示しないようにしたいため、ツールチップをタップされた時にその情報(tutorialTypeを元にした)を保存します。 ツールチップを表示する際に、CMPopTipViewのView.tagにtutorialTypeの値を入れておきます。

    self.popTipView = [[CMPopTipView alloc] initWithMessage:tutorialData.message];
    self.popTipView.tag = tutorialData.tutorialType;
    self.popTipView.delegate = self;

CMPopTipViewはツールチップがタップされたことはdelegateで取得できるため、 以下のようにdelegateで、popTipViewからtagを取り出してその値を保存します。

// ツールチップがクリックされたら、CheckPointを更新 -> KVOで検知
- (void)popTipViewWasDismissedByUser:(CMPopTipView *) popTipView {
    self.popTipView = nil;
    TutorialType tutorialType = (TutorialType)popTipView.tag;
    [self.tutorialModel setCheckPoint:tutorialType];
}

詳しくはazu/tooltip-navigation-appのコードを見たほうがわかりやすいですが、
値を保存する際に、checkPoint  というプロパティを経由するようにして、そのプロパティをKVOで監視するようにしているので、
値が保存された際に自動で、@selector(showNextPopTip) が呼ばれるようになっていて、次のツールチップが表示されます。

そのため、初回以外は明示的に@selector(showNextPopTip)を呼ぶ必要はなく、すべてのツールチップが表示->保存された時点でツールチップは表示されなくなります。
(サンプルではリセットボタンを左上に置いてある)

サンプルプロジェクトでは 、MainViewControllerとTutorialDataManagerに分けてチュートリアルのツールチップを管理できるようにしましたが、
まだ、お互いが依存しすぎてる気がするのでもっとキレイに書けるかもしれません。(PullRequestすればいいと思います)

Post Navigation