EDIT MODE

モバイルアプリ開発に関することを書いています

"WebエンジニアのためのiOSデバッグ速習会"で学んだことのまとめ

WebエンジニアのためのiOSデバッグ速習会@Wantedly に参加してきたので、内容を忘れないようまとめました。

wantedly.connpass.com

参加者は事前に Xcode を自分の PC にインストールしておき、講師の人が説明した後に実際に手を動かして確認していく、というスタイルでした。

また速習会中は、Sync を使って参加者同士がコミュニケーションを取るというのは、独特で面白かったです。

講師は @hedjirog さんでした。

やったこと

講師の方が資料を用意してくださったので、それに沿って以下のようなことをやりました。

テーマは iOS アプリのデバッグです。 使用したのは Xcode 7.1.1 です。

  1. 題材アプリ Artsy のインストール
  2. ブレークポイントを配置して任意のコードで実行を止める
  3. ビューデバッガでビュー構造からクラスなどを特定する
  4. chisel を使って LLDB コマンドでデバッグする
  5. ExceptionBreakpoint でアプリのクラッシュをデバッグする

おまけ: PonyDebugger を利用して通信内容を確認する

題材アプリ Artsy のインストール

デバッグ対象とするアプリは OSS で公開されている Artsy を使いました。 README にも書かれている通り、以下の手順でインストールできます。(結構時間かかります。)

git clone https://github.com/artsy/eigen.git
cd eigen
bundle install
make oss
bundle exec pod install
open Artsy.xcworkspace

注意点として普段 Ruby を使っていない PC だと、bundler がインストールされていないかもしれません。 そのときは、以下のコマンドでインストールされているか確認し、

gem contents bundler

見つからなければ、以下のコマンドでインストールします。

gem install bundler

パーミッションで怒られたら管理者権限で実行します。

sudo gem install bundler

Breakpoint を配置して任意のコードで実行を止める

普段何かしらの IDE を使っていれば、おなじみのデバッグ方法ですね。

Breakpoin の配置は、行番号のあたりをクリックするだけです。セットした Breakpoint は、左側の Breakpoint Navigator にリスト化されていきます。

f:id:androhi:20151127170505p:plain

さらに Xcode の場合は、セットした Breakpoint に任意のコマンド実行などを追加出来るとのこと。

これを使えば Breakpoint に到達したときに、音を鳴らすとかも簡単に出来るようです。

f:id:androhi:20151127170537p:plain

Breakpoint の削除は、Breakpoint をドラッグして離せば消えます。

ビューデバッガでビュー構造からクラスなどを特定する

ビューデバッグを開始(アプリ実行中にカルーセルみたいなボタンをクリック)すると、IDE 内にビュー構造を表したオブジェクトが立体表示出来ます。 (iOS エンジニアの人に聞いたら、割りと最近追加されたものらしいです。)

f:id:androhi:20151127170818p:plain

f:id:androhi:20151127170608p:plain

UI をクリックすると、その UI が何のクラスなのか分かります。そして、右側にはそのコンポーネントの詳細な情報が、確認出来るようになっています。

f:id:androhi:20151127170801p:plain

似たもので Reveal という、有償のビュー構造解析ツールがあるようです。(そっくりですね。こっちの方がサクサク動くらしい。)

AndroidStudio にも、この機能欲しいですね。

chisel を使って LLDB コマンドでデバッグする

chisel というデバッグツールがあるとのことです。 facebook 社が OSS として公開しています。

Chisel is a collection of LLDB commands to assist debugging iOS apps.

