ごんれのラボ

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

Recap of WWDC19 に参加してきた

イベント詳細

https://mercaridev.connpass.com/event/132676/

セッション内容

Project Catalyst: UIKit on the Mac is finally here! by Tim Oliver

  • iPhoneはmacOSとほぼ同じOSで動くというところが最初にあった
    • 実際はほぼ同じだけど、UI周りが違っている
  • AppKit(1994)、UIKit(2005)
    • UIKitに慣れている人がAppKitに入門するとAPIの違いなどで厳しい
      • わかる
      • NSViewがまず厳しい
  • Marzipanが発表された
    • macOSとiOSが統合されると思われていた
    • Appleが一つのアプリをMarzipanで作成されて公開された。Sneak Peek
  • 2019年にCatalystが登場!
  • 自分でアプリを作って試したみた、のデモ
    • AutoLayoutが聞いてるからmacOSアプリとしてビルドしても、自由自在にウィンドウの大きさを変えられる
    • 画面サイズに縛られないというのも、なるほどという感じだ(自分で変えられるから)
    • フォントサイズも違うらしい
    • 結局iOSアプリをmacOSで動かしている感じなので、ダイアログなどがiOSのものなのでユーザーにわかってしまう
      • macOSのユーザビリティとは一致しないところが多そうだ
  • まとめ
    • 作るの簡単。macにチェックを入れてビルドするだけ
    • Viewの切り分けなどが大変だ
      • まあ、そりゃそうですね…
    • 良いアプリを作ろうね!
      • macOSアプリ作りやすくなる…というわけではない感じだ
      • でも手軽でいいのかも

MVVM with Combine by Akifumi Fukaya

  • 資料
  • combineとは
    • Appleが発表した新しいフレームワーク
    • Reactiveに関する公式なフレームワーク
    • iOS13、watchOS6以上
      • 使えるのは再来年ぐらいかなー
  • 機能
    • Generic
    • Type Safe
    • Comptision first
    • Request driven
  • 構成要素
    • Publishers
  • Publishers
  • Subscribers
    • Publishersから値を受けとることのできるプロトコル
  • Opereater
    • 値をうけたときの振る舞い
    • value type -> Struct
  • MVVMでどう使うか(MVVMが適切かどうかは別の話…)
  • メモとるの、むずい…
    • コード読んだほうが良さそう
  • Runloop.mainってschedulerをセットしているけど、現在schedulerが実装されていないので、サンプルコードのdebounceの部分は動かない
    • schedulerはほぼ実装されていないんだそうな
    • そのうち実装されるのかな
      • じゃないとしんどそう
      • 次のリリースで実装されるっぽい

SwiftUIってこんなやつ Sato Takeshi

  • 資料
  • SwiftUIとは
    • UIKit, AppKitから切り離された新しいレンダリングシステム
    • Pure Swiftで書かれている
    • Previewでタップイベントも使える
  • Appleが公開しているチュートリアルがあるのでおすすめ
    • コードも確認できるよ
  • チュートリアルアプリをみてみよう
    • いままでとは違う書き方が使われている -> Swift5.1の新機能が多数使われている
  • Viewがプロトコルで定義されている
  • これもコードを追うタイプの話だからメモ取りづらい
  • Previewに修正が即座に反映されるの、すごくいいなぁ
    • 実装速度がすごくあがるなぁ、いいなぁ
  • Implicit returns from single-expression functions
  • Function Builder
    • View Bulder

WWDC19 Recap of ML by Akira Fukunaga

  • MLについて
  • Create ML
    • SwiftでML Modelを作れるフレームワーク
    • Xcode10上でGUIで触れた
    • Xcode11では分離したアプリケーションになった
      • Xcodeから遷移できる
    • 9つあるってセッションで言っていたけど、Seed1では2つしか選べない
    • デモ
      • カタリナにするとQuickTimeがスクリーンレコーディングすると落ちるらしい
  • Domain APIs
    • Appleがすでに学習済のモデルを提供してくれているので、それを使う
    • vision と natural language系にくわえて、たくさん追加された
    • Image Saliency
      • 写真の中でより重要、または判別しやすいものをSaliencyが高いというらしい
      • Appleが2つ用意している
        • Attention based
          • 人間が注目しやすそうなもの?
        • Objectness based
          • 前景、後景に関するもの
          • 前処理に使えそう
    • Sentiment analysis
      • ユーザーが入力した文字列を判定して感情を数値化する
      • 日本語はまだ対応していない…
  • Core ML
    • 端末で実行する機械学習
    • サーバと通信しないのでセキュア、という謳い文句
    • Model personalization
      • 端末でモデルをチューニング(微調整)できる
      • たとえば犬を判別するモデルがあったときに自分の犬だけを判別するようにチューニングできる

