ごんれのラボ

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

数年前に書いた習作のmacOSアプリをSwiftUIで書き直したら、意外とハマりどころがあって面白かった話

はじめに

本記事はSwift/Kotlin愛好会 Advent Calendar 2020の17日目の記事です。

qiita.com

数年ぶりにアドベントカレンダーに記事を書いてみたいと思って、思いついたのがSwift/Kotlin愛好会アドベントカレンダーでした。
会自体に参加したことはないので、来年は参加できるといいなと思っています。

概要

なんとなくSwiftUIでアプリのViewを書いてみたくなって、数年前にSwiftの勉強用に書いたmacOSアプリをSwiftUIで書き直してみました。
意外とハマりどころがあって面白かったので、誰かのお役に立てるかと思ってブログにまとめました。

実装環境

  • macOS 10.15.7
  • Xcode 12.2

AdobeなどBig Surでの動作に不安がありそうなアプリケーションを使用する機会が多く、Catalinaで実装しています。
最新版のSwiftUIではないので、すでに解消されているハマりどころもあるかもしれません。

ソースコード

GitHubにあげています。

github.com

RunningAppsターゲットがAppKit版、RunningAppsSwiftUIがSwiftUI版です。

書き換え対象のアプリケーション

起動しているアプリケーションを一覧表示して、行をクリックしたらそのアプリケーションがアクティブになる、いわゆるランチャー系のmacOSアプリです。
当時はSwiftUIもなかったので、AppKitで書いています。

当時の記事

www.macneko.com

構造はシンプルで、ViewControllerにNSTableViewを設置して、Cellを表示しているだけです。
アーキテクチャはMVVMを採用してはいます。

アプリケーションが起動・終了したときにViewを更新するロジックとして、Cocoa Bindingを採用しています。
なぜRxSwiftなどのライブラリを使用していないかというと、当時使ったことがなくて学習コストが高いと感じたのと、早く動くものを作らないと飽きて捨てることになるからで、深い意味はないです。

書き換えたアプリケーションのスクリーンショット

https://user-images.githubusercontent.com/5406126/102351849-74d5d500-3fea-11eb-9e21-50b18ea97b21.png

左がSwiftUI版、右がAppKit版です。
背景色がちょっと違いますが、ご愛嬌ということにしました。

書き換えにあたってハマったところ

さて、ここからが本題です。 いくつかのパートにわけて私がハマったところを紹介します。 私の知識不足が原因でSwiftUIの問題ではない部分もあるかと思いますが、そこはTwitterなどでやんわりご指摘いただけるとうれしいです。

ScrollViewでハマったところ

スクロールできなくなった

ScrollView のインジケータを表示したくないなぁと思って showsIndicatorsfalse にしたところ、スクロールできなくなりました。
いろいろ調べたのですが原因がわからず、なんとなくtrue に戻したところ、スクロールできるようになりました。
インジケータの値がスクロール可能かどうかに影響するとは…。

採用したコードは以下のような内容です。

// showsIndicatorsをfalseにするとスクロールできなくなるのでtrueにしている
ScrollView(.vertical, showsIndicators: true) {
    // 子Viewをなんか実装する
}

ウィンドウサイズを縮小したらクラッシュするようになった

AppKit版ではウィンドウサイズを一定の幅より縮めることはできず、好きな幅に拡げられるようになっていました。
SwiftUI版では少し仕様を変えて幅は子Viewの最大幅にして、伸縮できないようにすることにしました。
その試行錯誤の過程で ScrollViewframe(maxWidth: geometry.frame(in: .global).width, maxHeight: geometry.frame(in: .global).height) を設定したところ、ウィンドウサイズを指定したサイズより小さくすると「Contradictory frame constraints specified.」でクラッシュするようになってしまいました。

(この記事を書くために再現するコードを書こうと試行錯誤したのですが、クラッシュせずに普通に縮小できてしまいました。
ScrollView だけじゃなく、子Viewにも frame を設定していたので、そのあたりも影響していそうです)

調べたところ、以下の記事を見つけました。

swiftui-lab.com

この記事によると、どうやら最小サイズや理想的なサイズが最大サイズより大きくなってはいけないのに、ウィンドウを縮小したときにその制約が壊れてクラッシュしているっぽいです。
ですよねー。
そこで、 GeometryReader を使ってglobalのサイズを使ってたのをやめて minWidth だけ指定するようにしたら、サイズを変えてもクラッシュしないし、指定したいサイズより小さくできなくなりました。

