ごんれのラボ

iOS、Android、Adobe系ソフトの自動化スクリプトのことを書き連ねています。

Adobe CEPからQiitaのAPIにリクエストして、レスポンスのJSONをパネルに表示する(手抜き版)

概要

CEPから簡単にHTTPリクエストができると聞いたので、試しにQiitaのAPIにリクエストして、レスポンスのJSONをパネル上のテキストエリアに表示するサンプルを作った。

この記事でやること

  • CEPからHTTPリクエストを行って、レスポンスをCEP上に表示する

この記事でやらないこと

  • ExtendScriptとの連携

想定する環境

  • macOS 10.13.4
  • InDesign CC 2017

利用するAPI

最新の投稿を返却するAPI(https://qiita.com/api/v2/items)を利用する。
レスポンスはJSONで返ってくる。

CEPの実装をする

テンプレートプロジェクトを作成する

前回記事を参考に、テンプレートプロジェクトを作成する

manifest.xml を修正する

InDesign CC 2017で読み込めるようにする

テンプレートプロジェクトではInDesignの読み込みが無効化されているので、有効化して、かつバージョンにも対応する。

変更前

<!-- <Host Name="IDSN" Version="[10.0,11.9]" /> -->

変更後

<Host Name="IDSN" Version="[10.0,12.9]" />

Node.jsを有効にする

テンプレートプロジェクトではNode.jsが無効化されているので、有効化する。

変更前

<Resources>
<MainPath>./index.html</MainPath>
<ScriptPath>./jsx/hostscript.jsx</ScriptPath>
</Resources>

変更後

<Resources>
<MainPath>./index.html</MainPath>
<ScriptPath>./jsx/hostscript.jsx</ScriptPath>
<!-- ここから追加 -->
<CEFCommandLine>
    <Parameter>--enable-nodejs</Parameter>
</CEFCommandLine>
<!-- ここまで -->
</Resources>

index.html を修正する

リクエストを送信するボタンと、結果を表示するテキストエリアを用意する。

index.html

<!doctype html>
<html>
<head>
<meta charset="utf-8">

<link rel="stylesheet" href="css/topcoat-desktop-dark.min.css"/>
<link  id="hostStyle" rel="stylesheet" href="css/styles.css"/>

<title></title>
</head>

<body class="hostElt">
    <div id="content">
        <div>
            <button id="btn_send_request" class="topcoat-button--large hostFontSize">Send request</button>
        </div>
        <div>
            <textarea id="output_area">result</textarea>
        </div>
    </div>

    <script src="js/libs/CSInterface.js"></script>
    <script src="js/libs/jquery-2.0.2.min.js"></script>

    <script src="js/themeManager.js"></script>
    <script src="js/main.js"></script>


</body>
</html>

main.js を修正する

CEPからHTTPリクエストを送信するために、request モジュールを利用したかったんだけど、最新バージョンでは依存モジュールがCEP内蔵のNode.jsのバージョンでは利用できず、標準モジュールである http または https モジュールを利用した。
どちらのモジュールを利用するかは、リクエストする先のプロトコルにあったものを選択する。
例えば、http://www.macneko.com であれば http モジュール、https://www.google.co.jp であれば https モジュールを利用する。

main.js

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global $, window, location, CSInterface, SystemPath, themeManager*/

(function() {
    'use strict';

    var outputArea = $('#output_area');

    var isNodeJSEnabled = function () {
        if (typeof(require) !== 'undefined') {
            outputArea.val("Node.js is enabled");
        } else {
            outputArea.val("Node.js is disabled");
        }
    };

    var sendRequest = function () {
        // Node.jsが有効になっていないとエラーになるので、require()がエラーになるため、try〜catchする
        try {
            var https = require('https');
        } catch(err) {
            outputArea.val(err);
            return;
        }

        // QiitaのAPI
        var url = 'https://qiita.com/api/v2/items';

        // APIにリクエスト開始
        https.get(url, function(res) {
            var body = '';
            res.setEncoding('utf8');

            res.on('data', function (chunk) {
                body += chunk;
            });

            // リクエスト処理完了
            res.on('end', function (res) {
                // レスポンスのJSONをStringにして、テキストエリアに出力
                var result = JSON.parse(body);
                outputArea.val(JSON.stringify(result));
            });

        }).on('error', function (e) {
            outputArea.val(e.message); //エラー時
        });

    };

    // ボタンクリック
    $(function() {
        $('#btn_send_request').click(sendRequest);
    });
})();

動作確認

f:id:macneko-ayu:20180531174512g:plain
デモ

ソースコード

https://github.com/macneko-ayu/CEP-Extensions-Sample/tree/master/com.macneko.HttpClient

まとめ

Node.jsがデフォルトで無効化されていることに気づくまでに時間がかかった。
次はCEP上でレスポンスのJSONをテーブルで表示させるものを作ろうと思っている。

MacにAdobe CEPの開発環境を構築する

概要

Adobeアプリケーションのエクステンション(CEP:Common Extensibility Platform)をMacで開発するための開発環境を構築する。

この記事でやること

  • 開発環境について
  • 開発環境の構築について
  • サンプルエクステンションの作成・起動について

この記事でやらないこと

  • エクステンションの開発について

想定する環境

  • macOS 10.13.4
  • Adobe Creative Cloudの2015以降のアプリケーションがインストール済

開発環境について

開発環境の選定

CEPは、HTML5+CSS+JavaScriptで構成されているので、開発環境として専用のIDEがあるわけではなく、テキストが編集できるアプリケーションがあればすぐに開発に着手することができる。
標準のテキストエディタでも開発しようと思えばできなくはない。
とはいえ、便利なものを使ったほうが効率がいいので、私はBracketsを利用している。

Bracketsのすすめ

Bracketsは、Adobeが開発したオープンソースのWeb開発エディタで、軽快に動くので気に入っている。
brackets.io

また、他のエディタと同様(Visual Studio Codeなど)に拡張機能が豊富で、自分の使いたい機能を追加、もしくは開発して公開することが可能である。
CEPに関する拡張機能もいくつか公開されており、Bracketsを選択した理由もこの拡張機能を利用したかったからでもある。
どんな機能拡張があるかは、下記のサイトを参照。
ingo-richter.io

開発環境の構築

ここからは実際に開発環境を構築していく手順を紹介する。

Bracketsのインストール

公式サイトからダウンロードできる。
brackets.io

  1. 公式サイトから最新版のDMGをダウンロードする
  2. ダウンロードしたDMGを展開して、マウントする
  3. アプリケーションをApplicationsディレクトリにコピーする
  4. マウントしたDMGを取り出す
  5. コピーしたアプリケーション(Brackets)を起動する
  6. ダウンロードしたアプリケーションを起動してよいか確認されるので、許可する

Brackets拡張機能の Creative Cloud Extension Builder (CEP 7) をインストール

Creative Cloud Extension Builder (CEP 7) は、簡単にCEPのテンプレートプロジェクトを作成することができる拡張機能をインストールするパッケージ。

  1. Bracketsのメニューから ファイル>拡張機能マネージャー... を選択し、表示されたダイアログの 入手可能タブ をクリック
  2. 検索フィールドで「CEP」と入力、表示された Creative Cloud Extension Builder (CEP 7) をインストール
  3. ダイアログを閉じるとBracketsの再起動が行われるので、再起動後にメニューに CC Extension Builder が表示されていれば成功

検索した結果のリストに、CEP8対応版っぽい Adobe CC Extension Builder が表示されるんだけど、このパッケージをインストールして作成したテンプレートプロジェクトは、エクステンションとして読み込まれなかったので使わなかった。
気が向いたら調査してみる。

Player Debug Mode の設定

CEPをアプリケーションにインストールするには、本来はCEPのプロジェクトをパッケージしたzxpをインストールする必要がある。
そしてパッケージするためには、証明書が必要だったり、他ツールのインストールが必要だったりするんだけど、その準備がなかなか面倒くさく、かつデバッグのたびにパッケージするのも手間だ。
そこで、簡単にデバッグする方法を紹介する。
連番の箇条書きだと連番が途中で壊れるため、便宜上ただの箇条書きで記す。

  • アプリケーションのターミナルを起動する
  • 下記のコマンドを実行(コピー&ペーストして、エンターキーを押す)
defaults write com.adobe.CSXS.7 PlayerDebugMode 1 //ほぼCC 2017用
defaults write com.adobe.CSXS.8 PlayerDebugMode 1 //ほぼCC 2018用
  • 作成したCEPのプロジェクトを下記に設置する
    f:id:macneko-ayu:20180529162956p:plain
    公式ドキュメントより抜粋
    アプリケーションによってはアプリケーションフォルダ内の所定の位置に設置してもOK
/Applications/Adobe InDesign CC 2017/Resources/CEP/extensions
  • Adobeアプリケーションを再起動するとCEPが読み込まれる

サンプルエクステンションの作成・起動について

CEPの開発環境が構築できているか、テンプレートプロジェクトを作成して確認する。
連番の箇条書きだと連番が途中で壊れるため、便宜上ただの箇条書きで記す。

  • Bracketsのメニューから CC Extension Builder>New Creative Cloud Extension をクリック
  • ダイアログが表示されるので、内容を変更せず Create Extension ボタンをクリック
  • 成功すれば、テンプレートプロジェクトが作成された旨のアラートが表示されるので、アラートを閉じる
    テンプレートが作成されるのは下記パスにある
/Users/(user_name)/Library/Application Support/Adobe/CEP/extensions/com.example.helloworld
  • テンプレートプロジェクトがBrackets上で開かれているので、CSXS/manifest.xml を開く
  • テンプレートではInDesignでの読み込み部分がコメントアウトされているので、コメントアウトを外し、Version の値を変更する
    InDesign CC 2017で読み込むためには、バージョンを 12.9 に変更する

変更前

<!-- <Host Name="IDSN" Version="[10.0,11.9]" /> -->

変更後

<Host Name="IDSN" Version="[10.0,12.9]" />
  • InDesign CC 2017を起動する
  • メニューから ウィンドウ>エクステンション>Hello World を選択
  • パネルが表示されるので、Call ExtendScript ボタンをクリック
  • アラートが表示され、hello from ExtendScript というメッセージが表示されていれば成功

注意点

CEPが読み込まれない場合は、設定が誤っている可能性が高い。
簡単なチェックリストを紹介する。

  • Player Debug Mode が有効になっているか
  • 読み込みたいアプリケーションが manifest.xml でコメントアウトされていないか、もしくは記載が削除されていないか
  • 読み込みたいアプリケーションのバージョンが manifest.xml に含まれているか

公式ドキュメント

アプリケーションのIDや対応しているCEPのバージョン、各アプリケーションのCEP上でのバージョンなどは公式ドキュメントに記載してある。
github.com

最後に

環境構築の情報がまとまっておらず、四苦八苦している人が多く見受けられたので、備忘録含めて簡単にまとめたんだけど、公式でこの程度のチュートリアルを用意しておいてほしい。
次は、テンプレートに手を入れて簡単なエクステンションを開発する記事を予定している。

「InDesign JavaScript教室 ~入門・基礎編~」にスタッフとして参加します

大間知さんから声をかけていただき、大間知さんが開催される「InDesign JavaScript教室 ~入門・基礎編~」にスタッフとして参加します。

cs5.xyz

指導補助スタッフとして、参加者のサポートを行います。
資料を確認させていただいたのですが、僕が勉強を始めたときにこの資料があったら捗ったのに!って悔しくなるぐらい、まとまった内容となっています。
この資料だけ販売っていうルートがあってもいいなぁと思いました。

第2期の開催希望も受け付け中ですので、どしどしご希望をお送りください。

cs5.xyz

Adobe Illustrator CC 2017 SDKに同梱されているサンプルプラグインをビルドして読み込ませてみた

概要

唐突に「Adobe Illustrator CC 2017 SDKでプラグインの勉強をしてみよう!」と思い立った。
そこで、SDKに同梱されているサンプルプラグインを使って、どういうことができて、どういう風に実装すればよいのか、調査することにした。
この記事では、手始めとしてサンプルプラグインをビルドして読み込ませるところまでを紹介する。

私の開発環境

  • macOS 10.13.4
  • Xcode 9.3

Adobe Illustrator CC 2017 SDKが推奨する環境

getting-started-guide.pdfより、一部抜粋。

  • Mac OS 10.10 or higher
  • Xcode 7.3

Adobe Illustrator CC 2017 SDKを入手する

SDKをダウンロードする

  1. 公式サイトにアクセスする
    公式サイト:Adobe Illustrator SDKs | Adobe Developer Connection

  2. サイト内の「Adobe Illustrator CC 2017 SDK」をクリックすると、ダウンロードページに遷移するので、そこからダウンロードする

注意点として、Firefoxでregionが日本語のままダウンロードページにアクセスすると、日本語版のTOPページにリダイレクトされるという罠がある。
対策としては、サイト下部の「Change region」をクリックして、regionを「United States」に変更してから、ダウンロードページにアクセスすると良い。
もしくは、Chromeでアクセスしても良い。
おそらく、言語設定が日本語だと日本語ページにリダイレクトする仕様で、リダイレクト先のページが存在しないため、TOPページにリダイレクトされてしまうということなのだろう。
さすがAdodeである。

SDKをHDDにコピーする

  1. ダウンロードしたdmgをダブルクリックする
  2. ウィンドウが開くので、ウィンドウ内のディレクトリ「Adobe Illustrator CC 2017 SDK」をコピーする
  3. HDD内の任意のディレクトリにペーストする

Xcodeを入手する

Xcodeの入手方法はいくつかある。

App Storeから最新バージョンのXcodeをダウンロードする

一番簡単なのはApp Storeから最新のXcodeをダウンロードする方法。
App Storeアプリケーションを起動して、「Xcode」で検索すれば最新バージョンのXcodeが表示されるはずなので、そのページからダウンロードする。
ただし、最新バージョンのXcodeはmacOSが10.13.2以上でないと起動(インストールができないかも)ができない。
古いOSを使用している場合は、AppleのDeveloperサイトから過去バージョンのXcodeをダウンロードする必要がある。

AppleのDeveloperサイトから過去バージョンXcodeをダウンロードする

下記に過去バージョンをダウンロードする手順を記載する。

  1. Apple Developerにアクセス
  2. Apple IDを入力してログインする
  3. サイト下部の「See more downloads」をクリック
  4. 推奨環境に合致するバージョンのXcodeをダウンロードする

Xcodeのインストール

App Storeから入手

ダウンロードが完了するとApplicationsディレクトリにXcodeが格納されているので、Xcodeを起動する。
その際、Command Line Toolsのインストールの許可を求められると思うので、許可してインストールする。

AppleのDeveloperサイトから入手

ダウンロードしたXcode(バージョン表記).xipをダブルクリック。
解凍が完了するとXcode.appがディレクトリ内にできているので、Applicationsディレクトリに移動させる。
Xcodeを起動すると、Command Line Toolsのインストールの許可を求められると思うので、許可してインストールする。

サンプルプラグインをビルドする

  1. Xcodeで(コピーしたSDKを内包するディレクトリ)/Adobe Illustrator CC 2017 SDK/samplecode/MasterProjects/BuildAll.xcodeprojを開く。
  2. XcodeのメニューからProduct>Buildを選択(ショートカットはcmd+B)してビルドを実行
  3. ビルドが完了すると(コピーしたSDKを内包するディレクトリ)/Adobe Illustrator CC 2017 SDK/samplecode/outputディレクトリが生成されている
  4. (コピーしたSDKを内包するディレクトリ)/AdobeExtension/Adobe Illustrator CC 2017 SDK/samplecode/output/mac/releaseディレクトリに、コンパイルされたプラグインが生成されている(拡張子が.aipのファイルがプラグイン)

ビルドしたプラグインをIllustratorに読み込む

  1. Illustrratorを起動する
  2. 環境設定画面を開き、サイドパネルから「プラグイン・仮想記憶ディスク」を選択する
  3. 「追加プラグインフォルダー」のチェックボックスにチェックを入れて、「選択...」ボタンをクリックする
  4. 選択画面が表示されるので、(コピーしたSDKを内包するディレクトリ)/AdobeExtension/Adobe Illustrator CC 2017 SDK/samplecode/output/mac/releaseディレクトリを選択する
  5. プラグインを読み込むために再起動を促されるので、Illustratorを再起動する
  6. Illustratorのメニューの「ウィンドウ」に「SDK」が表示されていれば、プラグインの読み込みに成功している

プラグインのUIをIllustratorに読み込む

Adobe Extension SDK(CEP Extension)を使えるようにする

サンプルプラグインの中にはUIをExtensionで作成しているものがいくつかある。
これらのUIを読み込ませるためには、playerDebugModeの設定を行う必要がある。
参考:CEP 6 HTML Extension Cookbook for CC 2015

defaults write com.adobe.CSXS.7 PlayerDebugMode 1

CEP Extensionsを読み込む

  1. (コピーしたSDKを内包するディレクトリ)/Adobe Illustrator CC 2017 SDK/samplecode/ディレクトリを開く
  2. 〜UIという名称のディレクトリを、/Applications/Adobe Illustrator CC 2017/CEP/extensionsディレクトリにコピーする
  3. Illustratorを再起動する
  4. Illustratorのメニューからウィンドウ>SDK>Marked Objects>Marked Objectsを選択する
  5. パネルが表示されたらプラグイン用のUIの読み込みに成功している

まとめ

SDKの推奨する環境が昔のものだったので最新環境でビルドできるのか不安だったが、なんの問題もなくビルドでき、Illustratorで使用できることがわかった。
実装についてはまだ勉強を始めたところなので、別の機会に。

起動中のアプリケーションを切り替えるmacOSアプリケーションを作った

概要

Macにインストール済の特定のアプリケーションを一覧表示して、任意のバージョンを起動できるLauncherアプリケーションを作る その1(以下、前回記事)の続きを実装するにあたって、「メタ情報の取得をもっとスマートにできないか」「アプリケーションの起動状態をどう取得するか」「メタ情報をUIにどう結びつけるか」などの調査が必要になったので、 別のプロジェクトを作ることにした。
その結果が、本記事で紹介する 起動中のアプリケーションを切り替えるmacOSアプリケーション となる。

作ったもの

f:id:macneko-ayu:20180506151217p:plain

  • 起動中のアプリケーションを一覧表示する
    • 名前順でソートして表示
    • 名前はFinderで表示されている名前を表示
  • アプリケーションを起動すると一覧に追加する
  • アプリケーションを終了すると一覧から削除する
  • セルをクリックすると、そのアプリケーションを最前面に表示する

Githubで公開済。 github.com この記事はv1.0.1時点のもの。

メタ情報の取得をもっとスマートにできないか

前回記事ではinfo.plistからメタ情報を取得していたが、もっといい方法があるはずと思ってさらに調べたところ、標準APIであるBundleから取得する方法があった。

参考:Bundle - Foundation | Apple Developer Documentation

いままでBundleはアプリケーション自身のメタ情報なりを取得するためのものと思っていたけど、init(identifier:)init(url:) で任意のアプリケーションのメタ情報を取得することができたのね。

例として、FilePathからXcodeのBundleIdentifierを取得する方法を比較してみる。

info.plistを生成して取得(前回記事より一部抜粋)

// エンコーディングされているのでデコード
guard let filePath = fileUrl.path.removingPercentEncoding else {
    return nil
}
// 情報を取得するためにplistを取得
let plistPath = filePath + "/Contents/Info.plist"
if !FileManager.default.fileExists(atPath: plistPath) {
    return nil
}
guard let infoDictionary = NSDictionary(contentsOfFile: plistPath) else {
    return nil
}
// bundleIdentifierを取得
self.bundleIdentifier = infoDictionary["CFBundleIdentifier"] as? String ?? ""

Bundleを生成して取得

let url = URL(fileURLWithPath: "/Applications/Xcode.app")
// Bundleを生成
guard let bundle = Bundle(url: url) else {
    return nil
}
// bundleIdentifierを取得
let bundleIdentifier = bundle.bundleIdentifier ?? ""

すっきり取得できるようになった。

アプリケーションの起動状態をどう取得するか

Launcherアプリケーションというからには、該当アプリケーションの起動状態を管理する必要がある。 これについては前回記事の段階で調査をしていて、NSWorkspace のAPIを用いれば実現できそうだとあたりをつけていた。
参考:NSWorkspace - AppKit | Apple Developer Documentation

今回改めて調査したところ、そのものズバリなプロパティはないようで、ちょっと工夫しないと取得できないことがわかった。

例として、Xcodeの起動状態を取得するコードを紹介する。

Xcodeの起動状態を取得する

// 起動しているアプリケーションのリストを取得する(NSApplication.ActivationPolicy.regularはGUIアプリケーション)
let runningApps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == NSApplication.ActivationPolicy.regular }
// 取得したリストからpathでfilterをかけて、ヒットすればXcodeは起動中ということになる
let isRunning = runningApps.compactMap { $0.bundleURL }.filter { $0.path == "/Applications/Xcode.app" }.count > 0
// 結果: Xcodeが起動中=true, 未起動=false
print(isRunning)