What's new in UICollectionView by Masaki Haga

  • SwiftUIなどの影でUITableViewとUICollectionViewに大きな変更が入っていた
  • 〜 iOS12 までのUICollectionView
    • perforemBatchUpdateを呼んでData不整合によりCrash
      • DatasourceKit使ったりすると良い
    • 親ViewControllerのSizeにあわせたCellのSize指定がめんどくさい
    • 縦CollectionViewの中に横スクロールのUICollectionViewを何個かいれたいが、Scroll位置の保存などがめんどくさい
  • 新しいUICollectionView
    • 2種類のセッションがあるけど、どっちも必見
      • 昨日TLでも同じ話を聞いたな
  • Data Source
    • [old] UICollectionViewDataSource
    • [NEW] UICollectionViewDiffableDataSourceを使うようになる
  • 新しいData Sourceの宣言
    • UICollectionViewDiffableDataSourceをUICollectionViewとCellProviderを引数に初期化する( Delegate型ではなくClosure型のCallback)
    • snapshotをapplyしてCollectionViewに反映
  • 注意
    • snapshotにわたすScctionの値とItemの値はHashable
    • snapshotはHashValueによって値の同一性を認識する
    • defaultの実装では違うものと認識される(?)
      • hasherにidentifierだけを渡して判定させる
        • たとえばタイトルがかわってもidentifierは変わらないので同一と認識させることができる
  • Layout
    • [old] UIColloctionViewFlowLayout + UIColloctionViewDelegateFlowLayout
    • [NEW] メモしそこなった
      • またNSプレフィックスがでてくるのか…
  • 新しいLayoutの宣言
    • UIColloctionViewCompositionLayout
      • initの引数としてNSCollectionLayoutSectionか、SectionProviderというClosureを渡す
  • Nested Collection View
    • 縦スクロールの中の横スクロールが簡単にかけるようになった
      • 最高じゃん
    • UIKit側で_UICollectionViewOr~的なScrollViewを勝手にいれてくれる
    • 画面外から戻ってきてもとのCollectionViewが保存されている

感想

メモがんばった。
SwiftUIが一番関心があるけど、Catalystも気になるなー。
簡単にmacOSアプリが作れるようになってくれ…。

3DCG Meetup #15 でExtendScriptとCEPの話をしてきた

概要

だいぶ遅くなってしまったけど、勉強会で登壇してきたのでブログに残しておく。

スライド

Speakerdeckで公開している。

おそらく 2019/05/18 時点で、ExtendScript DebuggerからCEPまでを通しで説明して、かつ手順を書いている資料や記事はないはずので、勉強会に参加したひとはもちろん、SNSなどで見かけた人が読んでも伝わるように頑張ってまとめた。
スライドに手順を書くと陳腐化しやすいので避けるのが定石なんだろうけど、あとからスライドを見直したときに手順があると「手を動かして覚える」ことができるかなって思って、あえて手順を載せることにした。

サンプルコード

当日登壇中に行ったデモで利用したコード+時間配分的にデモできなかったコードを、GitHubで公開している。
READMEに各サンプルの概要を記載しているので、気になるものを動かしてほしい。

https://github.com/macneko-ayu/20190518-3dcg-meetup-sample

個人的に気に入っているのは GoogleスプレッドシートとPhotoshopを連携させたCEPのサンプル で、Googleスプレッドシートから値を取得するところはほぼGoogleのサンプルコードをそのまま使っているというお手軽なもの。
あまりに簡単に実装できるのでちょっと拍子抜けしたぐらい。
ただ、デモを動かすのには少し手順が必要なので、後日手順を記載した記事を公開する予定。

登壇した勉強会