さらに調べたところ、子Viewの中の一番大きなサイズにFitさせる fixedSize() の存在を知りました。
しかし、 fixedSize() を指定すると高さもFitするようになり、起動しているアプリケーションの数を増やすと行がMacの画面サイズをこえてしまって、かつスクロールできなくなってしまいました。
引数なしの場合は、幅も高さもFitさせるので、想定通りですね。
メソッドの引数を指定して fixedSize(horizontal: true, vertical: false) としたら幅は子Viewの最大幅、高さは前回終了時の高さ?で起動し、、スクロールもできるようになりました。
いろいろつまづきましたが、これで私がやりたかったことを実現できました。

採用したコードは以下のような内容です。

ScrollView(.vertical, showsIndicators: true) {
    // 子Viewをなんか実装する
}
// 子View内の最大サイズにあわせてFitする
.fixedSize(horizontal: true, vertical: false)

Buttonでハマったところ

グレーのViewが表示されて、そのViewだけしかタップできない

NSTableViewCell のように行全体をタップ範囲にすることを期待して、 Buttonlabel にViewを設定したところ、期待していたとおりにはいかず、 Button の真ん中あたりにグレーのViewが表示されただけでした。
子Viewの幅とも高さとも一致しておらず、タップ範囲もこのグレーの部分のみになっていました。

https://user-images.githubusercontent.com/5406126/102468266-a5724900-4094-11eb-9a8e-a4ef29e13861.png

画像の状態のコードは以下のような内容です。

Button(action: {
    // タップ時の処理
}) {
    AppListView(metaData: data)
}

なにかしら設定があるだろうとAppleのドキュメントを眺めていたところ、 buttonStyle の存在を知り、その中の PlainButtonStyle() を設定したところ、 label で指定したViewのサイズに沿った透明ボタンが作成されることがわかりました。
最初からドキュメント読みましょうという話ですね…。

採用したコードは以下のような内容です。

Button(action: {
    // タップ時の処理
}) {
    AppListView(metaData: data)
}
// これを指定するとボタン内のグレーのViewがなくなり、AppListViewのサイズの透明ボタンができる
.buttonStyle(PlainButtonStyle())

行の余白部分をタップしても反応しない

上述した処理でグレーのViewを消し去ることに成功したものの、今度は文字や画像はタップできるが、行の余白部分はタップできないという問題が発生しました。

この問題には結構時間を使ってしまったのですが、思いつきで Buttonforground で青を設定したら原因がわかりました。
以下の画像のように、アイコンと青い文字列のみが Button として認識されていたのでした。背景部分にはViewがないのでタップできないということですね。

https://user-images.githubusercontent.com/5406126/102470675-a2c52300-4097-11eb-80d8-056093b2da40.png

UIButton も同じ挙動になるので、理由がわかったときはかなりがっかりしました。
つい数ヶ月前にハマっているんですよね、これ…。

この問題は、 Buttonlabel に指定するView側で background() を指定して背景にViewを敷き、かつ contentShape() を指定することで、Viewの最大幅と最大高さまでタップ範囲を広げることができました。

採用したコードは以下のような内容です。

HStack {
    Image(nsImage: metaData.icon ?? NSImage())
        .resizable()
        .renderingMode(.original)
        .aspectRatio(contentMode: .fit)
        .frame(width: 36, height: 36, alignment: .center)

    VStack(alignment: .leading) {
        Text(metaData.name)
            .font(.system(size: 12, weight: .bold, design: .default))
            .padding(.bottom, 7)

        Text(metaData.versionDescription)
            .font(.system(size: 12, weight: .regular, design: .default))
    }
}
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
// 背景に透明なViewを敷く
.background(Rectangle().foregroundColor(.clear))
// contentShapeを設定する
.contentShape(Rectangle())

ForEachでハマったところ

各行の間に余分なpaddingが設定されてしまう

このアプリでは ScrollView の中で ForEach を使用して行となる各Viewを生成するようにしたのですが、各Viewの間に私が設定していない余分なpaddingが設定されてしまう問題が生じました。
ScrollViewButtonDivider のすべてに padding(0) を設定しても解消しなかったので、どこかで私の関知していないViewが生成されているようです。

調べたところ、以下の記事を見つけました。

www.reddit.com

記事によると、 ForEach でViewを生成すると内包したViewの間にpaddingが設定されてしまうようです。
この問題は、 ScrollViewForEach の間に VStack(spacing: 0) を追加することで、paddingを消すことができました。