つまり、起動しているアプリケーションのリストから、pathが一致すれば起動中、一致しなければ未起動ということになる。

メタ情報をUIにどう結びつけるか

メタ情報を取得することができたので、それをUIに反映する方法を調べる。
当初はRxSwiftを使ってバインディングしようと思っていたんだけど、調べているうちにCocoa Bindingという方法があることを知ったので、試しに採用してみることに。

参考:
What Are Cocoa Bindings?
Cocoa Bindings on macOS

Cocoa Bindingを使うには、バインディングするプロパティに @objc dynamic を付加する必要があり、そのためにモデルもStructからClassに変える必要があった。
また、NSArrayControllerをStoryboardで設定していて、後日コードでも設定できると教えてもらったんだけど、情報が出てこなくて諦めた…。
次のバージョンでは勉強の意味も含めてReactiveSwiftやRxSwiftに置き換えるようと検討中。

話を戻して。
UIに反映するタイミングは、RunningAppsが起動したとき、その他のアプリケーションが起動したとき、その他のアプリケーションが終了した場合の3パターンあるが、どのパターンもNSWorkspaceのNotificationを検知することで実現可能。
検知するNotificationは下記の2つ。

  • NSWorkspace.didLaunchApplicationNotification
    • アプリケーションが起動したタイミングで通知される
  • NSWorkspace.didTerminateApplicationNotification
    • アプリケーションが終了したタイミングで通知される