登壇したのは 2019/05/18 に開催された 3DCG Meetup #15
「3DCG制作者による情報交換&スキルアップ促進の為の勉強会」ということで、私以外は3DCGに関する登壇が行われていた。
当日はTA職の方やエンジニアの方の参加も多く、主催者の めんたいこ さんやスタッフの方々のフォローのおかげもあり、わりと場の空気に馴染んで気負うことなく登壇できたんじゃないかと思っている。
こういうフォローできる主催者、スタッフの動きは自分のもくもく会でも意識していきたいところ。
そういう面でも勉強になった勉強会だった。

登壇時のQA

記憶違いがあったらごめんなさい。

Q. ExtendScript Debuggerは過去のバージョンでも使える?

私は最新の2018、2019でしか使ってないけど、たぶん使えるんじゃないでしょうか。Adobeさんのさじ加減次第

Q. 自動化するときに100%を目指す?コストはどう考える?

自動化は70%程度を目指す。そこから100%までの30%を詰めるのがしんどくてコストもすごくかかるので、目指さないほうがよい。
自動化は効率化という趣旨で捉えられることが多いけど、もう一つの目的があってそれが「ミスを減らす」こと。
人間はミスをする生き物なので、そのミスを減らすための自動化という手段がある。
ミスを減らすためにコストをかけたほうがよい。

Q. デザイナーなどエンジニアじゃない人に喜ばれたものは?

IllustratorのレイヤーをPNGやJPEGに書き出すもの。
ブログでもたぶん一番見られている。
ワークフローを考えてっていうものよりは、定型作業のちょっとした手間を減らすものがウケがよい。

登壇してみて

登壇までずっと「どうやったら伝わるかなぁ」「スクリプトやCEPを使いたいって人がどれぐらいいるんだろうなぁ。そのうちのどのぐらいに刺さるんだろうなぁ」と悩んでいたんだけど、登壇後にたくさんの人から「参考になった」「ESTKをいまでも使っていて開発中止になったって知らなかったので情報が知れて助かった」「自動化することの目的がわかってよかった」など感想をいただけた。
投げたボールがちゃんと返ってきてよかった。
登壇の機会をいただいためんたいこさん、ありがとうございました。

MacにAdobe CEPの開発環境を構築する with Visual Studio Code

概要

Adobeアプリケーションのエクステンション(CEP:Common Extensibility Platform)をMacで開発するための開発環境を構築する。
過去にBracketsを利用した開発環境構築の記事を書いたが、Adobe公式のVisual Studio CodeプラグインであるExtendScript Debuggerが発表されたので、開発環境をVisual Studio Codeに移行することにし、記事にまとめることにした。

この記事でやること

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

この記事でやらないこと

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

想定する環境

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

開発環境について

開発環境の選定

概要でも述べたがAdobeが今後のスクリプト開発の環境としてVisual Studio Codeを選択した(気まぐれかもしれないが)ので、開発環境はVisual Studio Codeとなる。
Visual Studio CodeはBrackets以上に開発を効率化してくれるプラグインが豊富にあるため、HTML5+CSS+JavaScriptで構成されているCEPの開発にはもってこいだろう。

Visual Studio Codeとは

Visual Studio Codeは、Microsoftが開発したオープンソースのコードエディタ。
azure.microsoft.com

拡張機能が豊富で、自分の使いたい機能を追加、もしくは開発して公開することが可能である。
どんな機能拡張があるかは、下記のサイトを参照。
marketplace.visualstudio.com

開発環境の構築

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

Visual Studio Codeのインストール

前述した公式サイトからダウンロードできる。

  1. 公式サイトから最新版のzipをダウンロードする
  2. ダウンロードしたzipを展開したらそのフォルダ内に Visual Studio Code.app が現れる
  3. アプリケーションをApplicationsディレクトリにコピーする
  4. コピーしたアプリケーションを起動する

Visual Studio Code拡張機能の CC Extension Builder をインストール

marketplace.visualstudio.com

CC Extension Builder は、簡単にCEPのテンプレートプロジェクトを作成することができるプラグイン。

  1. Visual Studio Codeのサイドバーメニューの機能拡張を選択し、検索フィールドにCC Extension Builderと入力する
  2. 検索フィールドで「CEP」と入力、表示された CC Extension Builder をクリックする
  3. Visual Studio Code上で CC Extension Builder のページが開くので、インストールボタンをクリック
  4. さきほどクリックしたインストールボタンがアンインストールボタンになっていればインストール成功

