モバイルファースト室でiOSアプリケーションの開発を行っている@yuseinishiyamaです。
クックパッドでは日々の業務を効率よく行うためのツールを作り、公開するということが積極的に行われています。 社内のリポジトリや掲示板を探せば、便利なツールをたくさん見つけることができるような環境です。
こうした文化のお陰で、作業時間の短縮、自動化が容易となり、結果として「ユーザーの方々に価値を届ける」という本質的な作業に費やす時間を増やすことができます。
私も先日、iOSシミュレータをカスタマイズして作業効率を上げる機能を実装してみたので、その方法を紹介いたします。
動作環境
以下の環境で動作確認済みです。他の多くの環境でも動くと思われますが、保証できません。
- OSX 10.9.4 + Xcode5
- OSX 10.9.4 + Xcode6
Loadable Bundleについて
iOSシミュレータを拡張するために、Loadable Bundle
を作成します。Loadable Bundle
は通常のアプリケーションと同じ構造を持っています。つまり、実行可能なファイルやリソースファイルを含むディレクトリです。しかし、通常のアプリケーションとは異なり、main
関数を持っていません。Loadable Bundle
のロードは、それを利用するアプリケーション側の責任となっています。このLoadable Bundle
には以下のような用途があります。
コンポーネントの遅延ロードを行う。使用されていないコードがメモリ上に展開されないので、メモリの節約になります。
一部のソースコードを個別にコンパイル可能なコードに分ける。
アプリケーションにプラグイン機能を設ける。
詳しくは下記のリンクを参照してください。
EasySIMBLのインストール
EasySIMBLを利用すれば、既存のアプリケーションの実行時に、Loadable Bundle
をロードすることができます。
自前のクラスがロードされれば、+ (void)load
などをエントリーポイントとして、既存のアプリケーションを拡張することができます。
インストール方法は
https://github.com/norio-nomura/EasySIMBL
のHow to install
の項を参考にしてください。
バンドルファイルの作成準備
- Xcodeのメニューから
File->New->Project
を選択し、OSX
のカテゴリにあるFramework & Library
内のBundle
を選択します。
- 任意の情報を入力してください。
Bundle Extension
を変更する必要はありません。
- プロジェクトが作成されたら、ターゲットの
Info.plist
を編集します。
Info.plist
にSIMBLTargetApplications
というキーを追加します。それぞれ、拡張したいアプリケーションのバンドルID、拡張が実行可能なバージョンの最大値、最小値を記載します。
もし、特定のバージョンでしか拡張したくないという場合は、適宜バージョンの値を修正してください。
事前準備は以上で終了です。ただし、この状態のバンドルを組み込んでも何も起こらないので試しにログを出力できるようにしてみましょう。
ログを出力する
- 自前のクラスを作成し、ロード時にログを出力するように実装します。
EXSSimpleLog.h
#import <Foundation/Foundation.h> @interface EXSSimpleLog : NSObject @end
EXSSimpleLog.m
#import "EXSSimpleLog.h" @implementation EXSSimpleLog + (void)load { NSLog(@">>>>> Injection Succeed!"); } @end
- ビルドし、SIMBLのプラグインディレクトリに配置します。
ビルドすると、
~/Library/Developer/Xcode/DerivedData/(アプリ名+ハッシュ)/Build/Products/Debug
にビルドされたバンドルファイルが格納されるので、それを
~/Library/Application Support/SIMBL/Plugins
内に移動させます。
- iOSシミュレータを起動し、ログを確認します(起動中の場合は再起動してください)。
これで、自前のクラスがロードされたことが確認できました。しかし、これだけではつまらないですね。
【注意】不具合が出た場合
拡張内容に不備があったりすると、当然アプリケーションがクラッシュしてしまいます。その場合は、EasySIMBLのアプリケーションを開いて作成したアプリケーション名、もしくは、Use SIMBL
のところにあるチェックボックスをオフにしてください。
メニューへのアイテム追加
先ほど作成したクラスのロード時にメニューバーのアイテムを追加するコードを実行します。
<Cocoa/Cocoa.h>
をインポートするのを忘れないようにしてください。
EXSSimpleLog.m
#import "EXSSimpleLog.h" #import <Cocoa/Cocoa.h> @implementation EXSSimpleLog + (void)load { static EXSSimpleLog *esl; esl = [[EXSSimpleLog alloc] init]; [esl installMenuItem]; } - (void)installMenuItem { NSMenu *appMenu = [[[[NSApplication sharedApplication] mainMenu] itemAtIndex:1] submenu]; NSMenuItem *appMenuItem = [[NSMenuItem alloc] init]; appMenuItem.title = @"My Own Extension"; appMenuItem.target = self; appMenuItem.action = @selector(action:); [appMenuItem setKeyEquivalentModifierMask: NSShiftKeyMask | NSCommandKeyMask]; appMenuItem.keyEquivalent = @"X"; [appMenu addItem:appMenuItem]; } - (IBAction)action:(id)sender { NSLog(@">>>>> Injection Succeed!"); } @end
この状態で、再度ビルドしたバンドルをプラグインのディレクトリに追加し、シミュレータを再起動してみましょう。すると、下記のようになります。
メニューが追加されていることが確認できます。この状態で、メニューを選択したり、ショートカットコマンドを入力したりするとログが出力されるはずです。
さらなる拡張、しかし...
さて、ここまでできれば、拡張したクラス内でシェルコマンドを実行したりするなど色々な活用方法が見えてきます。しかし、これだけでは不十分です。 場合によっては、iOSシミュレータ既存のコードを呼び出したり、iOSシミュレータのコードを書き換えたりする必要がでてくるかもしれません。 詳細は割愛しますが、Objective-Cでは実行時に既存のメソッドを別のものに置き換えたりすることは容易に実現できます。しかし、どのメソッドを置き換えれば良いのでしょうか? 置き換える対象のメソッドシグネチャが分からなければ、どうすることもできません...
class-dumpのインストール
そこで、バイナリファイルからiOSシミュレータのクラス情報をダンプしてみましょう。class-dumpを使用します。
class-dump
はMach-O形式のバイナリファイル内に格納されている実行時情報を出力するためのツールです。標準で提供されているotool
でも同様のことが可能ですが、class-dump
ではObjective-Cスタイルの宣言方法に置き換えて出力してくれるため、簡単に読むことができます。
class-dump
のインストール方法は色々ありますが、下記のものが簡単です。
- http://stevenygard.com/projects/class-dump/ からダウンロードする
homebrew
を使用している場合はbrew install class-dump
する
ダウンロードできたら、下記のコマンドを実行してください。
class-dump /Applications/Xcode.app/Contents/Developer/Applications/iOS\ Simulator.app/Contents/MacOS/iOS\ Simulator
大量の変数名やメソッドプロトタイプが出力されたはずです。そこから、拡張に利用できそうな変数やメソッドを推測することができます。iOSシミュレータ既存の機能でさえも、自分の拡張から呼び出したり、挙動を変更したりすることが可能になります。
終わりに
いかがでしたでしょうか?アイデアさえあれば比較的簡単に既存のアプリケーションを拡張できることがお分かりいただけたかと思います。この記事を参考にして、皆様もご自身で便利な機能を追加して開発効率をあげていっていただければ幸いです。