今回はViewModelでNotificationをaddObserverし、通知があったらデリゲート経由でViewControllerのメソッドを叩き、バインディングしている配列の更新を行うようにした。

一部抜粋

/// ViewModelでデリゲートを定義
protocol UpdatedRunningAppsStateDelegate {
    func refresh()
}

/// ViewControllerでデリゲートメソッドを定義
extension RunningAppsViewController: UpdatedRunningAppsStateDelegate {
    
    /// リストを更新
    func refresh() {
        DispatchQueue.main.async {
            self.runningAppsArrayController.rearrangeObjects()
        }
    }
}

ソースコード

すべて記載すると長くなるので、ViewModelとViewControllerのコードを記載。

RunningAppsViewModel

//
//  RunningAppsViewModel.swift
//  RunningApps
//

import Cocoa

protocol UpdatedRunningAppsStateDelegate {
    func refresh()
}

class RunningAppsViewModel: NSObject {
    @objc dynamic var metaDatas = [ApplicationMetaData]()
    var delegate: UpdatedRunningAppsStateDelegate?
    
    override init() {
        super.init()
        self.setupObserver()
    }
    
    deinit {
        NSWorkspace.shared.notificationCenter.removeObserver(self)
    }
    
    // MARK: Public Methods

    /// 保持しているMetaDataを更新
    ///
    /// - Parameter bundleIdentifiers: 更新対象のBundleIndetifier
    public func updateMetaDatas(bundleIdentifiers: [String]) {
        bundleIdentifiers.forEach { (identifier: String) in
            let metaDatas = makeMetaDatas(bundleIndentifier: identifier)
            let filteredMetaDatas = metaDatas.filter { $0.isRunning && $0.identifier != Bundle.main.bundleIdentifier }
            let sortedMetaDatas = filteredMetaDatas.sorted(by: { $0.name < $1.name })
            sortedMetaDatas.forEach { self.metaDatas.append($0) }
        }
    }
    