Player Debug Mode の設定

CEPをアプリケーションにインストールするには、本来はCEPのプロジェクトをパッケージしたzxpをインストールする必要がある。
そしてパッケージするためには、証明書が必要だったり、他ツールのインストールが必要だったりするんだけど、その準備がなかなか面倒くさく、かつデバッグのたびにパッケージするのも手間だ。
そこで、デバッグモードを有効にする。

まず、メニューから 表示 > コマンド パレット... を選択して、コマンドパレットを表示する。ショートカットは cmd + shift +p だ。
表示されたパレットの検索フィールドに「cep debug」と入力し、絞り込まれた選択肢の中から「Extension Creator: Enable CEP Debug Mode」を選択し、デバッグモードを有効にする。

デバッグモードを有効にすることでインストールする手間がなくなり、所定のディレクトリにCEPを設置することでデバッグ実行が可能になる。
作成したCEPのプロジェクトを以下のディレクトリに設置し、対象のアプリケーションを再起動すると、アプリケーションに作成したCEPが読み込まれる。

f:id:macneko-ayu:20180529162956p:plain
公式ドキュメントより抜粋

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

CEPの開発環境が構築できているか、サンプルエクステンションを作成して確認する。

サンプルエクステンションの作成手順

  1. メニューから 表示 > コマンド パレット... を選択して、コマンドパレットを表示する
  2. 表示されたパレットの検索フィールドに「create cc」と入力し、絞り込まれた選択肢の中から「Extension Creator: Create a New CC Extension」を選択する
  3. 作成するExtension IDを入力するパレットが表示される。Extension IDはドメインを逆転させた形式となり、デフォルトでは com.example.helloworld と入力されているので、任意のIDを入力してEnterを押す
  4. 続いて作成するExtensitonの名前を入力するパレットが表示される。任意の名前を入力してEnterを押す
  5. 最後に作成するテンプレートの形式を選択する。「basic」「topcoat」「spectrum」「theme」の4種類が選択できる。各テンプレートにどんな差があるかは調べられていないので VSCodeで始めるAdobe CEP Extension開発 を読むと良さそう
  6. 以下のパスにサンプルエクステンションが作成されている。設定は次の項で行う
/Users/(user_name)/Library/Application Support/Adobe/CEP/extensions/com.example.helloworld

サンプルエクステンションの設定

作成したサンプルエクステンションの設定を変えて、InDesignで利用できるようにする。
変更するところは CSXS/manifest.xml にある以下の3つの項目。

  • InDesignのバージョン表記行
  • CEPのバージョン × 2ヶ所

前項で作成したサンプルエクステンションがVisual Studio Codeで開かれていると思うので、CSXS/manifest.xml を開く。

まず、InDesingのバージョン表記行を変更する。
作成された状態ではPhotoshopのみ有効になっているので、InDesignでの読み込み部分のコメントアウトを解除する。行頭の <!-- と行末の --> を削除するか、ショートカットの cmd + / でコメントアウトが解除できる。
そのあとは、エクステンションを利用できるInDesignのバージョンを変更する。 Version="[10.0,99.9]"[10.0,99.9] が「バージョン番号10から99.9まで対象」という意味になっている。
ここではInDesign CC 2018で利用するためにバージョンを [10.0,13.1] に変更する。

続いて、CEPのバージョンを変更する。
<ExtensionManifest Version="9.0" と記載されているところを探して、9.0 の部分を 7.0 に変更する。
そのあと <RequiredRuntime Name="CSXS" Version="9.0" /> と記載されているところを探して、9.0 の部分を 7.0 に変更する。

以上でInDesign 2018で利用できるように設定できた。
変更箇所を以下にまとめておく。

変更前

<!-- <Host Name="IDSN" Version="[10.0,99.9]" /> -->
<ExtensionManifest Version="9.0"
<RequiredRuntime Name="CSXS" Version="9.0" />

変更後

<Host Name="IDSN" Version="[10.0,13.1]" />
<ExtensionManifest Version="7.0"
<RequiredRuntime Name="CSXS" Version="7.0" />

