ごんれのラボ

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

Flutterの公式手順に沿ってMacで環境構築

概要

公式サイトのGet started1. install2: Configure editor の手順に沿って、Flutterの環境構築を行った。
手順通りに進めていきつつ、気になったところをピックアップした。

SDKのバージョンが古い?

Get startedにリンクが貼られているSDKは flutter_macos_v0.5.1-beta.zip なんだけど、どうやらバージョンが古い模様。
手順通り一時的にpathを通したあと、確認のためバージョンを確認するコマンドを実行したところ、WARNINGが出力された。

% flutter -v
  ╔════════════════════════════════════════════════════════════════════════════╗
  ║ WARNING: your installation of Flutter is 39 days old.                      ║
  ║                                                                            ║
  ║ To update to the latest version, run "flutter upgrade".                    ║
  ╚════════════════════════════════════════════════════════════════════════════╝

.zprofile にpathを追記したあとに再度確認したところ、WARNINGは出力されなかった。
調べたところ、公式のUpgrading Flutterで言及されていた。

We strongly recommend tracking the beta branch in the flutter repository, which is where we push ‘known good builds’ of Flutter. If you need to view the very latest changes, you can track the master branch, but note this is where we do our daily development, so stability is much lower.

betaブランチを追跡することが推奨されていて、最新版を追いたければmasterブランチを追跡すればいいけど開発で使われているから安定性は低いよってことらしい。
betaとは…。

Flutter doctorで構築のタスクを確認

! の項目を にすれば環境構築が完了ということのようだ。

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.io/setup/#android-setup for detailed instructions).
      If Android SDK has been installed to a custom location, set $ANDROID_HOME to that location.
[!] iOS toolchain - develop for iOS devices
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    ✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods installed but not initialized.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To initialize CocoaPods, run:
        pod setup
      once to finalize CocoaPods' installation.
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.24.1)
[!] Connected devices
    ! No devices available

! Doctor found issues in 4 categories.

iOSの環境設定

flutter doctorのエラーを修正する

いろいろなところでよく見るツール群をインストール。

% brew update
% brew install --HEAD libimobiledevice
% brew install ideviceinstaller ios-deploy cocoapods
% pod setup

再度 flutter doctor で確認。

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.io/setup/#android-setup for detailed instructions).
      If Android SDK has been installed to a custom location, set $ANDROID_HOME to that location.
[!] iOS toolchain - develop for iOS devices
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    ✗ Missing Xcode dependency: Python module "six".
      Install via 'pip install six' or 'sudo easy_install six'.
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.24.1)
[✓] Connected devices (1 available)

! Doctor found issues in 3 categories.

iOSの項目が解決できていないので、ログを確認して記載してあるコマンドを実行してみる。

% sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
% pip install six

再度 flutter doctor で確認。

% flutter doctor                                                       
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.io/setup/#android-setup for detailed instructions).
      If Android SDK has been installed to a custom location, set $ANDROID_HOME to that location.
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.24.1)
[✓] Connected devices (2 available)

! Doctor found issues in 2 categories.

解決できたっぽい。
[✓] Connected devices (2 available) はSimulatorとiPhoneを接続しているからだと思われるが、試しにiPhoneの接続を解除(ケーブルを抜いた)けど、数に変わりはなかった。
気にしないことにしよう。

サンプルプロジェクトを実行する

公式サイトの手順では自分のプロジェクトの設定をする流れになっているが、まだ自分のプロジェクトを作っていないので、SDKに含まれていたサンプルをビルドしてみることにした。
選んだサンプルプロジェクトは hello_world 、その中のiOSディレクトリにある Runner.xcworkspace を開く。

% cd (SDK))/examples/hello_world/
% open ios/Runner.xcworkspace

Xcodeでプロジェクトが開かれるので、手順を参考に設定する。
私の場合は General > Signing > Team を自分のアカウントに紐づけた。

Xcode上のエラーがなくなったので、Runしてみる。

% flutter run
More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

iPhone6 • (deviceId) • ios • iOS 11.3.1
iPhone X          • 836CDCA0-3655-405A-A646-631F77F322B4     • ios • iOS 11.4 (simulator)

ふむ、端末が複数あるときはIdかすべての端末を指定する必要があるのか。
ひとまず、罠が多そうな実機で実行してみる。

% flutter run -d (deviceId)
Launching lib/main.dart on iPhone6 in debug mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: 2FZSDA66L9
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    1.3s
 └─Compiling, linking and signing...                  2.0s
Xcode build done.                                            4.5s
Installing and launching...                                      
2018-07-08 17:11:21.940 ios-deploy[87674:1685516] [ !! ] Unable to locate DeviceSupport directory with suffix 'Symbols'. This probably means you don't have Xcode installed, you will need to launch the app manually and logging output will not be shown!
Could not install build/ios/iphoneos/Runner.app on (deviceId).
Try launching Xcode and selecting "Product > Run" to fix the problem:
  open ios/Runner.xcworkspace

Error launching application on iPhone6.

おっと、エラーになってしまった。
エラーの詳細がわからないので、ログにあるようにXcodeからRunしてみる。

/bin/sh: /packages/flutter_tools/bin/xcode_backend.sh: No such file or directory

Run時に実行するShell Scriptが見つからないという感じのエラーなので、Xcodeで Build Phases > Run Script を確認して、記載されているコマンドを確認する。

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

なるほど、 $FLUTTER_ROOT が設定されていないっぽい。
念のため、確認。