採用したコードは以下のような内容です。

ScrollView(.vertical, showsIndicators: true) {
    // ForEachで作られたView間にpaddingが設定されてしまうので、↓のVStack(spacing: 0)が必要
    // via https://www.reddit.com/r/SwiftUI/comments/e607z3/swiftui_scrollview_foreach_padding_weird/
    // 行の揃えもVStackで指定する
    VStack(alignment: .leading, spacing: 0) {
        ForEach(viewModel.metaData) { data in
            // 子Viewをなんか実装する
        }
    }
}
.fixedSize(horizontal: true, vertical: false)

Listでハマったところ

macOSではSeparatorが引かれない

List での実装を試したときに、なぜか Separator が引かれないという問題が生じました。
「SwiftUI List Separator」というワードでググってもiOSの情報しかなく、ほとんどが「Separatorを消すにはこうするとよいです!」という内容のものでした。
違うんだ、私はSeparatorを引きたいんだ…。

List 内に Text を描画するだけのシンプルなアプリを実装して試したのですが、iOSは自動でSeparatorが引かれるのに対して、macOSではやはりSeparatorは引かれませんでした。

試したコードは以下のような内容です。

struct ContentView: View {
    var body: some View {
        List(0 ..< 5) { index in
            Text("number \(index)")
        }
    }
}

OS間の差異なんですかねぇ。

この問題は、自前で Divider を引くことでSeparatorを引くことができました。

余白部分がタップできない

Button の項でも同じ問題にハマっていましたが、 List でもハマってしまいました。
ListScrollView に書き換えると問題なく動作するので、別の問題が起きていそうです。

これに関しては ScrollView に書き換えれば動くことがわかっているので、諦めました。

参考

SwiftUIの実装を始める前に佐藤さんの同人誌を読んで勉強して、実装中も何度も読み直しました。
実際に機能のあるアプリケーションを実装するパートもあって、読み応えがあります。
私は同人誌版を購入したのですが、現在は商業出版されているので、そちらのリンクを紹介します。

www.amazon.co.jp

まとめ

初めて自分でSwiftUIを使って実装したのですが、面白いですね。
Storyboardで実装しているときの触っただけで差分発生するイライラから解放されるって素晴らしい。
Previewも最高ですね。
AppKitとUIKitの違いに振り回されることも少なそうなので、今後macOSアプリを書くことがあればSwiftUIを採用すると思います!

Illustratorファイル(ai, eps)の作成アプリバージョンと保存バージョンを抽出するコードをSwiftで書いてみた

概要

敬愛するものかのさんが、Illustratorファイル(ai, eps)の作成アプリバージョンと保存バージョンを抽出するコードをPythonで書いたコードを公開されたので、同じようなものをSwiftで書いてみたいと思って書いてみました。
Tweetにあるとおり当初はCLIを書こうかと思ったんですが、ぐちゃっとしたコードを書き直しながらCLI化していると飽きてしまいそうなので、XcodeのPlaygroundにコードを貼り付けたら動くという状態で公開することにしました。

ソースコード

Extract the creation application version and saved version of an Illustrator file (ai, eps) ref: https://gist.github.com/monokano/8bffac0c07401627c5a1ebf020b93b0e

やっていること

  1. ファイルを1MBバイトずつ読み込んで、Data型に変換する
  2. バージョンが表記してある箇所の前後にある文字列をキーとして、バージョンが表記されているRangeを取得する
    • 作成バージョンは AI8_CreatorVersion:%%For の間にある
    • 保存バージョンは %Creator: Adobe Illustrator(R)%%AI8_CreatorVersion: の間にある
  3. 取得したRangeを使って1のDataから値を抽出してString型に変換する
  4. 作成バージョンと保存バージョンが抽出できたらクロージャを実行して、バージョンを出力する

やっていないこと

ものかのさんのPythonスクリプトではCCのバージョンへの読み替えを行っていますが、私のコードではやっていません。

まとめ

私、がんばった

SwiftでData(contentOf:)でファイル読み込む処理と、InputStreamでファイルを読み込む処理を書いた(適当な実行時間計測つき)

概要

ずっと書こうと思って放置していたツールを書くために、Swiftでファイルを読み込む処理について調べたので、サンプルコードを残しておく。

サンプルコード

Playgroundで書いたのでうっかりファイルを削除してもいいように、Gistにあげた。

A sample file loading process using swift.