サンプルエクステンションを利用する

  1. InDesign CC 2018を起動する
  2. メニューから ウィンドウ>エクステンション>Hello World を選択
  3. パネルが表示されるので、Call ExtendScript ボタンをクリック
  4. アラートが表示され、hello from ExtendScript というメッセージが表示されていれば成功

注意点

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

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

公式ドキュメント

アプリケーションのIDや対応しているCEPのバージョン、各アプリケーションのCEP上でのバージョンなどはGitHubのリポジトリから参照できる。 github.com

最後に

出遅れた感はあるが、Visual Studio CodeでCEP開発を行うための環境構築について簡単にまとめた。
筆者自身は最近CEPを書いていないので情報が不足している部分も多いと思うので、このブログを参照して不足部分を補った記事を公開して筆者に教えていただけると、とても嬉しい。

+DESIGNING vol.47の「ラクラボ。/知れば知るほどラクになるスクリプトのススメ[AppleScript編]」の執筆を担当しました

概要

本日(2019/03/29)発売の+DESIGNING vol.47の企画ページ、「ラクラボ。/知れば知るほどラクになるスクリプトのススメ[AppleScript編]」の執筆を担当をしました。

公式サイトは以下。
www.plus-designing.jp

サイトでは各企画の見開き状態の画像が見られ、私が執筆したラクラボ。も一見開き分だけ見られます。

書店、Amazonなどでぜひお買い求めください。

きっかけ

+DESIGNINGの編集長である小林さんと著者陣の一人である村上さんから、「AppleScriptについての記事をラクラボ。で書いてみませんか?」とお話をいただいたことがきっかけでした。
いままで編集はしたことがあっても商業誌の原稿を書くという経験はなかったので、二つ返事でお請けしました。

記事の内容

ターゲット層を絞る必要があり、スクリプト初心者向けの導入となる内容にするか、一歩先に進んだ初級〜中級者向けの内容にするか、かなり悩みました。
vol.46のラクラボ。で、友人の id:uske-s さんがスクリプトの導入的な記事を書いており、そこから先に進むための後押しができるような内容にもしたいと思いました。
最終的にはスクリプト初心者向けの内容にしたのですが、機会があればAppleScriptとJavaScriptで同じ処理を行うスクリプトを書いて比較するといったものも書いてみたいですね(需要があるのか…)

執筆の進め方

原稿はDropbox Paperで適宜確認できる形で進めるという方針でした。

私はいきなり文章を書くことができないタイプなので、登壇資料と同様にMarkdownでざっくりと草案を書くことにしました。

書きたいことに沿った適当な見出し、たとえば「AppleScriptとは」「短めのサンプルコード書く」などといったものでブロック分けして、そのあと文章を意識しないで箇条書きでざっと要素を洗い出しました。
その草案内にキャプチャを差し込んだり、サンプルコードを差し込んだりして、全体の内容を決めます。
そのあと見直しを行って粗目に編集、たとえばてにをはだったり、誤字脱字の調整作業を行います。 これで草案は完成なので、この時点のものを編集さんに確認していただきました。

最後に草案をもとに文章化して、構成のおかしいところの調整作業を行って完成です。

編集さんにはひと手間おかけてしまいましたが、草案の時点で指摘をしていただくことで精度をあげられたと思っています。

最後に

執筆の機会を与えていただき、小林さん、村上さん、ありがとうございました。

また、この記事を読んだ方がAppleScript…じゃなくてもいいのでスクリプトに興味をもっていただけると幸いです。
AppleScriptは独特な構文ということもあり敬遠される方も多いかもしれませんが、Macを使うならば使えるようになって損はしないと思います。

読んでくれたみなさんもAppleScriptの記事を書いてくれよな!

おまけ

DTPerのスクリプトもくもく会もよろしくお願いします!

dtpmkmk.connpass.com

明日3/30にもイベントを開催しますので、そちらもぜひご参加ください!

dtpmkmk.connpass.com

ExtendScript Debugger + TypeScript で ExtendScript を書くためのテンプレートを作った

概要

ExtendScript DebuggerというVisual Studio CodeプラグインがAdobeからリリースされた。
いままで利用されていたExtendScript Toolkitの開発が凍結されたので、以降の開発はこのExtendScript Debuggerを利用して行うことになった。
その都度環境を作るのが面倒で自分用に雑にテンプレートを作ったので、ついでにGitHubで公開することにした。