% echo $FLUTTER_ROOT

ふむ、やっぱり設定されていない。
ということで、SDKを展開したディレクトリのパスを .zprofile に設定しておく。

export FLUTTER_ROOT="(SDKの展開先)"

.zprofile を読み込み直して、再度 flutter run -d (deviceId) を実行。

% flutter run -d (deviceId)
Launching lib/main.dart on iPhone6 in debug mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: 2FZSDA66L9
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    1.0s
 └─Compiling, linking and signing...                  2.0s
Xcode build done.                                            4.2s
Installing and launching...                                      
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/copy.py", line 52, in <module>
    import weakref
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/weakref.py", line 14, in <module>
    from _weakref import (
ImportError: cannot import name _remove_dead_weakref
Syncing files to device iPhone6...                 2.8s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone6 is available at: http://127.0.0.1:8101/
For a more detailed help message, press "h". To quit, press "q".

実機で実行に成功した。
q を入力するとデバッグ終了の模様。

念のため、Simulatorでも実行できるか確認する。

% flutter run -d 836CDCA0-3655-405A-A646-631F77F322B4
Launching lib/main.dart on iPhone X in debug mode...
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    1.1s
 └─Compiling, linking and signing...                  6.7s
Xcode build done.                                            9.6s
Syncing files to device iPhone X...                          2.6s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:8102/
For a more detailed help message, press "h". To quit, press "q".

Simulatorでも実行できた。

Androidの環境設定

flutter doctorのエラーを修正する

Android Studioはダウンロードしただけだったので、まずAndroid Studioを起動して促されるままに設定していく。
Android Studio Setup Wizard が完了したところで、再度 flutter doctor で確認。

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK 28.0.1)
    ✗ Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.24.1)
[✓] Connected devices (2 available)

! Doctor found issues in 2 categories.

まだ解消できていないものがあるので、ログに記載してあるコマンドを実行する。

% flutter doctor --android-licenses

実行するとライセンスの確認をするか聞かれるので、 y と入力して、表示されるライセンスを確認しつつ、問題なければ y と入力する。
6ファイル分ぐらい確認すれば完了。

再度 flutter doctor で確認。

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code (version 1.24.1)
[✓] Connected devices (2 available)

! Doctor found issues in 1 category.

pluginがインストールされていない旨のメッセージがでているけど、先にAndroid emulatorの設定をしておく。
設定用に適当なプロジェクトを作って、それをAndroid emulatorで起動しつつ、設定した。

続いて、 2: Configure editor の手順に沿ってpluginをインストールして、再度 flutter doctor で確認。

% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)
[✓] Android Studio (version 3.1)
[!] VS Code (version 1.24.1)
[✓] Connected devices (2 available)

! Doctor found issues in 1 category.

Androidの問題は解決できた。

サンプルプロジェクトを実行する

手元にAndroidの実機がないので、さきほど作成したエミュレータ上でAndroidのサンプルプロジェクトを実行する。
emulatorの deviceId を調べるために flutter run を実行。

% flutter run
More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

Android SDK built for x86 • emulator-5554                        • android-x86 • Android 9 (API 28) (emulator)
iPhone X                  • 836CDCA0-3655-405A-A646-631F77F322B4 • ios         • iOS 11.4 (simulator)

deviceId がわかったので、その deviceId を対象に flutter run を実行する。

% flutter run -d emulator-5554
Using hardware rendering with device Android SDK built for x86. If you get graphics artifacts, consider enabling software rendering with "--enable-software-rendering".
Launching lib/main.dart on Android SDK built for x86 in debug mode...
Initializing gradle...                                      16.6s
Resolving dependencies...                                   103.9s
Running 'gradlew assembleDebug'...                          18.6s
Built build/app/outputs/apk/debug/app-debug.apk.
Installing build/app/outputs/apk/app.apk...                  1.1s
I/FlutterActivityDelegate( 6500): onResume setting current activity to this
I/Choreographer( 6500): Skipped 50 frames!  The application may be doing too much work on its main thread.
D/EGL_emulation( 6500): eglMakeCurrent: 0xe13338c0: ver 2 0 (tinfo 0xe2568b00)
I/OpenGLRenderer( 6500): Davey! duration=944ms; Flags=1, IntendedVsync=2356325086415, Vsync=2357158419715, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=2357173860878, AnimationStart=2357173896878, PerformTraversalsStart=2357173912878, DrawStart=2357181064878, SyncQueued=2357191369878, SyncStart=2357195961878, IssueDrawCommandsStart=2357196175878, SwapBuffers=2357230055878, FrameCompleted=2357273973878, DequeueBufferDuration=26623000, QueueBufferDuration=706000, 
Syncing files to device Android SDK built for x86...             
D/        ( 6500): HostConnection::get() New Host Connection established 0xe2567900, tid 6524
D/EGL_emulation( 6500): eglMakeCurrent: 0xe1333800: ver 2 0 (tinfo 0xe2568830)

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on Android SDK built for x86 is available at: http://127.0.0.1:8102/
For a more detailed help message, press "h". To quit, press "q".

Androidでのサンプルプロジェクトの実行もできた。

まとめ

公式の手順がちゃんとしているので、ハマる部分がほとんどなかった。
ソースコードに関しては今回は追いかけていないので、次の記事では公式のGet startedの Write Your First Flutter App を追ってみようと思っている。

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と認証システムの相性が悪かったりするのかな。
来年は使えるといいな。