【Xode】NavigationController配下にTabBarControllerを組み込む

【Xcode6.2 + iOS 8.4(iPhone5) + MacOX10.9.5】

文字通りのことをやろうと思ったんだけど、なかなかいい例がなかったのでメモ。

TabBarControllerとNavigationContorollerの組み合わせ例はいくつかあったんだけど、大抵がTabBarControllerをベースにして、子にNavigationContorollerを使うというパターン。

今回やりたかったのは、NavigationContorollerをベースにして、その配下でTabBarControllerを使うパターン。アプリの設定画面部分だけをTabBarControllerにするようなイメージです。

それから前提として「Viewは、プログラム生成ではなくStoryboardに配置する」ということ。AutoLayoutを使うことを考えると、レイアウトはStoryboard、処理はコードで分けるのがいいんじゃないかと。

Storyboardの構成はこんな感じです。
storyboard

・ViewController
・SettingViewController
・TimeSettingViewController
・SpeedSettingViewController

この4つは、それぞれ同名のカスタムクラスを作ってStoryboardで割り当てています。ViewControllerがrootControllerになっていて、ボタンタップで2つのタブ(TimeSettingViewController, SpeedSettingViewController)を持つTabBarController(SettingViewController)を呼び出す構成です。

※ちなみにTabBarControllerを配置すると自動的に接続されるSegueは削除してあります。その設定もコード上で試してみたかったので。

で、まずはUITabBarControllerの定義。どこで使うにせよ、これはAppDelegateの中に書くのが定石らしい。

【AppDelegate.h】
・Storyboardの各Viewを参照するので、その定義ファイル(.h)のimportと保存するUITabBarControllerの定義を追加する。

#import <UIKit/UIKit.h>
#import "SettingViewController.h"
#import "TimeSettingViewController.h"
#import "SpeedSettingViewController.h"

@interface AppDelegate : UIResponder <UIApplicationDelegate,UITabBarControllerDelegate>
{
    UITabBarController *myTabBarController;
}

@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) UITabBarController *myTabBarController;

@end

【AppDelegate.m】
・あとで参照する可能性もあるので、各タブのcontrollerも一応定義しておく。
・UITabBarController の生成は、didFinishLaunchingWithOptionsで行う。
・tabControllerの切替イベント(shouldSelectViewController, didSelectViewController)もココに定義。

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate
{
    TimeSettingViewController *TimeSettingVC;
    SpeedSettingViewController *SpeedSettingVC;
}

@synthesize myTabBarController;


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    //UITabBarControllerを生成(storyboard上のUITabBarControllerを指定)
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    myTabBarController = [storyboard instantiateViewControllerWithIdentifier:@"SettingVC"];
    
    //各タブ用UIViewControllerを生成(storyboard上のUIViewControllerを指定)
    TimeSettingVC = [storyboard instantiateViewControllerWithIdentifier:@"TimeSettingVC"];
    SpeedSettingVC = [storyboard instantiateViewControllerWithIdentifier:@"SpeedSettingVC"];
    
    //UITabBarControllerに各タブ用UIViewControllerを設定
    NSArray *controllers = [NSArray arrayWithObjects:TimeSettingVC,SpeedSettingVC, nil];
    [myTabBarController setViewControllers:controllers animated:YES];
    
    //デリゲートの設定
    myTabBarController.delegate = self;
    
    //初期表示のタブを指定
    myTabBarController.selectedViewController = myTabBarController.viewControllers[1];
    
    NSLog(@"--array: %@",myTabBarController.viewControllers);
    NSLog(@"--selectedViewController: %@",myTabBarController.selectedViewController);
    NSLog(@"--index: %d",myTabBarController.selectedIndex);

    return YES;
}

-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
     NSLog(@"--tabBarControllerを切り替えます!");
    NSLog(@"--selectedIndex: %d",tabBarController.selectedIndex);
    NSLog(@"-- vc: %@",viewController);
    
    return YES;
}

-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
    NSLog(@"--tabBarControllerが切り替わった!");
    NSLog(@"--selectedIndex: %d",tabBarController.selectedIndex);
    NSLog(@"-- vc: %@",viewController);
}

@end

※22行目〜28行目までは、tabBarControllerに各タブを定義する処理なので、Storyboard上でSegueが定義されている場合は不要。

ここでのポイントは、AppDelegateからStoryboardを参照するには、storyboardWithNameを使わないといけないということ。各Viewからなら、self.storyboardでいけるんだけど。

【参考】

次にtabBarContorollerを呼び出す処理を書きます。viewControllerにボタンを配置して、そこからリンクします。

【ViewController.h】
・AppDelegateをimport。
・showSettingViewはボタンタップ時の処理。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

@interface ViewController : UIViewController

- (IBAction)showSettingView:(id)sender;

@end

【ViewController.m】
・AppDelegateを参照してtabBarControllerを取得して、pushViewControllerで表示する。

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (IBAction)showSettingView:(id)sender {
    //AppDelegateを参照して、myTabBarControllerを呼び出す
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    [self.navigationController pushViewController:appDelegate.myTabBarController animated:YES];
}

@end

iOS View Controller カタログ(PDF)によれば・・・

Tab Barインターフェイスを作成する前に、その使い方を決定する必要があります。Tab Barインターフェイスはデータに対して支配的な構成となるため、用途は以下に限定されます。
・ウィンドウのルートView Controllerとして追加する。
・Split Viewインターフェイスの2つのView Controllerの1つとして追加する。(iPadのみ)
・別のView Controllerからモーダルモードで表示する。
・Popoverから表示する。(iPadのみ)

モーダルモードでのTab Bar Controllerの表示
(一般的ではありませんが)アプリケーションでTab Bar Controllerをモーダルモードで表示すること もできます。Tab Barインターフェイスは、通常はアプリケーションのメインウインドウにインストー ルされ、必要な場合にだけ更新されます。ただし、インターフェイスの設計において必要と認められ る場合には、Tab Bar Controllerをモーダルモードで表示することも可能です。たとえば、アプリケー ションの主たる操作モードを、Tab Barインターフェイスを使用したまったく別のモードに切り替える には、クロスフェードトランジションを使用して、第2のTab Bar Controllerをモーダルモードで表示します。
Tab Bar Controllerをモーダルモードで表示する場合は、必ずpresentModalViewController:animated: メソッドの第1パラメータにTab Bar Contrllerオブジェクトを渡します。このTab Bar Controllerは、表示する前にすでに設定されていなければなりません。したがって、Tab Barインターフェイスをメインウ インドウにインストールする場合とまったく同じ方法で、ルートView Controllerを作成して設定し、それらをTab Bar Controllerに追加する必要があります。

ということらしいので、

・TabBarControllerの定義は、AppDelegateで行う。
・表示は、別のView Controllerからモーダルモードで行う。

ということでいいようです。

【参考】
Tab Bar Controllers
Combined View Controller Interfaces
iOS View Controller カタログ(PDF/上記2つの日本語訳)
iOS View Controller プログラミングガイド(PDF)
StoryboardとSegueの基本 – Kesin’s diary
画面間でのデータの受け渡しに付いて: 永遠ログ
iOS – segue による画面遷移メモ – Qiita
日本語ドキュメント – Apple Developer
プログラム側からUITabBarControllerを切り替えて、さらに画面遷移させたい時 – makoラボ
Programming.log, [Objective-C] applicationDidFinishLaunching とは