ExtendScript Debuggerの導入

Adobe Tech BlogのExtendScript Debugger for Visual Studio Code Public Release を読む。
そのあと、id:uske_S さんのExtendScript Debugger for VSCodeがリリースされたので簡単にまとめてみたを読む。

仕様

  • 型定義としてten-A/Types-for-Adobeを利用した
  • TypeScriptで書けるようにした
  • 各種configは暫定的な内容になっている。随時更新予定

使い方

$ npm install -g typescript # when not installed typescript
$ git clone git@github.com:macneko-ayu/template-for-extendscript-using-typescript.git
$ cd template-for-extendscript-using-typescript
$ npm install 
$ npm run watch

src/app.ts を変更して保存すると、dist/ の中に app.js が書き出される。
書き出された app.js をVisual Studio Codeで実行するか、アプリケーションから実行する。

ソースコード

https://github.com/macneko-ayu/template-for-extendscript-using-typescript

Swiftで文字列の輪郭に線をつける(UIBezierPath版)

概要

Swiftで文字列の輪郭に線をつける という記事を書いたんだけど、仕上がりに納得がいかなかったので別のアプローチで実現してみた。

ゴール

Illustratorのアピアランス機能で作って画像化したものと近い感じにする。

illustrator

問題点

  • NSAttributedStringattributes で直接線をつけていたため、線の太さに応じて塗りの範囲が狭くなってしまった
  • 文字が角ばり、丸くできない

実現方法

文字を UIBezierPath に変換して描画するようにした。
実装は 【iOS】テキストの輪郭パスを取得する を参考にさせていただき、Swiftに書き直した。
あわせて TextOutlineShapeView というカスタムViewを作って、その中で描画するようにした。
構造としては文字列を UIBezierPath に変換して、そのPathをセットした CAShapeLayer を3つ重ねて、それぞれ「塗り」「線」「影」の設定を適用した。

TextOutlineShapeView の実装は以下の通り。

import UIKit

class TextOutlineShapeView: UIView {
    struct TextOptions {
        let text: String
        let font: UIFont
        let lineSpacing: CGFloat
        let textAlignment: NSTextAlignment
    }

    struct ShapeOptions {
        let lineJoin: CAShapeLayerLineJoin
        let fillColor: CGColor
        let strokeColor: CGColor
        let lineWidth: CGFloat
        let shadowColor: CGColor
        let shadowOffset: CGSize
        let shadowRadius: CGFloat
        let shadowOpacity: Float
    }

    private var textOptions: TextOptions?
    private var shapeOptions: ShapeOptions?

