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

夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【Objective-C】.plistファイルを経由せず直接.csvファイルからデータを読み込む方法

Objective-C スペースハリアー TravelShooting JP

これまで敵生成データ、敵の動きのデータ、敵の攻撃などのアクションのデータは.plistファイルから読み込んでいました。
.plistファイルからNSArrayへの読み込みは1行で書けるので便利なのですが、これらのデータをいきなり.plistファイルとして書くのは困難です。
通常は1行目をタイトル行、2行目以降をデータ行としてExcelなどの表計算ソフトで作成する事が多いと思います。
私はフリーのLibre Office Calcを使ってこんな感じで作っています。
f:id:takujidev:20131222211054p:plain
そこから何らかのツールを使ってタイトル行をキー、他の行を値としたディクショナリー形式にした.plistファイルを生成することになります。
私の場合は、
【Objective-C】.plistファイルの作り方と読み込み方 - 夏までにiPhone アプリつくってみっか!
に書かれているサイトにお世話になって.csvファイルを.plistファイルに変換していました。
ところが、昨日突然変換が上手くいかなくなり、0バイトのファイルが生成されるようになってしまいました。(もしかしたら今は復活しているかもしれません。)
そこで、今回は表計算ソフトで作成した.csvファイルをArrayに直接読み込めるようにチャレンジしてみました。

で、完成したのがこれです。
実際に使っているコードなので読み込み以外のコードも含まれていますが、参考までに。

- (id)init
{
    if ((self = [super init])) {
        _actionTable = [NSMutableArray array];
        NSBundle* bundle = [NSBundle mainBundle];
        NSArray* paths = [bundle pathsForResourcesOfType:@"csv" inDirectory:@"ActionTable"];
        NSAssert(paths.count != 0, @"No action data file found");
        
        for (NSString* path in paths) { //ディレクトリ内全.csvファイルをループ
            NSError* error = nil; // エラーが入る
            NSStringEncoding encoding; //エンコード方式が入る
            NSString* content = [NSString stringWithContentsOfFile:path usedEncoding:&encoding error:&error];
            if (error) {
                NSAssert1(NO, @"Reading action data file failed with error: %@", error.localizedDescription);
            }
            NSArray* rows = [content componentsSeparatedByString:@"\n"]; //ファイル内容を行毎に分解
            NSArray* titleRow = [[rows objectAtIndex:0] componentsSeparatedByString:@","]; //先頭行
            NSMutableArray* actions = [NSMutableArray array]; //結果を入れるArray
            for (int i = 1; i <= rows.count; i++) { //全データ行をループ
                NSArray* row = [[rows objectAtIndex:i] componentsSeparatedByString:@","];
                if ([[row objectAtIndex:0] isEqualToString:@"end"]) {
                    break; // 1ファイル終了
                }
                NSAssert1((row.count == titleRow.count), @"Column count mismatch in action data file %@", path); //列数が異常
                NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:row forKeys:titleRow]; //1行分をDictionaryに入れる
                [actions addObject:dictionary];
            }
            NSString* name = [[path lastPathComponent] stringByDeletingPathExtension]; //パスから拡張子なしのファイル名を取り出す
            Action* action = [[Action alloc] initWithActions:actions name:name];
            [_actionTable addObject:action];
        }
    }
    return self;
}

.csvファイルからデータを読み込んでarrayに入れている部分の処理の流れはこんな感じです。

1. pathsにディレクトリー内の全.csvファイルのパスを読み込む
2. paths内の各パスをループ
3. contentにファイル内容を文字列として読み込む
4. contentを改行文字で区切って分解し、rowsに各行を入れる
5. 先頭行にはキーが入っているのでカンマ分解し、titleRowに入れる
6. 残りの各行をループ
7. カンマで分解した物をrowに入れる
8. rowの先頭が@"end"なら最終行の印なのでそのファイルは終了
9. 最終行でなければtitleRowをキー、rowをオブジェクトとしてdictionaryを生成
10. dictionaryを目的のArrayに入れる(この場合はactions)

(注:上のプログラムは指定したディレクトリ内の全ての.csvファイルを読む方法です。メインバンドル内の指定した1つのファイル例えばtest.csvから読む場合は、
NSString* path = [bundle pathForResource:@"test" ofType:@"csv"];
などとし、最初のループは不要です。)

.plistファイルの読み込みと比べるとプログラムを組むのは面倒ですが、.cvsファイルを編集してセーブしたら即実行可能なのはかなり快適です。
今までは.csvファイルを直す度に毎回一手間掛けてplistファイルを作成するのが面倒で、データをちょこちょこ修正するのに気が進まなかったのですが、今や爆速でトライアル&エラーができてしまっています。

ところで、私はファイルの終了の目印として@"end"を使っていますが、行数があらかじめわかっているので必ずしも必要は無いです。
ただ、Libre Officeの場合、.csvファイルの最後に改行だけの行ができる事があり、この場合タイトル行とデータの数が合わないのでDictionary に入れる事ができません。
このプログラムではそのような場合はNSAssertで終了しています。
それを目印にループをbreakすれば@"end"による目印は不要になりますが、表計算ソフトによってはさらに違う動作をする物があるかもしれないので、私は明示的にデータ内に終了目印を入れる事にしました。