これを活用したデバッグ方法です。(*1

chisel は Homebrew でインストールできます。

brew update
brew install chisel

chisel を使うには、設定ファイルが必要です。ホームディレクトリ下に、 .lldbinit というファイルが無ければ作成します。 .lldbinit ファイルには、以下を記述しておきます。

command script import /path/to/fblldb.py

/path/to の部分は環境によって変わります。自分の環境下でのパスを確認するには、以下で出力されます。

brew info chisel

chisel を使う手順は、アプリを実行したら || ボタンで止めて、コンソールでコマンドを実行します。 コマンドの説明は chisel の README に載ってます。

今回試したのは、以下のコマンドです。

pviews

コマンドを叩くと、以下の様なテキストベースのビュー構造を返してくれます。 (長いので途中省略しています。)

<ARWindow: 0x7fdf40c1e960; baseClass = UIWindow; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x7fdf40c20e30>; layer = <UIWindowLayer: 0x7fdf40c1c760>>
   | ARTopMenuViewController:0x7fdf40d17cb0 1c.[ARNavigationController 0x7fdf41890000: ] <UIView: 0x7fdf40e3af30; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x7fdf40cc4ae0>; layer = <CALayer: 0x7fdf40e78c70>>
   |    | <UIView: 0x7fdf40ebb230; frame = (0 621; 375 46); layer = <CALayer: 0x7fdf40ebb3a0>>
   |    |    | <ARNavigationTabButton: 0x7fdf40eaed20; baseClass = UIButton; frame = (0 0.5; 46 45.5); opaque = NO; layer = <CALayer: 0x7fdf40ea9e30>>
   ...
   |    |    |    |    | <UIImageView: 0x7fdf40c1f910; frame = (10 10; 20 20); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7fdf40c55750>>
   |    |    |    |    | XX (<UIButtonLabel: 0x7fdf40d1f100; frame = (20 10.5; 0 19); hidden = YES; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x7fdf40d10790>>)
   |    | XX (<NPKeyboardLayoutGuide: 0x7fdf40ec8e90; frame = (0 667; 0 0); hidden = YES; layer = <CALayer: 0x7fdf40ec7a60>>)

f:id:androhi:20151127170939p:plain

taplog

コマンドを叩くと、シミュレーター(もしくは実機)がアクティブになります。

この状態でアプリ上のどこかをタップすると、そのイベントの開始位置で再び止まり、エディタには該当のコードを表示してくれます。

f:id:androhi:20151127170956p:plain

border

コマンドの後に、UI コンポーネントのアドレスを渡してあげると、シミュレーター(もしくは実機)の該当する UI 部分が赤枠で囲われます。

f:id:androhi:20151127171016p:plain

ExceptionBreakpoint でアプリのクラッシュをデバッグする

アプリがクラッシュしたときに、以下のように通常コンソールに出力される情報だけでは、発生元を特定するのが大変です。

f:id:androhi:20151127171042p:plain

そういうときは ExceptionBreakpoint を使うと、例外が発生した箇所で実行を止めてくれるとのことです。

使い方は、左側の Breakpoint Navigator に切り替え、アプリを実行させます。 その状態で、左下のプラスボタンから「Add Exception Breakpoint...」をクリックし、ExceptionBreakpoint を設定します。

f:id:androhi:20151127171101p:plain

実際にクラッシュを起こすと、以下のように例外が発生した場所のコードが表示され、そこで実行が止まります。

f:id:androhi:20151127171115p:plain

PonyDebuggerを利用して通信内容を確認する

Square 社が OSS で公開している PonyDebugger という、iOS 用の通信デバッグツールとのことです。

Remote network and data debugging for your native iOS app using Chrome Developer Tools

これは時間が足りず試せませんでした。 GitHub の README を見れば分かりますが、ブラウザで通信のリクエストやレスポンスが確認出来るようです。 数行アプリにもコードを書かないと行けないので、導入するときは一手間必要そうですね。

(ただ Android アプリ開発も行う人とかは特に、charles とか wireshark とかのツールで、両方ともカバーした方が便利かなと思いました。)

まとめ

  • 特定のコードでアプリを止めたい時は Breakpoint を設定する
  • Breakpoint はアクションを追加できるので必要に応じて活用する
  • UI まわりはビューデバッガを使うと便利
  • より細かいデバッグには chisel を活用すると便利
  • クラッシュ発生源の特定には ExceptionBreakpoint を使う

個人的な感想として僕は普段 Android アプリの開発がメインで、IDE は AndroidStudio を使うことが多いのですが、View まわりのデバッグXcode の方が良く出来てるなと感じました。

余談

懇親会中 wantedly 社の方にオフィスを案内してもらったのですが、とにかくお洒落すぎて素敵なオフィスでした。会議室の名前が全部ジョジョのスタンド名で、とにかくかっこいいです。

それとこちらで販売してる wantedly パーカーいいですねっていう話しになったのですが、高いだけあって物がいいらしいので気になってきました。

*1:なお chisel については、こちらのブログが詳しいようです。 yidev@恵比寿勉強会 で chisel について発表してきた (動画付き)

API level 23 未満を考慮した指紋認証 API の実装

前回、指紋認証 API のサンプルコードをざっと見てみたのですが、そのときに FingerprintManagerCompat クラスが Support Library v4 にあるのを見つけました。

androhi.hatenablog.com

このクラスを使えば Android 6.0 (API level 23 未満) も含め、スッキリと実装できるんじゃないかなと思い調べてみました。

FingerprintManager のインスタンス取得

前回のGoogleのサンプルコードでは、以下のようなコードでインスタンスを取得していました。

FingerprintManager manager = context.getSystemService(FingerprintManager.class);

ところが以下の2点で API level 23 未満では、getSystemService() 経由だと使えません。

  1. Context.getSystemService(Class<T>)API level 23から追加されたメソッド
  2. Context.getSystemService(String) だと引数のサービス名に Fingerprint は無い

そのため FingerprintManagerCompat では、別途 Context を使った方法が用意されていました。

// contextはActivityなど
FingerprintManagerCompat managerCompat = FingerprintManagerCompat.from(context);

FingerprintManagerCompat のソースコードを辿って行くと、内部で Build.VERSION.SDK_INT をチェックしていて、23 以上だと Api23FingerprintManagerCompatImpl 内部クラスを、23 未満だと LegacyFingerprintManagerCompatImpl 内部クラスを、それぞれインスタンス化していました。

指紋センサーの判定など

FingerprintManager では、端末に指紋センサーのハードウェアがあるか?や、指紋の登録があるか?などを判定するメソッドを提供しています。

まったく同じメソッドが、FingerprintManagerCompat クラスにも実装されていますが、どのような挙動になるのか確認してみました。

結論から書くと、API level 23 以上だとちゃんと FingerprintManager 経由で判定し、23 未満だと固定で false を返していました。

API level 23 未満

    private static class LegacyFingerprintManagerCompatImpl implements FingerprintManagerCompatImpl {

        public LegacyFingerprintManagerCompatImpl() {
        }

        @Override
        public boolean hasEnrolledFingerprints(Context context) {
            return false;
        }

        @Override
        public boolean isHardwareDetected(Context context) {
            return false;
        }

        ...
        }
    }