InputStream を使った新しめのサンプルコードがなくて、Appleのドキュメントを読んだり、Stackoverflowのコードを継ぎ接ぎしたりしたので、最適なコードにはなっていない可能性が高い。

また、サンプルコードの最後に実行時間を貼っておいた。 想定していた通り、 Data(contentOf:) で一気にファイルを読み込むより、InputStream で指定バイト数分ずつ読み込むほうが処理時間は短くすみそうだ

参考

https://developer.apple.com/documentation/foundation/data https://developer.apple.com/documentation/foundation/stream https://stackoverflow.com/questions/26360962/receiving-data-from-nsinputstream-in-swift https://stackoverflow.com/questions/6685785/stream-to-get-data-nsinputstream

InDesignのテキスト流し込みスクリプトの設計を見直してみた

概要

久しぶりにInDesignのテキスト(画像も含む)流し込みスクリプトを実装したときに、安易に過去のスクリプトを3行ぐらいコピペしたところでうんざりしたので、実装のしやすさと処理速度を両立できる設計を検討しつつ実装してみました。
いまさら?っていう内容かもしれないけど、そのときは読み流してそっと閉じてください。

環境

  • macOS 10.15.6
  • InDesign CC 2020

テキストデータの整形

Excel上でテキスト整形

今回の流し込みスクリプトはExcelファイルからテキストを生成することが要件でした。
私が流し込みスクリプトを設計するときはタブ区切りテキストを用意することが多いので、Excelはもってこい。
まず、支給されたExcelのシートとは別に流し込み用テキスト生成用のシートを追加して、そのシートに必要なセルを参照しました。
一行が1ページに流し込むテキストに相当します。
その際に、Excelのセル内改行を独自のメタ文字(適当な文字列)に置き換えたり、空白のセルは空文字になるようにしたり、ざっくりテキスト整形します。
実際は流し込みしてからセル内改行に気づいたんですが、数年ぶりに許しまじCRLFっていう気持ちになりました。許すまじ。

そんなこんなで書き出したテキストファイルをスクリプトで読み込みます。
特に工夫もなくすべての行数を一気に読み込んで、改行コードでsplitして配列にします。

ここまではみんな似たようなことをやりますよね。
ここから先が今回工夫したことです。

テキストの配列をオブジェクトに格納

まず、次のようにExcelの列番号を定数として定義します(ExtendScriptの場合、上書きできちゃうけど、気持ちは定数)。

var ID = 0;
var FULLNAME = 1;
var AGE = 2;
... // 他の列も定義する

続いて、次のようにオブジェクト(例ではdataArray)に格納します。
arrayがテキスト一行文、すなわち1ページに流し込むテキストのデータです。

// 流し込み用のデータを格納する配列
var dataArray = [];

// 1行目はヘッダーなので処理しない
for (var i = 0, iLen = array.length; i < iLen; i++) {
    var column = array[i].split('\t');
    var data = {
        ID: column[ID],
        fullName: column[FULLNAME],
        age: column[AGE],
        ...
    }
    // なにか加工する必要があれば、dataオブジェクトにアクセスして値を更新する

    // 新しくKeyValueを追加することも可能
    data.kind = 'Cat';

    // 配列に詰め込む
    dataArray.push(data);
}

流し込むときにこのオブジェクトから該当するテキストデータを取得すればよく、Excel上でセルの列の番号が変わっても、定数の列番号を新しいものに変更すれば対応可能です。

昔は array[0] で取り出していたんですが、さすがにそれはないだろうと思って、今回のやり方に変えました。

テキストデータの整形はこんなところです。

流し込みフォーマットの整形

流し込み対象のInDesignドキュメントにも工夫を施しました。
要件次第では必要のないものもありますが、一例として紹介します。

一点目は、各オブジェクトの名前をdataオブジェクトのKeyと同じ名前にしました。
これは単純に名前が一緒のほうがどのオブジェクトが対象かわかりやすくなるというだけでなく、流し込みロジックを簡略化できるという強力な効果があります。
流し込みロジックについては後述します。

二点目は、グループオブジェクトや線にも名前をつけました。
これは今回の要件として、以下にあげた項目があったからです。

  • フォーマットに配置されたフレームで足りないときは、スクリプトで必要数分フレームを増やす
  • ページによって必要のないフレームは削除する
  • 流し込み後にフレームの座標位置を上寄せにする
  • グループ内のテキストフレームにも流し込みを行う