    // MARK: Private Methods

    /// MetaDataを作成
    ///
    /// - Parameter bundleIndentifier: 作成対象のBundleIndetifier
    /// - Returns: 作成したMetaDataの配列
    private func makeMetaDatas(bundleIndentifier: String) -> [ApplicationMetaData] {
        // BundleIdentifierにマッチするApplicationのファイルUrlを取得
        // 同じBundleIdentifierのApplicationがインストールされている場合もあり得るので配列となる
        guard let appUrls = LSCopyApplicationURLsForBundleIdentifier(bundleIndentifier as CFString, nil)?.takeUnretainedValue() as? [URL] else {
            return []
        }
        let appMetaDatas = appUrls.compactMap { (appUrl: URL) -> ApplicationMetaData? in
            guard let bundle = Bundle(url: appUrl), let identifier = bundle.bundleIdentifier, let infoDictionary = bundle.infoDictionary else {
                return nil
            }
            // 重複を防ぐため、登録済かチェックする
            if let _ = self.metaDatas.index(where: { $0.identifier == identifier && $0.url.path == appUrl.path }) {
                return nil
            }

            let path = appUrl.path
            let name = self.extractName(path: path)
            let icon = NSWorkspace.shared.icon(forFile: path)
            let version = infoDictionary["CFBundleVersion"] as? String ?? "unknown"
            let versionDesctiption = "version \(version)"
            
            let runningApps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == NSApplication.ActivationPolicy.regular }
            let runningState = runningApps.compactMap { $0.bundleURL }.filter { $0.path == appUrl.path }.count > 0
            return ApplicationMetaData(name: name, url: appUrl, identifier: identifier, version: version, versionDesctiption: versionDesctiption, icon: icon, isRunning: runningState)
        }
        return appMetaDatas
    }
    
    /// ファイルパスからファイル名を抽出
    ///
    /// - Parameter path: ファイルパス
    /// - Returns: ファイル名
    private func extractName(path: String) -> String {
        var components = FileManager.default.displayName(atPath: path).split(separator: ".")
        
        components.removeLast()
        return components.joined(separator: ".") as String
    }
    
    /// Observerを設定
    private func setupObserver() {
        NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(appDidLaunch(notification:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
        NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(appDidTerminate(notification:)), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
    }
    
    // MARK: Notification Methods
    
    /// アプリ起動時にリストを更新(自身は含めない)
    ///
    /// - Parameter notification: Notification
    @objc private func appDidLaunch(notification: Notification) {
        guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
            let identifier = app.bundleIdentifier else { return }
        if identifier == Bundle.main.bundleIdentifier {
            return
        }
        updateMetaDatas(bundleIdentifiers: [identifier])
        delegate?.refresh()
    }
    
    /// アプリ終了時にリストを更新
    ///
    /// - Parameter notification: Notification
    @objc private func appDidTerminate(notification: Notification) {
        guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
            let identifier = app.bundleIdentifier,
            let appUrl = app.bundleURL,
            let index = metaDatas.index(where: { $0.identifier == identifier && $0.url.path == appUrl.path }) else { return }
        metaDatas.remove(at: index)
        delegate?.refresh()
    }
}

RunningAppsViewController

//
//  RunningAppsViewController.swift
//  RunningApps
//

import Cocoa

class RunningAppsViewController: NSViewController {

    @IBOutlet weak var tableView: NSTableView!
    @IBOutlet var runningAppsArrayController: NSArrayController!
    @objc let viewModel = RunningAppsViewModel()

    // MARK: Initialization
    
    deinit {
    }
    
    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }

    // MARK: LifeSycle

    override func viewDidLoad() {
        super.viewDidLoad()
        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:)))
        runningAppsArrayController.sortDescriptors = [sortDescriptor]
        tableView.selectionHighlightStyle = .regular
        loadItems()
    }

    // MARK: Private Methods
    
    /// 起動時に起動中のアプリケーション一覧を取得してArrayControllerに反映(自身は含めない)
    private func loadItems() {
        let runningApps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == NSApplication.ActivationPolicy.regular }
        let bundleIdentifiers = runningApps.compactMap { $0.bundleIdentifier }
        viewModel.updateMetaDatas(bundleIdentifiers: bundleIdentifiers)
    }
}