    init(textOptions: TextOptions, shapeOptions: ShapeOptions) {
        self.textOptions = textOptions
        self.shapeOptions = shapeOptions
        super.init(frame: .zero)
        configure()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

extension TextOutlineShapeView {
    private func configure() {
        guard let textOptions = self.textOptions,
            let shapeOptions = self.shapeOptions else {
                return
        }
        let textArray = textOptions.text.components(separatedBy: "\n")
        let attibutes: [NSAttributedString.Key: Any] = [.font: textOptions.font]
        let attibutedStrings = textArray.map { text -> NSAttributedString in
            return NSAttributedString(string: text, attributes: attibutes)
        }
        guard let bezier = makePathfromText(attibutedStrings: attibutedStrings, lineSpacing: textOptions.lineSpacing, textAlignment: textOptions.textAlignment) else { return }
        bezier.flip(direction: .y)

        let shadowLayer = CAShapeLayer()
        shadowLayer.lineJoin = shapeOptions.lineJoin
        shadowLayer.frame = self.bounds
        shadowLayer.shadowPath = bezier.cgPath
        shadowLayer.shadowColor = shapeOptions.shadowColor
        shadowLayer.shadowOffset = shapeOptions.shadowOffset
        shadowLayer.shadowRadius = shapeOptions.shadowRadius
        shadowLayer.shadowOpacity = shapeOptions.shadowOpacity
        self.layer.addSublayer(shadowLayer)

        let strokeLayer = CAShapeLayer()
        strokeLayer.lineJoin = shapeOptions.lineJoin
        strokeLayer.frame = self.bounds
        strokeLayer.strokeColor = shapeOptions.strokeColor
        strokeLayer.lineWidth = shapeOptions.lineWidth
        strokeLayer.path = bezier.cgPath
        self.layer.addSublayer(strokeLayer)

        let fillLayer = CAShapeLayer()
        fillLayer.lineJoin = shapeOptions.lineJoin
        fillLayer.frame = self.bounds
        fillLayer.fillColor = shapeOptions.fillColor
        fillLayer.path = bezier.cgPath
        self.layer.addSublayer(fillLayer)
    }

    private func makePathfromText(attibutedStrings: [NSAttributedString], lineSpacing: CGFloat, textAlignment: NSTextAlignment) -> UIBezierPath? {
        let path = UIBezierPath()
        path.move(to: .zero)

        var maxWidth: CGFloat = 0
        for attibutedString in attibutedStrings {
            let line = CTLineCreateWithAttributedString(attibutedString)
            let rect = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
            let width = rect.width

            if maxWidth < width {
                maxWidth = width
            }
        }

        for (i, attibutedString) in attibutedStrings.reversed().enumerated() {
            let letters = CGMutablePath()

            let line = CTLineCreateWithAttributedString(attibutedString)
            let rect = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
            let width = rect.width

            var margin: CGFloat = 0
            switch textAlignment {
            case .center:
                margin = (maxWidth - width) / 2
            case .right:
                margin = maxWidth - width
            default:
                break
            }

            let runArray: [CTRun] = cfArraytoArray(sourceArray: CTLineGetGlyphRuns(line))
            for run in runArray {
                let fontPointer = CFDictionaryGetValue(CTRunGetAttributes(run), Unmanaged.passUnretained(kCTFontAttributeName).toOpaque())
                let runFont = unsafeBitCast(fontPointer, to: CTFont.self)

                for index in 0..<CTRunGetGlyphCount(run) {
                    let thisGlyphRange = CFRange(location: index, length: 1)
                    var glyph = CGGlyph()
                    var position = CGPoint()
                    CTRunGetGlyphs(run, thisGlyphRange, &glyph)
                    CTRunGetPositions(run, thisGlyphRange, &position)

                    guard let letter = CTFontCreatePathForGlyph(runFont, glyph, nil) else { continue }
                    let px = position.x + margin
                    let py = position.y + path.bounds.height + ((i == 0) ? 0 : lineSpacing)
                    let t = CGAffineTransform(translationX: px, y: py)
                    letters.addPath(letter, transform: t)
                }
            }

            path.append(UIBezierPath(cgPath: letters))
        }

        return path
    }

    private func cfArraytoArray<T>(sourceArray: CFArray) -> [T] {
        var destinationArray = [T]()
        let count = CFArrayGetCount(sourceArray)
        destinationArray.reserveCapacity(count)
        for index in 0..<count {
            let untypedValue = CFArrayGetValueAtIndex(sourceArray, index)
            let value = unsafeBitCast(untypedValue, to: T.self)
            destinationArray.append(value)
        }
        return destinationArray
    }
}

最初実装したときになぜか上下反転して表示されてしまった。
why

transformがうまくいっていなかったようなので、 UIBezierPathのextensionを実装した。
extensionの実装は以下の通り。

import UIKit

extension UIBezierPath {
    enum InvertDirection {
        case none
        case x
        case y
        case both
    }

    func flip(direction: InvertDirection) {
        let rect = self.bounds
        switch direction {
        case .none:
            break
        case .x:
            self.apply(CGAffineTransform(translationX: -rect.origin.x, y: 0))
            self.apply(CGAffineTransform(scaleX: -1, y: 1))
            self.apply(CGAffineTransform(translationX: rect.origin.x + rect.width, y: 0))
        case .y:
            self.apply(CGAffineTransform(translationX: 0, y: -rect.origin.y))
            self.apply(CGAffineTransform(scaleX: 1, y: -1))
            self.apply(CGAffineTransform(translationX: 0, y: rect.origin.y + rect.height))
        case .both:
            self.apply(CGAffineTransform(translationX: -rect.origin.x, y: -rect.origin.y))
            self.apply(CGAffineTransform(scaleX: -1, y: -1))
            self.apply(CGAffineTransform(translationX: rect.origin.x + rect.width, y: rect.origin.y + rect.height))
        }
    }
}

使い方

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // For example
        let str = "我が家の猫は7.4kg\nゆえに重い"
        guard let font = UIFont(name: "HiraKakuProN-W6", size: 36) else { return }

        let textOptions = TextOutlineShapeView.TextOptions(text: str,
                                                           font: font,
                                                           lineSpacing: 10,
                                                           textAlignment: .left)
        let shapeOptions = TextOutlineShapeView.ShapeOptions(lineJoin: .round,
                                                             fillColor: UIColor.white.cgColor,
                                                             strokeColor: UIColor.blue.cgColor,
                                                             lineWidth: 5,
                                                             shadowColor: UIColor.black.cgColor,
                                                             shadowOffset: CGSize(width: 3, height: 3),
                                                             shadowRadius: 5,
                                                             shadowOpacity: 0.6)
        let shapeView = TextOutlineShapeView(textOptions: textOptions, shapeOptions: shapeOptions)
        self.view.addSubview(shapeView)

        shapeView.translatesAutoresizingMaskIntoConstraints = false
        let top = shapeView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10)
        let bottom = shapeView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 10)
        let leading = shapeView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 10)
        let trailing = shapeView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 10)
        NSLayoutConstraint.activate([top, bottom, leading, trailing])
    }
}