名前をつけることにより、その名前をキーにベースとなるグループオブジェクトを複製して配置したり、上寄せのときにテキストフレームと一緒にグループオブジェクトや線の移動ができたりします。
グループ内のグループオブジェクトにも名前をつけると、より細かい制御ができ、おすすめです。

余談ですが、オブジェクトの名前つけにもスクリプトを使用しました。
簡単なものなのでコードは省略しますが、レイヤーパレットでトリプルクリックを繰り返す作業から解放されました。

処理対象のオブジェクト化

データの流し込みの工夫について説明する前に、よくある実装方法をおさらいします。

テキストフレームにテキストを流し込む一番簡単な方法として、以下のようなコードがあげられます。
私のブログを含む、初心者向けの記事によくある書き方です。
内容としては「1ページ目にあるテキストフレームに対して、この本文はダミーですという文字列を流し込む」ものです。

var doc = app.activeDocument;
var myPage = doc.pages[0];
for (var i = 0, iLen = myPage.textFrames.length; i < iLen; i++) {
    var textFrame = myPage.textFrames[i];
    textFrame.contens = 'この本文はダミーです';
}

また、以下のように書くと、指定した名前(この場合はsample)のテキストフレームのみ、テキストの流し込みが行われます。

var doc = app.activeDocument;
var myPage = doc.pages[0];
for (var i = 0, iLen = myPage.textFrames.length; i < iLen; i++) {
    var textFrame = myPage.textFrames[i];
    if (textFrame.name == 'sample') {
        textFrame.contens = 'この本文はダミーです';
    }
}

さて、このふたつのコードにはとある問題が潜んでおり、今回私が担当した案件では期待した結果になりません。
実際にテキスト流し込み用のスクリプトを開発した経験がある方にはすぐ答えがわかってしまう問題ですが、このコードではグループ化されたオブジェクト内のテキストフレームには流し込みが行われません。

正しく処理を行うためには以下のように書き換える必要があります。

var doc = app.activeDocument;
var myPage = doc.pages[0];
for (var i = 0, iLen = myPage.allPageItems.length; i < iLen; i++) {
    var pageItems = myPage.allPageItems[i];
    if (pageItem.constructor.name == 'TextFrame' && pageItem.name == 'sample') {
        textFrame.contens = 'この本文はダミーです';
    }
}

すべてのtextframesを取得する myPage.textFrames ではなく、すべてのpageItemsを取得する myPage.allPageItems に対してfor文を回し、取得したpageItemsに対して Textframe かつ 名前がsampleか というif文で合致するテキストフレームをフィルタリングして、流し込みを行います。

当然流し込みを行うテキストフレームはひとつだけではないですし、テキストフレーム以外のオブジェクトも多数あります。
また、流し込みではありませんが、座標調整のためにグループオブジェクトも取得する必要があります。
そのたびに myPage.allPageItems にアクセスしていたのでは、処理時間が膨大にかかってしまいます。

そこで上述した問題を解決するために、処理対象のオブジェクトをオブジェクトに格納する方法を試しました。

実装はとても簡単で、以下のようなコードを各ページの繰り返し処理の先頭に記述するだけです。

var allTextFramesInSpread = {};
var allGroupsInSpread = {};
var allGraphicLinesInSpread = {};
var allRectanglesInSpread = {};
for (var i = 0, iLen = myPage.allPageItems.length; i < iLen; i++) {
    var pageItem = myPage.allPageItems[i];
    if (pageItem.constructor.name == 'TextFrame' && pageItem.name != "") {
        // 名前のついたテキストフレームをオブジェクトに格納する
        allTextFramesInSpread[pageItem.name] = pageItem;
    } else if (pageItem.constructor.name == 'Group' && pageItem.name != "") {
        // 名前のついたグループをオブジェクトに格納する
        allGroupsInSpread[pageItem.name] = pageItem;
    } else if (pageItem.constructor.name == 'GraphicLine' && pageItem.name != "") {
        // 名前のついた線をオブジェクトに格納する
        allGraphicLinesInSpread[pageItem.name] = pageItem;
    } else if (pageItem.constructor.name == 'Rectangle' && pageItem.name != "") {
        // 名前のついた長方形をオブジェクトに格納する
        allRectanglesInSpread[pageItem.name] = pageItem;
    }
}

意図しない結果の例では個別のフレーム名を指定してオブジェクトを特定しましたが、実際に案件で使用したコードでは名前のついているオブジェクト名だけをオブジェクトに格納しました。