extension RunningAppsViewController: NSTableViewDelegate {
    func tableViewSelectionDidChange(_ notification: Notification) {
        guard let arrangeObjects = runningAppsArrayController.arrangedObjects as? [ApplicationMetaData] else { return }
        let index = tableView.selectedRow
        let rowView = tableView.rowView(atRow: index, makeIfNecessary: false)
        rowView?.isEmphasized = true
        let item = arrangeObjects[index]
        guard let app = NSWorkspace.shared.runningApplications
            .filter ({ (app: NSRunningApplication) in app.activationPolicy == NSApplication.ActivationPolicy.regular })
            .filter ({ (app: NSRunningApplication) in app.bundleIdentifier == item.identifier && app.bundleURL?.path == item.url.path }).first
            else { return }
        app.activate(options: [])
        tableView.deselectRow(index)
    }
}

extension RunningAppsViewController: UpdatedRunningAppsStateDelegate {
    /// リストを更新
    func refresh() {
        DispatchQueue.main.async {
            self.runningAppsArrayController.rearrangeObjects()
        }
    }
}

次の予定

XMPをパースするためにどうするかの調査と実装をする。

技術書典4に行ってきた

概要

技術書オンリーのイベント「技術書典」に行ってきた。

目的

目的は2つあって、1つは今回はiOS系の本が多数頒布されるということで、それらをゲットすること。
もう1つは、フォロワーさんがサークル参加してて、日々進捗をTwitterで報告していたので、それを読んで私もどんどん楽しみになってたので、会場で購入したいと思った。

