2012/10/06

iOSアプリの画面遷移 + データの引き継ぎ(激闘編)

こんにちは。最近、本業がプチ炎上しているたーせるです。どうしてこうなったし。

閑話休題。

今日のお便りコーナーは、筑波の学園都市で暮らす ぬっくんから。

というわけで、ぬっくんが挫折したポイントに僕も挑んでみようと思います(下図は、前画面から引き継いだデータを表示するイメージです)。


あ、言い忘れましたが、ぬっくんは GUI ビルダー(Story Board)がお嫌いらしい。


ので、そこらへんも考慮して Story Board 不使用という縛りプレイで、画面遷移時のデータ引き継ぎを実装してみる事にしましょう。




【ゼロからのアプリ作成】

まずは基本中の基本、ボタンがいっこだけあるアプリから作り始めます。


Xcodeでアプリを作るときには、様々なプロジェクトの種類を選択する事ができますが、ここでは基本に忠実に、「Empty Application」を選びます。たぶんこれが知識として一番長持ちしそう。


プロジェクトを作成すると、自動的にいくつかのファイルが生成されます。

特に目に付くのが、AppDelegate.hAppDelegate.m、そして Suppoting Files ディレクトリ以下には main.m なんかもあります。

このままでは実行しても真っ白な画面しか表れないので、新たに画面表示用のクラスを追加します。

Objective-C class を選択して、


UIViewController を継承したクラスを適当に作ります。ここでは「ViewControllerA」と名付けました。


では、ViewControllerA.m を開いてプログラムを書いていきましょう。

ViewControllerA.h
#import <UIKit/UIKit.h>

@interface ViewControllerA : UIViewController

@end


ViewControllerA.m
#import "ViewControllerA.h"

@interface ViewControllerA ()

@end