さて、オブジェクト化することの利点とはなんでしょうか。
一度だけ myPage.allPageItems にアクセスしてオブジェクト化してしまえば、ページ全体のオブジェクトの構造が変わらないという条件はありますが、以降は処理対象へのアクセスを作成したオブジェクト経由で行えます。
さらに、名前がついていないオブジェクトをオブジェクト化しないことで、処理を行わないオブジェクトは無視されます。
どちらも処理速度の向上に貢献します。

テキスト流し込み処理の簡略化

ここまで説明してきた「テキストの配列をオブジェクトに格納」「流し込みフォーマットの整形」「処理対象のオブジェクト化」の3つの工夫により、テキスト流し込み処理を簡略化できました。

まず、テキストを流し込めばよいだけのテキストフレームの名前を配列に格納します。

var normalFlowTextFrameNames = [
    "fullName", 
    "age", 
];

続いて、テキストフレームを格納したオブジェクトから、名前をキーに対象を抽出するメソッドを定義します。

function getTextFrameInPage(frameName) {
    return allTextFramesInPage[frameName];
}

最後に、流し込みを行います。

// dataのキーと同じ名前のテキストフレームに値を流し込む
for (var i = 0, iLen = normalFlowTextFrameNames.length; i < iLen; i++) {
    var targetName = normalFlowTextFrameNames[i];
    getTextFrameInPage(targetName).contents = data[targetName];
}

こうすることで、テキストフレームごとに流し込むテキストを指定することなく、一気に流し込みを行えます。
設計・実装してみれば単純な流れではありますが、実装の意識を上寄せ処理やフレーム増減処理に集中させることができて、なかなかよくできた仕組みだと思っています。

注意点

最後に注意点です。
「処理対象のオブジェクト化」のところでも触れましたが、ページの構造が変わると意図しない動作になります。
特に注意すべきはグループオブジェクトで、グループオブジェクトをグループ解除すると、インスタンスの参照が切れてしまい、そのグループ内に含まれていたオブジェクトが取得できなくなります。

具体的には以下のコードのように、isValidでfalseになります。

getGroupInSpread('sample').ungroup();
for (var i = 0, iLen = getGroupInSpread('sample').allPageItems.length; i < iLen; i++) {
    var pageItem = getGroupInSpread('sample').allPageItems[0];
    if (pageItem.isValid) {
        // ここにはこない
    } else {
        // こっちにくる
    }
}

この問題の対策は案外面倒くさいです。
今回は名前をキーにしてグループオブジェクトを取得するメソッドを定義して、その中で対象のオブジェクトに対してisValidで判定してfalseが返ってきたら、ページの第一階層のグループオブジェクトの中から該当する名前のオブジェクトを返すようにしました。
構造を理解しているから使える手段ですが、自分でフォーマットを作る利点でもありますね。

最後に

あまりテキスト流し込みに関する実践的な内容を紹介した記事がなかったので、実際の案件ベースに現時点の私なりの最適解を紹介しました。
これを機に、より有用な記事が公開され、私のスクリプト開発がはかどるようになるといいなぁ。

ダイアログで指定した文字サイズ、文字数、行数をもとに、Illustratorのエリアテキストのサイズを変更するJavaScript「change-textarea-size-for-illustrator」の販売を開始しました

概要

お世話になっている知人から「Illustratorのエリアテキストのサイズを、ダイアログで指定した文字数や行数に応じたサイズに変更するJavaScriptがほしい」と相談を受けて作ったスクリプトです。
Twitterにて実装中のスクリプトの簡単なデモ動画を公開したところ、反響があったので、Boothで販売することにしました。

商品ページ

macneko.booth.pm

Youtubeに紹介動画をアップロードしました(内容はv1.0.0のものです)

change-textarea-size-for-illustratorとは

Illustratorドキュメント上で選択しているエリアテキストのサイズを、ダイアログで指定した文字サイズ、文字数、行数をもとに変更するスクリプトです。
エリアテキストの複数選択や縦組みエリアテキスト、エリアテキスト内に挿入点がある状態、エリアテキスト内の文字を選択した状態でのサイズ変更にも対応しています。
また、ポイントテキストを選択していた場合は該当するテキストの処理をスキップします。
スクリプト起動時に表示するダイアログに表示する初期設定の値は、スクリプトに同梱している「change-textarea-size-for-illustrator_config.txt」(設定ファイル)を書き換えることで、お好みの値に変更できます。

