<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>KAKELOG</title>
    <description>Kakera Games の技術ブログ。ゲーム制作に関するメモを投稿します。主にプログラミング。</description>
    <link>https://blog.kakeragames.com/</link>
    <atom:link href="https://blog.kakeragames.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sat, 07 Mar 2026 09:24:36 +0900</pubDate>
    <lastBuildDate>Sat, 07 Mar 2026 09:24:36 +0900</lastBuildDate>
    <generator>Jekyll v4.3.2</generator>
    
      <item>
        <title>Getting Started p5.js in TypeScript</title>
        <description>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://p5js.org/&quot;&gt;p5.js&lt;/a&gt; is a JavaScript port of &lt;a href=&quot;https://processing.org/&quot;&gt;Processing&lt;/a&gt; which is one of the most famous and easy to write frameworks for creative coding.
Though it is so powerful as-is, we can unleash developer experience such as code completion by using p5.js in TypeScript.&lt;/p&gt;

&lt;p&gt;In this post, we will explore a minimum configuration to get started developing a p5 sketch in TypeScript.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Install p5 and types by npm&lt;/li&gt;
  &lt;li&gt;Use &lt;a href=&quot;https://parceljs.org/&quot;&gt;Parcel&lt;/a&gt; to compile TS and run a dev web server&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;installing-packages&quot;&gt;Installing packages&lt;/h2&gt;

&lt;p&gt;First of all, create a default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; file and add the following Node packages.&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm init &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm i &lt;span class=&quot;nt&quot;&gt;--save&lt;/span&gt; p5
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm i &lt;span class=&quot;nt&quot;&gt;--save-dev&lt;/span&gt; @types/p5
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;npm i &lt;span class=&quot;nt&quot;&gt;--save-dev&lt;/span&gt; parcel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this post, we use &lt;a href=&quot;https://parceljs.org/&quot;&gt;Parcel&lt;/a&gt; to get the following benefits without writing much configurations.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Compiling TypeScript codes&lt;/li&gt;
  &lt;li&gt;Building HTML that references the compiled codes&lt;/li&gt;
  &lt;li&gt;Hosting a dev web server&lt;/li&gt;
  &lt;li&gt;Watching changes in the source codes to trigger re-compile &amp;amp; re-build&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;creating-the-first-sketch&quot;&gt;Creating the first sketch&lt;/h2&gt;

&lt;p&gt;Create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/index.html&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/sketch.ts&lt;/code&gt; files. Examples are as follows.&lt;/p&gt;

&lt;h3 id=&quot;srcsketchts&quot;&gt;src/sketch.ts&lt;/h3&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;p5&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;p5&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sketch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;p5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createCanvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;draw&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#3178c6&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;p5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sketch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;p5.js has “instance mode” which is explained in the &lt;a href=&quot;https://p5js.org/reference/#/p5/p5&quot;&gt;reference&lt;/a&gt; to avoid adding p5 functions to global scope.
Instead of declaring &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup()&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;draw()&lt;/code&gt; functions globaly, we can instantiate a new insatance of p5 with a runnable function.&lt;/p&gt;

&lt;h3 id=&quot;srcindexhtml&quot;&gt;src/index.html&lt;/h3&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Sketch&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;module&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sketch.ts&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;main&amp;gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/index.html&lt;/code&gt; references the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.ts&lt;/code&gt; version of the code.
Parcel will compile the TypeScript code and output a HTML that references the compiled JavaScript code.&lt;/p&gt;

&lt;p&gt;Also, the script tag has an attribute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=&quot;module&quot;&lt;/code&gt; that is required to use ES modules and CommonJS syntax for exports and imports in the referenced script.
This behavior is described in the &lt;a href=&quot;https://parceljs.org/getting-started/migration/#%3Cscript-type%3D%22module%22%3E&quot;&gt;official doc for migration&lt;/a&gt; from V1 to V2.&lt;/p&gt;

&lt;h2 id=&quot;running-dev-server&quot;&gt;Running dev server&lt;/h2&gt;

&lt;p&gt;Add a new property &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;soruce&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; file.
This indicates the entry point of the page, and Parcel uses this for the target of the commands.&lt;/p&gt;

&lt;p&gt;And add a new script in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts&lt;/code&gt; for short hand to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parcel&lt;/code&gt; command.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;src/index.html&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dev&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;parcel --port 8080&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With this script, a dev server can be launched by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$ npm run dev&lt;/code&gt; on port 8080.&lt;/p&gt;

&lt;p&gt;The sketch will be running on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:8080&lt;/code&gt; on a web browser.
And also, any changes in the source files will trigger auto re-build and reload of the page.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;p5.js and the definition of its types could be installed by npm.
And by using Parcel, we could set up TS dev environment with a little configuration.&lt;/p&gt;

&lt;p&gt;The code completion with TypeScript is so powerful, and I can not back to pure JS ever.&lt;/p&gt;

&lt;p&gt;:see_no_evil:&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;p5.js
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://p5js.org/&quot;&gt;home | p5.js&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://p5js.org/reference/#/p5/p5&quot;&gt;reference | p5()&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Parcel
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://parceljs.org/&quot;&gt;Parcel&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://parceljs.org/getting-started/migration/#%3Cscript-type%3D%22module%22%3E&quot;&gt;Migration&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 20 Feb 2022 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2022/02/20/p5-typescript.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2022/02/20/p5-typescript.html</guid>
        
        
      </item>
    
      <item>
        <title>Unity AR - 平面の上でキャラクターを歩かせる</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ar-plane-walker/ogp.jpg&quot; alt=&quot;OGP&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/2021/01/13/unity-ios-ar-getting-started.html&quot;&gt;前回&lt;/a&gt;までで AR シーンの中で平面検出ができるようになったので，今回はその平面の上でキャラクターを歩かせてみる．
歩くロジックはとても単純かつ雑なので，そんなに参考にはならないかもしれない．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;人型のモデルに Animator を設定して歩くアニメーションを再生する&lt;/li&gt;
  &lt;li&gt;ARPlane の FilterMesh から頂点を取得して，そこに向かって並行移動させる&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Unity 2020.1.17f1&lt;/li&gt;
  &lt;li&gt;AR Foundation 4.1.1&lt;/li&gt;
  &lt;li&gt;ARKit XR Plugin 4.1.1&lt;/li&gt;
  &lt;li&gt;iOS 14.3&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;アニメーションを再生する&quot;&gt;アニメーションを再生する&lt;/h2&gt;

&lt;p&gt;まずは通常のシーンのなかでキャラクターにアニメーションを設定して再生する．
モデルは出来合いのものを使っていく．
今回は Unity 公式の &lt;a href=&quot;https://assetstore.unity.com/packages/3d/characters/robots/space-robot-kyle-4696&quot;&gt;Space Robot Kyle&lt;/a&gt; を使う．&lt;/p&gt;

&lt;p&gt;シーンに配置してスケールを調整しておく．
机の上を動き回る小さなロボにしたいので，XYZ それぞれ調整して Transform でネストしておく．
ルートとなる Transform が &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(x,y,z) = (0,0,0)&lt;/code&gt; のときにモデルが接地しているように気を付ける．&lt;/p&gt;

&lt;p&gt;Kyle に含まれるのは 3D モデルとテクスチャのみで，アニメーションは設定されていないので，自前で設定していく．
といっても Animation Clip から作っていると終わらないので，Asset Store から良さそうなものを探して使っていく．
今回は &lt;a href=&quot;https://assetstore.unity.com/packages/3d/animations/basic-motions-free-pack-154271&quot;&gt;Basic Motions FREE Pack&lt;/a&gt; を使わせてもらう．&lt;/p&gt;

&lt;p&gt;Kyle のモデルはデフォルトでは Rig が &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Legacy&lt;/code&gt; になっているので，これを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Humanoid&lt;/code&gt; に変更する．
Apply を実行すればモデルを元に自動で rigging され，Animator に対応した Avatar が作られる．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Animation Type&lt;/th&gt;
      &lt;th&gt;Rigging Result&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img alt=&quot;model-rigging.png&quot; src=&quot;/images/pages/unity-ar-plane-walker/model-rigging.png&quot; width=&quot;640&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img alt=&quot;model-rigging-result.png&quot; src=&quot;/images/pages/unity-ar-plane-walker/model-rigging-result.png&quot; width=&quot;640&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;small&gt;Fig. Rigging の結果&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;あとは Kyle の Animator コンポーネントに Basic Motions Pack に含まれる適当な Animator Controller を設定すれば Kyle をアニメーションさせることができる．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ar-plane-walker/walking-animation.gif&quot; alt=&quot;walking-animation.gif&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Fig. 歩くアニメーション&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;アニメーションの状態を遷移させる&quot;&gt;アニメーションの状態を遷移させる&lt;/h2&gt;

&lt;p&gt;せっかくなので Animator Controller を作って状態遷移を定義し，Basic Motions Pack に含まれるいくつかのアニメーションを使えるようにしてみる（追記：でも歩くアニメーション以外は結局使わなかったという）．&lt;/p&gt;

&lt;p&gt;ここでは，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Idle&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Walking&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running Backward&lt;/code&gt; の4つの状態を定義する．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ar-plane-walker/walking-state.png&quot; alt=&quot;walking-state.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Fig. Animator (ステートマシン)&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Parameters には &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;velocity&lt;/code&gt; を追加して，状態遷移の条件（Conditions）に使用する．
適当にスクリプトを書いて Inspector から &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;velocity&lt;/code&gt; を手動で変更できるようにしてシーンを実行してみる．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;UnityEngine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;RequireComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Walker&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MonoBehaviour&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AnimatorVelocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;StringToHash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;velocity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Animator&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;velocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SetInteger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AnimatorVelocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;velocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ar-plane-walker/walking-state-test.gif&quot; alt=&quot;walking-state-test.gif&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Fig. アニメーションの切り替え&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;キャラクターの位置を動かす&quot;&gt;キャラクターの位置を動かす&lt;/h2&gt;

&lt;p&gt;歩くというアニメーションは Animator で再生できるようになったので，そのアニメーションに合わせてそれらしい速度で平行移動させる．
平面（Plane）のメッシュを与えて，その中の頂点（Vertex）をひとつランダムで選び，そこに向かって移動させる．&lt;/p&gt;

&lt;p&gt;Mesh の各頂点は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vertices&lt;/code&gt; で取得できる．
頂点の座標は Mesh に対するローカル座標なので， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TransformPoint()&lt;/code&gt; でワールド座標に変換する．
目的地の座標が取得できたら，あとは Kyle が目的地を向くように回転させて並行移動させる．
同時にアニメーションを再生して，歩いて移動しているっぽく見せる．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;UnityEngine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;RequireComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Walker&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MonoBehaviour&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AnimatorVelocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;StringToHash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;velocity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Animator&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;velocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;walkingSpeed&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.18f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;runningSpeed&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.54f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MeshFilter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Vector3&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Vector3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;animator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SetInteger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AnimatorVelocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;velocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;distance&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Vector3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Distance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;distance&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.001f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sharedMesh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

            &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)];&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;_destination&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;TransformPoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformVelocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.0f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;velocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformVelocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;runningSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;velocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformVelocity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;walkingSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformLerp&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deltaTime&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformVelocity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;distance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Vector3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Lerp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformLerp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;LookAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;up&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;適当なスクリプトなので，変数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ground&lt;/code&gt; として MeshFilter 型を取るものの，平面じゃない MeshFilter を与えられたら Kyle が地面にめりこんでしまうはずだが，今回は平面しか対象にしないので気にしないこととする．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ar-plane-walker/walking-test.gif&quot; alt=&quot;walking-test.gif&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Fig. Plane の上を歩くテスト&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;ar-シーンの中で動かす&quot;&gt;AR シーンの中で動かす&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/2021/01/13/unity-ios-ar-getting-started.html&quot;&gt;前回&lt;/a&gt;は検出した平面の上に樹木のモデルを置いていたが，同様に Kyle を置いてみる．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raycastManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Raycast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;touch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_raycastHits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TrackableType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PlaneWithinPolygon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_raycastHits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;First&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trackable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARPlane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Instantiate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GameObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arObjectPrefab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;walker&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Walker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trackable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MeshFilter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;walker&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;walker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SetGround&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;walker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SetWalking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;置くものは結局のところ GameObject なので &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Spawn()&lt;/code&gt; する &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arObjectPrefab&lt;/code&gt; を Kyle に差し替えるだけ．
ARPlane には &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MeshFilter&lt;/code&gt; が付いているはずなので，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetComponent()&lt;/code&gt; して地面として設定する．&lt;/p&gt;

&lt;p&gt;ついでに prefab を切り替えられるボタンを適当に作って，色々と置いてみる．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ar-plane-walker/walking-on-ar-plane.gif&quot; alt=&quot;walking-on-ar-plane.gif&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Fig. ARPlane の上を歩く&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;という感じで ARPlane の上を歩けるようになった．&lt;/p&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;p&gt;AR シーンの中で検出した平面の上でキャラクターを歩かせてみた．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;キャラクターは人型のアセット（具体的には &lt;a href=&quot;https://assetstore.unity.com/packages/3d/characters/robots/space-robot-kyle-4696&quot;&gt;Space Robot Kyle&lt;/a&gt;）を使用した&lt;/li&gt;
  &lt;li&gt;人型のアセットに歩くアニメーションを設定した（具体的には &lt;a href=&quot;https://assetstore.unity.com/packages/3d/animations/basic-motions-free-pack-154271&quot;&gt;Basic Motions FREE Pack&lt;/a&gt;）&lt;/li&gt;
  &lt;li&gt;Animator を組んで，状態遷移できるようにした&lt;/li&gt;
  &lt;li&gt;ARPlane から取得した MeshFilter の上を歩くようにした&lt;/li&gt;
  &lt;li&gt;MeshFilter からランダムに頂点（vertices）を取得して，それを目的地として歩くようにした&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity というゲームエンジンや Asset Store というエコシステムを使っているだけなので，技術的にどうこうということはなかった．&lt;/p&gt;

&lt;p&gt;というか，いまさらだが Terrain の上を歩かせる方法とか普通にある気がする（が気にしないこととする）．&lt;/p&gt;

&lt;p&gt;:dancer:&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Manual/class-AnimatorController.html&quot;&gt;Unity - Manual: Animator Controller&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Manual/class-MeshFilter.html&quot;&gt;Unity - Manual: Mesh Filter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assetstore.unity.com/packages/3d/characters/robots/space-robot-kyle-4696&quot;&gt;Space Robot Kyle | 3D Robots | Unity Asset Store&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assetstore.unity.com/packages/3d/animations/basic-motions-free-pack-154271&quot;&gt;Basic Motions FREE Pack | 3D Animations | Unity Asset Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 20 Jan 2021 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2021/01/20/unity-ar-plane-walker.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2021/01/20/unity-ar-plane-walker.html</guid>
        
        
      </item>
    
      <item>
        <title>Unity iOS の AR アプリ開発をはじめる</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-ar-getting-started/ogp.jpg&quot; alt=&quot;OGP&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Unity iOS で AR アプリの開発をはじめるためのセットアップ方法のメモ．
AR Foundation の提供する仕組みの範囲で，簡単な平面検出と 3D モデルの配置をやってみる．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;p&gt;Unity iOS で AR アプリ開発ことはじめとして，次のことをやってみる．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;AR Foundation &amp;amp; ARKit をセットアップする&lt;/li&gt;
  &lt;li&gt;AR Plane Manager で平面検出し，デフォルトのビジュアライザで平面を表示する&lt;/li&gt;
  &lt;li&gt;AR Raycast Manager で AR Plane のどこをタッチしたのか判定する&lt;/li&gt;
  &lt;li&gt;タッチした場所に適当な 3D オブジェクトを表示する&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Unity 2020.1.17f1&lt;/li&gt;
  &lt;li&gt;AR Foundation 4.1.1&lt;/li&gt;
  &lt;li&gt;ARKit XR Plugin 4.1.1&lt;/li&gt;
  &lt;li&gt;iOS 14.3&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;unity-で-arkit-を使う準備をする&quot;&gt;Unity で ARKit を使う準備をする&lt;/h2&gt;

&lt;h3 id=&quot;パッケージのインポート&quot;&gt;パッケージのインポート&lt;/h3&gt;