@implementation ViewControllerA {
    // ボタンです
    UIButton* button;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // ボタンを初期化するよー
        button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.
    
    button.frame = CGRectMake(0.0, 160.0, 320.0, 40.0);
    [button setTitle:@"こんにちはボタン" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)action
{
    NSLog(@"ボタンが押されました");
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

あとは、アプリを起動したときに ViewControllerA の内容が表示されるよう、AppDelegate.m も少しいじります。

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

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

AppDelegate.m
#import "AppDelegate.h"

// 初期画面となるVCのヘッダファイルをインポート
#import "ViewControllerA.h"

@implementation AppDelegate {
    // 初期画面用のビューコントローラ
    UIViewController* viewController;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    
    // 初期画面をてきとうに作るよー!!
    viewController = [[ViewControllerA alloc] init];
    [self.window addSubview:viewController.view];
    
    [self.window makeKeyAndVisible];
    return YES;
}

@end


これでよし。ビルドしてみると、ボタンの一個ついたアプリが完成しました。やったね。さっそく売ろう。

……というのは冗談です。



【画面遷移を実現しよう】

画面遷移を実現するために、UIKit には様々なクラスが用意されていますが、今回は、UINavigationController クラスを使って画面遷移を実現しています。もちろん Story Board は使いません。

完成イメージとしては、ボタンを押すごとに、下図の左右の画面が相互に遷移する感じです。しゅごい。
 

画面遷移なので、画面クラスも複数必要になります。

先ほどと同じ要領で、「ViewControllerB」というクラスを追加しましょう。

で、各実装ファイルのソースコードを以下のように書き換えます(ヘッダファイルはそのままでOK)。

AppDelegate.m
#import "AppDelegate.h"

// 初期画面となるVCのヘッダファイルをインポート
#import "ViewControllerA.h"

@implementation AppDelegate {
    // 初期画面用のビューコントローラ
    UIViewController* viewController;
    
    // 画面遷移用のナビゲーションコントローラ
    UINavigationController* navigationController;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    
    // 初期画面をてきとうに初期化するよー!!
    viewController = [[ViewControllerA alloc] init];
    navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    
    [self.window addSubview:navigationController.view];
    [self.window bringSubviewToFront:viewController.view];
    
    [self.window makeKeyAndVisible];
    return YES;
}

@end

ViewControllerA.m
#import "ViewControllerA.h"

// 遷移先の画面となるVCのヘッダファイルもインポート
#import "ViewControllerB.h"

@interface ViewControllerA ()

@end

@implementation ViewControllerA {
    UIButton* button;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectMake(0.0, 160.0, 320.0, 40.0);
    [button setTitle:@"こんにちはボタン" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)action
{
    NSLog(@"ボタンが押されました");
    
    // 次画面を指定して遷移
    ViewControllerB* nextViewController = [[ViewControllerB alloc] init];
    if(nextViewController) {
        [self.navigationController pushViewController:nextViewController animated:YES];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end


ViewControllerB.m
#import "ViewControllerB.h"

@interface ViewControllerB ()

@end

@implementation ViewControllerB {
    UIButton* button;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.
    
    button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectMake(0.0, 160.0, 320.0, 40.0);
    [button setTitle:@"こんにちはボタン2" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)action
{
    // さっきの画面に戻るよー
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

ここまでは、実は Story Board を使えば割と一瞬で作れるのですが。



【いよいよデータの受け渡し】

まずは基本となる実装方針のご紹介。

僕だったらこうする、というアイディアをらくがきにしてみました。


つまり、共有データをアプリ内の一箇所にまとめてしまい、画面のみなさんは好きなときにそれを参照するのです。

さすがにここまでの前置きが長すぎて、読む方もだいぶ疲れた頃だと思うので、このへんで本日の最終成果を載せておきましょう。

 
  

ちょっと見づらいですが、ボタンのテキストが前の画面のデータを引きずってきて、未練がましく表示するサンプルです。

ちなみにここで使用する「共有データ置き場」はインスタンスとして実現できますが、「アプリ内にひとつだけ」という制約を設ける必要があります。

ご存知の方も多いかと思いますが、Singleton パターンが使えそうですね。

というわけで、まずは共有データ置き場のコードを書いてみます。

SharedData.h
#import <Foundation/Foundation.h>

@interface SharedData : NSObject
+ (id)instance;

// データをキーとともに追加します
- (void)setData:(id)anObject forKey:(id) aKey;

// 指定したキーに対応するデータを返します
- (id)getDataForKey:(id)aKey;

// 指定したキーと、それに対応するデータを、辞書から削除します
- (void)removeDataForKey:(id)aKey;

@end

SharedData.m
#import "SharedData.h"

@implementation SharedData {
    NSMutableDictionary* dictionary;
}
// 初期化
- (id)init
{
    self =  [super init];
    if(self) {
        dictionary = [[NSMutableDictionary alloc] init];
    }
    return self;
}

// インスタンスの取得(外部のクラスからはこちらを呼ぶ)
+ (id)instance
{
    static id _instance = nil;
    @synchronized(self) {
        if(!_instance) {
            _instance = [[self alloc] init];
        }
    }
    return _instance;
}

// データをキーとともに追加します
- (void)setData:(id) anObject forKey:(id) aKey
{
    @synchronized(dictionary) {
        [dictionary setObject:anObject forKey:aKey];
    }
}

// 指定したキーに対応するデータを返します
- (id)getDataForKey:(id)aKey
{
    id retval = [dictionary objectForKey:aKey];
    return retval != [NSNull null] ? retval : nil;
}

// 指定したキーと、それに対応するデータを、辞書から削除します
- (void)removeDataForKey:(id)aKey
{
    @synchronized(dictionary) {
        [dictionary removeObjectForKey:aKey];
    }
}

@end

データ置き場は、NSMutableDictionary のインスタンスを内包しており、「キー(識別子)」を用いて参照できるようにしてあります。意味のあるデータ集合を、識別子とともに格納する事により、DTO のように振る舞う事ができます。

続いて、共有データを参照できるよう、各ビューコントローラクラスを以下のように書き換えます。

ViewControllerA.m
#import "ViewControllerA.h"
#import "ViewControllerB.h"

// 共有データ置き場のヘッダファイルをインポート
#import "SharedData.h"

@interface ViewControllerA ()

@end

@implementation ViewControllerA {
    
    // 共有データ置き場へのポインタ
    SharedData* sharedData;
    
    UIButton* button;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        
        // 共有データインスタンスを取得
        sharedData = [SharedData instance];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectMake(0.0, 160.0, 320.0, 40.0);
    [button setTitle:@"こんにちはボタン" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)action
{
    NSLog(@"ボタンが押されました");
    
    // 共有データにメッセージをセットして遷移
    [sharedData setData:@"これは、初期画面の遺言です" forKey:@"メッセージ"];
    
    ViewControllerB* nextViewController = [[ViewControllerB alloc] init];
    if(nextViewController) {
        [self.navigationController pushViewController:nextViewController animated:YES];
    }
}

// 別の画面から遷移してきたとき
- (void)viewWillAppear:(BOOL)animated
{
    // 共有データからデータを読み出し
    NSString* message = [sharedData getDataForKey:@"メッセージ"];
    if(message != nil) {
        // データがあればボタンのタイトルを書き換え
        [button setTitle:message forState:UIControlStateNormal];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end


ViewControllerB.m
#import "ViewControllerB.h"

// 共有データ置き場のヘッダファイルをインポート
#import "SharedData.h"

@interface ViewControllerB ()

@end

@implementation ViewControllerB {
    // 共有データ置き場へのポインタ
    SharedData* sharedData;
    
    UIButton* button;
}

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {

        // 共有データインスタンスを取得
        sharedData = [SharedData instance];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.
    
    button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame = CGRectMake(0.0, 160.0, 320.0, 40.0);
    [button setTitle:@"こんにちはボタン2" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)action
{
    // 共有データに書き込んでさっきの画面に戻るよー
    [sharedData setData:@"これは、画面2の遺言です" forKey:@"メッセージ"];
    [self.navigationController popViewControllerAnimated:YES];
}

// 別の画面から遷移してきたとき
- (void)viewWillAppear:(BOOL)animated
{
    // 共有データからデータを読み出し
    NSString* message = [sharedData getDataForKey:@"メッセージ"];
    if(message != nil) {
        // データがあればボタンのタイトルを書き換え
        [button setTitle:message forState:UIControlStateNormal];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end


…残りのAppDelegateクラスや各種ヘッダファイルはそのままで OK。

これを実行すると、前画面が今際のきわに遺したメッセージを、遷移先がきちんと受け継いで表示している事が分かります。やったね。

というわけで、ぬっくんはがんばれ。

2 件のコメント:

  1. お疲れ様です(*´ω`*)
    AppDelegateまわりの記述も結構手こずった記憶があったのでこれ見て復習してみます!

    返信削除
  2. コメントありがとうございます。

    実は、AppDelegateクラスには「Template Methodパターン」というデザインパターンの一種が使われています(たぶん)。

    最初は、「なんでそんなややこしいことを!!」と思われるかも知れませんが、プログラマがやる事は「オーバーライドメソッドの中身を埋めるだけ」です。これらのメソッドは自動的に呼び出されます。

    ふつうのプログラムが「自由記述問題」ならば、Template Methodは「穴埋め問題」のようなもので、慣れてくると解答スピードが上がる、もとい生産性が向上するので、様々なところで活用されているパターンです(Java ServletやProcessingなど…)。

    ですので、ここでいくつかのデザインパターンを身につけておくと、他の環境でオブジェクト指向プログラミングをするときにもラクになれます。

    【おすすめ書籍】

    ・ 荻原剛志 『詳解Objective-C 2.0 第3版』

    ・ 所友太 『iPhoneプログラミング UIKit 詳解リファレンス』
    ⇒ Story Boardを使わずに画面まわりを作りたい人に

    ・ 木下誠 『Dynamic Objective-C』
    ⇒ GoFのデザインパターンをObjective-Cで解説しています

    3冊目はちょっと難易度が高めなので、難しく感じる場合は、結城さんの『増補改訂版 Java言語で学ぶデザインパターン入門』を先に読むとよいと思います。

    返信削除

ひとことどうぞφ(・ω・,,)