sample

いい感じ。
実用するなら描画後に画像化したほうがパフォーマンスがいいと思う。

大きく表示したサンプル

角の処理結果が見づらいので、文字サイズを大きくした。

NSAttributedString で作ったもの。美しくない…。
bad

UIBezierPath で作ったもの。美しい。
good

ソースコード

https://github.com/macneko-ayu/TextOutlineShapeView

Swiftで文字列の輪郭に線をつける

概要

Swiftで袋文字を実現したいという話を聞いて、調べたら実現方法がさっくり見つかったので残しておくことにした。

実現方法

UILabelextension を定義して、そのメソッドを使うだけ。
参考記事のままだとエラーになるのでよしなに修正してある。

extension UILabel{

    /// makeOutLine
    ///
    /// - Parameters:
    ///   - strokeWidth: 線の太さ。負数
    ///   - oulineColor: 線の色
    ///   - foregroundColor: 縁取りの中の色
    func makeOutLine(strokeWidth: CGFloat, oulineColor: UIColor, foregroundColor: UIColor) {
        let strokeTextAttributes = [
            .strokeColor : oulineColor,
            .foregroundColor : foregroundColor,
            .strokeWidth : strokeWidth,
            .font : self.font
        ] as [NSAttributedString.Key : Any]
        self.attributedText = NSMutableAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
    }
}

見た目があまり美しくないので、静的な用途であればIllustratorなどのアプリケーションでSVG書き出ししたものを使うと良い。

Playground

Playgroundにコードをコピペして実行するとどんな感じが確認できる。

import UIKit

extension UILabel{

    /// makeOutLine
    ///
    /// - Parameters:
    ///   - strokeWidth: 線の太さ。負数
    ///   - oulineColor: 線の色
    ///   - foregroundColor: 縁取りの中の色
    func makeOutLine(strokeWidth: CGFloat, oulineColor: UIColor, foregroundColor: UIColor) {
        let strokeTextAttributes = [
            .strokeColor : oulineColor,
            .foregroundColor : foregroundColor,
            .strokeWidth : strokeWidth,
            .font : self.font
        ] as [NSAttributedString.Key : Any]
        self.attributedText = NSMutableAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
    }
}

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 500, height: 60))
label.font = UIFont.boldSystemFont(ofSize: 50)
label.text = "我が家の猫は7.4kg"
label.makeOutLine(strokeWidth: -2.0, oulineColor: .white, foregroundColor: .blue)

キャプチャ

uilabel-outline

おまけ

NSAttributedString.Key でいろいろ装飾できるので、ドキュメントを読むと面白い。
NSAttributedString.Key

iOSでチラシレイアウトも実現できる。需要はない。
iOSでチラシっぽい価格レイアウトを再現してみた

参考

元ネタ

Custom Label Effects in Swift 4

個人的に毎度読むスライド

最高
Mastering Textkit