おすすめポイント

  • エリアテキストを複数選択して一気にサイズ変更ができます
  • 自分でサイズを計算しなくても、ダイアログに入力したサイズや文字数などに応じてサイズを変更できます
    • さらに「先頭の文字のサイズにする」を選択することで、文字サイズ・行送りを指定せずにいい感じにサイズを変更できます
  • 設定ファイルの値を書き換えることで、Illustratorの再起動をしなくてもダイアログに表示する初期値を変更できます
  • 複数の単位に対応しており、プルダウンで基準にしたい単位を選択するだけで単位換算ができます

仕様

ダイアログ

スクリプトを起動時に表示するダイアログの各項目の説明は、次の画像を参照ください。

dialog-description

設定ファイル

ダイアログに表示する初期設定の値を変更する場合は、同梱している「change-textarea-size-for-illustrator_config.txt」をテキストエディタで開き、タブ区切りの右側(赤枠で囲んだ部分)の値を書き換えて保存し、スクリプトを再度起動してください。

config-description

動作環境

作者の開発環境を記載します。 - macOS 10.15.5 - Adobe Illustrator 2020 - Adobe Illustrator CC 2019

他のバージョンのIllustratorやWindowsは未検証のため、自己責任でお願いします。

インストール方法

以下のフォルダ内に、「change-textarea-size-for-illustrator」という名前のフォルダを作成して、その中に「change-textarea-size-for-illustrator_(バージョン名).jsx」「change-textarea-size-for-illustrator_config.txt」を移動、またはコピーしてください。

  • Adobe Illustrator 2020
    • /Applications/Adobe Illustrator 2020/Presets.localized/ja_JP/スクリプト
  • Adobe Illustrator CC 2019
    • /Applications/Adobe Illustrator CC 2019/Presets.localized/ja_JP/スクリプト

v1.1.0で追加・改修した機能

2つの機能を追加・改修しました。
どちらもユーザーさんからの要望を取り入れたもので、さらに便利になったと思います。

ダイアログに入力した文字サイズと行送りをエリアテキストに適用する

「入力したサイズから計算する」のラジオボタンを選択し、かつ「文字サイズと行送りを変更する」のチェックボックスにチェックをいれると、エリアテキストの文字数と行送りが、入力された値に変更される機能です。
「先頭の文字サイズから計算する」のラジオボタンを選択すると、「文字サイズと行送りを変更する」のチェックボックスのチェックは自動的に外れます。
また、設定ファイルに「文字サイズと行送りを変更する」の設定値を追加していますので、状況に応じて初期値を変更してお使いいただけます。

ダイアログの行数に0を入力すると行方向のサイズ変更を行わない

見出しのままですが、ダイアログの行数に0を入力したら行方向のサイズ変更を行いません。 エリア内文字オプションの自動サイズ調整をオンにしているときなど、Illustratorにいい感じに調整させたいときにご利用ください。

ドキュメントに使われているが、環境にないフォントの名前をテキストに書き出すJavaScriptを書いた

概要

ドキュメントに使われているけど環境にないフォントはフォント検索を使えば確認できる。

できるんだけど、誰かに伝えるにはキャプチャを撮ったり、自力でフォント名をコピペしたりしないといけなくて、スマートじゃないよね。

そんなわけで、そんなフォントの名前をテキストに書き出すJavaScriptを書いた。

コード

font.name はフォントスタイル(RやDBなど)が重複しちゃっていることがあるんだけど、今回はスルーした。

var doc = app.activeDocument;
var results = [];
for (var i = 0, iLen = doc.fonts.length; i < iLen; i++) {
    if (doc.fonts[i].status != FontStatus.INSTALLED) {
        results.push(doc.fonts[i].name);
    }
}
var writeStr = results.join('\n');
writeStringToFile('~/Desktop/result_' + getNowYMDHMS() + '.txt', writeStr);

/**
 * テキストファイル書き出し
 * @param  {String} filePath      保存先のファイルパス
 * @param  {String} stringToWrite 保存するテキスト
 */
function writeStringToFile(filePath, stringToWrite) {
    var fileObj = new File(filePath);
    if (fileObj) {
        try {
            fileObj.encoding = "UTF-8"
            var flag = fileObj.open("w");
            if (flag == true) {
                fileObj.write(stringToWrite);
            }
        } catch (e) {

        } finally {
            fileObj.close();
        }
    }
}

/**
 * 現在時刻を文字列で取得
 */