会場の雰囲気

開場1時間前に秋葉原UDXについたんだけど、その時点でものすごく人がいた。
整理券は900番台後半で、入場できるまでしばらく待つのかと思いきや、開場20分後くらいには入場できたので良かった。
ただ、1100番台以降の人は先に入場した人がなかなか退場しないようで、結構待たされていた模様。
会場内も人だらけで(当たり前か)、事前に購入を決めていたサークルさんのところに行って買うだけで、一苦労。
あの混み具合だと表紙を見て興味をもったり、サンプルを読ませてもらうのは、厳しいと思った。
気になる本は何冊かあったものの、諦めて早々に退場した。

戦利品

会場で買ったもの(リンク先は公式のサークル詳細)

f:id:macneko-ayu:20180425231133j:plain
戦利品

帰宅後に通販で買ったもの(リンク先は購入先)

まとめ

今回は天気にも恵まれ、ほしい本も買えたので、良かった。
残念なのは、後払いアプリiOS版がいつまで経っても認証コードが届かず、使えなかったことかな。
知人のiOSエンジニアさんたちも届かなかったようなので、iOSと認証システムの相性が悪かったりするのかな。
来年は使えるといいな。

Objective-CでUITestを利用してスクリーンショットを撮る方法

概要

SwiftでUITestを利用してスクリーンショットを撮るサンプルは見つけられたが、Objective-Cの例が見つからなかったので、自分用に残しておく。