API level 23 以上

    private static class Api23FingerprintManagerCompatImpl implements FingerprintManagerCompatImpl {

        public Api23FingerprintManagerCompatImpl() {
        }

        @Override
        public boolean hasEnrolledFingerprints(Context context) {
            return FingerprintManagerCompatApi23.hasEnrolledFingerprints(context);
        }

        @Override
        public boolean isHardwareDetected(Context context) {
            return FingerprintManagerCompatApi23.isHardwareDetected(context);
        }
        
        ...
public final class FingerprintManagerCompatApi23 {

    private static FingerprintManager getFingerprintManager(Context ctx) {
        return ctx.getSystemService(FingerprintManager.class);
    }

    public static boolean hasEnrolledFingerprints(Context context) {
        return getFingerprintManager(context).hasEnrolledFingerprints();
    }

    public static boolean isHardwareDetected(Context context) {
        return getFingerprintManager(context).isHardwareDetected();
    }
    
    ...

分かりやすくいいですね。

指紋認証

実際に指紋センサーから指紋を読取るときは、 FingerprintManager#authenticate(CryptoObject, CancellationSignal, int, AuthenticationCallback, Handler) メソッドを使ってましたが、これも先ほどと同じ形でした。

API level 23 以上では、FingerprintManager 経由で処理し、23 未満では何もしません。

API level 23 未満

    private static class LegacyFingerprintManagerCompatImpl
            implements FingerprintManagerCompatImpl {
        ...

        @Override
        public void authenticate(Context context, CryptoObject crypto, int flags,
                CancellationSignal cancel, AuthenticationCallback callback, Handler handler) {
            // TODO: Figure out behavior when there is no fingerprint hardware available
        }
    }

API level 23 以上

    private static class Api23FingerprintManagerCompatImpl implements FingerprintManagerCompatImpl {
        ...
        @Override
        public void authenticate(Context context, CryptoObject crypto, int flags,
                CancellationSignal cancel, AuthenticationCallback callback, Handler handler) {
            FingerprintManagerCompatApi23.authenticate(context, wrapCryptoObject(crypto), flags,
                    cancel != null ? cancel.getCancellationSignalObject() : null,
                    wrapCallback(callback), handler);
        }
public final class FingerprintManagerCompatApi23 {
    ...
    public static void authenticate(Context context, CryptoObject crypto, int flags, Object cancel,
            AuthenticationCallback callback, Handler handler) {
        getFingerprintManager(context).authenticate(wrapCryptoObject(crypto),
                (android.os.CancellationSignal) cancel, flags,
                wrapCallback(callback), handler);
    }

まとめ

当分 API level 23 以上でアプリを開発出来る状況は来ないので、開発しているアプリに指紋認証を導入したいときは、この FingerprintManagerCompat を使うのが良さそうです。

今まで割と自力で API level 判定して処理を分けていたようなところも、xxxCompat クラスがいい感じに内部で切り替えてくれるケースが増えてきた気がします。

新しい機能が出てきたら、まずは xxxCompat クラスも一緒に探すと、既存アプリへの導入が楽になりそうですね。

Android 6.0 の指紋認証 API を触ってみた感想文

10/26(月)に Android Developers Blog で、New in Android Sample: Authenticating to remote servers using the Fingerprint API という記事が公開されました。

さらに 10/19(月)にNexus5Xが発送されたにも関わらず全然触ることが出来なかったのですが...

今日やっとセットアップを終えたということもあり「思ったより指紋認証よさそう」って高まってるので、実際にサンプルコードを見て触ってみました。

指紋認証 API

先程のブログ記事によると、Android 指紋認証 API は安全に利用することが出来るよう、デバイスのセキュアなハードウェアに含まれているとのことです。

指紋認証 API の利用の仕方としては2種類の方法を、アプリケーションのニーズに合う方を選ぶのが望ましいようです。

例えば、オフラインのファイルやデータベースにアクセスするときは、パスワードと同じような Symmetric Keys を使い、ネットワークへのログインやオンライン取引のようなケースでは、公開鍵と秘密鍵で構成される Key Pair による Asymmetric Keys を使う、といった形です。

サンプルコード

Google が公開している指紋認証機能に関するサンプルコードは、AsymmetricFingerprintDialogFingerPrintDialog の2つあります。

f:id:androhi:20151028030749p:plain

AsymmetricFingerprintDialog の方は、名前の通りペアの鍵を用いてクライアント/サーバー間で認証を行うサンプルです。今回新たに追加されたサンプルは、こちらのようです。

FingerprintDialog の方は、Android 6.0 が public preview のときに公開されたサンプルで、とりあえずどうやって指紋認証をアプリに実装するかを示したものです。

両者のサンプルコードを比較してみると、違うのはサーバー側の処理のフェイクを実装している部分と、鍵の生成に KeyGenerator クラス使ってるか KeyPairGenerator クラスを使ってるかっていうあたりでした。

f:id:androhi:20151028030950p:plain f:id:androhi:20151028031002p:plain

認証フロー的な説明は、公式ブログが詳しいのでそっちを読めばいいとして、もう少し細かいとこを見てみました。

指紋認証の可否判定

サンプルコードを見てみると、2つのチェックを行っていました。1つは画面ロックに、スワイプ/パターン/PIN/パスワードのいづれかが設定されているかどうか、もう1つは端末に指紋が登録されているかどうかです。

画面ロックの有無をチェックするコード

getSystemService(KeyguardManager.class).isKeyguardSecure();

設定 > セキュリティ > 画面ロック の設定をチェックできる

指紋登録の有無をチェックするコード

getSystemService(FingerprintManager.class).hasEnrolledFingerprints();

設定 > セキュリティ > Nexus Imprint の設定をチェックできる(Nexus5Xの場合)

f:id:androhi:20151028030837p:plain

指紋の読み取り

サンプルアプリを動かしてみると、下図のような指紋センサーにタッチさせるダイアログが出るのですが、このダイアログが表示されてるときに指紋センサーが反応するようです。

f:id:androhi:20151028030912p:plain

どういう風に実装されてるか見てみました。(公式ブログにも載ってますが...)

細かい部分は割愛しますが、以下のコードで実現していました。

指紋の読み取りを開始するコード

getSystemService(FingerprintManager.class).authenticate(cryptoObject, cancellationSignal, 0, this, null);

FingerprintManager#authenticate(FingerprintManager.CryptoObject, CancellationSignal, int, FingerprintManager.AuthenticationCallback, Handler) メソッドで読み取り開始〜コールバック受け取りまで出来るようです。

一応 Support Library にも FingerprintManagerCompat クラスが用意されいて、API level 23 より前のデバイスで指紋機能なしとして振る舞ってくれるらしいです。試してません。

あとは FingerprintManager クラスには、ハードウェアとして指紋機能が有効かチェックする isHardwareDetected() というメソッドもありました。

まとめ

ざっくり見た感じでは、Asymmetoric Keys のやり方でもサンプルコードのおかげで、割とすぐに実装できそうな気がしました。

アプリのログインが指紋認証で出来るのは、すごく楽だと思うので積極的に取り入れていきたい気持ちになりました。

エンジニア向けの Sketch3 入門を DevDays と potatotips で発表しました

少し前に、仕事で Android アプリのデザインリニューアルを行う際、Sketch3 を使ってデザイナーさんと UI デザインを共有する機会がありました。

そのことがきっかけとなり、自分でも Sketch3 を使えるようになりたいなと思い、ちょっとずつ練習していった内容などを、タイトルにある2つのイベントで発表させて頂きました。

Stack Overflow DevDays

みんな大好き Stack Overflow が、日本で初めて主催したイベントです。 個人開発者向けとしては珍しい平日開催のイベントでした。

イベントページでは最終的に149名の申し込みがあったようですが、(数えていませんが)当日はその半分くらいな印象でした。 ちょっとギリギリまでイベントの内容も分からなかったので、予定に組み込むのが難しかったのかなと、個人的には思います。

イベント全体の振り返りは、以下のブログが詳しいので、ここでは自分の発表について触れるだけにします。

発表資料

15分の発表枠を頂いていたのですが、Sketch3 の良さが伝わるようデモをメインで行いました。 Shapeを組み合わせて、シンプルなアイコンなら割と簡単に出来るということと、画像の書き出しがとても便利だということをデモしました。

ちなみにこのとき手順メモを手元で見るために、モニタの設定をミラーリングにしないでプロジェクタを見つつデモしたのですが、ものすごくやりづらかったです。デモはやっぱりミラーリングにして、手元を見ながらやるべきですね...。

potatotips #22

AndroidiOS の Tips 共有にフォーカスした勉強会です。 毎月1回のペースで、いろいろな企業が主催する形で開催されています。今回はメルカリさん主催で行われました。 大人気のイベントで、発表枠もオーディエンス枠も抽選を勝ち抜かないと参加出来ません。

この勉強会は、参加枠に「ブログまとめ枠」という形式があり、イベントについてまとめる専任者がいらっしゃるので、こちらも全体については、以下が詳しいのでそちらをご覧ください。

発表資料

前述の DevDays と日程が近く、せっかくなので DevDays での発表内容に、もっとエンジニア向けの内容を追加して資料を再構築したのが、この資料です。 本当はプラグインについても触れたいなと思ったのですが、そもそも DevDays に参加したという potatotips 参加者が少なかったら、Sketch3 についても改めて説明を入れたかったので、プラグインに触れるのは止めました。

ということで、実際に発表の冒頭で DevDays に参加した人に挙手してもらったら1名のみでしたので、プラグインに触れなくても時間ギリギリの発表になってしまいました。

まとめ

Mac OS 専用ソフトではあるものの、モバイルやWebの仕事でデザイナーさんと一緒に作業をする機会があれば、Sketch3 は絶対にエンジニアも使うべきです。

それ以外でもプライベートで何か作っているのであれば、Sketch3 は購入を検討するに値する価値があると思います。 出来れば OSS ライセンス版みたいのが、あると嬉しいんですけどね。

最後に、僕もまだまだ Sketch3 使い始めたばかりなので、ノウハウに飢えてます。 もし Sketch3 の勉強会情報とか知ってる方は、@androhi にメンションいただけると泣いて喜びます。

IntelliJ Plugin 勉強会を開催しました

Twitter で Plugin 開発の辛みなどを書いていたら、@konifar さんや @shiraj_i さんと話が合い、勢いで勉強会を企画することになったのですが、先日無事に開催することが出来ました。(残念ながら @konifar さんは当日参加出来なかったのですが...)

イベントページは、こんな感じでした。 IntelliJ Plugin 勉強会

会場は 株式会社Gunosy さんに提供頂き、おいしいエスプレッソとクリームどら焼きで楽しく過ごせました。

f:id:androhi:20150813230542p:plain

当日の人数は全部で7人で、自己紹介しつつ Plugin 開発の経験を確認したところ、以下のような内訳でした。

  • 経験有り(かつ Plugin Repository への公開も有り):3人
  • 少し経験有り:1人
  • 未経験(もしくは始めたばかり):3人

思ったよりきれいに分かれたので、みんなが「知りたいこと」を挙げては誰かが答えるという感じで、かなりゆるく進めていきました。

そのときのやり取りは、GitHub Pages にまとめて公開しています。

いくつか盛り上がったポイントがあったのですが、個人的なハイライトは Plugin のデバッグ実行に Android Studio が使える方法が分かった点です。本当にこれを知ることが出来て良かった!

今回の参加者にサムライズムの @yusuke さんがいらっしゃって、誰かが「 AndroidStudio でデバッグ出来たらいいのに」と言ったら、さらっと「たぶん出来るよ」と。さすがプロだと思いました。

そしてステッカーまで頂くことができました。

f:id:androhi:20150813232959p:plain

いろいろと知見を共有することが出来たし、楽しい時間を作る事が出来たので、また来月もどこかで第2回を開催したいなと思っています。興味持っていただけた方は、ぜひ参加してみてください。

最後に。

Plugin 開発もライブラリ開発と同じように自分で欲しい機能を作ることができるし、それが誰かの役に立つかもしれないという点で、とても楽しいです。

技術的には Java Swing がベースなので、割と敷居は低いと思います。ただドキュメントを始めとした情報量が少ないのが玉にキズかなと思います。

今回の勉強会を通して、そのあたりを和らげるような勉強会になっていくといいなと思いました。

ToolWindow タイプの Intellij Plugin 作成の Tips

初めて Intellij Plugin を作ってみました。

どんな Plugin を作ったかは、前の記事に書いたので興味あるかたは読んでみてください。 また作成した Plugin のコードGitHub で公開しています。

Intellij Plugin を作ってみて感じこと

ちょっとググると分かりますが、言語を限定しなくても関連する情報が少ないです。

後述するブログやサイトと、その中で紹介されてるブログやサイトでほぼめぼしい情報が底をつきます。

あとはもう、OSS として公開されてる Intellij Plugin のコードを参考にするか、Intellij IDEA CE のコードを参考にするしかありませんでした。

とはいえ、メインの技術は Java Swing という枯れた技術なので、そこは情報量豊富でした。 そのため調べながら作るとしたら、以下のような組み合わせを繰り返す感じになると思います。

  • Plugin 作成の流れや基本 >> 公式サイトや Plugin 作成について書かれたブログを見る
  • UI まわりの基本 >> Swing 関連のサイトやブログを見る
  • ピンポイントでの実装方法 >> OSS の Plugin のコードを読む
  • 上記でも解決できない問題 >> Intellij IDEA CE のコードを読む

Plugin のタイプ

AndroidStudio や Intellij IDEA などを実際に使ってる人には説明不要かと思いますが、Plugin をざっくり3タイプに分けるとこんな感じだと思ってます。

  1. ほぼ UI を持たないサポート系
  2. 特定のタイミングで機能するダイアログ系
  3. 常駐?して機能するツール

1 は CodeGenerator とか Editor 系の Plugin を指してます。

2 はそのまんまですが Importer 系とか FileGenerator とかいろいろありますね。

3 は ProjectTree とか BuildTool 系で、いわゆる画面の左端や右端、もしくは下部に表示/非表示を切り替えられるタイプの Plugin です。

ToolWindow の作成

Plugin プロジェクトの作成方法などは、後述するサイトなどで詳しく紹介されているのでここでは触れません。 まずは ToolWindow を表示するために必要な手順を書いていこうと思います。

1. ToolWindow にコンテンツをセットするクラスを作る

以下のように ToolWindow に表示する UI をセットするためのクラスを追加します。ポイントは、ToolWindowFactory インターフェースを実装する点です。

public class AndroidDrawableViewerToolWindowFactory implements ToolWindowFactory {

    @Override
    public void createToolWindowContent(Project project, ToolWindow toolWindow) {
        JLabel label = new JLabel("Hello World");

        final ContentManager contentManager = toolWindow.getContentManager();
        final Content content = contentManager.getFactory().createContent(label, null, false);
        contentManager.addContent(content);
    }
}

createContent(JComponent, String, boolean) メソッドで、第一引数に Swing の UIConponent をセットでき、主にそれが ToolWindow 内に表示されます。

2. plugin.xml に ToolWindow を生成する宣言を記述する

作成する Plugin の初期化時に、1 で作成した処理が動くように宣言する必要があります。 記述するのは、 <extension> タグの中です。

  <extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <toolWindow anchor="right" canCloseContents="true" id="DrawableViewer"
                factoryClass="com.androhi.androiddrawableviewer.AndroidDrawableViewerToolWindowFactory" />
  </extensions>

各属性について詳しくは公式サイトの ここ に書かれていますが、補足すると factoryClass の指定は完全修飾名(パッケージ名込み)で書かないとダメでした。

3. ToolWindow を表示する

1 と 2 を作成して実行すると、1 で createContent(JComponent, String, boolean) メソッドに渡した JComponent(ここだと"Hello World"と表示するラベル)が表示される ToolWindow を表示できると思います。

ToolWindow の表示切り替えは、メニューから行う方法と ToolButtons で行う方法があります。

メニューでの切り替え

View > ToolWindowsにPlugin名

f:id:androhi:20150723231446p:plain

ToolButtons での切り替え

View > Tool Buttons がオンの状態で plugin.xml で指定した anchor の場所のボタン(ここではWindow右端)

f:id:androhi:20150723231510p:plain

ToolWindow に ToolBar を設置する

下図のように ToolWindow 内には、何かしらのボタン郡があるものが多いと思います。この領域は ToolBar と呼ばれるものらしく、コンテンツ部分とは独立して配置されるようです。

f:id:androhi:20150723233841p:plain

このパターンの ToolWindow には、SimpleToolWindowPanel という JPanel のサブクラスを使うのが一般的らしいです。(主に Intellij CE のコードを見て知りました。)

public class DrawableViewer extends SimpleToolWindowPanel {

    public DrawableViewer(final Project project) {
        super(true, true);
        this.project = project;

        setToolbar(createToolbarPanel());
        setContent(createContentPanel());
    }

    private JComponent createToolbarPanel() {
        final DefaultActionGroup actionGroup = new DefaultActionGroup();
        actionGroup.add(new AnAction());
        final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("AndroidDrawableViewer", actionGroup, true);
        return actionToolbar.getComponent();
    }

    private JScrollPane createContentPanel() {
        String projectPath = project.getBasePath();
        ...
    }
    ....
}

上のコードで、setToolBar(JComponent) メソッドで ToolBar 領域の UI を、setContent(JComponent) メソッドで Content 領域の UI をそれぞれセットする仕組みになっています。

setContent() の方は、割といろいろな JComponent がセットできますが、setToolBar() の方は AnAction クラスもしくはサブクラス(要はアイコンボタン)のグループを内包した ActionToolbar の Component をセットするのがセオリーなようです。

ここでは見やすいように new Action() をセットしていますが、実際には固有のアイコンや処理を定義するので、AnAction のサブクラスのインスタンスをセットします。そのあたりは、GitHubのコード で全体を見ていただいた方が分かり易いかもしれません。

ToolWindow の表示更新

これがかなり悩みました。

以下に書いた内容も正しいか自信がありません。もっとよい方法をご存知の方は、ぜひ教えてください。

やりたかったことは、Action があったときに ToolWindow のコンテンツを新しいデータを反映した後、再描画するという内容です。

デコンパイルした Intellij 側のコード見たりいろいろと試したのですが再描画できず、結局コンテンツを再生成してセットし直すことで、期待した動きになりました。

以下は先程の ToolBar のボタンをクリックしたときのアクションクラスです。

public class EditTargetResDirAction extends AnAction {

    public EditTargetResDirAction() {
        super("Edit directory", "Edit target resource directory", AllIcons.General.Settings);
    }

    @Override
    public void actionPerformed(AnActionEvent anActionEvent) {
        Project project = anActionEvent.getProject();
        ...
        final VirtualFile file = FileChooser.chooseFile(descriptor, project, selectDir);
        if (file != null) {
            ...
            resetContent(project);
        }
    }

    private void resetContent(Project project) {
        DrawableViewer drawableViewer = new DrawableViewer(project);
        ContentManager contentManager = ToolWindowManager.getInstance(project)
                .getToolWindow(DrawableViewer.TOOL_WINDOW_ID).getContentManager();
        Content content = contentManager.getFactory().createContent(drawableViewer, null, false);

        contentManager.removeAllContents(true);
        contentManager.addContent(content);
    }

getToolWindow(String) メソッドに plugin.xml に記述した ToolWindow の ID を渡すと同じ ToolWindow が取得できるので、それに対して再生成した JComponent インスタンス(=drawableViewer)をセットし直しています。 セットするときは、Add するのでその前に古い方は Remove しています。

まとめ

ToolWindow まわりは Swing では無く Intellij の仕組みなので、特に公開されてる情報が少なく途中で心が折れかけました。

でも ToolWindow で実現する Plugin は、そのコンテンツを見ながらコードを書けたりするので、いろいろと楽しい Plugin が作れるんじゃないかなーと感じました。

加えて Intellij IDEA 14 から実装されたデコンパイル機能が、ものすごく重宝しました。これのおかげで公開まで辿りつけた気がします。

そしてめでたく JetBrains Plugin Repository に公開されたので、もし興味ある方は感想などいただけると嬉しいです。

AndroidDrawableViewer f:id:androhi:20150723230925p:plain

参考にしたサイト

Android Drawable Viewer という Intellij Plugin を作ってます

Pluginを作り始めた経緯など

Androidアプリ開発をしていて、今のプロジェクトのdrawable配下の画像を一覧で確認したいなと思い、そういったIntellij IDEA Pluginを探したのですが見つからなかったので自作することにしました。

プロジェクトも比較的大きなものだと、いろいろな画像ファイルをアプリにバンドルします。特にアイコン関連が多くなりがちです。 さらにAndroidでは様々なディスプレイ解像度に対応するため、同じ画像を2パターン、3パターン、あるいはそれ以上必要になってくるかと思います。

そのあたりの管理うんぬんの前に、画像のプレビュー/ファイル名/対応する解像度など、全体を把握したいことが多々ありました。今作っているのは、それらを解決するためのPluginです。

どんなPluginか?

Androidプロジェクトのdrawableフォルダに入っている画像ファイルの情報を、リスト化するPluginです。まだbeta版ですが、ソースGitHubに上げています。

実行サンプルは、こんな感じです。

f:id:androhi:20150716024808p:plain

今後追加したい機能

まだまだ機能やUIは改善すべきとこが多く、継続してアップデートしたいと思っています。 直近で検討している改善点は、以下のようなものです。

  • リソースディレクトリの設定
  • 画像の詳細情報(サイズや色)の表示
  • 任意のdrawableフォルダの絞り込み表示

このあたりが実装できたら、Jetbrains Plugin Repositoryにも公開したいなと考えています。

Plugin作成で参考にしたサイト

Intellij IDEAを触ったことがあれば、実装自体はそれほど難しくない気がします。個人的にはSwingのUI Componentsを初めて触ったので、その使い方に四苦八苦してます。

以下、メインで参考にさせて頂いたサイトなどです。執筆者や開発者の方々、ありがとうございます。