複数種類のiPhoneアプリを同時開発するときのTips

先日弊社からiPhoneアプリをリリースしました。

スポーツ動画アプリ全10種類 | 株式会社CFlatスポーツ動画アプリ全10種類 | 株式会社CFlat

iPhoneで気軽にスポーツ動画を楽しめるアプリケーションをリリースしました。AppStoreにて配信中です。

このアプリではスポーツのハイライトや試合結果、最新ニュースなどの動画を検索することなく閲覧することができます。スポーツが好きな方、スキマ時間を有効に使いたい方に最適な動画アプリです。


このアプリはあるスポーツについての最新の試合結果などを検索せずに見ることができるというものです。アプリはスポーツ毎に分かれているので、サッカーを見たい人はサッカー動画用のアプリ、野球を見たい人は野球用のアプリを使うことになります。

ここまで言うと大体お分かりいただけると思うのですが、アプリの基本部分の作りはどのアプリにも共通したものになっています。つまりソースコードが共有されているわけです。アプリ間で異なるのはアイコン画像やアプリ名、まとめた動画情報など。また内部ではParse.comのIDや広告のIDなどもそうですね。逆に言うとそれ以外の部分が共通しているということです。

このようなケース、ほとんどのソースコードは一緒だけど一部だけ違うアプリを複数同時にリリースするというケースではいろいろなことが面倒になります。

  • ソース管理はどうするのか
  • テスト配布用ipaファイルをどうやって作るか
  • ipaのアップロードはどうやってやるのか

もちろん10種類分のアプリがあるので手動ではやってられないため、できるところはほとんどを自動化することにしました。

このようなケースはあまりないのかもしれませんが、今日の記事ではどのようなやり方をしたかを紹介してみたいと思います。

ソース管理

ほとんどのソースコードが共通なので同じGitリポジトリで管理することにしています。そして各スポーツに対して一つずつブランチを用意します。

$ git br -a
  01.soccer
  02.cricket
  # ...
  10.icehockey
  master
* work-branch01

こんな風に10個分のブランチを用意しました。

全体に関わる変更

全体に関わるコミットはトピックブランチを経てmasterブランチに適用されます。そしてそれらの変更は$ git rebase masterによって各ブランチに取り込まれることになります。

10個のブランチに対してrebaseコマンドを叩いていくのは非常に面倒です。そこで私はRubyrakeを使うことにしました。

$ rake rebaseとすると、ブランチを切り替えながら$ git rebase masterしてくれるというスクリプトです。

(ブランチ名の一覧はyaml形式のデータファイルの書いておいてそこから取っています。$ git branch -aの結果を使って頑張ることもできますが、別の用途で作ったyamlがあったのでそちらを利用。)

desc "Run 'git rebase master' on all sports branches"
task :rebase do
  config = YAML.load_file( File.expand_path("../../data.yml", __FILE__) )
  config.keys.each_with_index do |key, i|
    branch_name = "#{sprintf('%02d', i+1)}.#{key}"
    system "git checkout #{branch_name}"
    system "git rebase master"
    system "git checkout ."
  end
  system "git checkout master"
end

各ブランチに対する変更

スポーツ毎に異なるものとして次のようなものがあります。

  • 各種外部サービスのキーやトークン
  • ホーム画面での表示名
  • 動画用データの参照先
  • アイコン
  • ProvisioningProfile(*)
  • bundle id(*)

大体こんなところでした。このうち(*)がついていないものは、各スポーツごとのブランチに対して手動でコミットしておきました。残りの(*)で印をした項目はプログラムを書いて動的に変更するようにしました。

というのは、これらのファイルを変更してコミットしてしまうとmasterに対してrebaseを行うときに100%コンフリクトしてしまうために自動処理ができなくなってしまうわけです。具体的にはpbxprojファイルとplistファイルがそれにあたります。

ProvisioningProfileやbundle idがを変更する必要があるのは各バージョンをビルドするときです。基本的には放っておき、ビルドする際にpbxprojファイルとplistファイル内の文字列の置換により値を置き換えます。値の対応付けは外部のyamlファイルに書いておきました。

ビルドの自動化

ブランチを切り替えながら10個分のビルドを済ませるのも一苦労です。ここでもrakeを用いた自動化を行っています。

require "yaml"