コードを書かずに自動でスクリーンショットを撮る方法

Objective-C関係なく、Xcodeの設定のみで可能。

f:id:macneko-ayu:20180423115255p:plain
Xcodeのテスト用スキーマ

スキーマのTestの設定を開いて、チェックボックスを下記のようにする。

  • UI TestingのCapture screenshots automaticallyにチェックを入れる
    • 遷移した画面すべてを撮影してくれるようになる
  • UI TestingのDelete when eath test succeedsのチェックを外す
    • テスト成功時にスクリーンショットを削除しないようになる
  • AttachmentsのDelete when eath test succeedsのチェックを外す
    • テスト成功時にスクリーンショットを削除しないようになる

注意点としてものすごい量のスクリーンショットが作成されるので、必要のないもので必要なものが埋もれる可能性大。
管理が面倒臭い人は、任意の箇所で撮影する方法がいいかもしれない。

任意の箇所でスクリーンショットを撮る方法

2つの方法をあげているが、テスト完了後にレポートを見た際の見やすさが変わるだけなので、スクリーンショットを撮るコード自体は同じ。
用途に応じて選択すると吉。

シンプルにスクリーンショットを撮る方法

- (void)testTakeScreenShot {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app.buttons[@"ボタン"] tap];
    XCUIScreen *screen = [XCUIScreen mainScreen];
    // スクリーンショットを撮影
    XCUIScreenshot *screenShot = [screen screenshot];
    // アタッチメントを生成
    XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenShot];
    // テスト実行後に削除しない
    attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
    // スクリーンショットのファイル名のprefixに付加されるので設定しておくと楽
    attachment.name = @"tapped-button";
    [self addAttachment:attachment];
}

アクテビティを使ってログをグループ化する方法

- (void)testTakeScreenShot {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app.buttons[@"ボタン"] tap];
    [XCTContext runActivityNamed:@"ボタンをタップ" block:^(id<XCTActivity>  _Nonnull activity) {
        XCUIScreen *screen = [XCUIScreen mainScreen];
        XCUIScreenshot *screenShot = [screen screenshot];
        XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenShot];
        attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
        attachment.name = @"tapped-button";
        [activity addAttachment:attachment];
    }];
}

撮影後のスクリーンショットの保存場所

下記ディレクトリ内に保存される。

/Users/(UserName)/Library/Developer/Xcode/DerivedData/(Project)/Logs/Test/Attachments/

参考

Xcode 9からテストが大幅に進化します! - Timers Tech Blog
Hands-on XCUITest Features with Xcode 9
iOS 11 Programming