&lt;p&gt;まずは Unity で AR を使うための準備をする．
Package Manager を開いて，次のパッケージをインポートする．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;AR Foundation (Version 4.1.1)&lt;/li&gt;
  &lt;li&gt;ARKit XR Plugin (Version 4.1.1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity はマルチプラットフォームの AR アプリ開発をサポートしている．
AR Foundation は各プラットフォームをまとめて抽象化するラッパーで，各プラットフォームごとの実装は plugin として分離されている．&lt;/p&gt;

&lt;h3 id=&quot;arkit-の有効化&quot;&gt;ARKit の有効化&lt;/h3&gt;

&lt;p&gt;インポートが完了したら Project Settings を開いて ARKit を有効化する．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;XR Plug-in Management
    &lt;ul&gt;
      &lt;li&gt;iOS &amp;gt; Plug-in Providers &amp;gt; ARKit にチェックを入れる&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;XR Plug-in Management &amp;gt; ARKit
    &lt;ul&gt;
      &lt;li&gt;Requirement を Required にする&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Player &amp;gt; iOS &amp;gt; Other Settings
    &lt;ul&gt;
      &lt;li&gt;Target minimum iOS Version を適当なものにする&lt;/li&gt;
      &lt;li&gt;Requires ARKit support にチェックを入れる&lt;/li&gt;
      &lt;li&gt;Camera Usage Description に適当な文言を入れる&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;ar-session-の配置&quot;&gt;AR Session の配置&lt;/h3&gt;

&lt;p&gt;Scene に &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AR Session&lt;/code&gt; と &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AR Session Origin&lt;/code&gt; を配置する．ただそれだけ．&lt;/p&gt;

&lt;h3 id=&quot;ar-session-state-の表示&quot;&gt;AR Session State の表示&lt;/h3&gt;

&lt;p&gt;Session が動いているのか分かるように，状態を画面に表示しておく．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;System.Collections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;UnityEngine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;UnityEngine.XR.ARFoundation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AppController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MonoBehaviour&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARSession&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Text&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sessionStateText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Awake&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ARSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stateChanged&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OnARStateChanged&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;IEnumerator&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ARSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARSessionState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARSessionState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CheckingAvailability&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CheckAvailability&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ARSession&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARSessionState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unsupported&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;OnARStateChanged&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ARSessionStateChangedEventArgs&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sessionStateText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ToString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ここまでできたら iOS アプリをビルドして実機で実行してみる．
カメラが表示できて，Session の状態が &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SessionTracking&lt;/code&gt; になっていれば成功しているはず．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-ar-getting-started/session-tracking.png&quot; alt=&quot;session-tracking.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;平面を検出して表示する&quot;&gt;平面を検出して表示する&lt;/h2&gt;

&lt;p&gt;試しに平面を検出して表示してみる．&lt;/p&gt;

&lt;p&gt;AR Foundation は何種類かの &lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/trackable-managers.html&quot;&gt;Trackable Managers&lt;/a&gt; を提供している．
その中の &lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/plane-manager.html&quot;&gt;AR Plane Manager&lt;/a&gt; を使うと，その名のとおり平面の検出ができる．&lt;/p&gt;

&lt;p&gt;Plane Manager の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Plane Prefab&lt;/code&gt; には任意の prefab を設定できるが，今回は出来合いの &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AR Default Plane&lt;/code&gt; を使う．GameObject &amp;gt; XR &amp;gt; AR Default Plane を一旦 scene に配置してから prefab 化し，prefab を Plane Manager に設定する．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-ar-getting-started/ar-plane-manager.png&quot; alt=&quot;ar-plane-manager.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;検出された平面にポリゴンが表示されるようになる．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-ar-getting-started/plane-tracking.png&quot; alt=&quot;plane-tracking.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;平面をタッチしてオブジェクトを置く&quot;&gt;平面をタッチしてオブジェクトを置く&lt;/h2&gt;

&lt;p&gt;試しに適当なオブジェクトを平面の上に置いてみる．&lt;/p&gt;

&lt;p&gt;いわゆる Raycasting が必要になるが，AR Foundation の提供する &lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/raycast-manager.html&quot;&gt;AR Raycast Manager&lt;/a&gt; を使えば簡単にできる．
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AR Raycast Manager&lt;/code&gt; を AR Session Origin にコンポーネントとして追加する．
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Raycast Prefab&lt;/code&gt; は任意なので None のままにする．&lt;/p&gt;

&lt;p&gt;適当なクラスを作り，Raycast Manager を使って AR Plane との交点を検出し，そこに適当なオブジェクトを置いてみる．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AppController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MonoBehaviour&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARRaycastManager&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;raycastManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SerializeField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GameObject&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arObjectPrefab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ARRaycastHit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_raycastHits&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ARRaycastHit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;touchCount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;touch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetTouch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;touch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;phase&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TouchPhase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Ended&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raycastManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Raycast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;touch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_raycastHits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TrackableType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PlaneWithinPolygon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_raycastHits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;First&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trackable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ARPlane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;nf&quot;&gt;Instantiate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arObjectPrefab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;少し注意するべき点としては，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Raycast()&lt;/code&gt; するときに &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TrackableType.PlaneWithinPolygon&lt;/code&gt; を指定するようにする．
デフォルトだと &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;All&lt;/code&gt; になるようで，全ての平面が対象になるかつ Polygon に閉じない無限平面上で当たり判定されてしまう（違う，そうじゃない）．&lt;/p&gt;

&lt;p&gt;3D モデルは Unity Asset Store から &lt;a href=&quot;https://assetstore.unity.com/packages/3d/environments/landscapes/low-poly-simple-nature-pack-162153&quot;&gt;Low-Poly Simple Nature Pack&lt;/a&gt; を使わせてもらった．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-ar-getting-started/ar-raycasting.png&quot; alt=&quot;ar-raycasting.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;まとめ感想&quot;&gt;まとめ・感想&lt;/h2&gt;

&lt;p&gt;Unity iOS で AR アプリ開発ことはじめとして，次のことをやってみた．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;AR Foundation &amp;amp; ARKit をセットアップした&lt;/li&gt;
  &lt;li&gt;AR Plane Manager で平面検出し，デフォルトのビジュアライザで平面を表示した&lt;/li&gt;
  &lt;li&gt;AR Raycast Manager で AR Plane のどこをタッチしたのか判定した&lt;/li&gt;
  &lt;li&gt;タッチした場所に適当な 3D オブジェクトを表示した&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AR Foundation によって AR を始めるための敷居はかなり低くなっている．
一方で Plane Manager の精度はデフォルトだと驚きがある程は高くなく，エッジがずれていたり，10cm ぐらいの段差が丸められていたりする．
もしかして LiDAR が有効になっていない？のは今後の課題とする．&lt;/p&gt;

&lt;p&gt;部屋中に木をわさわさ生やすと楽しい．&lt;/p&gt;

&lt;p&gt;:herb:&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/index.html&quot;&gt;About AR Foundation | AR Foundation | 4.1.1&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arkit@4.1/manual/index.html&quot;&gt;About ARKit XR Plugin | ARKit XR Plugin | 4.1.1&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/trackable-managers.html&quot;&gt;Trackable managers | AR Foundation | 4.1.1&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/raycast-manager.html&quot;&gt;AR Raycast Manager | AR Foundation | 4.1.1&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assetstore.unity.com/packages/3d/environments/landscapes/low-poly-simple-nature-pack-162153&quot;&gt;Low-Poly Simple Nature Pack | 3D Landscapes | Unity Asset Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 13 Jan 2021 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2021/01/13/unity-ios-ar-getting-started.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2021/01/13/unity-ios-ar-getting-started.html</guid>
        
        
      </item>
    
      <item>
        <title>Unity iOS プロジェクトを Bitrise でビルドする</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-on-bitrise/ogp.jpg&quot; alt=&quot;OGP&quot; /&gt;&lt;/p&gt;

&lt;p&gt;アプリ開発において CI が重要なのは Unity iOS プロジェクトでも同じだ．
個人開発として趣味の範囲で細々とやっている身であっても，むしろであるからこそ，CI による自動化で作業リソースや集中力を節約したい．&lt;/p&gt;

&lt;p&gt;今回の検証では，iOS/Android アプリ開発でよく使われる CI のひとつである Bitrise を使って Unity iOS プロジェクトをビルドする方法を考える．
名もなき developer なので，Unity Personal ライセンスの有効化問題や Bitrise のビルド制限時間問題など「お金で解決できるはずの課題」が立ち塞がるが，強い気持ちと workaround でなんとかしていく．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Ubuntu で Unity エディタを動かして Xcode プロジェクトを書き出す&lt;/li&gt;
  &lt;li&gt;Game CI が公開している Docker イメージを使うと便利&lt;/li&gt;
  &lt;li&gt;書き出したプロジェクトを macOS の workflow でビルドする&lt;/li&gt;
  &lt;li&gt;プロジェクトの受け渡しは Bitrise の Artifacts で行う&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Unity 2020.1.1f1&lt;/li&gt;
  &lt;li&gt;Docker 20.10.0&lt;/li&gt;
  &lt;li&gt;Bitrise Stacks
    &lt;ul&gt;
      &lt;li&gt;Android &amp;amp; Docker, on Ubuntu 16.04 - LTS Stack&lt;/li&gt;
      &lt;li&gt;Xcode 12.3.x, on macOS 10.15.5 (Catalina)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;

&lt;p&gt;Unity iOS プロジェクトをビルドしたい場合，CI の選択肢としては大きく分けて次の3つの方針がある．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;セルフホスト（以下 Jenkins を想定）&lt;/li&gt;
  &lt;li&gt;Unity Cloud Build&lt;/li&gt;
  &lt;li&gt;その他 iOS 向け CI サービス&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;もちろん，それぞれについて利点と課題がある．&lt;/p&gt;

&lt;h3 id=&quot;jenkins&quot;&gt;Jenkins&lt;/h3&gt;

&lt;p&gt;Jenkins は，ある意味最も便利な選択肢と言える．
Worker のマシンを直接セットアップできるので Unity はもちろん，必要なソフトウェアを予めインストールしておくことも簡単だし，いざとなればマシンに直接ログインしてデバッグをすることもできる．
一方で，セルフホストであることに伴うあれこれが，そのまま課題とも言える．&lt;/p&gt;

&lt;p&gt;個人的にも以前は Jenkins を使って CI を構築していた．&lt;/p&gt;

&lt;h3 id=&quot;unity-cloud-build&quot;&gt;Unity Cloud Build&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://unity3d.com/unity/features/cloud-build&quot;&gt;Unity Cloud Build&lt;/a&gt;（以下 UCB）は，素直に一番簡単な選択肢だと思う．
&lt;a href=&quot;https://unity3d.com/get-teams&quot;&gt;Unity Teams Advanced&lt;/a&gt; に$9/月を支払う必要があるが，Web の UI から必要最小限の設定で動かし始めることができる．
個人的にもかれこれ1年以上使っているが，CI のメンテがほぼ不要なのでとても楽になった．&lt;/p&gt;

&lt;p&gt;しかし，UCB に課題がないわけでもない．
UI が微妙で使い難かったり，自動ビルドが自動でオフになったり，証明書の更新が面倒だったりと気になる点はあるが，なかでも一番気になるのはビルドに時間がかかることだ．
ビルド時間はプロジェクト構成に依存するので結果の数字だけ話しても大した意味はないが，UCB だと Git リポジトリに push してからビルド完了通知が届くまで60分程かかっている．&lt;/p&gt;

&lt;p&gt;このスピードだと commit 毎にビルドするのは現実的ではないので main ブランチ更新があったタイミングでビルドするようにしている．
夜寝る前に feature ブランチをマージしておけば，翌朝には（運が良ければ）ビルドが配信されているはず…という感じで運用している．&lt;/p&gt;

&lt;h3 id=&quot;bitrise&quot;&gt;Bitrise&lt;/h3&gt;

&lt;p&gt;その他 iOS 向け CI サービスは幾つかあるが，今回は Bitrise を対象とする．
これは単にいつも使っているからという個人的な理由による．&lt;/p&gt;

&lt;p&gt;利点としては，一言で言えば iOS 開発の現場で実際に使えるレベルの CI だということで，わざわざあれこれ挙げる必要はないだろう．Bitrise だと無料枠で macOS を使うことができる点も有難い．ただし，Unity iOS プロジェクトをビルドする場合に特有の問題として Unity エディタのライセンス有効化がある．&lt;/p&gt;

&lt;h3 id=&quot;unity-ライセンス有効化問題&quot;&gt;Unity ライセンス有効化問題&lt;/h3&gt;

&lt;p&gt;Unity エディタのライセンスを CLI から有効化する場合，Unity マニュアルの &lt;a href=&quot;https://docs.unity3d.com/Manual/CommandLineArguments.html&quot;&gt;Command line arguments&lt;/a&gt; の定義に依れば &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-username&lt;/code&gt; と &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-password&lt;/code&gt; を渡せば良さそうに見えるが，実はこの方法で有効化できるのは有償のライセンスのみで，無料の Unity Personal ライセンスには対応していない．&lt;/p&gt;

&lt;p&gt;では Personal ライセンスは CLI で有効化できないのかというと，実はできないこともない．
手動のライセンス有効化（&lt;a href=&quot;https://docs.unity3d.com/520/Documentation/Manual/ManualActivationGuide.html&quot;&gt;Manual Activation&lt;/a&gt;）という方法があって，次の手順で有効化できる．&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;ライセンス有効化ファイル（.alf）を Unity エディタから書き出す&lt;/li&gt;
  &lt;li&gt;Web ブラウザで &lt;a href=&quot;https://license.unity3d.com/manual&quot;&gt;Unity - Activation&lt;/a&gt; を開いてライセンスファイル（.ulf）を取得する&lt;/li&gt;
  &lt;li&gt;ライセンスファイル（.ulf）を Unity エディタに渡してライセンスを有効化する&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;手順2において手動によるアカウント認証が必要（アカウント設定によっては 2FA も必要）になるが，その前後の手順は CLI で実行できる．&lt;/p&gt;

&lt;p&gt;ただし，この方法ではライセンスファイルが worker のマシン ID と紐づくので，毎回別の仮想マシンインスタンスを立ち上げる CI では，ビルドの度にマシン ID が変わってしまうため，ライセンスファイルを再利用することができないという問題がある．
この問題について&lt;a href=&quot;https://www.google.com/search?q=Unity+CI+ALF&quot;&gt;「Unity CI ALF」&lt;/a&gt;で検索すると，これまでも色々と試行錯誤されてきたことがわかる．
主な方針としては仮想マシンの ID を固定するか，Puppeteer などを使ってライセンスファイル取得手順を自動化して毎回取得しなおすかになるが，macOS では前者の方針を取ることが難しい．&lt;/p&gt;

&lt;p&gt;もし，Android など Linux でビルドできるプラットフォームが対象であれば，マシン ID を固定した Linux を使うことができる．
では iOS を対象とする場合にはどうするか．&lt;/p&gt;

&lt;h3 id=&quot;ubuntu-を使って-xcode-プロジェクトを書き出す&quot;&gt;Ubuntu を使って Xcode プロジェクトを書き出す&lt;/h3&gt;

&lt;p&gt;というのが解決策となる（TL;DR）．&lt;/p&gt;

&lt;p&gt;Unity エディタの CLI では &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-buildTarget&lt;/code&gt; を指定することで任意の &lt;a href=&quot;https://docs.unity3d.com/ScriptReference/BuildTarget.html&quot;&gt;Build Target&lt;/a&gt; に対して実行ファイルやプロジェクトを書き出すことができる．
そして，実は iOS 用の Xcode プロジェクト（.xcodeproj）を書き出すことは &lt;strong&gt;Ubuntu の上で実行している Unity エディタでも可能&lt;/strong&gt;となっている．&lt;a href=&quot;https://www.google.com/search?q=%E3%81%AA%E3%82%93%E3%81%A8%E3%81%A3%EF%BC%81&quot;&gt;なんとっ！&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;もちろん，書き出した Xcode プロジェクトから iOS アプリをビルドするためには Xcode とそれを動かすための macOS が必要となるが，Xcode プロジェクトを書き出すまでであれば Ubuntu でも可能だ．
つまり，ビルドの workflow を次のように分割すれば良い．&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Unity エディタから Xcode プロジェクトを書き出す（on Ubuntu）&lt;/li&gt;
  &lt;li&gt;Xcode プロジェクトから .ipa をビルドする（on macOS）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unity のライセンスが必要になるのは手順1のみなので，macOS で Unity ライセンスを有効化できない問題は問題にならない．
Xcode プロジェクトが書き出せてしまえば，後は基本的には通常の iOS アプリのビルドと変わらないので，特に難しいところもない．&lt;/p&gt;

&lt;h3 id=&quot;先行事例&quot;&gt;先行事例&lt;/h3&gt;

&lt;p&gt;有志による &lt;a href=&quot;https://game.ci/docs&quot;&gt;GameCI&lt;/a&gt; というプロジェクトがある．
Unity のライセンス有効化やプロジェクトのビルドを行うスクリプトを公開している．
Unity エディタをインストールした Ubuntu ベースの Dockerfile や Docker イメージも提供されている．&lt;/p&gt;

&lt;p&gt;対象とする CI サービスについては GitHub Actions や Travis CI などの説明があるが，今回対象とする Bitrise については記述が無かった．
また，Xcode プロジェクトから iOS アプリをビルドする方法についても具体的に説明が無かった．
そこで，今回の検証では次の点について新たに考えて補足する．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Bitrise で Unity iOS プロジェクトをビルドする&lt;/li&gt;
  &lt;li&gt;Bitrise の複数 workflow 間で artifact を共有する&lt;/li&gt;
  &lt;li&gt;Unity iOS の Xcode プロジェクトから .ipa をビルドするときの注意点&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以上で背景説明は終わり．&lt;/p&gt;

&lt;h2 id=&quot;技術構成&quot;&gt;技術構成&lt;/h2&gt;

&lt;h3 id=&quot;bitrise-1&quot;&gt;Bitrise&lt;/h3&gt;

&lt;p&gt;今回の検証の中心．
Stack は次の2つを使う．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Android &amp;amp; Docker, on Ubuntu 16.04 - LTS Stack&lt;/li&gt;
  &lt;li&gt;Xcode 12.3.x, on macOS 10.15.5 (Catalina)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workflow と stack は 1:1 になるので，Xcode プロジェクト書き出し用と Xcode ビルド用で workflow を分けて，前段の workflow から後続の workflow をトリガーする形になる．
Workflow を分割せずに，macOS の stack で Docker を動かして Ubuntu を起動する方法もありそうだが，次の2つの理由により Ubuntu stack を使うことにした．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;デフォルトの macOS stack には，Docker がインストールされていない&lt;/li&gt;
  &lt;li&gt;ビルド時間が伸びると workflow の制限時間内にビルドが終わらない可能性がある&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;特に，制限時間については Free プランでは30分と短めなので，少し気を付ける必要がある．&lt;/p&gt;

&lt;h3 id=&quot;unity-エディタ-on-ubuntu&quot;&gt;Unity エディタ on Ubuntu&lt;/h3&gt;

&lt;p&gt;Unity エディタをインストールした Ubuntu マシンを用意する．
簡単のため &lt;a href=&quot;https://github.com/game-ci&quot;&gt;GameCI&lt;/a&gt; プロジェクトが提供している Docker イメージを使う．&lt;/p&gt;

&lt;p&gt;なお，Dockerfile も &lt;a href=&quot;https://github.com/game-ci&quot;&gt;GitHub の GameCI&lt;/a&gt; で公開されているので，自前で Docker イメージを用意したり，そもそも Docker を使わずに Ubuntu を直接セットアップすることも可能．&lt;/p&gt;

&lt;h3 id=&quot;bitrise-artifacts&quot;&gt;Bitrise Artifacts&lt;/h3&gt;

&lt;p&gt;分割した workflows 間で Xcode プロジェクトを受け渡すために &lt;a href=&quot;https://devcenter.bitrise.io/jp/tips-and-tricks/attach-any-file-to-build/&quot;&gt;Bitrise Artifacts&lt;/a&gt; の仕組みを使う．&lt;/p&gt;

&lt;p&gt;後続の workflow では，&lt;a href=&quot;https://devcenter.bitrise.io/api/api-index/&quot;&gt;Bitrise API&lt;/a&gt; を使って artifacts をダウンロードする形で Xcode プロジェクトを受け取る．&lt;/p&gt;

&lt;h2 id=&quot;ライセンス有効化ファイルの書き出し&quot;&gt;ライセンス有効化ファイルの書き出し&lt;/h2&gt;

&lt;p&gt;まずは Unity エディタからライセンスファイルを書き出す．
基本的には1回実行すれば良いものなので，個別の Workflow として構築しておく．
Docker を使うので Ubuntu の stack を使う．&lt;/p&gt;

&lt;p&gt;GameCI 提供の Docker イメージが &lt;a href=&quot;https://hub.docker.com/r/unityci/editor&quot;&gt;Docker Hub&lt;/a&gt; にあるので，そちらから pull して使う．&lt;a href=&quot;https://hub.docker.com/r/unityci/editor/tags?page=1&amp;amp;ordering=last_updated&amp;amp;name=ios&quot;&gt;Tags 一覧の ios モジュールを含むもの&lt;/a&gt;から自分のプロジェクトに合ったバージョンを選んで，Env Vars に &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNITY_DOCKER_TAG&lt;/code&gt; として設定しておく．&lt;/p&gt;

&lt;p&gt;ビルドスクリプトは大した量でもないので Workflow に &lt;a href=&quot;https://www.bitrise.io/integrations/steps/script&quot;&gt;Script&lt;/a&gt; ステップを追加して書いてしまう．
Unity を batch モードで起動して，書き出された .alf ファイルのパスを環境変数に書き込む．
なお，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unity-editor&lt;/code&gt; というのは GameCI の Docker イメージに設定されている bash スクリプトで，UnityEditor を batch モードで &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xvfb-run&lt;/code&gt; してくれるラッパーになっている．
詳しくは &lt;a href=&quot;https://github.com/game-ci/docker&quot;&gt;GitHub&lt;/a&gt; にある Dockerfile を見れば分かる．&lt;/p&gt;

&lt;noscript&gt;&lt;pre&gt;#!/usr/bin/env bash
set -x

docker pull unityci/editor:$UNITY_DOCKER_TAG
docker run --rm -v $PWD:/build -w /build unityci/editor:$UNITY_DOCKER_TAG \
  bash -c &amp;quot;unity-editor -nographics -logFile /dev/stdout -quit -createManualActivationFile&amp;quot;

UNITY_EXIT_CODE=$?
UNITY_ALF=$(find $PWD -name &amp;quot;*.alf&amp;quot; -print -quit)

if [[ -n &amp;quot;$UNITY_ALF&amp;quot; ]]; then
  envman add --key UNITY_ALF --value &amp;quot;$UNITY_ALF&amp;quot;
  echo &amp;quot;Open https://license.unity3d.com/manual and get License file (.ulf)&amp;quot;
else
  echo &amp;quot;License activation file (.alf) is not found&amp;quot;
  exit $UNITY_EXIT_CODE
fi
&lt;/pre&gt;&lt;/noscript&gt;
&lt;script src=&quot;https://gist.github.com/thedoritos/146ad7fa67ea0aa3672406735459857e.js?file=export_license_request_file.sh&quot;&gt; &lt;/script&gt;

&lt;p&gt;後続の &lt;a href=&quot;https://www.bitrise.io/integrations/steps/deploy-to-bitrise-io&quot;&gt;Deploy to Bitrise.io&lt;/a&gt; ステップを使って .alf ファイルを Bitrise に artifact としてアップロードする．&lt;/p&gt;

&lt;p&gt;Workflow を実行して，実行結果のページから .alf ファイルを手動でダウンロードして，&lt;a href=&quot;https://license.unity3d.com/manual&quot;&gt;Unity - Activation&lt;/a&gt; を開き，Unity アカウントで認証してライセンスファイル（.ulf）を取得する．
取得したライセンスファイルを Code Signing の Generic File Storage に ID: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNITY_LICENSE_FILE&lt;/code&gt; としてアップロードしておく．&lt;/p&gt;

&lt;h2 id=&quot;xcode-プロジェクトの書き出し&quot;&gt;Xcode プロジェクトの書き出し&lt;/h2&gt;

&lt;p&gt;Unity エディタから Xcode プロジェクトを書き出す．
こちらも Docker を使うので Ubuntu の stack を使う．&lt;/p&gt;

&lt;p&gt;ライセンスファイルを復元して，Unity CLI に渡してエディタを有効化する．&lt;/p&gt;

&lt;p&gt;肝心の Xcode プロジェクトの書き出しは，いつもどおり batch モードを使うだけで特に難しいところはない．
引数は最小限のものだけ渡しているが，いつもどおり &lt;a href=&quot;https://docs.unity3d.com/Manual/CommandLineArguments.html&quot;&gt;Command line arguments&lt;/a&gt; から必要なものを使えば良い．&lt;/p&gt;

&lt;noscript&gt;&lt;pre&gt;#!/usr/bin/env bash
set -x

UNITY_ULF=Unity_vX.ulf
curl $BITRISEIO_UNITY_LICENSE_FILE_URL | tr -d &amp;#39;\r&amp;#39; &amp;gt; $UNITY_ULF

if [[ -z &amp;quot;$PROJECT_PATH&amp;quot; ]]; then
  PROJECT_SETTINGS_PATH=$(find . -name &amp;quot;ProjectSettings&amp;quot; -print -quit)
  PROJECT_PATH=$(dirname &amp;quot;$PROJECT_SETTINGS_PATH&amp;quot;)
fi

docker pull unityci/editor:$UNITY_DOCKER_TAG
docker run --rm -v $PWD:/build -w /build unityci/editor:$UNITY_DOCKER_TAG \
  bash -c &amp;quot;
    unity-editor -nographics -logFile /dev/stdout -quit \
      -manualLicenseFile $UNITY_ULF;

    unity-editor -nograhpics -logFile /dev/stdout -quit \
      -projectPath $PROJECT_PATH \
      -buildTarget iOS \
      -executeMethod $EXECUTE_METHOD;

    UNITY_EXIT_CODE=\$?;

    find . -type l -exec bash -c &amp;#39;cp --remove-destination \$(readlink {}) {}&amp;#39; \; ;

    exit \$UNITY_EXIT_CODE;
  &amp;quot;

UNITY_EXIT_CODE=$?
XCODE_PROJECT_PATH=$(find $PWD -name &amp;quot;Unity-iPhone.xcodeproj&amp;quot; -print -quit)

if [[ -z &amp;quot;XCODE_PROJECT_PATH&amp;quot; ]]; then
  echo &amp;quot;Xcode project is not found&amp;quot;
  exit $UNITY_EXIT_CODE
fi

BUILD_DIR=$(dirname $XCODE_PROJECT_PATH)

envman add --key BUILD_DIR --value &amp;quot;$BUILD_DIR&amp;quot;
&lt;/pre&gt;&lt;/noscript&gt;
&lt;script src=&quot;https://gist.github.com/thedoritos/146ad7fa67ea0aa3672406735459857e.js?file=export_xcode_project.sh&quot;&gt; &lt;/script&gt;

&lt;p&gt;地味に重要なのが &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find -type l -exec&lt;/code&gt; しているところ．
ビルドした Xcode プロジェクトが入ったディレクトリをまるっと artifact にアップロードしたいので，symlink を解決して実際のファイルに差し替えている．
Symlink のままだと artifact の中に実際のファイルは含まれないため，後続の macOS を使った workflow でビルドするときにファイルを見つけられずリンカエラーになってしまう．&lt;/p&gt;

&lt;p&gt;ということで，再び &lt;a href=&quot;https://www.bitrise.io/integrations/steps/deploy-to-bitrise-io&quot;&gt;Deploy to Bitrise.io&lt;/a&gt; ステップを使って &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$BUILD_DIR&lt;/code&gt; をまるっとアップロードする．
ディレクトリを Zip してアップロードするオプションがあるので &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt; にしておくこと．&lt;/p&gt;

&lt;p&gt;アップロードした artifact を後続の workflow に渡すために，ダウンロード URL を取得して workflow に引数として渡したい．
&lt;a href=&quot;https://devcenter.bitrise.io/api/managing-build-artifacts/&quot;&gt;Bitrise の API ドキュメント&lt;/a&gt;を参照すると GET リクエストで artifact の情報を JSON で取得できることが分かる．
Artifact を指定するための slug は Deploy ステップが出力する &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$BITRISE_PERMANENT_DOWNLOAD_URL_MAP&lt;/code&gt; から雑に抜き出す．&lt;/p&gt;

&lt;p&gt;実際の artifact ファイルをダウンロードできる URL は JSON の中に記録されているが，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expiring_download_url&lt;/code&gt; という名前のとおり有効期限付きの URL になっているので，実際にファイルをダウンロードする直前に JSON を取得するのが良い．&lt;/p&gt;

&lt;noscript&gt;&lt;pre&gt;#!/usr/bin/env bash
set -x -e

ARTIFACT_SLUG=$(echo $BITRISE_PERMANENT_DOWNLOAD_URL_MAP | sed &amp;#39;s/^.*artifact\/\(.*\)\/download.*$/\1/&amp;#39;)
ARTIFACT_URL=&amp;quot;https://api.bitrise.io/v0.1/apps/$BITRISE_APP_SLUG/builds/$BITRISE_BUILD_SLUG/artifacts/$ARTIFACT_SLUG&amp;quot;

envman add --key ARTIFACT_URL --value &amp;quot;$ARTIFACT_URL&amp;quot;
&lt;/pre&gt;&lt;/noscript&gt;
&lt;script src=&quot;https://gist.github.com/thedoritos/146ad7fa67ea0aa3672406735459857e.js?file=get_artifact_url.sh&quot;&gt; &lt;/script&gt;

&lt;p&gt;GET リクエストを送る先の URL を &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ARTIFACT_URL&lt;/code&gt; に設定して，後続の workflow の入力として渡す．
Workflow の実行は &lt;a href=&quot;https://www.bitrise.io/integrations/steps/trigger-bitrise-workflow&quot;&gt;Trigger Bitrise workflow&lt;/a&gt; ステップを使えば良い．
Workflow をトリガーするためのトークンは Bitrise アプリの Code タブから取得できる．&lt;/p&gt;

&lt;h2 id=&quot;xcode-プロジェクトのビルド&quot;&gt;Xcode プロジェクトのビルド&lt;/h2&gt;

&lt;p&gt;最後に Xcode プロジェクトから iOS アプリをビルドする．&lt;/p&gt;

&lt;p&gt;Artifact のダウンロード URL を取得して，Zip ファイルをダウンロードして，Zip を展開する．
Bitrise API を叩くための API トークンが必要なので，&lt;a href=&quot;https://app.bitrise.io/me/profile#/security&quot;&gt;Account Settings&lt;/a&gt; ページで生成して，Secrets の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BITRISE_API_TOKEN&lt;/code&gt; に設定したものを読んでいる．&lt;/p&gt;

&lt;noscript&gt;&lt;pre&gt;#!/usr/bin/env bash
set -e -x

ARTIFACT_DOWNLOAD_URL=$(curl -H &amp;quot;Authorization: token $BITRISE_API_TOKEN&amp;quot; &amp;quot;$ARTIFACT_URL&amp;quot; | jq -j &amp;quot;.data.expiring_download_url&amp;quot;)

curl -o Build.zip $ARTIFACT_DOWNLOAD_URL
mkdir Build
unzip Build.zip -d Build
&lt;/pre&gt;&lt;/noscript&gt;
&lt;script src=&quot;https://gist.github.com/thedoritos/146ad7fa67ea0aa3672406735459857e.js?file=get_artifact.sh&quot;&gt; &lt;/script&gt;

&lt;p&gt;ここまでできれば，あとは通常の iOS アプリのビルドと変わらないので要点だけ補足する．
少し気を付ける必要があった点は次のとおり．&lt;/p&gt;

&lt;h3 id=&quot;cocoapods-に時間がかかる問題&quot;&gt;CocoaPods に時間がかかる問題&lt;/h3&gt;

&lt;p&gt;CocoaPods が必要なプロジェクト（Firebase SDK を使っているなど）の場合，Xcode プロジェクト書き出しを Ubuntu で行っているため，Unity の PostProcessBuild では &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pod install&lt;/code&gt; に失敗しているはずなので，macOS 側で &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xcodebuild&lt;/code&gt; する前に実行しておく．
なお，Firebase SDK が生成する Podfile の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; が GitHub を参照していて &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pod install&lt;/code&gt; にかなーり時間がかかるため，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; の URL には &lt;a href=&quot;https://blog.cocoapods.org/CocoaPods-1.7.2/&quot;&gt;CDN を使う&lt;/a&gt;ように書き換えておくと時間を短縮できる．&lt;/p&gt;

&lt;noscript&gt;&lt;pre&gt;#!/usr/bin/env bash
set -e -x

sed -i &amp;#39;&amp;#39; &amp;#39;s/github\.com\/CocoaPods\/Specs\.git/cdn\.cocoapods\.org/&amp;#39; $BUILD_DIR/Podfile
&lt;/pre&gt;&lt;/noscript&gt;
&lt;script src=&quot;https://gist.github.com/thedoritos/146ad7fa67ea0aa3672406735459857e.js?file=replace_pod_source.sh&quot;&gt; &lt;/script&gt;

&lt;h3 id=&quot;xcode-アーカイブに時間がかかる問題&quot;&gt;Xcode アーカイブに時間がかかる問題&lt;/h3&gt;

&lt;p&gt;ひとまず Bitcode のオプションを切って短縮した．
ただし，これだと App Store 版がビルドできなくなるので，それは今後の課題とする．
なお，お金を払うという最強のソリューションが（以下略）&lt;/p&gt;

&lt;p&gt;最後に Slack に通知して完了．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/unity-ios-on-bitrise/notification.png&quot; alt=&quot;通知&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;まとめ感想&quot;&gt;まとめ・感想&lt;/h2&gt;

&lt;p&gt;Unity iOS プロジェクトを Bitrise でビルドすることができた．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Game CI プロジェクトが提供する Docker イメージを使った&lt;/li&gt;
  &lt;li&gt;Ubuntu を使うことで Unity Personal ライセンスでも CI の上で Unity を動かすことができた&lt;/li&gt;
  &lt;li&gt;iOS アプリのビルドは Workflow を分割して実装した
    &lt;ul&gt;
      &lt;li&gt;Ubuntu で Unity エディタから Xcode プロジェクトを書き出した&lt;/li&gt;
      &lt;li&gt;macOS で Xcode プロジェクトから iOS アプリをビルドした&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;肝心のビルド時間については，Bitrise だと30分強くらいで完了しているので，40-50% 程度は短縮できている．
ただし，CI を構築できるまでに Unity Cloud Build の100倍ぐらい試行錯誤しているのは忘れることにする．&lt;/p&gt;

&lt;p&gt;:doughnut:&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://unity3d.com/unity/features/cloud-build&quot;&gt;Services - Cloud Build - Unity&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://unity3d.com/get-teams&quot;&gt;Get Teams - Unity&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/Manual/CommandLineArguments.html&quot;&gt;Unity - Manual: Command line arguments&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/520/Documentation/Manual/ManualActivationGuide.html&quot;&gt;Unity - Manual: Step-by-Step Guide to Manual Activation of Unity&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.unity3d.com/ScriptReference/BuildTarget.html&quot;&gt;Unity - Scripting API: BuildTarget&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://game.ci/docs&quot;&gt;GameCI&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/game-ci&quot;&gt;GameCI (GitHub)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://devcenter.bitrise.io/api/managing-build-artifacts/&quot;&gt;Managing build artifacts | Bitrise DevCenter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.cocoapods.org/CocoaPods-1.7.2/&quot;&gt;CocoaPods 1.7.2 — Master Repo CDN is Finalized! - CocoaPods Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 06 Jan 2021 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2021/01/06/unity-ios-on-bitrise.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2021/01/06/unity-ios-on-bitrise.html</guid>
        
        
      </item>
    
      <item>
        <title>Apple Developer 登録端末の大掃除</title>
        <description>&lt;p&gt;この記事は &lt;a href=&quot;https://qiita.com/advent-calendar/2020/goodpatch&quot;&gt;Goodpatch Advent Calendar 2020&lt;/a&gt; の20日目です．&lt;/p&gt;

&lt;p&gt;年末が近づいているということで，どこの会社でもどこのご家庭でも一年間溜まったアレの片付けに追われているのではないでしょうか．
そうです Apple Developer に登録した端末一覧です．
今年の端末は今年の内に．新年を清々しい気持ちで迎えるぞ．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Apple Developer に登録している古くなった端末を削除したい&lt;/li&gt;
  &lt;li&gt;Swift Package Manager を使ってコマンドラインツールを作る&lt;/li&gt;
  &lt;li&gt;端末の操作は App Store Connect API で行う
    &lt;ul&gt;
      &lt;li&gt;端末の一覧を取得する&lt;/li&gt;
      &lt;li&gt;古くなった端末を Disabled にする（削除はできない仕様）&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;コードは &lt;a href=&quot;https://github.com/thedoritos/apple-device-manager&quot;&gt;GitHub&lt;/a&gt; にあります．&lt;/p&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Swift 5.3.1&lt;/li&gt;
  &lt;li&gt;App Store Connect API v1&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;apple-developer-管理画面における端末管理&quot;&gt;Apple Developer 管理画面における端末管理&lt;/h2&gt;

&lt;p&gt;iOS アプリを自由にインストールできる端末は開発者ライセンスと証明書によって制限されています．
その中で，比較的制約条件が少ない Ad Hoc 方式を使いたい場合，Apple Developer にインストール対象端末をひとつひとつ登録する必要があります．&lt;/p&gt;

&lt;p&gt;端末の UDID を集めて登録するのは一手間かかる作業ですが，多くの鍛えられた iOS Developer はこの手間に慣れっこです．
UDID 確認方法の手順書を用意していたり，Firebase App Distribution など 3rd パーティ製のインフラに乗るようにしていたり，既に解決手段を持っていると思います．&lt;/p&gt;

&lt;p&gt;それでは，ここで登録した端末はその後どうなっているでしょうか？&lt;/p&gt;

&lt;p&gt;個人の経験や想像でしかありませんが，多くの場合「そのまま放置されている」のではないでしょうか．
年に一回の開発者ライセンス更新タイミングで，Apple から登録端末リセットの機会を与えられたものの，端末一覧を見ただけではどれがリセットして良い端末かは判断できないのが実情だと思います．
とどのつまり，削除を諦めて「現状維持」とするか，強い気持ちとマナコストを支払って「全体除去」を唱えるかといった選択肢しかありません．&lt;/p&gt;

&lt;h2 id=&quot;app-store-connect-api&quot;&gt;App Store Connect API&lt;/h2&gt;

&lt;p&gt;それでは「もう少し良い方法はないのか？」というと，なくはないです．&lt;/p&gt;

&lt;p&gt;Apple は &lt;a href=&quot;https://developer.apple.com/app-store-connect/api/&quot;&gt;App Sotre Connect API&lt;/a&gt; の提供を WWDC 2018 で発表しました．
この API を使うことで，開発者は Apple Developer アカウントや App Store Connect の管理項目をプログラムから変更できるようになります．&lt;/p&gt;

&lt;p&gt;API の使い方については&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi&quot;&gt;公式ドキュメント&lt;/a&gt;に詳しくあるとおりですが，その中でも &lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/devices&quot;&gt;Devices&lt;/a&gt; の使い方を調べてみると，API を使えば各端末について以下のようなプロパティが取得できることが分かります．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Name&lt;/th&gt;
      &lt;th&gt;Type&lt;/th&gt;
      &lt;th&gt;Note&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;deviceClass&lt;/td&gt;
      &lt;td&gt;string&lt;/td&gt;
      &lt;td&gt;Possible values: APPLE_WATCH, IPAD, IPHONE, IPOD, APPLE_TV, MAC&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;model&lt;/td&gt;
      &lt;td&gt;string&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;name&lt;/td&gt;
      &lt;td&gt;string&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;platform&lt;/td&gt;
      &lt;td&gt;BundleIdPlatform&lt;/td&gt;
      &lt;td&gt;Possible values: IOS, MAC_OS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;status&lt;/td&gt;
      &lt;td&gt;string&lt;/td&gt;
      &lt;td&gt;Possible values: ENABLED, DISABLED&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;udid&lt;/td&gt;
      &lt;td&gt;string&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;addedDate&lt;/td&gt;
      &lt;td&gt;date-time&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;参考：&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/device/attributes&quot;&gt;Device.Attributes | Apple Developer Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ここで，端末管理のために特に使えそうなものが &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addedDate&lt;/code&gt; です．
他のプロパティは管理画面の一覧や詳細を見れば確認することができるのですが，なぜか端末の登録日を管理画面から知ることはできません．&lt;/p&gt;

&lt;p&gt;端末の登録日が分かれば，例えば端末管理ルールとして「登録から2年以上経過している端末は削除する」といったものを考えることができそうです．&lt;/p&gt;

&lt;p&gt;なお，端末のモデルを使ってルールを決めることもできそうですが，端末を積極的に削除することができなかったり，あえて古めの端末を登録している場合を考慮できなかったりといった問題がありそうです．
例えば2020年末時点で iOS 13.x までしかサポートしなくて良いとした場合でも，iPhone 6s 以降の端末については使われている可能性が残ります．
端末一覧にリストされているその iPhone 6s は既に数年前から使われていない端末かもしれませんし，つい数ヶ月前にテスト用に登録した端末かもしれません．&lt;/p&gt;

&lt;p&gt;今回の検証では「登録から〇〇以上経過している端末は削除する」のルールを考え，これを API を使って自動で運用する方法を検証します．&lt;/p&gt;

&lt;h2 id=&quot;技術構成&quot;&gt;技術構成&lt;/h2&gt;

&lt;p&gt;今回の検証では技術構成を次のように考えました．&lt;/p&gt;

&lt;h3 id=&quot;app-store-connect-api-1&quot;&gt;App Store Connect API&lt;/h3&gt;

&lt;p&gt;既に説明したとおりです．
&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi&quot;&gt;公式ドキュメント&lt;/a&gt;の Topics &amp;gt; Essentials を読んでいけば特に難しいことはないと思います．&lt;/p&gt;

&lt;h3 id=&quot;swift&quot;&gt;Swift&lt;/h3&gt;

&lt;p&gt;基本的には App Store Connect API を叩くだけなので，プログラミング言語は好きなものを使えば良さそうです．
今回は Apple のお膝元ということで Swift を使ってみます．
API を叩くために有志によって開発されている OSS のラッパーを使うこともできますが，それではネタがなくなりそうなので自前で書くことにします．&lt;/p&gt;

&lt;h3 id=&quot;swift-argument-parser&quot;&gt;Swift Argument Parser&lt;/h3&gt;

&lt;p&gt;Swift でコマンドラインツールを作りたいわけですが，コマンドライン引数をパースすることは地味に面倒なので Apple が公開している &lt;a href=&quot;https://github.com/apple/swift-argument-parser&quot;&gt;Swift Argument Parser&lt;/a&gt; を使います．
Protocol 準拠した struct を定義すれば引数やオプションを簡単にパースでき，さらに help オプションまで用意してくるのでとても便利です．&lt;/p&gt;

&lt;h3 id=&quot;jwtkit&quot;&gt;JWTKit&lt;/h3&gt;

&lt;p&gt;App Store Connect API は &lt;a href=&quot;https://jwt.io/&quot;&gt;JSON Web Tokens&lt;/a&gt;（JWT）による認証を要求します．
JWT の header や payload の構造は自前で定義しますが，それを JWT にエンコードする処理はライブラリに任せることにします．
今回は Vapor プロジェクトでも使われている &lt;a href=&quot;https://github.com/vapor/jwt-kit&quot;&gt;JWTKit&lt;/a&gt; を採用します．&lt;/p&gt;

&lt;h2 id=&quot;プロジェクトを作る&quot;&gt;プロジェクトを作る&lt;/h2&gt;

&lt;p&gt;Swift Package Manager を使ってプロジェクトを作ります．
まずは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift&lt;/code&gt; コマンドで雛形に従ったプロジェクトを作ります．&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;apple-device-manager
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;apple-device-manager
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;swift package init &lt;span class=&quot;nt&quot;&gt;--type&lt;/span&gt; executable

Creating executable package: apple-device-manager
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/apple-device-manager/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/apple-device-managerTests/
Creating Tests/apple-device-managerTests/apple_device_managerTests.swift
Creating Tests/apple-device-managerTests/XCTestManifests.swift
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;続いて，今回使用するパッケージを Package.swift ファイルに追記します．
Package.swift ファイルを Xcode で開くと Xcode Workspace が作られるので，以降の編集作業はそちらで進めます．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// swift-tools-version:5.3&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;PackageDescription&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;apple-device-manager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;platforms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;macOS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;v10_15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://github.com/apple/swift-argument-parser&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;0.3.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://github.com/vapor/jwt-kit.git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;4.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;targets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;apple-device-manager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ArgumentParser&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;swift-argument-parser&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;JWTKit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;jwt-kit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;testTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;apple-device-managerTests&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;apple-device-manager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Package.swift を更新して保存すると，Xcode は自動でパッケージのインストールを開始します．&lt;/p&gt;

&lt;p&gt;インストールが完了した後，Xcode からプログラムを実行（Run）すると Console にメッセージが表示されるはずです．&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Hello, world!
Program ended with exit code: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;認証する&quot;&gt;認証する&lt;/h2&gt;

&lt;p&gt;API の認証を通過するためには，API トークンが必要です．
まず Apple Developer 管理画面で &lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api&quot;&gt;App Store Connect の API キーを作る&lt;/a&gt;手順に従って API キー（.p8）を作ります．
さらにその API キーを元に &lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests&quot;&gt;API トークンを生成する&lt;/a&gt;と実際の HTTP リクエストで使うトークン（JWT）を得ることができます．&lt;/p&gt;

&lt;p&gt;App Store Connect API の仕様で，トークンの有効期限は20分以内に設定する必要があるため，トークンは適宜作り直す必要があります．
キーの &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;issuerId&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt;（値そのもの）を入力として，JWT にエンコードされたトークンを出力してみます．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Foundation&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;issuerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Foundation&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;JWTKit&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIToken&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;expiration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;JWTPayload&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;/// Your issuer ID from the API Keys page in App Store Connect (Ex: 57246542-96fe-1a63-e053-0824d011072a)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;iss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;IssuerClaim&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;/// The token&apos;s expiration time, in Unix epoch time; tokens that expire more than 20 minutes in the future are not valid (Ex: 1528408800)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ExpirationClaim&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;aud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AudienceClaim&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;appstoreconnect-v1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;signer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;JWTSigner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;verifyNotExpired&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIToken&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;expiration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addingTimeInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;issuerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;signer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;JWTSigner&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;es256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;pem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jwt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;signer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;kid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;expiration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;試しに Swift のコードを実行して API トークンを生成して出力してみます．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Foundation&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;keyId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;XXXXXXXXXX&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;keyIssuerId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;keyFilePath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/full/path/to/AuthKey_XXXXXXXXXX.p8&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;keyValue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;contentsOfFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keyFilePath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keyId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;issuerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keyIssuerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keyValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;xxxxxx.xxxxxxxxxxxxx.xxxxxxxx
Program ended with exit code: 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;得られたトークンを使って curl でリクエストを投げてみます．
端末の一覧が JSON 形式で取得できれば成功です．&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl https://api.appstoreconnect.apple.com/v1/devices &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Authorization: Bearer xxxxxx.xxxxxxxxxxxxx.xxxxxxxx&apos;&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;data&quot;&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;type&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;devices&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;xxxxxxxxxx&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;attributes&quot;&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;addedDate&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;yyyy-MM-ddTHH:mm:ss.000+0000&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;name&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;xxxx&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;deviceClass&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;IPHONE&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;model&quot;&lt;/span&gt; : null,
      &lt;span class=&quot;s2&quot;&gt;&quot;udid&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;xxxxxxxx-xxxxxxxxxxxxxxxx&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;platform&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;IOS&quot;&lt;/span&gt;,
      &lt;span class=&quot;s2&quot;&gt;&quot;status&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;ENABLED&quot;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;links&quot;&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;self&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;https://api.appstoreconnect.apple.com/v1/devices/xxxxxxxxxx&quot;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;, &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    /&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; ... &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;/
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;links&quot;&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;self&quot;&lt;/span&gt; : &lt;span class=&quot;s2&quot;&gt;&quot;https://api.appstoreconnect.apple.com/v1/devices&quot;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;meta&quot;&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;paging&quot;&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;total&quot;&lt;/span&gt; : 5,
      &lt;span class=&quot;s2&quot;&gt;&quot;limit&quot;&lt;/span&gt; : 20
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;コマンドライン引数から入力を受け取る&quot;&gt;コマンドライン引数から入力を受け取る&lt;/h2&gt;

&lt;p&gt;前述どおり &lt;a href=&quot;https://github.com/apple/swift-argument-parser&quot;&gt;Swift Argument Parser&lt;/a&gt; を使います．&lt;/p&gt;

&lt;p&gt;README が充実しているので特に説明することはありませんが，Subcommands や ParsableArguments を使うとコマンドを簡単に構造化できます．
ここでは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adm list&lt;/code&gt; で端末リストを出力するようなコマンドを考えました．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AppleDeviceManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ParsableCommand&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;configuration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CommandConfiguration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;commandName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;adm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;abstract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Apple Device Manager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;subcommands&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AppleDeviceManager&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ParsableArguments&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;@Option&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Path to the private key.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;completion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;.p8&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;keyPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;@Option&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Id of the private key.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;keyId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;@Option&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Id of the issuer of private key.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;issuerId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ParsableCommand&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;@OptionGroup&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;baseOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Options&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;mutating&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;// Do something&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;AppleDeviceManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;コマンドライン引数やオプションの parse は煩雑になりがちですが，Swift Argument Parser を使うと簡単に可読性高いコードで記述することができ，ひとことで言うとめっちゃ良いです．&lt;/p&gt;

&lt;h2 id=&quot;端末一覧を取得する&quot;&gt;端末一覧を取得する&lt;/h2&gt;

&lt;p&gt;正直なところ，API トークンを得られた後は普通に API を叩くだけの簡単なお仕事なので特に説明するところがありません．&lt;/p&gt;

&lt;p&gt;リクエストやレスポンスは APIKit で型付けしています．
レスポンスが返ってくるまでコマンドをサスペンドしておく必要があるので，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DispatchSemaphore&lt;/code&gt; を使って &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wait&lt;/code&gt; しています．
APIKit の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Session.send&lt;/code&gt; のコールバックは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sessionQueue&lt;/code&gt; にしておかないと &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;semaphore.signal()&lt;/code&gt; が発火しないので注意します．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ParsableCommand&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@OptionGroup&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;baseOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Options&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;mutating&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;baseOptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;semaphore&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DispatchSemaphore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;GetDeivcesRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;callbackQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sessionQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;DevicePrinter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;semaphore&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;failure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;AppleDeviceManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;withError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;semaphore&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;端末のステータスを更新する&quot;&gt;端末のステータスを更新する&lt;/h2&gt;

&lt;p&gt;API には&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device&quot;&gt;登録済み端末を編集する&lt;/a&gt;エンドポイントが用意されているので，これを使って端末のステータスを &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DISABLED&lt;/code&gt; に更新します．
簡単なお仕事なので（以下略）．&lt;/p&gt;

&lt;h2 id=&quot;コマンドをビルドする&quot;&gt;コマンドをビルドする&lt;/h2&gt;

&lt;p&gt;ビルドコマンド &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swift build&lt;/code&gt; を実行してバイナリを書き出します．
書き出しに成功したら，適当に PATH が通っているところに移動して実行します．&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;swift build &lt;span class=&quot;nt&quot;&gt;--configuration&lt;/span&gt; release
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; .build/release/apple-device-manager /usr/local/bin/adm

&lt;span class=&quot;c&quot;&gt;# Swift Argument Parser が生成してくれる help を見る&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;adm &lt;span class=&quot;nt&quot;&gt;--help&lt;/span&gt;

OVERVIEW: Apple Device Manager

USAGE: adm &amp;lt;subcommand&amp;gt;

OPTIONS:
  &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt;, &lt;span class=&quot;nt&quot;&gt;--help&lt;/span&gt;              Show &lt;span class=&quot;nb&quot;&gt;help &lt;/span&gt;information.

SUBCOMMANDS:
  list
  disable

  See &lt;span class=&quot;s1&quot;&gt;&apos;adm help &amp;lt;subcommand&amp;gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;detailed help.

&lt;span class=&quot;c&quot;&gt;# 端末一覧を表示する&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;adm list &lt;span class=&quot;nt&quot;&gt;--key-id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;xxxx &lt;span class=&quot;nt&quot;&gt;--issuer-id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;xxxx &lt;span class=&quot;nt&quot;&gt;--key-path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/xxx/xxx/xxxx.p8

XXXXXXXXXX E 2020-07-12 10:22:31 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;iPhone SE&quot;&lt;/span&gt;
XXXXXXXXXX E 2019-10-30 13:30:12 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;Xxxxxxxx - iPhone X&quot;&lt;/span&gt;
XXXXXXXXXX E 2020-04-20 00:44:59 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;Xxxxxxxx - iPhone 8 Plus&quot;&lt;/span&gt;
XXXXXXXXXX E 2020-04-29 11:28:08 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;Xxxxxxxx - iPhone 11 Pro&quot;&lt;/span&gt;
XXXXXXXXXX E 2019-10-30 14:08:55 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;Xxxxx - iPhone X&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 登録から1年以上経過している端末は Disabled にする&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;adm disable &lt;span class=&quot;nt&quot;&gt;--age&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1 &lt;span class=&quot;nt&quot;&gt;--key-id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;xxxx &lt;span class=&quot;nt&quot;&gt;--issuer-id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;xxxx &lt;span class=&quot;nt&quot;&gt;--key-path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/xxx/xxx/xxxx.p8

XXXXXXXXXX D 2019-10-30 13:30:12 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;Xxxxxxxx - iPhone X&quot;&lt;/span&gt;
XXXXXXXXXX D 2019-10-30 14:08:55 +0000 &lt;span class=&quot;s2&quot;&gt;&quot;Xxxxx - iPhone X&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;という感じで，Apple Developer に登録している端末に対して，コマンドラインツールを用いて削除する（フラグを立てる）ことができるようになりました．
あとは好きな CI に乗せて定期的に実行すれば良さそうです．
なお，GitHub Actions だとビルド時間が気になるかな？と思いましたが杞憂でした．私の mac 遅すぎ．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/year-end-cleaning/notification.png&quot; alt=&quot;通知&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;p&gt;Apple Developer に登録している端末が放置されている問題（仮説）に対して，古い端末を自動で削除する（フラグを立てる）方法を検証しました．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;App Store Connect API で端末一覧を取得することや，端末のプロパティを編集することができた&lt;/li&gt;
  &lt;li&gt;Swift Argument Parser はめちゃくちゃ便利だった&lt;/li&gt;
  &lt;li&gt;ただし Swift で書くのが楽かというと…うーん&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;なにはともあれ，今年もあと10日ほど．端末の掃除も終わった（？）ということで来年も良い年になりますように．&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://qiita.com/advent-calendar/2020/goodpatch&quot;&gt;Goodpatch Advent Calendar 2020&lt;/a&gt; はもう少しだけ続きます．&lt;/p&gt;

&lt;p&gt;:sunrise:&lt;/p&gt;

&lt;h2 id=&quot;one-more-thing&quot;&gt;One More Thing&lt;/h2&gt;

&lt;h3 id=&quot;linux-用にビルドする&quot;&gt;Linux 用にビルドする&lt;/h3&gt;

&lt;p&gt;今回は Swift Package Manager でプロジェクトを作りました．つまり Linux 用にもビルドできるのでは？
ということで，Docker を使って Linux 用のバイナリを書き出します．&lt;/p&gt;

&lt;p&gt;Docker Image については Vapor プロジェクトでも使われている &lt;a href=&quot;https://hub.docker.com/_/swift&quot;&gt;Swift 公式の Docker Image&lt;/a&gt; を使えば良さそうです．
適当に pull して run してビルドします．&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker pull swift:5.3-focal
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PWD&lt;/span&gt;:/adm &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; /adm swift:5.3-focal swift build &lt;span class=&quot;nt&quot;&gt;--configuration&lt;/span&gt; release
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;がしかし，ここでビルドエラーになります．やはり Write once, run anywhere とはいかないようです．&lt;/p&gt;

&lt;h3 id=&quot;urlsession-が解決できない問題を解決する&quot;&gt;URLSession が解決できない問題を解決する&lt;/h3&gt;

&lt;p&gt;Linux の場合 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;URLSession&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;URLRequest&lt;/code&gt; などは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FoundationNetworking&lt;/code&gt; という別モジュールから読む必要があるようです．
そこで，コンパイラディレクティブを使って，Linux の場合のみ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FoundationNetworking&lt;/code&gt; を import します．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Foundation&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#if os(Linux)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;FoundationNetworking&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;本編では APIKit を使っていましたが，同様のエラーが APIKit の内部でも出ます．
ということで一旦 APIKit を使うことを諦めることにします．
かと言っていまさらコードを書き換えるのも面倒なので，適当にパッチを書いてコンパイルを通しました．&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;$ docker run --rm -it -v $PWD:/adm -w /adm swift:5.3-focal swift build --configuration release
$ docker run --rm -it -v $PWD:/adm -w /adm swift:5.3-focal .build/release/apple-device-manager --help

OVERVIEW: Apple Device Manager

USAGE: adm &amp;lt;subcommand&amp;gt; # 以下略
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Linux 向けにビルドするつもりなら，ライブラリを選ぶときは少し気をつけたほうが良さそう．&lt;/p&gt;

&lt;p&gt;:memo:&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/app-store-connect/api/&quot;&gt;App Store Connect API - Apple Developer&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi&quot;&gt;App Store Connect API | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/devices&quot;&gt;Devices | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api&quot;&gt;Creating API Keys for App Store Connect API | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests&quot;&gt;Generating Tokens for API Requests | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/apple/swift-argument-parser&quot;&gt;apple/swift-argument-parser: Straightforward, type-safe argument parsing for Swift&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/vapor/jwt-kit&quot;&gt;vapor/jwt-kit: 🔑 JSON Web Token signing and verification (HMAC, RSA, ECDSA) using BoringSSL.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hub.docker.com/_/swift&quot;&gt;swift - Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 20 Dec 2020 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2020/12/20/year-end-cleaning.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2020/12/20/year-end-cleaning.html</guid>
        
        
      </item>
    
      <item>
        <title>Abstract API を使った CI の構成を考える</title>
        <description>&lt;p&gt;この記事は &lt;a href=&quot;https://qiita.com/advent-calendar/2020/goodpatch&quot;&gt;Goodpatch Advent Calendar 2020&lt;/a&gt; の6日目です．&lt;/p&gt;

&lt;p&gt;モバイルアプリの UI デザインツールとして Sketch はデファクトと言っても良いくらい広く使われるようになりました．
それに伴って Sketch の利用を前提としたデザインコラボレーションの手段を提供する Abstract のようなサービスも現れました．&lt;/p&gt;

&lt;p&gt;Sketch や Abstract は基本的には GUI での利用を前提としたツールですが，それぞれ CLI ツールや Web API も提供しています．
今回はこれらを利用した UI デザインのポストプロセス自動化について考えます．&lt;/p&gt;

&lt;p&gt;…考えます．そうです「考える」だけなのです．ブラウザの戻るは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cmd + [&lt;/code&gt; です．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;プログラミングの観点で UI デザインワークフローの効率化を考える&lt;/li&gt;
  &lt;li&gt;CI による自動化を考える&lt;/li&gt;
  &lt;li&gt;Abstract を中心とした CI の構成を考える&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;なお，文中で補足しますが，ここでいう「UI デザイン」はかなり狭義な範囲を指しています．&lt;/p&gt;

&lt;h2 id=&quot;プログラミングの観点で-ui-デザインのワークフローの効率化を考える&quot;&gt;プログラミングの観点で UI デザインのワークフローの効率化を考える&lt;/h2&gt;

&lt;h3 id=&quot;プログラミングのワークフロー&quot;&gt;プログラミングのワークフロー&lt;/h3&gt;

&lt;p&gt;近年のモバイルアプリ開発において，プログラミングのワークフローはおおよそベストプラクティスと呼べるものが定まっていると思います．
もちろん，各々のプロジェクトの意向によって構成は様々だと思いますが，例えば以下のようなワークフローを組んでいるのプロジェクトは多いのではないでしょうか．&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Git-flow に従い，作業ブランチを作って実装を進める&lt;/li&gt;
  &lt;li&gt;実装が完了したら GitHub で Pull Request を提出する&lt;/li&gt;
  &lt;li&gt;Pull Request でコードレビューを受ける&lt;/li&gt;
  &lt;li&gt;CI による自動テストを通過する&lt;/li&gt;
  &lt;li&gt;レビュアーからの承認を得る&lt;/li&gt;
  &lt;li&gt;Pull Request をマージする&lt;/li&gt;
  &lt;li&gt;CI によって自動でビルドが作成される&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ci-with-abstract-api/dev-wf.png&quot; alt=&quot;プログラミングのワークフロー&quot; /&gt;&lt;/p&gt;

&lt;p&gt;特に，コードの品質を保つためのレビューや作業効率化のための自動化は，アジャイルな開発スタイルでは欠かせない要素となっています．&lt;/p&gt;

&lt;p&gt;さらに，ワークフローの中でも「実装」部分に着目すると，ここでも作業効率化のために様々な仕組みを使うことが当たり前となっていると思います．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;テキストエディタの機能を使ってコードの補完や自動編集を行うこと&lt;/li&gt;
  &lt;li&gt;Linter などの静的解析ツールを使って機械的に検出可能なミスを減らすこと&lt;/li&gt;
  &lt;li&gt;ユニットテストを用意して回帰テストを行うこと&lt;/li&gt;
  &lt;li&gt;さらには TDD のようにテストによって製品コードの設計の質を上げること&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ワークフローを定めたり様々なレイヤーで効率化を行うことで，エンジニアはエンジニアが本当に注力すべきクリエイティブな仕事に集中できます．&lt;/p&gt;

&lt;h3 id=&quot;ui-デザインのワークフロー&quot;&gt;UI デザインのワークフロー&lt;/h3&gt;

&lt;p&gt;それでは，プログラミングのワークフローと比較して UI デザインのワークフローはどうなっているでしょうか．
ただし，ひとまとめに「UI デザイン」と言ってしまうととても話をまとめきれないほど大きな概念なので，ここでは上記のプログラミングのワークフローに相当する部分についてのみ考え，それを狭義の「UI デザイン」と呼ぶことにします．&lt;/p&gt;

&lt;p&gt;プログラミングのワークフローで挙げた項目それぞれに相当するものを考えると，例えば以下のようなフローが考えられます．
Abstract のようなツールの登場により，プログラミングと同じようなフローが組めるようになっています．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;プログラミング&lt;/th&gt;
      &lt;th&gt;UI デザイン&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;作業ブランチ&lt;/td&gt;
      &lt;td&gt;Abstract でブランチを作ることができる&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Pull Request を提出する&lt;/td&gt;
      &lt;td&gt;ブランチ単位で Review に出すことができる&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;コードレビュー&lt;/td&gt;
      &lt;td&gt;Review の上でコメントをすることができる&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CI による自動テスト&lt;/td&gt;
      &lt;td&gt;？？？&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;レビュアーからの承認&lt;/td&gt;
      &lt;td&gt;Review の上で承認を得ることができる&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Pull Request のマージ&lt;/td&gt;
      &lt;td&gt;ブランチを Master にマージする&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CI による自動ビルド&lt;/td&gt;
      &lt;td&gt;？？？&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ci-with-abstract-api/design-wf.png&quot; alt=&quot;UI デザインのワークフロー&quot; /&gt;&lt;/p&gt;

&lt;p&gt;こうして比較すると，Abstract や Sketch を As-Is の範疇で使うと考えた場合，自動化の観点が抜けているのではないか？ということに気付きます．&lt;/p&gt;

&lt;p&gt;わざわざ「As-Is で」と書いたのは，これらの点についてツールの開発者は既に考慮していて，Abstract には Web API が，Sketch には sketchtool という CLI ツールが提供されているためです．
プロジェクトに合わせて CI を組んだりコードを書いたりする必要がありますが，自動化するために必要なツールは揃っている状況です．&lt;/p&gt;

&lt;p&gt;続いて，ワークフローの中で「実装」に相当するような，Sketch ファイルの編集作業について比較してみます．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;プログラミング&lt;/th&gt;
      &lt;th&gt;UI デザイン&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;コードの補完や自動編集&lt;/td&gt;
      &lt;td&gt;サードパーティ製の Sketch プラグインで検索や置換などが可能&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;静的解析ツール&lt;/td&gt;
      &lt;td&gt;？？？（Assistants を利用可能）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;回帰テスト&lt;/td&gt;
      &lt;td&gt;？？？&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;テストによって製品コードの設計の質を上げる&lt;/td&gt;
      &lt;td&gt;？？？&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;プログラミングの場合と比較すると，UI デザインの効率化はかなり遅れているように感じます．
ただし，これはあくまでプログラミングのフローを元に一方的な視点で比較しているので，恣意的でかなりアンフェアな比較です．
それでも，現状多くのプロジェクトにおいて Sketch ファイルを「きれいに」保つのは，UI デザイナの注意力に頼っている状況であると感じている人も多いのではないでしょうか．&lt;/p&gt;

&lt;p&gt;なお，&lt;a href=&quot;https://www.sketch.com/blog/2020/08/11/sketch-68-is-out-now/&quot;&gt;2020年8月&lt;/a&gt;から Sketch では &lt;a href=&quot;https://developer.sketch.com/assistants/&quot;&gt;Assistants&lt;/a&gt; という機能を使えるようになりました．
Assistants については今回検証しませんが，静的解析による効率化が期待できる機能です．&lt;/p&gt;

&lt;h2 id=&quot;ci-による自動化&quot;&gt;CI による自動化&lt;/h2&gt;

&lt;p&gt;それではプログラムの力を使って Sketch ファイルに対する単純作業を自動化できないでしょうか？
例えば「CI による自動ビルド」に相当するような作業を Sketch に対して行えるでしょうか？&lt;/p&gt;

&lt;p&gt;実は CI に sketchtool を組み込むというアイデア自体は，既に数年前から各所で事例が共有されています．
例えば iOSDC Japan 2017 では&lt;a href=&quot;https://iosdc.jp/2017/node/1442&quot;&gt;アイコンや画像の配置をCIで自動化する&lt;/a&gt;という発表があり，CI と sketchtool を使って iOS の Asset Catalog を書き出して GitHub に Pull Request を出すまでを自動化したとあります（より詳しくは &lt;a href=&quot;https://konifar.hatenablog.com/entry/2017/09/17/231704&quot;&gt;Konifar’s WIP&lt;/a&gt; を参照）．&lt;/p&gt;

&lt;p&gt;ただし，これらの事例は Git を中心としたエンジニア寄りのワークフローになっています．
Git を使うこと自体は CI の組みやすさという点でメリットもあり，また，デザイナーが Git を使う障壁も GUI ツールを使えばそこまで高くない（個人の経験）ですが，Abstract を使っているフローの中で「Git リポジトリにも push してください」というのは明らかに面倒です．&lt;/p&gt;

&lt;p&gt;よって Abstract API を使えば Git の場合と同様に CI を組めるのではないか？というのが今回の検証内容となります．
ようやく少し技術ブログらしい感じになってきました．&lt;/p&gt;

&lt;h2 id=&quot;abstract-を中心とした-ci-の構成&quot;&gt;Abstract を中心とした CI の構成&lt;/h2&gt;

&lt;p&gt;Abstract を中心とした CI の構成を考えます．
基本的には Git を中心とした構成と変わらず，Sketch ファイル更新の検知と Sketch ファイルの取得を Abstract 経由に置き換える形です．&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;デザイナーが Abstract の Master ブランチを更新する&lt;/li&gt;
  &lt;li&gt;Master ブランチの更新を検知する&lt;/li&gt;
  &lt;li&gt;Master ブランチから Sketch ファイルを取得する&lt;/li&gt;
  &lt;li&gt;Sketch ファイルからアセットを書き出す&lt;/li&gt;
  &lt;li&gt;Google Drive にデプロイする&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ci-with-abstract-api/ci-flow.png&quot; alt=&quot;CI の構成&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上記は自分のプロジェクト環境に合わせて組んでみた構成ですが，もちろんアセットの出力内容や出力先を変えることは自由にできます．
また，それらステップ 4-5 については既存の事例を Web で見つけることができるので，今回は説明しません．&lt;/p&gt;

&lt;h2 id=&quot;abstract-api&quot;&gt;Abstract API&lt;/h2&gt;

&lt;p&gt;今回の検証の主な対象である Abstract 公式の API です．
API の仕様については &lt;a href=&quot;https://sdk.abstract.com/docs/abstract-api/&quot;&gt;Abstract SDK API Reference&lt;/a&gt; にまとまっています．
SDK が提供されているので，それを使って API を叩く形になります．&lt;/p&gt;

&lt;p&gt;SDK は npm パッケージとして提供されていて TypeScript で使うことができます．
API トークンはプロジェクトに対する Viewer 権限があれば発行できるようなので，比較的簡単に使い始めることができます．&lt;/p&gt;

&lt;h3 id=&quot;api-でできること&quot;&gt;API でできること&lt;/h3&gt;

&lt;p&gt;詳細については API Reference を参照するとして，例えば以下のようなことができます．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;API&lt;/th&gt;
      &lt;th&gt;できること&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client.branches.list(...)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;ブランチの一覧を取得する．&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client.files.raw(...)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;任意の Sketch ファイルを取得する．&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client.previews.raw(...)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;任意の Layer を png として書き出す．なお svg や pdf には非対応．&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;少なくともこれだけできれば，ブランチに更新があるか調べて，更新があった場合には Sketch ファイルを書き出すといったことが可能です．&lt;/p&gt;

&lt;h3 id=&quot;webhooks-について&quot;&gt;Webhooks について&lt;/h3&gt;

&lt;p&gt;更新を push 型で受け取る仕組みとして Abstract は &lt;a href=&quot;https://sdk.abstract.com/docs/webhooks/&quot;&gt;Webhooks&lt;/a&gt; を提供しているようです．
ただし Webhooks の機能は2020年11月時点では beta となっています．
実は今回の検証のために &lt;a href=&quot;https://docs.google.com/forms/d/e/1FAIpQLSevRBz_upT8p2YrieDRrlIKyAUAOHQ5A1xZFn2AMLlrae2rOA/viewform&quot;&gt;Request beta access now&lt;/a&gt; から何度かリクエストを送ったのですが，残念ながら返答がありませんでした．&lt;/p&gt;

&lt;p&gt;Webhooks が使えないので，検証では Crontab による定期トリガーを設定しました．
トリガーさえしてしまえば pull 型の差分チェックはできるのでヨシ！とします．（嘘です．Webhooks 使いたいです．よろしくお願いします…）&lt;/p&gt;

&lt;h3 id=&quot;sketch-ファイルを書き出す&quot;&gt;Sketch ファイルを書き出す&lt;/h3&gt;

&lt;p&gt;Abstract のファイル構造としては，組織（Organization）&amp;gt; プロジェクト（Projects） &amp;gt; Sketch ファイル（Files）という形になっています．
また，ブランチについては，プロジェクト &amp;gt; ブランチ（Branches）という形でプロジェクトに紐づきます．
したがって，最初に API トークンと Organization ID さえ分かっていれば，あとはトップダウンでクエリしていくことができます．&lt;/p&gt;

&lt;p&gt;以下のコードは実際のコードではなく説明用に簡略化したコードですが，Sketch ファイルを書き出すまでの流れはおおよそ把握できるかと思います．&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;abstract-sdk&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;accessToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ABSTRACT_API_TOKEN&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;organization&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;organizationId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ABSTRACT_ORGANIZATION_ID&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;projects&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;projects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;organization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;project&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;projects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;GR@DATE WING&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Master ブランチの ID は &apos;master&apos; と決まっている．&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;branches&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;projectId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;master&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;master&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;headCache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;master&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;headCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// 最新コミットの SHA は &apos;latest&apos; でクエリできる．&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;baseQuery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;projectId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;branchId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;master&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;sha&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;latest&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;baseQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fileQuery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;baseQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;fileId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;raw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fileQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.sketch`&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;実際のコードでは head の差分比較を別スクリプトとして分離したり，書き出す Sketch ファイルの条件を設定ファイルで指定できるようにしたり，といった考慮がされています．
実際のプロジェクトだと「アイデア置き場」としての Sketch ファイルなど，書き出す必要のない中間成果物もあるためです（まさにスケッチ！）．&lt;/p&gt;

&lt;h2 id=&quot;sketchtool-のインストール&quot;&gt;sketchtool のインストール&lt;/h2&gt;

&lt;p&gt;Sketch ファイルが取得できたので，あとは sketchtool をインストールすればアセットを書き出すことができます．&lt;/p&gt;

&lt;p&gt;インストールからアセット書き出しまでの手順は前述の iOSDC トークに関連する &lt;a href=&quot;https://github.com/konifar/sketch-export-sample&quot;&gt;GitHub リポジトリ&lt;/a&gt;などに詳しく紹介があります．
ただし，Sketch の ZIP ファイルのダウンロード URL が古くなっているようで，新しい URL は次のとおりでした．&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;https://download.sketch.com/sketch.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;ちなみに，ファイルのダウンロードや Unzip などはシェルのコマンドで実行する方が楽なので，Node.js から &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;child_process.exec()&lt;/code&gt; を呼びまくるという，やや強引な方法で実装しました．
いっそのことシェルスクリプトの方が良いかもしれません．&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;promisify&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;util&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;child_process&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;child_process&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;promisify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;child_process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`curl -L -o sketch.zip https://download.sketch.com/sketch.zip`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;その他の技術的要素&quot;&gt;その他の技術的要素&lt;/h2&gt;

&lt;p&gt;その他の技術的要素について，ひとことふたこと説明しておきます．&lt;/p&gt;

&lt;p&gt;Google Drive へのアップロードは &lt;a href=&quot;https://developers.google.com/drive/api/v3/reference&quot;&gt;Google Drive API Reference&lt;/a&gt; を参照すれば，少々面倒なことはあっても難しいことはないと思います．
なお2020年11月時点で最新版の V3 を使いました．
共有ドライブ（Shared Drive）を使っている場合は，オプションの指定方法を変えるなど&lt;a href=&quot;https://developers.google.com/drive/api/v3/enable-shareddrives&quot;&gt;ちょっとした対応&lt;/a&gt;が必要です．&lt;/p&gt;

&lt;p&gt;CI ツールは &lt;a href=&quot;https://docs.github.com/en/free-pro-team@latest/actions&quot;&gt;GitHub Actions&lt;/a&gt; を使いました．
定期トリガーは &lt;a href=&quot;https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#onschedule&quot;&gt;on.schedule&lt;/a&gt; で設定できます．
Head 情報はファイルに書いて &lt;a href=&quot;https://github.com/actions/cache&quot;&gt;actions/cache@v2&lt;/a&gt; でキャッシュしました．&lt;/p&gt;

&lt;p&gt;最後に Slack に通知して完了です．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ci-with-abstract-api/notification.png&quot; alt=&quot;通知&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Sketch ファイルからアセットを自動で書き出す CI を組みました&lt;/li&gt;
  &lt;li&gt;先行事例を参考にしつつ，Git 中心ではなく Abstract を中心とした構成にしました&lt;/li&gt;
  &lt;li&gt;Abstract API を使って差分があるかどうかを検知したり，最新の Sketch ファイルを取得したりできました&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;one-more-thing&quot;&gt;One More Thing&lt;/h3&gt;

&lt;p&gt;静的解析の可能性について，本文中では Sketch Assistants について少しだけ言及しました．
Assistants は Sketch アプリの中で動く拡張機能で，こちらも Node.js と TypeScript で開発できます．&lt;/p&gt;

&lt;p&gt;試しに文言のバリデーションルールを書いてみたところ，不可視のレイヤーに古い文言が残っているなど，目視では発見しにくいミスを見つけることができました．
Sketch を使ったワークフローにおいて，早い段階でミスを検知するための有力な選択肢になりそうです．&lt;/p&gt;

&lt;p&gt;もしかすると &lt;a href=&quot;https://qiita.com/advent-calendar/2020/goodpatch&quot;&gt;Goodpatch Advent Calendar 2020&lt;/a&gt; で誰かが何か書いてくれるかもしれません．&lt;/p&gt;

&lt;p&gt;:bulb: 追記：Advent Calendar 15日目で @mnkd が Assistants について書いてくれました．
日本語で丁寧に説明された文章は貴重だと思いますので，気になった方々はチェックしてみてください．&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://qiita.com/mnkd/items/c85509a4e68571b208ab&quot;&gt;Sketch Assistants で DesignOps をアシスト - Qiita&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.sketch.com/cli/&quot;&gt;Sketch Developer — Command-line interface&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.sketch.com/assistants/&quot;&gt;Sketch Developer — Assistants&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.sketch.com/blog/2020/08/11/sketch-68-is-out-now/&quot;&gt;Sketch — Sketch 68 is out now&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://sdk.abstract.com/docs/abstract-api/&quot;&gt;Client · Abstract SDK&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://sdk.abstract.com/docs/webhooks/&quot;&gt;Webhooks · Abstract SDK&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://speakerdeck.com/konifar/import-sketch-icons-to-assets-catalog-on-ci&quot;&gt;Import Sketch Icons to Assets Catalog on CI - Speaker Deck&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://konifar.hatenablog.com/entry/2017/09/17/231704&quot;&gt;iOSDCで『Sketchからアイコン切り出し ⇒ Asset Catalogを自動生成 ⇒ 差分があればPullRequest』という流れをCIで自動化する話をしました - Konifar’s WIP&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/konifar/sketch-export-sample&quot;&gt;konifar/sketch-export-sample: Sample repository to export the icons in Sletch file to iOS/Android projects.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/drive/api/v3/reference&quot;&gt;API Reference | Google Drive API | Google Developers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/drive/api/v3/enable-shareddrives&quot;&gt;Implement shared drive support |  Google Drive API | Google Developers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/free-pro-team@latest/actions&quot;&gt;GitHub Actions Documentation - GitHub Docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://qiita.com/mnkd/items/c85509a4e68571b208ab&quot;&gt;Sketch Assistants で DesignOps をアシスト - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 06 Dec 2020 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2020/12/06/ci-with-abstract-api.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2020/12/06/ci-with-abstract-api.html</guid>
        
        
        <category>UI</category>
        
        <category>Design</category>
        
        <category>Abstract</category>
        
        <category>Sketch</category>
        
        <category>CI</category>
        
      </item>
    
      <item>
        <title>Attribute を使って Unity から Cloud Firestore の読み書きを簡単にする</title>
        <description>&lt;p&gt;Unity で簡単な PvP ゲームを作りたいとき，Firebase の Cloud Firestore を使えばバックエンドのコーディングをしなくても動かしてみることができる．
Firestore への読み書きは&lt;a href=&quot;https://firebase.google.com/docs/firestore/query-data/get-data&quot;&gt;ドキュメント&lt;/a&gt;に書かれた方法でも OK だが，Attribute を使うともっと簡単にできる．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;p&gt;Cloud Firestore への読み書きを Attribute と独自クラスを使って簡単にする．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SDK で定義された &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreData&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreProperty&lt;/code&gt; を使う&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DocumentSnapshot&lt;/code&gt; から独自クラスに変換できるようにして，読み込みを簡単にする&lt;/li&gt;
  &lt;li&gt;独自クラスからディクショナリ型に変換できるようにして，書き込みを簡単にする&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Unity 2020.1.3f1&lt;/li&gt;
  &lt;li&gt;Cloud Firestore for Firebase 6.13.0&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;attribute-を使った読み込み基礎編&quot;&gt;Attribute を使った読み込み（基礎編）&lt;/h2&gt;

&lt;p&gt;Cloud Firestore 読み書きの&lt;a href=&quot;https://firebase.google.com/docs/firestore/query-data/get-data&quot;&gt;初歩的なドキュメント&lt;/a&gt;では，取得した &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DocumentSnapshot&lt;/code&gt; を &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ToDictionary()&lt;/code&gt; でディクショナリ型に変換して値を読み書きする方法が紹介されている．
ドキュメントの目的が Getting Started だと考えればこれはこれで正しい方法なのだが，次第にコードが増えていくと &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; のキャストも増えていくのが気になってくる．&lt;/p&gt;

&lt;p&gt;実は Firestore SDK には &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreData&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreProperty&lt;/code&gt; という Attribute がある．
これらを使いつつ独自クラスを定義してあげると，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DocumentSnapshot&lt;/code&gt; 型から独自クラスへの変換が &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvertTo&amp;lt;T&amp;gt;()&lt;/code&gt; メソッドを呼び出すだけで可能になる．&lt;/p&gt;

&lt;p&gt;例えば Room を表すクラスを以下のように定義する．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestoreData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RoomEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestoreProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestoreProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerCount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;この &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RoomEntity&lt;/code&gt; を使うと &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DocumentSnapshot&lt;/code&gt; から &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvertTo&amp;lt;RoomEntity&amp;gt;()&lt;/code&gt; で &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RoomEntity&lt;/code&gt; にキャストされたオブジェクトを得ることができる．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FirebaseFirestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DefaultInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomDocument&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;rooms&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;room1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetSnapshotAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomDocument&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ConvertTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RoomEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;独自クラスの定義さえ間違えていなければ良いので，都度 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; をキャストするよりも簡単で安全と言える．&lt;/p&gt;

&lt;h2 id=&quot;attribute-を使った読み込み応用編&quot;&gt;Attribute を使った読み込み（応用編）&lt;/h2&gt;

&lt;p&gt;場合によっては，もう少し凝ったクラスを定義したいこともある．&lt;/p&gt;

&lt;h3 id=&quot;命名規則が違う場合&quot;&gt;命名規則が違う場合&lt;/h3&gt;

&lt;p&gt;Firestore のフィールド名と C# コードのプロパティ名の命名規則が違う場合でも， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreProperty&lt;/code&gt; の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Name&lt;/code&gt; プロパティで紐づければ変換できるようになる．&lt;/p&gt;

&lt;p&gt;例えば &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RoomEntity&lt;/code&gt; に対応する Room ドキュメントが Snake Case で定義されている場合．&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;player_ids&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;player_count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;以下のように &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreProperty&lt;/code&gt; の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Name&lt;/code&gt; を指定すればよい．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestoreData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RoomEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FirestoreProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;player_ids&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FirestoreProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;player_count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerCount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;プロパティを-readonly-にしたい場合&quot;&gt;プロパティを Readonly にしたい場合&lt;/h3&gt;

&lt;p&gt;独自クラスによっては，プロパティの書き込みを制限したい場合がある．
例えば &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RoomEntity&lt;/code&gt; でいうと，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerCount&lt;/code&gt; は &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerIds&lt;/code&gt; の要素数と整合性をとりたいので，Readonly にしたい．&lt;/p&gt;

&lt;p&gt;結果から示してしまうと，以下のように Setter を private や空実装にすればよい．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestoreData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RoomEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FirestoreProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;player_ids&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerIds&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FirestoreProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;player_count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerCount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlayerIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;これは &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreProperty&lt;/code&gt; の実装に依存すると思われるが，Setter を private にしても問題なく変換することができた．
ただし，Setter は private や空実装でも残しておく必要がある．
Setter を定義しない場合でもコンパイルや実行は可能だが，実行時に警告が出てしまう．&lt;/p&gt;

&lt;h2 id=&quot;独自クラスを使った書き込み&quot;&gt;独自クラスを使った書き込み&lt;/h2&gt;

&lt;p&gt;Firestore に書き込む場合には &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Set&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Add&lt;/code&gt; といったメソッドを使うが，これらはディクショナリ型の引数を取る．&lt;/p&gt;

&lt;p&gt;Attribute を使った読み込みができるようになったが，その反対に独自クラスをディクショナリ型に変換する方法は SDK では（おそらく）提供されていないので，簡単な Extension を実装する．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FirestoreExtension&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Dictionary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ToFirestoreDictionary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetProperties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Attribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;IsDefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestorePropertyAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ToDictionary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetCustomAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FirestorePropertyAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;この Extension を使うと Room は以下のように更新できる．&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FirebaseFirestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DefaultInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomDocument&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;rooms&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;room1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetSnapshotAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomEntity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomDocument&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ConvertTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RoomEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;();&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;roomEntity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PlayerIds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;player4&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;roomDocument&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Reference&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;UpdateAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ToFirestoreDictionary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;なお，実際にはクライアントサイドからドキュメントを直接更新することは少ないかもしれない．
特に複数人で共有するリソースの場合，権限判断を含めてサーバーサイドで書き込む方が普通だろう．&lt;/p&gt;

&lt;h2 id=&quot;まとめ--感想&quot;&gt;まとめ &amp;amp; 感想&lt;/h2&gt;

&lt;p&gt;Cloud Firestore への読み書きを簡単にする方法をまとめた．&lt;/p&gt;

&lt;p&gt;SDK で定義された &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreData&lt;/code&gt; や &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirestoreProperty&lt;/code&gt; を使うとドキュメントへの読み書きを簡単にすることができた．
クラスの定義を少し工夫すると，フィールド名の差を吸収したり，プロパティを Readonly にして書き込みミスを減らすこともできた．&lt;/p&gt;

&lt;p&gt;クライアントサイドから読み書きできるから，プロトタイピングとの相性が良さそう．
本番実装の際に書き込みのロジックはサーバーサイドに移行するとしても，リアルタイム読み込みのロジックは残せるところが多いかも．&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://firebase.google.com/docs/firestore/query-data/get-data&quot;&gt;Get data with Cloud Firestore  |  Firebase&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 13 Sep 2020 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2020/09/13/firebase-data-attribute.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2020/09/13/firebase-data-attribute.html</guid>
        
        
      </item>
    
      <item>
        <title>UIView に elevation っぽさを付ける</title>
        <description>&lt;p&gt;この記事は &lt;a href=&quot;https://qiita.com/advent-calendar/2019/goodpatch&quot;&gt;Goodpatch Advent Calendar 2019&lt;/a&gt; の23日目です．&lt;/p&gt;

&lt;p&gt;iOS アプリの UI デザインで elevation について意識することは Android の場合と比較すると少ないと思います．
ただし，Material Design 自体はプラットフォームを限定せずに適用可能な考え方であり，elevation は Material Components を使うかどうかを別としても UI をデザインするために有用です．
ということで UIView で elevation を表現する方法について Material Components も参考にしながら実装してみました．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;UIView の CALayer に影を付けて elevation っぽい表現をする&lt;/li&gt;
  &lt;li&gt;Elevation の値に応じて影の radius や offset を設定する&lt;/li&gt;
  &lt;li&gt;影は ambient と key の2種類をつける&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;という方針で実装しました．&lt;/p&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Xcode 11.0&lt;/li&gt;
  &lt;li&gt;iOS Simulator 13.0&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;mdc-の影を試す&quot;&gt;MDC の影を試す&lt;/h2&gt;

&lt;p&gt;Material Components for iOS の影がどのような挙動になっているのかデモを作って確認してみます．&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;MDC elevation 1&quot; src=&quot;/images/pages/uiview-elevation/mdc_1.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;MDC elevation 2&quot; src=&quot;/images/pages/uiview-elevation/mdc_2.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;MDC elevation 3&quot; src=&quot;/images/pages/uiview-elevation/mdc_3.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;MDC elevation 5&quot; src=&quot;/images/pages/uiview-elevation/mdc_5.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;MDC elevation 8&quot; src=&quot;/images/pages/uiview-elevation/mdc_8.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;MDC elevation 13&quot; src=&quot;/images/pages/uiview-elevation/mdc_13.png&quot; width=&quot;48%&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;uiview-に影をつける&quot;&gt;UIView に影をつける&lt;/h2&gt;

&lt;p&gt;Material Components (MDC) を参考にしながら，UIView に影をつけていきます．&lt;/p&gt;

&lt;h3 id=&quot;radius-を設定する&quot;&gt;Radius を設定する&lt;/h3&gt;

&lt;p&gt;まずは UIView の layer に最も単純な形で影をつけてみます．
とりあえず elevation の高さ分だけ shadowRadius をつけてみます．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CardView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setShadowElevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;elevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;cornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cornerRadius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cornerRadius&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIColor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgColor&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowRadius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elevation&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOffset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOpacity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.33&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt=&quot;Shadow radius 1&quot; src=&quot;/images/pages/uiview-elevation/radius_1.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow radius 2&quot; src=&quot;/images/pages/uiview-elevation/radius_2.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow radius 3&quot; src=&quot;/images/pages/uiview-elevation/radius_3.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow radius 5&quot; src=&quot;/images/pages/uiview-elevation/radius_5.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow radius 8&quot; src=&quot;/images/pages/uiview-elevation/radius_8.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow radius 13&quot; src=&quot;/images/pages/uiview-elevation/radius_13.png&quot; width=&quot;48%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;UIView の方はカードの中心（よりやや下？）に点光源があるような影のつき方で，MDC のような強い elevation は感じられません．&lt;/p&gt;

&lt;h3 id=&quot;offset-を変更する&quot;&gt;Offset を変更する&lt;/h3&gt;

&lt;p&gt;続いて shadowOffset も設定してみます．
とりあえず elevation の高さ分だけ Y 軸方向にずらします．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOffset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt=&quot;Shadow offset 1&quot; src=&quot;/images/pages/uiview-elevation/offset_1.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow offset 2&quot; src=&quot;/images/pages/uiview-elevation/offset_2.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow offset 3&quot; src=&quot;/images/pages/uiview-elevation/offset_3.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow offset 5&quot; src=&quot;/images/pages/uiview-elevation/offset_5.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow offset 8&quot; src=&quot;/images/pages/uiview-elevation/offset_8.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow offset 13&quot; src=&quot;/images/pages/uiview-elevation/offset_13.png&quot; width=&quot;48%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;だいぶそれらしくなりました．
ぱっと見はどちらも区別がつかないかもしれません．
ただし，よく見てみると，なんとなく MDC の方が「カード」らしい存在感があるようにも見えます．
特に elevation が小さいときが顕著です．&lt;/p&gt;

&lt;p&gt;そこで &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elevation = 1&lt;/code&gt; の両者を拡大して並べてみると，MDC の方は edge 付近の影が濃くなっていることが分かります．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Top&lt;/th&gt;
      &lt;th&gt;Bottom&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/uiview-elevation/mdc_vs_uiview_top.png&quot; alt=&quot;Comparing MDC and UIView 1&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/uiview-elevation/mdc_vs_uiview_bottom.png&quot; alt=&quot;Comparing MDC and UIView 2&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MDC（左半分）の方が影の領域が広い．&lt;/td&gt;
      &lt;td&gt;MDC（左半分）は edge 付近の影がより濃い．薄くなっていく勾配も線形ではない．&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;2つの影をつける&quot;&gt;2つの影をつける&lt;/h3&gt;

&lt;p&gt;Material Design Guideline の &lt;a href=&quot;https://material.io/design/environment/light-shadows.html&quot;&gt;Light and shadows&lt;/a&gt; を読むと，Material Design では ambient と key の2種類の影が必要なことが分かります．&lt;/p&gt;

&lt;p&gt;その実装である &lt;a href=&quot;https://github.com/material-components/material-components-ios/blob/develop/components/ShadowLayer/src/MDCShadowLayer.m&quot;&gt;MDCShadowLayer.m&lt;/a&gt; を読むと，ガイドラインどおり ambient と key の2種類の影が設定されていることが分かります．
Ambient は elevation によって offset が変わらない影で，opacity もかなり薄くつけられています．
また Radius の値は elevation の一次関数で決まっていますが，ambient と key でそれぞれに係数が調整されています．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Ambient Opacity&lt;/th&gt;
      &lt;th&gt;Key Opacity&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;0.08&lt;/td&gt;
      &lt;td&gt;0.26&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Elevation&lt;/th&gt;
      &lt;th&gt;Ambient Radius&lt;/th&gt;
      &lt;th&gt;Key Radius&lt;/th&gt;
      &lt;th&gt;Key Offset&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;-.–&lt;/td&gt;
      &lt;td&gt;-.–&lt;/td&gt;
      &lt;td&gt;-.–&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0.88&lt;/td&gt;
      &lt;td&gt;0.66&lt;/td&gt;
      &lt;td&gt;1.19&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;1.77&lt;/td&gt;
      &lt;td&gt;1.33&lt;/td&gt;
      &lt;td&gt;2.42&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;2.66&lt;/td&gt;
      &lt;td&gt;1.99&lt;/td&gt;
      &lt;td&gt;3.65&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;4.44&lt;/td&gt;
      &lt;td&gt;3.33&lt;/td&gt;
      &lt;td&gt;6.11&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8&lt;/td&gt;
      &lt;td&gt;7.11&lt;/td&gt;
      &lt;td&gt;5.33&lt;/td&gt;
      &lt;td&gt;9.81&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;13&lt;/td&gt;
      &lt;td&gt;11.56&lt;/td&gt;
      &lt;td&gt;8.66&lt;/td&gt;
      &lt;td&gt;15.96&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;というわけで，ambient 用の CALayer を subLayer として追加します．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CardView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;weak&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ambientShadowLayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CALayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;maxElevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setShadowElevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;elevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;cornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cornerRadius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cornerRadius&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIColor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgColor&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowRadius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elevation&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOffset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elevation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOpacity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.33&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ambientShadowLayer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CALayer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;superLayer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;layer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CAShapeLayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bounds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;superLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bounds&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;anchorPoint&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIBezierPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;roundedRect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;superLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bounds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;cornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;superLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgPath&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fillColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;backgroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgColor&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;maskPath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIBezierPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;rect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;superLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bounds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;insetBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;dx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxElevation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxElevation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;maskPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CAShapeLayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maskPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgPath&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bounds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;superLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bounds&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;anchorPoint&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fillRule&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;evenOdd&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fillColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;backgroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgColor&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;

            &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addSublayer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIColor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;black&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cgColor&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowRadius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elevation&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOffset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ambientShadowLayer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shadowOpacity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.11&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;レイヤーに設定した影が描画されるためにはレイヤーに塗りが必要ですが，レイヤーに塗りがあると元の UIView の描画を邪魔してしまうので mask を作って元の UIView 部分をくり抜いて透明化しています．&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;Shadow ambient 1&quot; src=&quot;/images/pages/uiview-elevation/ambient_1.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient 2&quot; src=&quot;/images/pages/uiview-elevation/ambient_2.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient 3&quot; src=&quot;/images/pages/uiview-elevation/ambient_3.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient 5&quot; src=&quot;/images/pages/uiview-elevation/ambient_5.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient 8&quot; src=&quot;/images/pages/uiview-elevation/ambient_8.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient 13&quot; src=&quot;/images/pages/uiview-elevation/ambient_13.png&quot; width=&quot;48%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;かなりそれらしくなりました．
あとは opacity や offset の係数を調整して各々のいい感じに調整していけば良さそうです．&lt;/p&gt;

&lt;h3 id=&quot;one-more-thing&quot;&gt;One More Thing&lt;/h3&gt;

&lt;p&gt;Ambient と key の2つの影を設定できるようになったので，それぞれに black 以外の色を指定することもできます．
試しに orange を設定してみると次のようになりました．&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;Shadow ambient colored 1&quot; src=&quot;/images/pages/uiview-elevation/ambient_colored_1.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient colored 2&quot; src=&quot;/images/pages/uiview-elevation/ambient_colored_2.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient colored 3&quot; src=&quot;/images/pages/uiview-elevation/ambient_colored_3.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient colored 5&quot; src=&quot;/images/pages/uiview-elevation/ambient_colored_5.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient colored 8&quot; src=&quot;/images/pages/uiview-elevation/ambient_colored_8.png&quot; width=&quot;48%&quot; /&gt;
&lt;img alt=&quot;Shadow ambient colored 13&quot; src=&quot;/images/pages/uiview-elevation/ambient_colored_13.png&quot; width=&quot;48%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;アプリの性格やコンテンツの内容に合わせて色付きの影（ただし，もっとさりげない色で）を入れてみるのも良さそうです．&lt;/p&gt;

&lt;h2 id=&quot;まとめ感想&quot;&gt;まとめ・感想&lt;/h2&gt;

&lt;p&gt;UIView で Material Design のような elevation の表現を実装する方法をまとめました．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Elevation によって影の radius と offset を設定した&lt;/li&gt;
  &lt;li&gt;Ambient と key の2つの影を設定した
    &lt;ul&gt;
      &lt;li&gt;Ambient は元の UIView のサブレイヤーとして追加した&lt;/li&gt;
      &lt;li&gt;そのままだと UIView の描画を邪魔するのでマスクをかけた&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;コスパを重視するなら key シャドーだけでも良さそうですが，ambient が加わるだけでカードの存在感が増したり，表現の幅が広がったりして面白いです．&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://material.io/design/environment/light-shadows.html&quot;&gt;Light and shadows - Material Design&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/material-components/material-components-ios/blob/develop/components/ShadowLayer/src/MDCShadowLayer.m&quot;&gt;material-components-ios/MDCShadowLayer.m at develop · material-components/material-components-ios&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 23 Dec 2019 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2019/12/23/uiview-elevation.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2019/12/23/uiview-elevation.html</guid>
        
        
        <category>iOS</category>
        
        <category>UI</category>
        
        <category>Design</category>
        
        <category>Material</category>
        
        <category>Components</category>
        
      </item>
    
      <item>
        <title>UILabel の改行をいい感じにする実験</title>
        <description>&lt;p&gt;この記事は &lt;a href=&quot;https://qiita.com/advent-calendar/2019/goodpatch&quot;&gt;Goodpatch Advent Calendar 2019&lt;/a&gt; の16日目です．&lt;/p&gt;

&lt;p&gt;API から動的にテキストを取得する場合，改行の位置を制御することはむずかしいです．
UILabel には Word Wrap の機能がありますが，日本語では使えません．
そこで，なんとか自力で改行格好悪い問題に対処できないか，実験をしてみました．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;iOS SDK の自然言語処理で単語の区切りを取得する&lt;/li&gt;
  &lt;li&gt;UILabel の改行位置を取得する&lt;/li&gt;
  &lt;li&gt;テキストの改行が単語の途中で挟まらないように自分で改行を挟んでから流し込む&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;という方針で試していきます．実験に使ったソースコードは GitHub にあります．&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/thedoritos/NaturalLabel&quot;&gt;thedoritos/NaturalLabel: Experiment to break line naturally in UILabel&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;環境&quot;&gt;環境&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;iOS Simulator iOS 13&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;通常のレイアウト&quot;&gt;通常のレイアウト&lt;/h2&gt;

&lt;p&gt;まず，お題となるアプリを用意するために，通常どおり UILabel を使って標準的なレイアウトをしてみます．&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://developers.google.com/books/&quot;&gt;Google Books API&lt;/a&gt; を使って適当な書籍の情報を取得して TableView に表示しました．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;iPhone 11 Pro&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;iPhone 11 Pro Max&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/0_original_page1_pro.png&quot; alt=&quot;original layout iPhone Pro page 1&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/0_original_page2_pro.png&quot; alt=&quot;original layout iPhone Pro page 2&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/0_original_page1_promax.png&quot; alt=&quot;original layout iPhone Pro Max page 1&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/0_original_page2_promax.png&quot; alt=&quot;original layout iPhone Pro Max page 2&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;意外とそんなに気にならない結果になったような気もします．&lt;/p&gt;

&lt;p&gt;完！！！&lt;/p&gt;

&lt;p&gt;…では実験にならないので，もう少しいい感じにできる余地があるか試していきます．
今回は次の3タイトルをベンチマークとして調整していきます．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;やはり俺の青春ラブコメはまちがっている。14&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;隠れオタな彼女と、史上最高のラブコメをさがしませんか？&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;裏方キャラの青木くんがラブコメを制すまで。&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;遊びなので妥当性は気にしません．
目についたものを選んだだけで他意はありません．&lt;/p&gt;

&lt;h2 id=&quot;natural-language&quot;&gt;Natural Language&lt;/h2&gt;

&lt;p&gt;iOS には端末で自然言語処理を行える &lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage&quot;&gt;Natural Language&lt;/a&gt; Framework があります．
これを使って日本語の改行を「いい感じ」にすることができないか試してみます．&lt;/p&gt;

&lt;h3 id=&quot;tokenizing-natural-language-text&quot;&gt;Tokenizing Natural Language Text&lt;/h3&gt;

&lt;p&gt;まずは文庫本のタイトルを単語単位に分割してみます．
ドキュメントの &lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage/tokenizing_natural_language_text&quot;&gt;Tokenizing Natural Language Text&lt;/a&gt; に従って次のようにしました．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIKit&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NaturalLanguage&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NaturalLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UILabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;naturalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;debugPrint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tokenizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NLTokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startIndex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;debugPrint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;3タイトルはそれぞれ次のように分割できました．&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;やはり俺の青春ラブコメはまちがっている。14&quot;
[&quot;やはり&quot;, &quot;俺&quot;, &quot;の&quot;, &quot;青春&quot;, &quot;ラブコメ&quot;, &quot;は&quot;, &quot;まちがっ&quot;, &quot;て&quot;, &quot;いる&quot;, &quot;14&quot;]

&quot;隠れオタな彼女と、史上最高のラブコメをさがしませんか？&quot;
[&quot;隠れ&quot;, &quot;オタ&quot;, &quot;な&quot;, &quot;彼女&quot;, &quot;と&quot;, &quot;史上&quot;, &quot;最高&quot;, &quot;の&quot;, &quot;ラブコメ&quot;, &quot;を&quot;, &quot;さがし&quot;, &quot;ませ&quot;, &quot;ん&quot;, &quot;か&quot;]

&quot;裏方キャラの青木くんがラブコメを制すまで。&quot;
[&quot;裏方&quot;, &quot;キャラ&quot;, &quot;の&quot;, &quot;青木&quot;, &quot;くん&quot;, &quot;が&quot;, &quot;ラブコメ&quot;, &quot;を&quot;, &quot;制&quot;, &quot;す&quot;, &quot;まで&quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;単語の途中で改行がはさまらないようにできれば，「まち\nがっている」「史上最\n高」「ラブコ\nメ」といった不自然な改行は改善できそうです．
一方で，「まちがっ\nている」「史上\n最高の」「制\nすまで」といった，間違いとは言えないが少し気持ち悪いかな（個人の感想です）という改行を防ぐことはできなさそうです．&lt;/p&gt;

&lt;h3 id=&quot;identifying-parts-of-speech&quot;&gt;Identifying Parts of Speech&lt;/h3&gt;

&lt;p&gt;もう少し手がかりを得るために，各単語（品詞）の種類を取得してみます．
再びドキュメントの &lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage/identifying_parts_of_speech&quot;&gt;Identifying Parts of Speech&lt;/a&gt; を参考にして次のようにしました．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NaturalLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UILabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;naturalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;debugPrint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tagger&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NLTagger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;tagSchemes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lexicalClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tagger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NLTagger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Options&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;omitPunctuation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;omitWhitespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tagger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startIndex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;scheme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lexicalClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Unknown&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rawValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;debugPrint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;3タイトルはそれぞれ次のようにタグ付けされました．&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;やはり俺の青春ラブコメはまちがっている。14&quot;
[(&quot;やはり&quot;, &quot;OtherWord&quot;), (&quot;俺&quot;, &quot;OtherWord&quot;), (&quot;の&quot;, &quot;OtherWord&quot;), (&quot;青春&quot;, &quot;OtherWord&quot;), (&quot;ラブコメ&quot;, &quot;OtherWord&quot;), (&quot;は&quot;, &quot;OtherWord&quot;), (&quot;まちがっ&quot;, &quot;OtherWord&quot;), (&quot;て&quot;, &quot;OtherWord&quot;), (&quot;いる&quot;, &quot;OtherWord&quot;), (&quot;14&quot;, &quot;OtherWord&quot;)]

&quot;隠れオタな彼女と、史上最高のラブコメをさがしませんか？&quot;
[(&quot;隠れ&quot;, &quot;OtherWord&quot;), (&quot;オタ&quot;, &quot;OtherWord&quot;), (&quot;な&quot;, &quot;OtherWord&quot;), (&quot;彼女&quot;, &quot;OtherWord&quot;), (&quot;と&quot;, &quot;OtherWord&quot;), (&quot;史上&quot;, &quot;OtherWord&quot;), (&quot;最高&quot;, &quot;OtherWord&quot;), (&quot;の&quot;, &quot;OtherWord&quot;), (&quot;ラブコメ&quot;, &quot;OtherWord&quot;), (&quot;を&quot;, &quot;OtherWord&quot;), (&quot;さがし&quot;, &quot;OtherWord&quot;), (&quot;ませ&quot;, &quot;OtherWord&quot;), (&quot;ん&quot;, &quot;OtherWord&quot;), (&quot;か&quot;, &quot;OtherWord&quot;)]

&quot;裏方キャラの青木くんがラブコメを制すまで。&quot;
[(&quot;裏方&quot;, &quot;OtherWord&quot;), (&quot;キャラ&quot;, &quot;OtherWord&quot;), (&quot;の&quot;, &quot;OtherWord&quot;), (&quot;青木&quot;, &quot;OtherWord&quot;), (&quot;くん&quot;, &quot;OtherWord&quot;), (&quot;が&quot;, &quot;OtherWord&quot;), (&quot;ラブコメ&quot;, &quot;OtherWord&quot;), (&quot;を&quot;, &quot;OtherWord&quot;), (&quot;制&quot;, &quot;OtherWord&quot;), (&quot;す&quot;, &quot;OtherWord&quot;), (&quot;まで&quot;, &quot;OtherWord&quot;)]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;全て &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OtherWord&lt;/code&gt; という手がかりゼロの結果になりました．
ちなみに英語だと &lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage/nltag&quot;&gt;NLTag&lt;/a&gt; の種類どおりにタグ付けされるのですが，日本語だと文字通り何の成果も得られませんでした．&lt;/p&gt;

&lt;p&gt;ということで，気を取り直して単語の切れ目だけを手がかりとして進めます．&lt;/p&gt;

&lt;h2 id=&quot;通常の改行位置を取得する&quot;&gt;通常の改行位置を取得する&lt;/h2&gt;

&lt;p&gt;まずは通常の UILabel に流し込んだ String がどの位置で改行されるのかを取得します．
iOS にはテキストをローレベルで触れる &lt;a href=&quot;https://developer.apple.com/documentation/coretext&quot;&gt;Core Text&lt;/a&gt; Framework があるので，ドキュメントを読みながら各行を取得するメソッドを書きました．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NaturalLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UILabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getLines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;font&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;font&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;attributedString&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NSMutableAttributedString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;attributedString&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NSRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attributedString&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;frameSetter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CTFramesetterCreateWithAttributedString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attributedString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;framePath&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;rect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGRect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frame&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;greatestFiniteMagnitude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;frame&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CTFramesetterCreateFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frameSetter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CFRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;framePath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;lines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CTFrameGetLines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CTLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;cfRange&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CTLineGetStringRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;nsRange&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NSRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cfRange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cfRange&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NSString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;substring&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nsRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最終的には CTLine の Array が欲しい &amp;gt; CTFrameGetLines を呼び出したい &amp;gt; CTFrame が欲しい &amp;gt; CTFramesetterCreateFrame を呼び出したい &amp;gt; … という感じでドキュメントを辿っていけば良いです．&lt;/p&gt;

&lt;h2 id=&quot;いい感じの改行をする&quot;&gt;いい感じの改行をする&lt;/h2&gt;

&lt;p&gt;ようやく準備が整ったので，改行のロジックを考えて実装します．
今回は遊びということでパッと思いついたもので実装していきます．&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;文字列を tokenize する&lt;/li&gt;
  &lt;li&gt;文字列を UILabel に流し込んで改行位置を取得する&lt;/li&gt;
  &lt;li&gt;Token を先頭から順に改行位置との衝突が起こるまで読んでいく（例：「ラブ\nコメ」で衝突）&lt;/li&gt;
  &lt;li&gt;衝突が起こる直前までの文字列を確定させて，確定行の String 配列に積む&lt;/li&gt;
  &lt;li&gt;衝突が起こる直前までの token を消費して，判定対象の token から除く&lt;/li&gt;
  &lt;li&gt;残りの文字列を UILabel に流し込んで改行位置を取得する&lt;/li&gt;
  &lt;li&gt;3-6 を token がなくなるまで繰り返す&lt;/li&gt;
  &lt;li&gt;残った文字列を確定行配列に積む&lt;/li&gt;
  &lt;li&gt;確定行配列を改行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n&lt;/code&gt; で join する&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;日本語にすると難しいですが，コードだったらまだ分かりやすいかもしれません．
実験用なので，衝突判定を String#contains で雑にやっていたり，文字列が長いときのパフォーマンスなどは考慮していません．&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NaturalLabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UILabel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;naturalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replacingOccurrences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tokenizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NLTokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startIndex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;words&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fixedLines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fixedTokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fixedTokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;remainingTokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dropFirst&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fixedTokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;remainingText&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remainingTokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lowerBound&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;remainingLines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getLines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;remainingText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;brokenToken&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;remainingTokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;word&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remainingLines&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;contains&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;word&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;fixedLines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remainingText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;fixedTokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;remainingTokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;n&quot;&gt;fixedLines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remainingTokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lowerBound&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;brokenToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lowerBound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])]&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fixedTokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;remainingTokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;firstIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;brokenToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fixedLines&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;joined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;アプリを実行してみた結果は次のようになりました．&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;iPhone 11 Pro&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;iPhone 11 Pro Max&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/1_fixed_page1_pro.png&quot; alt=&quot;fixed layout iPhone Pro page 1&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/1_fixed_page2_pro.png&quot; alt=&quot;fixed layout iPhone Pro page 2&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/1_fixed_page1_promax.png&quot; alt=&quot;fixed layout iPhone Pro Max page 1&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;img src=&quot;/images/pages/natural-label-experiment/1_fixed_page2_promax.png&quot; alt=&quot;fixed layout iPhone Pro Max page 2&quot; /&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Before&lt;/th&gt;
      &lt;th&gt;After&lt;/th&gt;
      &lt;th&gt;Size&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;やはり俺の青春ラブコメはまち&lt;br /&gt;がっている。14&lt;/td&gt;
      &lt;td&gt;やはり俺の青春ラブコメは&lt;br /&gt;まちがっている。14&lt;/td&gt;
      &lt;td&gt;11 Pro Max&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;隠れオタな彼女と、史上最&lt;br /&gt;高のラブコメをさがしませんか？&lt;/td&gt;
      &lt;td&gt;隠れオタな彼女と、史上&lt;br /&gt;最高のラブコメをさがし&lt;br /&gt;ませんか？&lt;/td&gt;
      &lt;td&gt;11 Pro&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;裏方キャラの青木くんがラブコ&lt;br /&gt;メを制すまで。&lt;/td&gt;
      &lt;td&gt;裏方キャラの青木くんが&lt;br /&gt;ラブコメを制すまで。&lt;/td&gt;
      &lt;td&gt;11 Pro Max&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;思ってたよりも良くなっているような気がします（個人の感想です）．&lt;/p&gt;

&lt;h2 id=&quot;まとめ感想&quot;&gt;まとめ・感想&lt;/h2&gt;

&lt;p&gt;日本語のテキストの改行問題に対して自然言語処理で解決できそうか実験してみました．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;iOS の Natural Language Framework を使ってテキストを単語単位に分割した&lt;/li&gt;
  &lt;li&gt;Core Text Framework を使って UILabel の改行位置を取得した&lt;/li&gt;
  &lt;li&gt;単語の中で改行が行われないように &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n&lt;/code&gt; を挟んでみた&lt;/li&gt;
  &lt;li&gt;思ってたより良くなった（個人の感想です）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;想像していたとおり，「史上\n最高」などは防げませんでしたが，改行問題をある程度解決することができました．
ただし，実用に足りるかというと微妙かもしれません．&lt;/p&gt;

&lt;p&gt;Apple の API で品詞の種類が取得ができなかったときは少しあせりましたが，なんにせよ Apple が「ラブコメ」を1単語として認識してくれていたのは良かったです．これがなかったら完全に詰んでいました．&lt;/p&gt;

&lt;p&gt;2019年，完結おめでとうございます．アニメ3期もよろしくお願いします．&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/books/&quot;&gt;Google Books APIs | Google Developers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage&quot;&gt;Natural Language | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage/tokenizing_natural_language_text&quot;&gt;Tokenizing Natural Language Text | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage/identifying_parts_of_speech&quot;&gt;Identifying Parts of Speech | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/naturallanguage/nltag&quot;&gt;NLTag - Natural Language | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/coretext&quot;&gt;Core Text | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/coretext/ctframesetter-2eg&quot;&gt;CTFramesetter | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFDesignConcepts/Articles/tollFreeBridgedTypes.html#//apple_ref/doc/uid/TP40010677&quot;&gt;Toll-Free Bridged Types&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 16 Dec 2019 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2019/12/16/natural-label-experiment.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2019/12/16/natural-label-experiment.html</guid>
        
        
        <category>iOS</category>
        
        <category>UI</category>
        
        <category>Design</category>
        
        <category>NLP</category>
        
      </item>
    
      <item>
        <title>UI デザイナーのための iOS レイアウト入門</title>
        <description>&lt;p&gt;この記事は &lt;a href=&quot;https://qiita.com/advent-calendar/2019/goodpatch&quot;&gt;Goodpatch Advent Calendar 2019&lt;/a&gt; の9日目です．&lt;/p&gt;

&lt;p&gt;iOS レイアウトの仕組みを知らずに iOS の UI デザインをすることはできません．
つまり UI デザイナーは，iOS の提供する UI コンポーネントや Auto Layout について基本的な仕組みは理解しておく必要があります．&lt;/p&gt;

&lt;p&gt;その助けになるといいなと思ってこの記事を書きました．&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;概要&quot;&gt;概要&lt;/h2&gt;

&lt;p&gt;この記事では次の内容について書きます．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Auto Layout のドキュメントを紹介する&lt;/li&gt;
  &lt;li&gt;超基本的な UIKit コンポーネントを紹介する&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;iOS レイアウトの基礎に限ったとしても，この記事だけで内容を網羅することは到底できません．
この記事では，iOS レイアウトを学び始めるための入り口やキーワードを提供することを目的とします．&lt;/p&gt;

&lt;h3 id=&quot;補足-swiftui&quot;&gt;補足: SwiftUI&lt;/h3&gt;

&lt;p&gt;2019 年の WWDC で新しい UI システムの &lt;a href=&quot;https://developer.apple.com/xcode/swiftui/&quot;&gt;SwiftUI&lt;/a&gt; が発表されましたが，この記事は従来の UIKit の使用を想定して書いています．
もし，これから新しく iOS アプリを作るために実装まで本格的に学びたいのであれば SwiftUI を学ぶと良いです．
ただし，UIKit について知っていることは無駄にならないし，ここ 1-2 年では新規/継続開発を問わず UIKit はまだ使われ続けると予想します．&lt;/p&gt;

&lt;h2 id=&quot;auto-layout-公式ドキュメント&quot;&gt;Auto Layout 公式ドキュメント&lt;/h2&gt;

&lt;p&gt;Auto Layout については Apple Developer のページに必要十分なドキュメントがあります．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html&quot;&gt;Auto Layout Guide: Understanding Auto Layout&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting Started と Auto Layout Cookbook のセクションについては目を通しておくと良いです．
英語に苦手意識がある場合は，ざっと眺めて雰囲気だけ掴んでおき，以下で紹介する日本語の記事などを読んで基礎知識をつけてから戻ってくると良いです．
ただし，最終的には公式ドキュメントは必ず読むことになると思います．&lt;/p&gt;

&lt;h2 id=&quot;入門ドキュメント&quot;&gt;入門ドキュメント&lt;/h2&gt;

&lt;p&gt;Zaim のブログに akatsuki174 さんが投稿した記事が分かりやすくまとまっています．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.zaim.co.jp/n/n0d2850ec5099&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-基礎編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://note.com/akatsuki174/n/n59280cde6b71&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-応用編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://note.com/akatsuki174/n/nd89569f867b6&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-実践編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.zaim.co.jp/n/n69d066a9bdf4&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-コミュニケーション編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;公式ドキュメントを網羅する内容ではないので，公式ドキュメントは別途読むようにします．
一方で，公式ドキュメントに載っていない図解や練習問題もあるので，併せて読んでみると良いと思います．&lt;/p&gt;

&lt;h2 id=&quot;uikit-コンポーネント&quot;&gt;UIKit コンポーネント&lt;/h2&gt;

&lt;p&gt;Auto Layout について基本的な仕組みが分かったら，UIKit のコンポーネントについて理解していきます．&lt;/p&gt;

&lt;p&gt;まず，前提として HIG を読んでいて基本的なコンポーネントについては知っているものとします．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/design/human-interface-guidelines/ios/overview/themes/&quot;&gt;Themes - iOS - Human Interface Guidelines - Apple Developer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;その上で，一般的なアプリを実装するために，まずは次のコンポーネントについてより深く知っておくと良いです．
以下のリンクは Developer 向けの内容なので詳細まで読み込む必要はないですが，UI を実装するために Developer がどういう手段を持っているのか触りだけでも知っておくと UI デザインをするときに考慮すべきポイントが分かるようになると思います．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uilabel&quot;&gt;UILabel - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiimageview&quot;&gt;UIImageView - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uibutton&quot;&gt;UIButton - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uistackview&quot;&gt;UIStackView - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;各コンポーネントには設定できる attributes（属性）があり，主要なものは Interface Builder Attributes の項に記述されているので必ず読むようにします．
少なくともこれら attributes の観点を考慮できているだけで，かなり実装しやすいデザインになります．&lt;/p&gt;

&lt;h3 id=&quot;uilabel&quot;&gt;UILabel&lt;/h3&gt;

&lt;p&gt;UILabel はテキストを表示するための基本コンポーネントです．
次の画像は Xcode の Attributes Inspector で見ることができる UILabel の attributes です．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uilabel-attributes.png&quot; alt=&quot;UILabel attributes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;詳しい説明は前述の Interface Builder attributes の項で確認するとして，個人的な意見を補足すると次のとおりです．&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Font&lt;/code&gt;: System フォントを使うのが，文字の読みやすさという点でもアプリの容量の点でも有利になる．&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lines&lt;/code&gt;: 最大2行まで表示して末尾は Truncate という場合や，最後の行まで全て表示する（Lines = 0）という場合に指定する．
なお，この attribute を見ると UILabel は「最大文字数」を指定する設計ではないということが分かる．&lt;/p&gt;

&lt;p&gt;経験上 UILabel のサイズは可変にすることが多く，width や height 制約を直接的には指定しないことが多い．
その理由としては，テキストというコンテンツが内容や多言語化（i18n）によって大きく分量が変わるため．
また，UIImageView のようにサイズやアスペクト比を指定しないとコンテンツが pixelate してしまうといった強い制約がないため．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uilabel-width-variation.png&quot; alt=&quot;UILabel width variation&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Autoshrink&lt;/code&gt;: 最終手段としてのみ使い，乱用すべきではない．
文字の大きさは意味的に重要なため．
文字数が収まらないときはレイアウトを工夫するか truncate で対応する．&lt;/p&gt;

&lt;p&gt;UILabel は最も頻出するコンポーネントなので，他の attributes についてもしっかり確認しておくことをおすすめします．&lt;/p&gt;

&lt;h3 id=&quot;uiimageview&quot;&gt;UIImageView&lt;/h3&gt;

&lt;p&gt;UIImageView は画像を表示するための基本コンポーネントです．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uiimageview-attributes.png&quot; alt=&quot;UIImageView attributes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;同様に個人的な意見を補足すると次のとおりです．&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content Mode&lt;/code&gt;: ドキュメントの Understanding How Images Are Scaled の項にも記述がある，最重要 attribute．指定できる値については&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiview/contentmode&quot;&gt;ドキュメント&lt;/a&gt;に詳しく説明があるが，通常はアスペクト比を保つことができる &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AspectFit (scaleAspectFit)&lt;/code&gt; か &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Aspect Fill (scaleAspectFill)&lt;/code&gt; を使う．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uiimageview-content-mode.png&quot; alt=&quot;UIImageView content mode&quot; /&gt;&lt;/p&gt;

&lt;p&gt;なお，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Aspect Fill&lt;/code&gt; は画像の中心がくり抜かれるので「画像の上端 &amp;amp; 左右両端を合わせて，下端は裁ち落とし」というレイアウトはひと工夫必要だということが分かる．
また，iOS の ImageView にはコンテンツサイズに合わせて動的に大きさを変える attribute がないことも分かる．
他のプラットフォームでは Android の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrap_content&lt;/code&gt; や Unity UI の &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Preferred Size&lt;/code&gt; など指定ができることが多いので，少し意外な設計だと思っている．&lt;/p&gt;

&lt;p&gt;UIImageView も頻出するコンポーネントであり，かつ前述のように制約が強くレイアウトの基準になりやすいコンポーネントなので，レイアウトの仕組みについて必ず知っておくべきです．&lt;/p&gt;

&lt;h3 id=&quot;uibutton&quot;&gt;UIButton&lt;/h3&gt;

&lt;p&gt;UIButton はその名のとおり，ボタンを表示するための基本コンポーネントです．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uibutton-attributes.png&quot; alt=&quot;UIButton attributes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;State Config&lt;/code&gt;: UIButton は Default，Highlighted，Selected，Disabled の状態に対して別々に attributes を設定できるようになっている．
裏を返すとこれらの状態については UI デザインで考慮しておくべき．
ただし，Selected 状態を使わない場合も多いので，必要最低限としてはその他3状態を考慮しておくと良い．
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Type&lt;/code&gt; 属性にデフォルト値である &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System&lt;/code&gt; を指定すると，Default 状態を設定するだけで他の状態には Tint カラーを使って対応してくれるので，第1の選択肢として検討する．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uibutton-states.png&quot; alt=&quot;UIButton states&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Image&lt;/code&gt;: Title の隣に表示する画像を指定できる．
ただし，レイアウトの自由度が低く，標準の UIButton の提供する仕組みでは UI デザインを再現できない場合も多い．&lt;/p&gt;

&lt;p&gt;UIButton はシンプルですが少しクセのあるコンポーネントです．
Sketch ファイルのような静的な UI デザイン成果物上では各種状態が考慮できていなかったり，なんとなくで置かれている（ように見える）アイコンを実装するために独自カスタマイズが必要になったりということがあります．
UIButton の仕様を知っているとこういった考慮漏れや「なんとなく」が少なくなると思います．&lt;/p&gt;

&lt;h3 id=&quot;uistackview&quot;&gt;UIStackView&lt;/h3&gt;

&lt;p&gt;UIStackView はコンポーネントを並べるためのコンポーネントです．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uistackview-attributes.png&quot; alt=&quot;UIStackView attributes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Distribution&lt;/code&gt;: 指定できる値について詳しくは&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uistackview/distribution&quot;&gt;ドキュメント&lt;/a&gt;に説明があるが，Fill 系と Spacing 系の2種類がある．
基本的にはコンポーネントを縦（or 横）に等間隔で並べるために Spacing 系を，等しい高さ（幅）で敷き詰めるために Fill 系を使う．&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/pages/ios-layout/uistackview-distribution.png&quot; alt=&quot;UIStackView distribution&quot; /&gt;&lt;/p&gt;

&lt;p&gt;UIStackView を用いてコンポーネントを縦横に整列するレイアウトは，iOS UI デザインにおいて頻出するパターンです．
UIStackView の仕組みを知っていれば，iOS アプリの UI を見たときに View の構造を推測しやすくなると思います．&lt;/p&gt;

&lt;h2 id=&quot;next-action&quot;&gt;Next Action&lt;/h2&gt;

&lt;p&gt;まだ紹介していないコンポーネントについても，Developer Documentation を読んで見ると良いかもしれません．
特に UITextField，UISegmentedControl, UITableViewCell あたりは頻出なのでおすすめです．&lt;/p&gt;

&lt;p&gt;また，macOS を使っているなら誰でも AppStore から Xcode をインストールできるので，実際に Auto Layout がどんなものか触ってみてください．
YouTube をはじめとして，動画で解説してくれているリソースもたくさんあります．
あるいは友達や同僚に iOS Developer がいるなら，Xcode を開いて質問にいけばきっと喜んで教えてくれると思います．&lt;/p&gt;

&lt;h2 id=&quot;まとめ感想&quot;&gt;まとめ・感想&lt;/h2&gt;

&lt;p&gt;この記事では次の内容について書きました．&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Auto Layout のドキュメントを紹介した
    &lt;ul&gt;
      &lt;li&gt;公式&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html&quot;&gt;ドキュメント&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;Zaim の&lt;a href=&quot;https://blog.zaim.co.jp/n/n0d2850ec5099&quot;&gt;ブログ記事&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;超基本的な4つの UIKit コンポーネントを紹介した
    &lt;ul&gt;
      &lt;li&gt;UILabel (&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uilabel&quot;&gt;ドキュメント&lt;/a&gt;)&lt;/li&gt;
      &lt;li&gt;UIImageView (&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiimageview&quot;&gt;ドキュメント&lt;/a&gt;)&lt;/li&gt;
      &lt;li&gt;UIButton (&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uibutton&quot;&gt;ドキュメント&lt;/a&gt;)&lt;/li&gt;
      &lt;li&gt;UIStackView (&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uistackview&quot;&gt;ドキュメント&lt;/a&gt;)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;iOS のレイアウトを学び始めるためのとっかかりを得られたと感じてもらえれば嬉しいです．&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html&quot;&gt;Auto Layout Guide: Understanding Auto Layout&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uilabel&quot;&gt;UILabel - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiimageview&quot;&gt;UIImageView - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uibutton&quot;&gt;UIButton - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uistackview&quot;&gt;UIStackView - UIKit | Apple Developer Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.zaim.co.jp/n/n0d2850ec5099&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-基礎編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://note.com/akatsuki174/n/n59280cde6b71&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-応用編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://note.com/akatsuki174/n/nd89569f867b6&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-実践編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.zaim.co.jp/n/n69d066a9bdf4&quot;&gt;非 iOS エンジニアのための Auto Layout 入門-コミュニケーション編- #Zaim｜akatsuki174&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 09 Dec 2019 00:00:00 +0900</pubDate>
        <link>https://blog.kakeragames.com/2019/12/09/ios-layout-introduction.html</link>
        <guid isPermaLink="true">https://blog.kakeragames.com/2019/12/09/ios-layout-introduction.html</guid>
        
        
        <category>iOS</category>
        
        <category>UI</category>
        
        <category>Design</category>
        
      </item>
    
  </channel>
</rss>