function getNowYMDHMS() {
    var dt = new Date();
    var y = dt.getFullYear();
    var m = ('00' + (dt.getMonth()+1)).slice(-2);
    var d = ('00' + dt.getDate()).slice(-2);
    var h = ('00' + dt.getHours()).slice(-2);
    var mm = ('00' + dt.getMinutes()).slice(-2);
    var s = ('00' + dt.getSeconds()).slice(-2);
    var result = y + m + d + h + mm + s;
    return result;
}

使い方

  1. ドキュメントを開く
  2. スクリプトを実行する
  3. デスクトップに result_20200213000220.txt (数値の部分は一意)といったテキストファイルが生成される

最後に

標準機能でテキストに書き出す機能があったらいいのにね

SwiftUIゴリゴリキャッチアップ会に参加してきた

概要

自分のメモを参加ログとして公開

イベントページ

https://wwdc-gorilla.connpass.com/event/134116/

セッションメモ

over view

  • 見たほうがいいセッション
    • SwiftUI Essentials
    • Data Flow ~
    • Integrating SwiftUI
    • Builting Custom Views in SwiftUI
    • SwiftUI On All Devices
  • キャッチアップ先
    • hackingwithswift.com
    • designcode.io

Unidirectional Data Flow Through SwiftUI

  • 資料
  • SwiftUIはReact+MobXみたいな感じ
    • 単一方向にデータが流れるメリット
      • 疎結合にしやすい
      • テストが書きやすい
        • MVVMはVMをきれいに書けていないとテストがごちゃっとなりやすい
      • スケーラブル
        • 個人の学習コストを少なくアーキテクチャで強制する

SE-XXXX Function Builders を読み解く / Reading the spec of Function Builders

SwiftUI(beta 3)でどこまで書ける(書けない)?

  • beta3 の時点の話なので、将来的に変わる可能性がかなり高い
  • 現時点では結構厳しいところもあるけど、みんなで良くしていきたい
  • 特徴
    • Declarative
      • いい面もあり悪い面もあり
      • 複雑なものは難しいのでは
    • Struct
      • 小さいコンポーネントを組み合わせて構築していく
    • Single Source Of Truth
      • ビジュアルエディターで実装してもすべてコードとして残る
      • コードとして管理できるのがとても良い点
  • がんばって書くのか、頑張っても書けないのかの線引が難しい
    • UIKitで書いたほうがいいところ、SwiftUIで書いたほうがいいところの判断が必要
      • いまのところUIKitメインがいい
    • チュートリアルを全部やってみてできることを学ぶ
      • チュートリアルでできることがSwiftUIでできることの最大限ではないか(現時点かな?)
  • SwiftUI vs UIKit
    • iOS13でできることで比較することになる
    • UICollectionViewCompositionLayout
      • カスタムがしやすくなっている
      • 90%ぐらいはこれで書けるようになる
  • View vs Struct
    • Visual View Hierachy
      • 直接ビューを触ることができない
        • 実際のViewを触ることができない
        • 変換されて描画されるので成果物はさわれない
      • インタラクションをどうする?
        • 複雑なインタラクションがある場合は諦めたほうがいいかも
  • スクロールビューのスクロール量の取得ができないので、カルーセル的なものは厳しい
    • iOS13ならCollectionViewを使ったほうがいいだろう
    • スクロールの内部状態を取得できる口がほしい
  • 続きを読むを押したら文章がすべて表示されるようなものは、やればできる感じ
    • Viewを作り変えるとアニメーションがきれいじゃない
    • 高さを変えるならmodefierでやる
      • lengthにnilを渡せるっぽいぞ
  • エッジに沿うような画像の実装が難しい?
  • サインイン画面はできる
    • テキストフィールドのデリゲートは使いものにならないので、テキストをバインディングしてValidateする
  • Formっぽいものは得意
    • 設定画面とかかな
    • テキストフィールドがあるものは厳しい
      • キーボードでテキストフィールドが隠れてしまう
        • スクロール量を制御できないのでスクロールさせることすらできない
  • SwiftUIのテキストフィールドは日本語が入力できないバグがある
    • 中国語もだめ
  • 増減するフォーム
    • まあまあできる
  • キーボードを隠すこともやりにくい
    • 個別のViewにisEditingが送れない
    • やるならRootViewに対してisEditingに送る
  • Viewの途中の状態を操作することが難しい
    • Staticな画面を作ることにしか使えないのでは

まとめ

  • beta版だから今後に期待って感じかな
  • チュートリアルの完走とセッションの動画を観よう