desc "create xcarchive and ipa"
task :archive do
  time = Time.now 
  archive_dir = time.strftime("archive_%Y%m%d%H%M%S")
  ipa_dir = time.strftime("ipa_%Y%m%d%H%M%S")
  Dir.mkdir(archive_dir)
  Dir.mkdir(ipa_dir)

  profiles = YAML.load_file( File.expand_path("../profiles.yml", __FILE__) )

  profiles.each do |k,v|
    next if k == "master"
    
    # change branch
    system "git co . && git co #{k}"

    # アイコン、表示名、プロビジョニングを変更するrakeタスク
    system "rake provision"
    
    # create archive
    system %Q{
      xcodebuild \
      -workspace MyApp.xcworkspace \
      -scheme MyApp \
      -sdk iphoneos \
      -configuration Release \
      archive -archivePath #{archive_dir}/#{k}
    }

    ipa_path = File.expand_path("../../#{ipa_dir}", __FILE__)

    # create ipa
    system %Q{
      xcrun \
      -sdk iphoneos \
      PackageApplication #{archive_dir}/#{k}.xcarchive/Products/Applications/MyApp.app \
      -o #{ipa_path}/#{k}.ipa \
      -embed ~/Library/MobileDevice/Provisioning Profiles/#{v['release']}.mobileprovision
    }
  end
  system "git co master"
end
  1. 10個分のブランチに対してループを回す
  2. 対象ブランチをチェックアウト
  3. アイコン、表示名、プロビジョニングを変更するタスクを実行
  4. xcodebuildコマンドでxcarchiveを作る
  5. xcrunコマンドでxcarchiveからipaファイルを作る

xcodebuildxcrunがここでの肝です。使い方は上のコードを見てもらえば大体わかってもらえると思います。

リリース(アップロード)の自動化

iTunesConnectへのアップロードもなかなか骨が折れます。アプリを作って紹介文を書いてスクリーンショットを登録してアップロード待ち状態にして、、、という作業を10個分やる必要があるので。

幸い、そこまでを手動でやっておけばあとは自動でアップロードを行うことができます。

さっきコマンドで作ったipaファイルとxcrunコマンドを使います。

$ security add-generic-password -s Xcode:itunesconnect.apple.com -a YOUR_ID -w YOUR_PASSWORD -U 

$ xcrun -sdk iphoneos Validation -online -upload -verbose "path to ipa" 
# 10種類分....

$ security delete-generic-password  -s Xcode:itunesconnect.apple.com -a YOUR_ID

iTuneConnectへのログインが必要になるので、xcrunコマンドの前後にsecurityコマンドによる認証情報の追加と削除が必要なことに注意。

スクリーンショット撮影の自動化

10種類分のスクリーンショットを撮るのも一苦労なので、そこも自動化しました。本来はUIのテストに用いるKIFというライブラリを使いました。

60分で始めるiOSアプリのUI自動テスト - 株式会社CFlatの明後日スタイルのブログ60分で始めるiOSアプリのUI自動テスト - 株式会社CFlatの明後日スタイルのブログはてなブックマーク - 60分で始めるiOSアプリのUI自動テスト - 株式会社CFlatの明後日スタイルのブログ

KIFを使うことで自動で画面遷移をさせることができるので、画面遷移をしつつスクリーンショットが欲しいところで画面を画像ファイルに書き出せばいい感じで自動化できます。

表示中の画面を画像に書き出す方法は次の通りです。

- (void)captureScreen:(NSString*)name
{
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString* dir = [paths objectAtIndex:0];
   
    UIWindow* window = [[UIApplication sharedApplication] keyWindow];
    UIGraphicsBeginImageContextWithOptions(window.bounds.size, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    for (UIWindow* win in [[UIApplication sharedApplication] windows]) {
        [win.layer renderInContext:context];
    }
   
    UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
   
    [UIImagePNGRepresentation(image) writeToFile:[dir stringByAppendingPathComponent:name] atomically:YES];
}

なおこの方法ではステータスバーの部分は描画されませんが、今回はそれで良しとしました。回避策としては、ダミーのステータスバー画像を用意しておいて出力画像に合成させるというものがあります。

KIFを使う=テストの実行ということになります。テストもxcodebuildで実行できるはずなので、本来はブランチ切替→テストも完全自動化できると思いますが、このときは手動でテストを実行していたので検証はしていません。

最後に

今回のアプリのように複数種類を同時に扱うには自動化が不可欠でした。コマンドラインでいろいろサポートされていて助かりました。

iTunesConnectでの操作(アプリ追加、バージョン追加、スクリーンショット追加など)もコマンドラインでできるととても助かるのですが。。。