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

iOSシミュレータをカスタマイズして、オリジナルの機能を追加しよう

モバイルファースト室で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を選択します。

20140910214630

  • 任意の情報を入力してください。Bundle Extensionを変更する必要はありません。

20140910214631

  • プロジェクトが作成されたら、ターゲットのInfo.plistを編集します。

20140910214632

  • Info.plistSIMBLTargetApplicationsというキーを追加します。それぞれ、拡張したいアプリケーションのバンドルID、拡張が実行可能なバージョンの最大値、最小値を記載します。

20140910214633

もし、特定のバージョンでしか拡張したくないという場合は、適宜バージョンの値を修正してください。

事前準備は以上で終了です。ただし、この状態のバンドルを組み込んでも何も起こらないので試しにログを出力できるようにしてみましょう。

ログを出力する

  • 自前のクラスを作成し、ロード時にログを出力するように実装します。

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シミュレータを起動し、ログを確認します(起動中の場合は再起動してください)。

20140910214634

これで、自前のクラスがロードされたことが確認できました。しかし、これだけではつまらないですね。

【注意】不具合が出た場合

拡張内容に不備があったりすると、当然アプリケーションがクラッシュしてしまいます。その場合は、EasySIMBLのアプリケーションを開いて作成したアプリケーション名、もしくは、Use SIMBLのところにあるチェックボックスをオフにしてください。

20140910214635

メニューへのアイテム追加

先ほど作成したクラスのロード時にメニューバーのアイテムを追加するコードを実行します。 <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

この状態で、再度ビルドしたバンドルをプラグインのディレクトリに追加し、シミュレータを再起動してみましょう。すると、下記のようになります。

20140910214636

メニューが追加されていることが確認できます。この状態で、メニューを選択したり、ショートカットコマンドを入力したりするとログが出力されるはずです。

さらなる拡張、しかし...

さて、ここまでできれば、拡張したクラス内でシェルコマンドを実行したりするなど色々な活用方法が見えてきます。しかし、これだけでは不十分です。 場合によっては、iOSシミュレータ既存のコードを呼び出したり、iOSシミュレータのコードを書き換えたりする必要がでてくるかもしれません。 詳細は割愛しますが、Objective-Cでは実行時に既存のメソッドを別のものに置き換えたりすることは容易に実現できます。しかし、どのメソッドを置き換えれば良いのでしょうか? 置き換える対象のメソッドシグネチャが分からなければ、どうすることもできません...

class-dumpのインストール

そこで、バイナリファイルからiOSシミュレータのクラス情報をダンプしてみましょう。class-dumpを使用します。 class-dumpMach-O形式のバイナリファイル内に格納されている実行時情報を出力するためのツールです。標準で提供されているotoolでも同様のことが可能ですが、class-dumpではObjective-Cスタイルの宣言方法に置き換えて出力してくれるため、簡単に読むことができます。 class-dumpのインストール方法は色々ありますが、下記のものが簡単です。

ダウンロードできたら、下記のコマンドを実行してください。

class-dump  /Applications/Xcode.app/Contents/Developer/Applications/iOS\ Simulator.app/Contents/MacOS/iOS\ Simulator

大量の変数名やメソッドプロトタイプが出力されたはずです。そこから、拡張に利用できそうな変数やメソッドを推測することができます。iOSシミュレータ既存の機能でさえも、自分の拡張から呼び出したり、挙動を変更したりすることが可能になります。


終わりに

いかがでしたでしょうか?アイデアさえあれば比較的簡単に既存のアプリケーションを拡張できることがお分かりいただけたかと思います。この記事を参考にして、皆様もご自身で便利な機能を追加して開発効率をあげていっていただければ幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/